"""命中查询与处置——默认只显示 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)