Files
Mutual_Fund/service/risk/handle.py
T

278 lines
9.6 KiB
Python
Raw Normal View History

"""风控人工处置:放行 / 拦截 / 冻结(挂起自动,处置人工)。
处置接口的 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
2026-09-13 22:21:49 +08:00
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
2026-09-13 22:21:49 +08:00
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,
)