278 lines
9.6 KiB
Python
278 lines
9.6 KiB
Python
"""风控人工处置:放行 / 拦截 / 冻结(挂起自动,处置人工)。
|
|
|
|
处置接口的 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.pagination import normalize_pagination, pagination_result
|
|
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,
|
|
*,
|
|
page: int = 1,
|
|
page_size: int = 10,
|
|
) -> dict:
|
|
"""分页查询风控预警,默认每页 10 条。"""
|
|
page, page_size, offset = normalize_pagination(page, page_size)
|
|
repo = FinRiskAlertRepo(db)
|
|
alerts = await repo.list_by_status(
|
|
status,
|
|
limit=page_size,
|
|
offset=offset,
|
|
)
|
|
return pagination_result(
|
|
[_alert_resp(a).model_dump(mode="json") for a in alerts],
|
|
await repo.count_by_status(status),
|
|
page=page,
|
|
page_size=page_size,
|
|
)
|