feat: 每日热点问题共享库(定时生成+管理员手动更新+点击埋点)
数据库: - 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:
@@ -18,6 +18,7 @@ from bossai import (
|
||||
ExpertStore, ConsultationStore, RatingStore,
|
||||
DocumentGenerator, RegulationUpdateManager, FactFinder,
|
||||
ChatStore, redact,
|
||||
DailyTopicStore, generate_daily_topics,
|
||||
)
|
||||
|
||||
# ===== 日志脱敏 =====
|
||||
@@ -90,12 +91,50 @@ consultation_store = ConsultationStore()
|
||||
rating_store = RatingStore()
|
||||
chat_store = ChatStore()
|
||||
metrics = Metrics()
|
||||
daily_topic_store = DailyTopicStore()
|
||||
try:
|
||||
kimi = KimiClient()
|
||||
except RuntimeError:
|
||||
kimi = None
|
||||
|
||||
|
||||
# ===== 每日热点问题定时任务(每天凌晨3点) =====
|
||||
def _daily_topics_scheduler():
|
||||
"""后台线程:每天凌晨3点自动生成热点问题。
|
||||
|
||||
使用轮询方式实现,避免引入 APScheduler 依赖。
|
||||
AI 不可用时跳过,保留已有数据。
|
||||
"""
|
||||
import threading
|
||||
from datetime import datetime, time as dt_time
|
||||
|
||||
def run():
|
||||
while True:
|
||||
now = datetime.now()
|
||||
# 计算到下一个凌晨3点的秒数
|
||||
target = datetime.combine(now.date(), dt_time(3, 0))
|
||||
if now >= target:
|
||||
# 今天3点已过,等明天
|
||||
from datetime import timedelta
|
||||
target = target + timedelta(days=1)
|
||||
wait_seconds = (target - now).total_seconds()
|
||||
threading.Event().wait(timeout=wait_seconds)
|
||||
# 执行生成
|
||||
if kimi:
|
||||
try:
|
||||
logging.info("开始生成每日热点问题...")
|
||||
results = generate_daily_topics(kimi, daily_topic_store)
|
||||
logging.info(f"每日热点问题生成完成: {results}")
|
||||
except Exception as e:
|
||||
logging.warning(f"每日热点问题生成失败: {type(e).__name__}: {e}")
|
||||
|
||||
thread = threading.Thread(target=run, daemon=True, name="daily-topics-scheduler")
|
||||
thread.start()
|
||||
|
||||
|
||||
_daily_topics_scheduler()
|
||||
|
||||
|
||||
# ===== 请求计时 + 日志脱敏中间件 =====
|
||||
@app.middleware("http")
|
||||
async def metrics_middleware(request: Request, call_next):
|
||||
@@ -2959,3 +2998,73 @@ def update_execution_result(matter_id: int, body: dict,
|
||||
return {"updated": True}
|
||||
finally:
|
||||
conn.close()
|
||||
|
||||
|
||||
# ===== 每日热点问题库 =====
|
||||
|
||||
@app.get("/topics/daily")
|
||||
def get_daily_topics(scene: str):
|
||||
"""获取当天热点问题(公开接口,无需登录)。
|
||||
|
||||
无当天数据时返回空数组,前端用硬编码兜底。
|
||||
"""
|
||||
if scene not in DailyTopicStore.SCENES:
|
||||
raise HTTPException(400, f"场景应为 {DailyTopicStore.SCENES} 之一")
|
||||
return {"scene": scene, "questions": daily_topic_store.get_today(scene)}
|
||||
|
||||
|
||||
@app.post("/topics/click")
|
||||
def record_sample_click(body: dict, authorization: str | None = Header(default=None)):
|
||||
"""记录用户点击示例问题(埋点)。"""
|
||||
scene = body.get("scene", "")
|
||||
question = body.get("question", "")
|
||||
if scene not in DailyTopicStore.SCENES or not question:
|
||||
raise HTTPException(400, "缺少 scene 或 question")
|
||||
phone = get_current_phone(authorization)
|
||||
daily_topic_store.record_click(scene, question, phone)
|
||||
return {"recorded": True}
|
||||
|
||||
|
||||
# ===== 管理端:热点问题管理 =====
|
||||
|
||||
@app.get("/admin/daily-topics")
|
||||
def admin_get_daily_topics(date: str | None = None):
|
||||
"""管理端:获取指定日期(默认今天)所有场景的热点问题。"""
|
||||
from datetime import date as date_cls
|
||||
target = date or str(date_cls.today())
|
||||
return {"results": daily_topic_store.get_by_date(target)}
|
||||
|
||||
|
||||
class DailyTopicUpdateRequest(BaseModel):
|
||||
"""管理员手动更新热点问题。"""
|
||||
scene: str
|
||||
questions: list[str]
|
||||
date: str | None = None # 指定日期,默认今天
|
||||
|
||||
|
||||
@app.post("/admin/daily-topics")
|
||||
def admin_update_daily_topics(req: DailyTopicUpdateRequest):
|
||||
"""管理端:手动更新某场景的热点问题。"""
|
||||
if req.scene not in DailyTopicStore.SCENES:
|
||||
raise HTTPException(400, f"场景应为 {DailyTopicStore.SCENES} 之一")
|
||||
if not req.questions:
|
||||
raise HTTPException(400, "问题列表不能为空")
|
||||
result = daily_topic_store.upsert(
|
||||
req.scene, req.questions, source="admin", date_str=req.date,
|
||||
)
|
||||
return {"updated": True, "topic": result}
|
||||
|
||||
|
||||
@app.post("/admin/daily-topics/generate")
|
||||
def admin_generate_daily_topics():
|
||||
"""管理端:手动触发 AI 生成当天热点问题(4个场景)。"""
|
||||
if not kimi:
|
||||
raise HTTPException(503, "AI 服务不可用,请检查 KIMI_API_KEY 配置")
|
||||
results = generate_daily_topics(kimi, daily_topic_store)
|
||||
return {"generated": True, "results": results}
|
||||
|
||||
|
||||
@app.get("/admin/sample-clicks")
|
||||
def admin_sample_click_stats(days: int = 14):
|
||||
"""管理端:获取示例问题点击统计。"""
|
||||
return {"results": daily_topic_store.click_stats(days)}
|
||||
|
||||
Reference in New Issue
Block a user