"""风控人工处置:放行 / 拦截 / 冻结(挂起自动,处置人工)。 处置接口的 handler_id 一律取自当前登录用户(后端注入),不信任前端; 用条件更新 WHERE status='未处理' 防并发重复处置; 处置与工单流转均写 audit_log 留痕。 """ from __future__ import annotations from datetime import datetime from decimal import Decimal from sqlalchemy.ext.asyncio import AsyncSession from model.audit_log import AuditLog from model.biz_work_order import BizWorkOrder from model.fin_risk_alert import FinRiskAlert from model.fin_transaction import FinTransaction from model.sys_message import SysMessage from model.sys_user import SysUser from model.trade_order import TradeOrder from repositories.fin_risk_alert import FinRiskAlertRepo from repositories.sys_user import SysUserRepo from repositories.trade_order import TradeOrderRepo from schemas.risk import RiskAlertResp from service.risk.settle import settle from utils.exceptions import NotFoundError, ParamError from utils.order_no import gen_order_no # 预警级别 → 工单优先级 _LEVEL_PRIORITY = {"低": "普通", "中": "紧急", "高": "特急"} def _audit(db: AsyncSession, user: SysUser, module: str, action: str, target, detail: str) -> None: db.add( AuditLog( user_id=user.id, username=user.real_name or user.username, module=module, action=action, target=str(target), detail=detail, status="成功", ) ) def _build_work_order( handler: SysUser, alert: FinRiskAlert, order: TradeOrder, order_type: str ) -> BizWorkOrder: """拦截/冻结时生成工单:预填类型、客户、提交人、优先级、业务内容。""" return BizWorkOrder( work_order_no=gen_order_no("WO"), order_type=order_type, sub_type=alert.alert_type, customer_id=order.customer_id, submitter_id=handler.id, handler_id=None, current_node="初审", priority=_LEVEL_PRIORITY.get(alert.alert_level, "普通"), status="待处理", biz_content={ "order_no": order.order_no, "alert_id": alert.id, "alert_type": alert.alert_type, "alert_level": alert.alert_level, "trigger_detail": alert.trigger_detail, "confidence": str(alert.confidence), }, ) def _alert_resp(alert: FinRiskAlert) -> RiskAlertResp: return RiskAlertResp( id=alert.id, customer_id=alert.customer_id, order_id=alert.order_id, alert_type=alert.alert_type, alert_level=alert.alert_level, trigger_detail=alert.trigger_detail, transaction_ids=alert.transaction_ids, confidence=alert.confidence, status=alert.status, handler_id=alert.handler_id, handle_result=alert.handle_result, handle_time=alert.handle_time, create_time=alert.create_time, ) async def _get_pending_alert(db: AsyncSession, alert_id: int) -> tuple[FinRiskAlert, TradeOrder]: alert = await FinRiskAlertRepo(db).get(alert_id) if alert is None: raise NotFoundError("预警不存在") order = await TradeOrderRepo(db).get(alert.order_id) if order is None: raise NotFoundError("关联订单不存在") if order.status != "风控挂起": raise ParamError("订单状态异常,无法处置") return alert, order async def release(db: AsyncSession, handler: SysUser, alert_id: int) -> dict: """放行:订单已确认 + 落账 + 写流水;预警已排除。放行失败(余额不足)订单转失败。""" alert_repo = FinRiskAlertRepo(db) order_repo = TradeOrderRepo(db) alert, order = await _get_pending_alert(db, alert_id) now = datetime.now() try: if not await alert_repo.conditional_handle( alert_id, handler_id=handler.id, new_status="已排除", handle_result="放行", handle_time=now, ): raise ParamError("该预警已被处理") try: await settle(db, order) except ParamError as e: # 放行失败:订单转失败,预警回写已确认 await order_repo.update_status( order.id, status="失败", cancel_reason=f"放行失败:{e.message}" ) await alert_repo.update_status( alert_id, status="已确认", handle_result=f"放行失败:{e.message}" ) _audit(db, handler, "risk", "release_failed", alert_id, f"放行失败:{e.message}") await db.commit() return { "alert_id": alert_id, "status": "已确认", "order_status": "失败", "message": "放行失败,订单已作废", } db.add( FinTransaction( transaction_no=gen_order_no("TR"), order_id=order.id, customer_id=order.customer_id, product_id=order.product_id, operator_id=handler.id, transaction_type=order.order_type, amount=order.amount, shares=order.shares, nav=order.nav, fee=order.fee or Decimal("0"), status="已确认", create_time=now, ) ) await order_repo.update_status(order.id, status="已确认", confirm_time=now) _audit(db, handler, "risk", "release", alert_id, f"放行订单 {order.order_no}") await db.commit() return { "alert_id": alert_id, "status": "已排除", "order_status": "已确认", "order_no": order.order_no, } except Exception: await db.rollback() raise async def block(db: AsyncSession, handler: SysUser, alert_id: int) -> dict: """拦截:订单失败 + 站内信通知;预警已确认 + 生成「可疑交易上报」工单。""" alert_repo = FinRiskAlertRepo(db) order_repo = TradeOrderRepo(db) alert, order = await _get_pending_alert(db, alert_id) now = datetime.now() try: if not await alert_repo.conditional_handle( alert_id, handler_id=handler.id, new_status="已确认", handle_result="拦截", handle_time=now, ): raise ParamError("该预警已被处理") await order_repo.update_status(order.id, status="失败", cancel_reason="风控拦截") db.add( SysMessage( user_id=order.customer_id, msg_type="风控", title="交易被拦截", content=f"您的{order.order_type}订单 {order.order_no} 因风险控制被拦截,如有疑问请联系客服。", biz_id=order.order_no, is_read=0, create_time=now, ) ) db.add(_build_work_order(handler, alert, order, "可疑交易上报")) _audit(db, handler, "risk", "block", alert_id, f"拦截订单 {order.order_no}") await db.commit() return { "alert_id": alert_id, "status": "已确认", "order_status": "失败", "order_no": order.order_no, } except Exception: await db.rollback() raise async def freeze(db: AsyncSession, handler: SysUser, alert_id: int) -> dict: """冻结:订单失败 + 客户冻结 + 站内信通知;预警已确认 + 生成「冻结复核」工单。""" alert_repo = FinRiskAlertRepo(db) order_repo = TradeOrderRepo(db) alert, order = await _get_pending_alert(db, alert_id) now = datetime.now() try: if not await alert_repo.conditional_handle( alert_id, handler_id=handler.id, new_status="已确认", handle_result="冻结", handle_time=now, ): raise ParamError("该预警已被处理") await order_repo.update_status(order.id, status="失败", cancel_reason="风控冻结") await SysUserRepo(db).update_status(order.customer_id, "冻结") db.add( SysMessage( user_id=order.customer_id, msg_type="风控", title="账户已被冻结", content="您的账户因风险控制已被冻结,请联系客服处理。", biz_id=order.order_no, is_read=0, create_time=now, ) ) db.add(_build_work_order(handler, alert, order, "冻结复核")) _audit(db, handler, "risk", "freeze", alert_id, f"冻结客户 {order.customer_id},订单 {order.order_no}") await db.commit() return { "alert_id": alert_id, "status": "已确认", "order_status": "失败", "customer_status": "冻结", "order_no": order.order_no, } except Exception: await db.rollback() raise async def list_alerts(db: AsyncSession, status: str | None = None) -> list[RiskAlertResp]: alerts = await FinRiskAlertRepo(db).list_by_status(status) return [_alert_resp(a) for a in alerts]