diff --git a/yunke-server/app/api/routers/analytics.py b/yunke-server/app/api/routers/analytics.py index 6e919b4..4f46f3f 100644 --- a/yunke-server/app/api/routers/analytics.py +++ b/yunke-server/app/api/routers/analytics.py @@ -5,7 +5,7 @@ from typing import Annotated, Any, Literal import sqlalchemy as sa from fastapi import APIRouter, Depends, HTTPException, Query -from pydantic import BaseModel +from pydantic import BaseModel, Field from sqlalchemy.ext.asyncio import AsyncSession from app.analytics import health as health_svc @@ -150,20 +150,24 @@ async def delete_word(session: SessionDep, word_id: int) -> dict: class DisposeIn(BaseModel): - ids: list[int] + # 巨 IN 列表会撞 MySQL max_allowed_packet 裸 500 → pydantic 层拦成干净 422 + ids: list[int] = Field(max_length=1000) disposition: str +DispositionParam = Literal["pending", "confirmed", "false_positive", "ignored"] + + @router.get("/sensitive/hits") async def list_hits( session: SessionDep, - disposition: str | None = None, + disposition: DispositionParam | 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 供词详情下钻。""" + 处置完即出队列);word_id 供词详情下钻。disposition 拼错 422 不静默空列表。""" return await hits_svc.list_hits( session, disposition=disposition, word_id=word_id, page=page, page_size=page_size, diff --git a/yunke-server/tests/test_sensitive.py b/yunke-server/tests/test_sensitive.py index 86c0d21..72f1117 100644 --- a/yunke-server/tests/test_sensitive.py +++ b/yunke-server/tests/test_sensitive.py @@ -201,6 +201,14 @@ async def test_hits_list_default_filter_and_dispose(make_client): session.add(SensitiveHit(word_id=w.id, msg_id="m2", channel="wechat", ts=1756900001000, snippet="…私单…", disposition="ignored")) + # 排序/word_id 过滤双断言用:同 disposition 两行、不同词、ts 拉开 + w2 = await words_svc.create_word(session, word="飞单") + session.add(SensitiveHit(word_id=w.id, msg_id="m3", channel="wechat", + ts=1756900005000, snippet="…", + disposition="false_positive")) + session.add(SensitiveHit(word_id=w2.id, msg_id="m4", channel="wechat", + ts=1756900002000, snippet="…", + disposition="false_positive")) await session.commit() client = await make_client(seed) @@ -217,3 +225,16 @@ async def test_hits_list_default_filter_and_dispose(make_client): r4 = await client.get("/api/analytics/sensitive/hits", params={"disposition": "confirmed"}) assert r4.json()["total"] == 1 + # ts 降序:m3(ts 更大)在前;m4 属他词被 word_id 过滤掉 + r5 = await client.get("/api/analytics/sensitive/hits", + params={"disposition": "false_positive"}) + fp = r5.json() + assert fp["total"] == 2 + assert [h["msg_id"] for h in fp["items"]] == ["m3", "m4"] + r6 = await client.get("/api/analytics/sensitive/hits", + params={"disposition": "false_positive", + "word_id": fp["items"][0]["word_id"]}) + assert r6.json()["total"] == 1 + r7 = await client.get("/api/analytics/sensitive/hits", + params={"disposition": "bogus"}) + assert r7.status_code == 422 # 拼错值响亮 422,不静默空列表