feat: 运营报告生成功能
- 后端新增 /admin/stats/report 接口,生成14天运营数据报告 - 报告内容:垃圾信息变化趋势、高频风险词Top10、误判率趋势 - 前端运营看板增加"生成报告"按钮,展示完整报告 - 支持复制报告文本到剪贴板 Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
This commit is contained in:
@@ -147,6 +147,114 @@ def stats():
|
||||
)
|
||||
|
||||
|
||||
@admin_bp.get("/stats/report")
|
||||
@admin_required
|
||||
def generate_report():
|
||||
"""生成运营报告:垃圾信息变化、风险词排名、误判率趋势"""
|
||||
now = datetime.utcnow()
|
||||
week_ago = now - timedelta(days=13) # 近14天
|
||||
|
||||
# 1. 垃圾信息数量变化(近14天)
|
||||
blocked_trend_rows = (
|
||||
db.session.query(func.date(ContentPost.created_at), func.count(ContentPost.id))
|
||||
.filter(ContentPost.created_at >= week_ago, ContentPost.status == "blocked")
|
||||
.group_by(func.date(ContentPost.created_at))
|
||||
.all()
|
||||
)
|
||||
published_trend_rows = (
|
||||
db.session.query(func.date(ContentPost.created_at), func.count(ContentPost.id))
|
||||
.filter(ContentPost.created_at >= week_ago, ContentPost.status == "published")
|
||||
.group_by(func.date(ContentPost.created_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
blocked_map = {_day_key(day): int(count or 0) for day, count in blocked_trend_rows}
|
||||
published_map = {_day_key(day): int(count or 0) for day, count in published_trend_rows}
|
||||
|
||||
spam_trend = []
|
||||
today = now.date()
|
||||
for offset in range(13, -1, -1):
|
||||
day = today - timedelta(days=offset)
|
||||
key = day.isoformat()
|
||||
spam_trend.append({
|
||||
"date": key,
|
||||
"label": day.strftime("%m-%d"),
|
||||
"blocked": blocked_map.get(key, 0),
|
||||
"published": published_map.get(key, 0),
|
||||
"total": blocked_map.get(key, 0) + published_map.get(key, 0)
|
||||
})
|
||||
|
||||
# 2. 高频风险词排名(近14天)
|
||||
blocked_logs = (
|
||||
ContentPost.query.filter(ContentPost.created_at >= week_ago, ContentPost.status == "blocked")
|
||||
.order_by(ContentPost.id.desc())
|
||||
.limit(1000)
|
||||
.all()
|
||||
)
|
||||
token_counter = Counter()
|
||||
for row in blocked_logs:
|
||||
token_counter.update(_tokenize(row.text))
|
||||
top_keywords = [{"token": token, "count": count} for token, count in token_counter.most_common(20)]
|
||||
|
||||
# 3. 误判率趋势(近14天,基于人工复核)
|
||||
review_trend_rows = (
|
||||
db.session.query(func.date(ContentPost.manual_review_at), func.count(ContentPost.id))
|
||||
.filter(ContentPost.manual_review_at >= week_ago, ContentPost.manual_review_status != "none")
|
||||
.group_by(func.date(ContentPost.manual_review_at))
|
||||
.all()
|
||||
)
|
||||
approved_trend_rows = (
|
||||
db.session.query(func.date(ContentPost.manual_review_at), func.count(ContentPost.id))
|
||||
.filter(ContentPost.manual_review_at >= week_ago, ContentPost.manual_review_status == "approved_ham")
|
||||
.group_by(func.date(ContentPost.manual_review_at))
|
||||
.all()
|
||||
)
|
||||
|
||||
review_map = {_day_key(day): int(count or 0) for day, count in review_trend_rows}
|
||||
approved_map = {_day_key(day): int(count or 0) for day, count in approved_trend_rows}
|
||||
|
||||
misjudge_trend = []
|
||||
for offset in range(13, -1, -1):
|
||||
day = today - timedelta(days=offset)
|
||||
key = day.isoformat()
|
||||
reviewed = review_map.get(key, 0)
|
||||
approved = approved_map.get(key, 0)
|
||||
misjudge_rate = round(approved / reviewed, 4) if reviewed > 0 else 0
|
||||
misjudge_trend.append({
|
||||
"date": key,
|
||||
"label": day.strftime("%m-%d"),
|
||||
"reviewed": reviewed,
|
||||
"approved": approved,
|
||||
"misjudge_rate": misjudge_rate,
|
||||
"misjudge_rate_text": f"{misjudge_rate * 100:.1f}%"
|
||||
})
|
||||
|
||||
# 4. 汇总统计
|
||||
total_blocked_14d = sum(blocked_map.values())
|
||||
total_published_14d = sum(published_map.values())
|
||||
total_reviews_14d = sum(review_map.values())
|
||||
total_approved_14d = sum(approved_map.values())
|
||||
avg_misjudge_rate = round(total_approved_14d / total_reviews_14d, 4) if total_reviews_14d > 0 else 0
|
||||
|
||||
return ok({
|
||||
"report_date": now.isoformat(),
|
||||
"period": "近14天",
|
||||
"spam_trend": spam_trend,
|
||||
"top_keywords": top_keywords,
|
||||
"misjudge_trend": misjudge_trend,
|
||||
"summary": {
|
||||
"total_blocked": total_blocked_14d,
|
||||
"total_published": total_published_14d,
|
||||
"total_posts": total_blocked_14d + total_published_14d,
|
||||
"blocked_ratio": round(total_blocked_14d / (total_blocked_14d + total_published_14d), 4) if (total_blocked_14d + total_published_14d) > 0 else 0,
|
||||
"total_reviews": total_reviews_14d,
|
||||
"total_approved": total_approved_14d,
|
||||
"avg_misjudge_rate": avg_misjudge_rate,
|
||||
"avg_misjudge_rate_text": f"{avg_misjudge_rate * 100:.1f}%"
|
||||
}
|
||||
})
|
||||
|
||||
|
||||
@admin_bp.get("/detection/threshold")
|
||||
@admin_required
|
||||
def get_threshold():
|
||||
|
||||
Reference in New Issue
Block a user