feat(analytics): C1 敏感词库 CRUD+加词预览——防一加就淹没

Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
浅念
2026-09-04 19:31:06 +08:00
parent 1e0db3913c
commit 0c14be0777
4 changed files with 263 additions and 0 deletions
+67
View File
@@ -4,10 +4,12 @@ from __future__ import annotations
from typing import Annotated, Any, Literal
from fastapi import APIRouter, Depends, HTTPException, Query
from pydantic import BaseModel
from sqlalchemy.ext.asyncio import AsyncSession
from app.analytics import health as health_svc
from app.analytics import metrics as metrics_svc
from app.analytics.sensitive import words as words_svc
from app.api.deps import get_session
router = APIRouter(prefix="/api/analytics", tags=["analytics"])
@@ -69,3 +71,68 @@ async def unreplied_list(
return await metrics_svc.unreplied(
session, channel=channel, hours=hours, page=page, page_size=page_size
)
class WordIn(BaseModel):
word: str
category: str | None = None
min_len: int = 2
exclude_words: list[str] | None = None
class WordPreviewIn(BaseModel):
word: str
exclude_words: list[str] | None = None
@router.get("/sensitive/words")
async def list_words(session: SessionDep) -> list[dict]:
"""C1 词库清单(enabled=false 的词 scanner 跳过)。"""
return [
{"id": w.id, "word": w.word, "category": w.category, "enabled": w.enabled,
"min_len": w.min_len, "exclude_words": w.exclude_words or []}
for w in await words_svc.list_words(session)
]
@router.post("/sensitive/words")
async def create_word(session: SessionDep, body: WordIn) -> dict:
"""加词(重复 409)。词内容是敏感数据,错误信息只回 id 不回词。"""
try:
w = await words_svc.create_word(
session, word=body.word, category=body.category,
min_len=body.min_len, exclude_words=body.exclude_words,
)
await session.commit()
return {"id": w.id, "word": w.word, "enabled": w.enabled}
except ValueError as exc:
raise HTTPException(409, str(exc)) from exc
@router.post("/sensitive/words/preview")
async def preview_word(session: SessionDep, body: WordPreviewIn) -> dict:
"""加词前命中量预览(count 为 MySQL 上限语义;warn=超阈值建议收窄)。"""
n = await words_svc.preview_count(session, body.word, body.exclude_words)
return {"count": n, "warn": n >= words_svc.PREVIEW_WARN_THRESHOLD}
@router.patch("/sensitive/words/{word_id}")
async def update_word(session: SessionDep, word_id: int, body: dict) -> dict:
"""改词(启停/分类/排除词);字段 None=不改。"""
try:
w = await words_svc.update_word(session, word_id, **body)
await session.commit()
return {"id": w.id, "enabled": w.enabled, "category": w.category}
except LookupError as exc:
raise HTTPException(404, str(exc)) from exc
@router.delete("/sensitive/words/{word_id}")
async def delete_word(session: SessionDep, word_id: int) -> dict:
"""删词=级联删命中(UI 二次确认)。"""
try:
await words_svc.delete_word(session, word_id)
await session.commit()
return {"ok": True}
except LookupError as exc:
raise HTTPException(404, str(exc)) from exc