feat: 新增风险图谱算法与系统仪表盘及操作反馈体系
后端新增风险图谱算法模块、风险观察与反馈服务、规则 DSL 校验器和可解释性引擎,完善系统仪表盘和财务仪表盘统计, 优化 agent 运行和编排执行链路,清理旧开发文档,前端新增 系统趋势、负载热力图等多种仪表盘图表组件,完善操作反馈 对话框和工作台日期选择器,优化报销创建和审批详情交互, 补充单元测试覆盖。
This commit is contained in:
146
server/src/app/api/v1/endpoints/risk_observations.py
Normal file
146
server/src/app/api/v1/endpoints/risk_observations.py
Normal file
@@ -0,0 +1,146 @@
|
||||
from __future__ import annotations
|
||||
|
||||
from typing import Annotated
|
||||
|
||||
from fastapi import APIRouter, Depends, HTTPException, Query, status
|
||||
from sqlalchemy.orm import Session
|
||||
|
||||
from app.api.deps import get_db
|
||||
from app.schemas.common import ErrorResponse
|
||||
from app.schemas.risk_observation import (
|
||||
RiskObservationDashboardRead,
|
||||
RiskObservationFeedbackCreate,
|
||||
RiskObservationFeedbackRead,
|
||||
RiskObservationListRead,
|
||||
RiskObservationRead,
|
||||
)
|
||||
from app.services.risk_observations import RiskObservationService
|
||||
|
||||
router = APIRouter(prefix="/risk-observations")
|
||||
DbSession = Annotated[Session, Depends(get_db)]
|
||||
|
||||
|
||||
@router.get(
|
||||
"",
|
||||
response_model=RiskObservationListRead,
|
||||
summary="查询风险观察列表",
|
||||
description="按单据、风险等级、风险信号、状态和来源筛选统一风险观察池。",
|
||||
)
|
||||
def list_risk_observations(
|
||||
db: DbSession,
|
||||
claim_id: Annotated[str | None, Query(max_length=80)] = None,
|
||||
run_id: Annotated[str | None, Query(max_length=80)] = None,
|
||||
execution_log_id: Annotated[str | None, Query(max_length=80)] = None,
|
||||
risk_level: Annotated[str | None, Query(max_length=20)] = None,
|
||||
risk_signal: Annotated[str | None, Query(max_length=100)] = None,
|
||||
status_value: Annotated[
|
||||
str | None,
|
||||
Query(alias="status", max_length=30),
|
||||
] = None,
|
||||
source: Annotated[str | None, Query(max_length=60)] = None,
|
||||
limit: Annotated[int, Query(ge=1, le=200)] = 50,
|
||||
offset: Annotated[int, Query(ge=0)] = 0,
|
||||
) -> RiskObservationListRead:
|
||||
items, total = RiskObservationService(db).list_observations(
|
||||
claim_id=claim_id,
|
||||
run_id=run_id,
|
||||
execution_log_id=execution_log_id,
|
||||
risk_level=risk_level,
|
||||
risk_signal=risk_signal,
|
||||
status=status_value,
|
||||
source=source,
|
||||
limit=limit,
|
||||
offset=offset,
|
||||
)
|
||||
return RiskObservationListRead(items=items, total=total, limit=limit, offset=offset)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/dashboard",
|
||||
response_model=RiskObservationDashboardRead,
|
||||
summary="查询风险看板聚合",
|
||||
description="返回风险观察池的总量、分布、算法效果和近期高风险记录。",
|
||||
)
|
||||
def summarize_risk_observations(
|
||||
db: DbSession,
|
||||
window_days: Annotated[int, Query(ge=1, le=365)] = 30,
|
||||
limit: Annotated[int, Query(ge=1, le=2000)] = 500,
|
||||
) -> RiskObservationDashboardRead:
|
||||
return RiskObservationService(db).summarize_dashboard(
|
||||
window_days=window_days,
|
||||
limit=limit,
|
||||
)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/claim/{claim_id}",
|
||||
response_model=list[RiskObservationRead],
|
||||
summary="查询单据风险观察",
|
||||
description="按报销单 ID 返回该单据关联的风险观察,供单据详情证据链使用。",
|
||||
)
|
||||
def list_claim_risk_observations(claim_id: str, db: DbSession) -> list[RiskObservationRead]:
|
||||
return RiskObservationService(db).list_claim_observations(claim_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/execution-log/{execution_log_id}",
|
||||
response_model=list[RiskObservationRead],
|
||||
summary="查询数字员工工作记录风险观察",
|
||||
description="按数字员工执行日志 ID 返回本次任务生成的风险观察。",
|
||||
)
|
||||
def list_execution_log_risk_observations(
|
||||
execution_log_id: str,
|
||||
db: DbSession,
|
||||
) -> list[RiskObservationRead]:
|
||||
return RiskObservationService(db).list_execution_log_observations(execution_log_id)
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{observation_key_or_id}",
|
||||
response_model=RiskObservationRead,
|
||||
summary="读取风险观察详情",
|
||||
description="按观察 key 或 ID 返回风险评分、证据链、图谱节点、制度引用和决策追踪。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "风险观察不存在。",
|
||||
}
|
||||
},
|
||||
)
|
||||
def get_risk_observation(
|
||||
observation_key_or_id: str,
|
||||
db: DbSession,
|
||||
) -> RiskObservationRead:
|
||||
observation = RiskObservationService(db).get_observation(observation_key_or_id)
|
||||
if observation is None:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Risk observation not found",
|
||||
)
|
||||
return observation
|
||||
|
||||
|
||||
@router.post(
|
||||
"/{observation_key_or_id}/feedback",
|
||||
response_model=RiskObservationFeedbackRead,
|
||||
summary="写入风险观察反馈",
|
||||
description="记录人工确认、误报、忽略、已处理或备注反馈,并同步更新观察状态。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "风险观察不存在。",
|
||||
}
|
||||
},
|
||||
)
|
||||
def create_risk_observation_feedback(
|
||||
observation_key_or_id: str,
|
||||
payload: RiskObservationFeedbackCreate,
|
||||
db: DbSession,
|
||||
) -> RiskObservationFeedbackRead:
|
||||
try:
|
||||
return RiskObservationService(db).create_feedback(observation_key_or_id, payload)
|
||||
except LookupError:
|
||||
raise HTTPException(
|
||||
status_code=status.HTTP_404_NOT_FOUND,
|
||||
detail="Risk observation not found",
|
||||
) from None
|
||||
Reference in New Issue
Block a user