"""线索看板与处置 API(R7/R17/R18/R20)。 注意:不提供删除线索的端点(R19 线索不可删,独立性硬约束)。 """ from __future__ import annotations import uuid from fastapi import APIRouter, Depends, HTTPException, Query from sqlalchemy.orm import Session from app.api.schemas import ( AdjudicateRequest, AssignRequest, ClueOut, DashboardSummary, ) from app.clues import service as clue_svc from app.clues.models import Clue, ClueStatus, ConfidenceTier from app.db import get_session router = APIRouter(prefix="/clues", tags=["clues"]) @router.get("", response_model=list[ClueOut]) def list_clues( status: ClueStatus | None = Query(default=None), scenario_code: str | None = Query(default=None), confidence: ConfidenceTier | None = Query(default=None), session: Session = Depends(get_session), ) -> list[Clue]: return clue_svc.list_clues( session, status=status, scenario_code=scenario_code, confidence=confidence ) @router.get("/summary", response_model=DashboardSummary) def summary(session: Session = Depends(get_session)) -> DashboardSummary: """运营看板汇总(R18/R21 的基础指标)。""" clues = session.query(Clue).all() by_status: dict[str, int] = {} by_conf: dict[str, int] = {} by_scenario: dict[str, int] = {} total_amount = 0.0 for c in clues: by_status[c.status.value] = by_status.get(c.status.value, 0) + 1 by_conf[c.confidence.value] = by_conf.get(c.confidence.value, 0) + 1 by_scenario[c.scenario_code] = by_scenario.get(c.scenario_code, 0) + 1 total_amount += c.amount_involved or 0.0 return DashboardSummary( total=len(clues), by_status=by_status, by_confidence=by_conf, by_scenario=by_scenario, total_amount_involved=total_amount, ) @router.get("/{clue_id}", response_model=ClueOut) def get_clue(clue_id: uuid.UUID, session: Session = Depends(get_session)) -> Clue: clue = session.get(Clue, clue_id) if clue is None: raise HTTPException(status_code=404, detail="线索不存在") return clue @router.post("/{clue_id}/assign", response_model=ClueOut) def assign_clue( clue_id: uuid.UUID, req: AssignRequest, session: Session = Depends(get_session) ) -> Clue: clue = session.get(Clue, clue_id) if clue is None: raise HTTPException(status_code=404, detail="线索不存在") return clue_svc.assign(session, clue, assignee=req.assignee, actor=req.actor) @router.post("/{clue_id}/adjudicate", response_model=ClueOut) def adjudicate_clue( clue_id: uuid.UUID, req: AdjudicateRequest, session: Session = Depends(get_session) ) -> Clue: clue = session.get(Clue, clue_id) if clue is None: raise HTTPException(status_code=404, detail="线索不存在") clue_svc.adjudicate(session, clue, confirmed=req.confirmed, actor=req.actor, note=req.note) return clue