feat: 每日热点问题共享库(定时生成+管理员手动更新+点击埋点)
CI / 后端测试 (push) Has been cancelled
CI / 前端构建 (push) Has been cancelled

数据库:
- daily_topics表:每天每场景一条热点问题记录
- sample_clicks表:记录用户点击示例问题行为

后端:
- DailyTopicStore: CRUD + 埋点 + 点击统计
- generate_daily_topics: AI生成4场景各25-30个问题
- 定时任务:每天凌晨3点自动生成(后台线程轮询)
- GET /topics/daily: 获取当天热点(公开)
- POST /topics/click: 点击埋点
- GET /admin/daily-topics: 管理端查看
- POST /admin/daily-topics: 管理员手动更新
- POST /admin/daily-topics/generate: 手动触发AI生成
- GET /admin/sample-clicks: 点击统计

前端:
- ChatPanel挂载时取当天热点,无则用硬编码兜底
- 点击示例问题时记录埋点
- AdminDailyTopics管理页面:查看/编辑/手动生成
- 路由 /admin/daily-topics + 导航配置

Generated with [Devin](https://devin.ai)

Co-Authored-By: Devin <158243242+devin-ai-integration[bot]@users.noreply.github.com>
This commit is contained in:
selfrelease
2026-08-31 17:04:33 +08:00
co-authored by Devin
parent 35cc6e54ea
commit a7ef530841
9 changed files with 638 additions and 3 deletions
+205
View File
@@ -3170,6 +3170,211 @@ def build_profile_context(profile: UserProfile) -> str:
return profile.to_context()
# ===== 每日热点问题库 =====
class DailyTopicStore:
"""每日热点问题库:每天 AI 生成或管理员手动更新。
每个场景(legal/decision/life/emotion)每天一条记录,
questions 为 JSONB 数组,前端取当天问题展示给用户。
"""
SCENES = ["legal", "decision", "life", "emotion"]
def __init__(self, dsn: str | None = None):
self.dsn = dsn or os.getenv(
"DATABASE_URL", "host=localhost port=5432 dbname=mcp user=postgres"
)
def _conn(self):
return psycopg2.connect(self.dsn)
def get_today(self, scene: str) -> list[str]:
"""获取当天热点问题,无记录返回空列表。"""
from datetime import date
with self._conn() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT questions FROM daily_topics "
"WHERE date = %s AND scene = %s",
(date.today(), scene),
)
r = cur.fetchone()
return r[0] if r else []
def get_by_date(self, date_str: str) -> list[dict]:
"""获取指定日期所有场景的热点问题(管理端用)。"""
with self._conn() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT id, date, scene, questions, source, created_at "
"FROM daily_topics WHERE date = %s ORDER BY scene",
(date_str,),
)
rows = cur.fetchall()
return [
{"id": r[0], "date": str(r[1]), "scene": r[2],
"questions": r[3], "source": r[4], "created_at": str(r[5])}
for r in rows
]
def upsert(self, scene: str, questions: list[str],
source: str = "ai", date_str: str | None = None) -> dict:
"""插入或更新当天(或指定日期)的热点问题。
Args:
scene: 场景 legal/decision/life/emotion
questions: 问题列表
source: ai(自动生成) / admin(管理员手动)
date_str: 指定日期(YYYY-MM-DD),默认今天
Returns:
更新后的记录 dict
"""
from datetime import date
import json
target_date = date_str if date_str else str(date.today())
with self._conn() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO daily_topics (date, scene, questions, source) "
"VALUES (%s, %s, %s, %s) "
"ON CONFLICT (date, scene) DO UPDATE "
"SET questions = EXCLUDED.questions, source = EXCLUDED.source, "
"created_at = now() "
"RETURNING id, date, scene, questions, source, created_at",
(target_date, scene, json.dumps(questions), source),
)
r = cur.fetchone()
return {"id": r[0], "date": str(r[1]), "scene": r[2],
"questions": r[3], "source": r[4], "created_at": str(r[5])}
def record_click(self, scene: str, question: str,
user_phone: str | None = None) -> bool:
"""记录用户点击示例问题(埋点)。"""
with self._conn() as conn:
with conn.cursor() as cur:
cur.execute(
"INSERT INTO sample_clicks (user_phone, scene, question) "
"VALUES (%s, %s, %s)",
(user_phone, scene, question),
)
return cur.rowcount > 0
def click_stats(self, days: int = 14) -> list[dict]:
"""获取近 N 天示例问题点击统计(管理端用)。"""
with self._conn() as conn:
with conn.cursor() as cur:
cur.execute(
"SELECT scene, question, COUNT(*) as clicks "
"FROM sample_clicks "
"WHERE clicked_at >= now() - interval '%s days' "
"GROUP BY scene, question ORDER BY clicks DESC LIMIT 100",
(days,),
)
rows = cur.fetchall()
return [
{"scene": r[0], "question": r[1], "clicks": r[2]}
for r in rows
]
def generate_daily_topics(kimi_client, store: DailyTopicStore) -> dict:
"""AI 生成当天 4 个场景的热点问题并存库。
每个场景生成 20-30 个问题,聚焦老板焦虑和关注点。
AI 不可用时跳过(保留已有数据)。
Returns:
各场景生成结果 {scene: {count, source}}
"""
from datetime import datetime
scene_desc = {
"legal": "法律/法务问题,聚焦中小企业老板的焦虑:劳动用工风险、合同纠纷、债权债务、股东纠纷、税务风险、诉讼仲裁、婚姻财产分割、股权分配、知识产权、行政处罚等",
"decision": "经营决策问题,聚焦中小企业老板的焦虑:扩张收缩、项目承接、定价策略、招人裁员、融资合伙、业务转型、现金流管理、竞争应对、投资决策等",
"life": "个人生活问题,聚焦中小企业老板的焦虑:家庭与事业平衡、子女教育规划、父母养老、健康风险、睡眠压力、时间管理、财富传承等",
"emotion": "情绪与心理问题,聚焦中小企业老板的焦虑:创业压力、合伙人冲突、孤独感、决策焦虑、拖延症、自我怀疑、危机应对等",
}
# 获取当前日期用于 prompt
today_str = datetime.now().strftime("%Y年%m月%d日")
# 获取当前热点事件提示(简单版:用日期季节推断)
month = datetime.now().month
season_hints = {
1: "年初:年度规划、去年总结、新政策生效、春节前后用工",
2: "春节后:招工难、复工、年度目标分解",
3: "两会政策、年报截止、个税汇算",
4: "Q1复盘、社保基数调整预告、春招",
5: "劳动节、用工合规检查、年中冲刺",
6: "年中复盘、半年报、高温补贴、用电高峰",
7: "下半年规划、社保基数调整、学生毕业季",
8: "秋招准备、Q3冲刺、税务自查",
9: "Q3复盘、中秋、国庆备货、年度预算",
10: "国庆后冲刺、年终规划、个税专项附加扣除",
11: "年底冲刺、年终奖规划、来年预算、欠薪风险",
12: "年终总结、欠薪清查、税务清算、年度复盘、春节前用工",
}
season = season_hints.get(month, "")
results = {}
for scene, desc in scene_desc.items():
prompt = f"""你是左右(BossAi)的内容编辑,负责为中小企业老板生成今日热点问题。
当前日期:{today_str}
季节性背景:{season}
场景:{desc}
请生成25-30个该场景下老板最焦虑、最关注的热点问题。
要求:
1. 问题必须从老板/雇主视角出发(BossAi 用户是企业老板)
2. 结合当前季节性和时事热点(如政策变化、节日前后、经济形势)
3. 问题要具体、有场景感、能直接开问,不要空泛
4. 涵盖该场景下不同方向,不要集中在某一类
5. 问题长度 10-30 字
6. 返回JSON数组格式:["问题1","问题2",...]
7. 只返回JSON,不要其他文字"""
try:
answer = kimi_client.chat(
[{"role": "user", "content": prompt}],
temperature=0.8, timeout=90,
)
answer = answer.strip()
if answer.startswith("```"):
answer = answer.split("\n", 1)[1] if "\n" in answer else answer[3:]
if answer.endswith("```"):
answer = answer[:-3]
answer = answer.strip().replace("\u201c", '"').replace("\u201d", '"')
start = answer.find("[")
end = answer.rfind("]")
if start == -1 or end == -1:
results[scene] = {"count": 0, "source": "ai", "error": "JSON解析失败"}
continue
questions = json.loads(answer[start:end + 1])
# 过滤和去重
valid = []
seen = set()
for q in questions:
q = str(q).strip()
if q and q not in seen and len(q) <= 50:
valid.append(q)
seen.add(q)
if len(valid) < 10:
results[scene] = {"count": 0, "source": "ai", "error": "有效问题不足10个"}
continue
store.upsert(scene, valid[:30], source="ai")
results[scene] = {"count": len(valid[:30]), "source": "ai"}
except Exception as e:
import logging
logging.warning(f"generate_daily_topics[{scene}] 失败: {type(e).__name__}: {e}")
results[scene] = {"count": 0, "source": "ai", "error": str(e)}
return results
# ===== 对话历史落库 =====
class CryptoUtil: