feat(analytics): C1 命中列表默认过滤+批量处置
Co-Authored-By: Claude Code <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,65 @@
|
|||||||
|
"""命中查询与处置——默认只显示 pending+confirmed(误报治理闭环)。"""
|
||||||
|
from __future__ import annotations
|
||||||
|
|
||||||
|
import sqlalchemy as sa
|
||||||
|
from sqlalchemy.ext.asyncio import AsyncSession
|
||||||
|
|
||||||
|
from app.db.models import SensitiveHit, SensitiveWord
|
||||||
|
|
||||||
|
DISPOSITIONS = ("pending", "confirmed", "false_positive", "ignored")
|
||||||
|
DEFAULT_SHOW = ("pending", "confirmed")
|
||||||
|
|
||||||
|
|
||||||
|
async def list_hits(
|
||||||
|
session: AsyncSession, *, disposition: str | None = None,
|
||||||
|
word_id: int | None = None, page: int, page_size: int,
|
||||||
|
) -> dict:
|
||||||
|
conds = []
|
||||||
|
if disposition:
|
||||||
|
conds.append(SensitiveHit.disposition == disposition)
|
||||||
|
else:
|
||||||
|
conds.append(SensitiveHit.disposition.in_(DEFAULT_SHOW))
|
||||||
|
if word_id:
|
||||||
|
conds.append(SensitiveHit.word_id == word_id)
|
||||||
|
|
||||||
|
total = (
|
||||||
|
await session.execute(
|
||||||
|
sa.select(sa.func.count()).select_from(SensitiveHit).where(*conds)
|
||||||
|
)
|
||||||
|
).scalar_one()
|
||||||
|
rows = (
|
||||||
|
await session.execute(
|
||||||
|
sa.select(SensitiveHit, SensitiveWord.word, SensitiveWord.category)
|
||||||
|
.join(SensitiveWord, SensitiveHit.word_id == SensitiveWord.id)
|
||||||
|
.where(*conds)
|
||||||
|
# ts 可 NULL/并列,id 决胜列保证翻页稳定
|
||||||
|
.order_by(SensitiveHit.ts.desc(), SensitiveHit.id.desc())
|
||||||
|
.offset((page - 1) * page_size)
|
||||||
|
.limit(page_size)
|
||||||
|
)
|
||||||
|
).all()
|
||||||
|
items = [
|
||||||
|
{
|
||||||
|
"id": h.id, "word_id": h.word_id, "word": word, "category": category,
|
||||||
|
"msg_id": h.msg_id, "channel": h.channel,
|
||||||
|
"owner_account_id": h.owner_account_id, "talker_id": h.talker_id,
|
||||||
|
"room_id": h.room_id, "ts": h.ts, "snippet": h.snippet,
|
||||||
|
"disposition": h.disposition,
|
||||||
|
}
|
||||||
|
for h, word, category in rows
|
||||||
|
]
|
||||||
|
return {"items": items, "total": int(total), "page": page, "page_size": page_size}
|
||||||
|
|
||||||
|
|
||||||
|
async def dispose(session: AsyncSession, ids: list[int], disposition: str) -> int:
|
||||||
|
"""批量改 disposition(pending 合法——用于「重开」误报);不写日志(词/snippet 红线)。"""
|
||||||
|
if disposition not in DISPOSITIONS:
|
||||||
|
raise ValueError(f"bad disposition: {disposition!r}")
|
||||||
|
if not ids:
|
||||||
|
return 0
|
||||||
|
res = await session.execute(
|
||||||
|
sa.update(SensitiveHit)
|
||||||
|
.where(SensitiveHit.id.in_(ids))
|
||||||
|
.values(disposition=disposition)
|
||||||
|
)
|
||||||
|
return int(res.rowcount or 0)
|
||||||
@@ -10,6 +10,7 @@ from sqlalchemy.ext.asyncio import AsyncSession
|
|||||||
|
|
||||||
from app.analytics import health as health_svc
|
from app.analytics import health as health_svc
|
||||||
from app.analytics import metrics as metrics_svc
|
from app.analytics import metrics as metrics_svc
|
||||||
|
from app.analytics.sensitive import hits as hits_svc
|
||||||
from app.analytics.sensitive import words as words_svc
|
from app.analytics.sensitive import words as words_svc
|
||||||
from app.api.deps import get_session
|
from app.api.deps import get_session
|
||||||
|
|
||||||
@@ -146,3 +147,35 @@ async def delete_word(session: SessionDep, word_id: int) -> dict:
|
|||||||
return {"ok": True}
|
return {"ok": True}
|
||||||
except LookupError as exc:
|
except LookupError as exc:
|
||||||
raise HTTPException(404, str(exc)) from exc
|
raise HTTPException(404, str(exc)) from exc
|
||||||
|
|
||||||
|
|
||||||
|
class DisposeIn(BaseModel):
|
||||||
|
ids: list[int]
|
||||||
|
disposition: str
|
||||||
|
|
||||||
|
|
||||||
|
@router.get("/sensitive/hits")
|
||||||
|
async def list_hits(
|
||||||
|
session: SessionDep,
|
||||||
|
disposition: str | None = None,
|
||||||
|
word_id: int | None = None,
|
||||||
|
page: int = Query(1, ge=1),
|
||||||
|
page_size: int = Query(20, ge=1, le=100),
|
||||||
|
) -> dict:
|
||||||
|
"""C1 命中列表——不传 disposition 默认只回 pending+confirmed(治理闭环:
|
||||||
|
处置完即出队列);word_id 供词详情下钻。"""
|
||||||
|
return await hits_svc.list_hits(
|
||||||
|
session, disposition=disposition, word_id=word_id,
|
||||||
|
page=page, page_size=page_size,
|
||||||
|
)
|
||||||
|
|
||||||
|
|
||||||
|
@router.patch("/sensitive/hits")
|
||||||
|
async def dispose_hits(session: SessionDep, body: DisposeIn) -> dict:
|
||||||
|
"""批量处置(pending 合法——重开误报);UPDATE 不写日志(词/snippet 红线)。"""
|
||||||
|
try:
|
||||||
|
n = await hits_svc.dispose(session, body.ids, body.disposition)
|
||||||
|
await session.commit()
|
||||||
|
return {"updated": n}
|
||||||
|
except ValueError as exc:
|
||||||
|
raise HTTPException(422, str(exc)) from exc
|
||||||
|
|||||||
@@ -188,3 +188,32 @@ async def test_disposition_preserved(make_client):
|
|||||||
assert hit2.disposition == "false_positive"
|
assert hit2.disposition == "false_positive"
|
||||||
|
|
||||||
await make_client(seed)
|
await make_client(seed)
|
||||||
|
|
||||||
|
|
||||||
|
# ------------------------------------------------------------- C1 命中列表/处置
|
||||||
|
|
||||||
|
async def test_hits_list_default_filter_and_dispose(make_client):
|
||||||
|
async def seed(session):
|
||||||
|
w = await words_svc.create_word(session, word="私单")
|
||||||
|
session.add(SensitiveHit(word_id=w.id, msg_id="m1", channel="wechat",
|
||||||
|
ts=1756900000000, snippet="…私单…",
|
||||||
|
disposition="pending"))
|
||||||
|
session.add(SensitiveHit(word_id=w.id, msg_id="m2", channel="wechat",
|
||||||
|
ts=1756900001000, snippet="…私单…",
|
||||||
|
disposition="ignored"))
|
||||||
|
await session.commit()
|
||||||
|
|
||||||
|
client = await make_client(seed)
|
||||||
|
r = await client.get("/api/analytics/sensitive/hits")
|
||||||
|
body = r.json()
|
||||||
|
assert body["total"] == 1 # 默认只 pending+confirmed
|
||||||
|
ids = [h["id"] for h in body["items"]]
|
||||||
|
r2 = await client.get("/api/analytics/sensitive/hits",
|
||||||
|
params={"disposition": "ignored"})
|
||||||
|
assert r2.json()["total"] == 1
|
||||||
|
r3 = await client.patch("/api/analytics/sensitive/hits",
|
||||||
|
json={"ids": ids, "disposition": "confirmed"})
|
||||||
|
assert r3.json()["updated"] == 1
|
||||||
|
r4 = await client.get("/api/analytics/sensitive/hits",
|
||||||
|
params={"disposition": "confirmed"})
|
||||||
|
assert r4.json()["total"] == 1
|
||||||
|
|||||||
Reference in New Issue
Block a user