feat:新增接口
This commit is contained in:
+102
-26
@@ -1,31 +1,53 @@
|
||||
"""赎回服务:校验持仓 → 减仓 + 余额入账(单事务原子)。"""
|
||||
"""赎回服务:校验持仓 → 建单(待确认) → 风控检测 → 落账/挂起(单事务原子)。"""
|
||||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
from decimal import ROUND_HALF_UP, Decimal
|
||||
|
||||
from sqlalchemy.ext.asyncio import AsyncSession
|
||||
|
||||
from model.fin_risk_alert import FinRiskAlert
|
||||
from model.fin_transaction import FinTransaction
|
||||
from model.sys_user import SysUser
|
||||
from model.trade_order import TradeOrder
|
||||
from repositories.fin_account import FinAccountRepo
|
||||
from repositories.fin_holdings import FinHoldingsRepo
|
||||
from repositories.fin_product import FinProductRepo
|
||||
from repositories.trade_order import TradeOrderRepo
|
||||
from schemas.holdings import HoldingResp
|
||||
from schemas.redeem import RedeemResp
|
||||
from service.risk.engine import RiskEngine, summarize
|
||||
from service.risk.settle import settle
|
||||
from utils.exceptions import ForbiddenError, NotFoundError, ParamError
|
||||
from utils.order_no import gen_order_no
|
||||
|
||||
_MONEY = Decimal("0.01")
|
||||
_SHARES = Decimal("0.0001")
|
||||
|
||||
|
||||
def _holding_resp(holding) -> HoldingResp:
|
||||
return HoldingResp(
|
||||
id=holding.id,
|
||||
customer_id=holding.customer_id,
|
||||
product_id=holding.product_id,
|
||||
shares=holding.shares,
|
||||
cost_amount=holding.cost_amount,
|
||||
current_value=holding.current_value,
|
||||
profit_loss=holding.profit_loss,
|
||||
profit_ratio=holding.profit_ratio,
|
||||
status=holding.status,
|
||||
)
|
||||
|
||||
|
||||
async def redeem(
|
||||
db: AsyncSession, user: SysUser, product_id: int, shares: Decimal
|
||||
) -> RedeemResp:
|
||||
"""赎回基金:校验通过后减仓并按净值入账,全程单事务。
|
||||
"""赎回基金:校验持仓 → 建单 → 风控检测 → 落账/挂起,全程单事务。
|
||||
|
||||
- 仅客户可赎回;
|
||||
- 产品须存在且净值非空;
|
||||
- 持仓须存在且份额 > 0,赎回份额不得超过持仓份额;
|
||||
- 减仓(归 0 时置'已清仓')+ 入账要么都成、要么都回滚。
|
||||
- 产品须存在且净值非空,持仓须足额;
|
||||
- 未命中规则:减仓 + 入账 + 写流水,订单「已确认」;
|
||||
- 命中规则:生成预警,订单「风控挂起」,账户与持仓不变。
|
||||
"""
|
||||
if user.user_type != "CUSTOMER":
|
||||
raise ForbiddenError("仅客户账号可赎回")
|
||||
@@ -40,8 +62,8 @@ async def redeem(
|
||||
if product.nav is None:
|
||||
raise ParamError("产品暂无净值,无法赎回")
|
||||
|
||||
account_repo = FinAccountRepo(db)
|
||||
holdings_repo = FinHoldingsRepo(db)
|
||||
account_repo = FinAccountRepo(db)
|
||||
|
||||
holding = await holdings_repo.get_by_customer_product(user.id, product_id)
|
||||
if holding is None or holding.shares <= 0:
|
||||
@@ -53,28 +75,82 @@ async def redeem(
|
||||
if credited <= 0:
|
||||
raise ParamError("赎回金额过低")
|
||||
|
||||
now = datetime.now()
|
||||
order = TradeOrder(
|
||||
order_no=gen_order_no("PO"),
|
||||
customer_id=user.id,
|
||||
product_id=product_id,
|
||||
advisor_id=None,
|
||||
order_type="赎回",
|
||||
amount=credited,
|
||||
shares=shares,
|
||||
nav=product.nav,
|
||||
fee=Decimal("0"),
|
||||
status="待确认",
|
||||
create_time=now,
|
||||
)
|
||||
db.add(order)
|
||||
await db.flush() # 生成 order.id,供流水/预警关联
|
||||
|
||||
hits = await RiskEngine(db).detect(order, user)
|
||||
|
||||
try:
|
||||
if not await holdings_repo.redeem(user.id, product_id, shares):
|
||||
raise ParamError("可赎回份额不足")
|
||||
await account_repo.credit_balance(user.id, credited)
|
||||
if not hits:
|
||||
# 未命中:减仓 + 入账 + 写流水 + 订单确认
|
||||
await settle(db, order)
|
||||
db.add(
|
||||
FinTransaction(
|
||||
transaction_no=gen_order_no("TR"),
|
||||
order_id=order.id,
|
||||
customer_id=user.id,
|
||||
product_id=product_id,
|
||||
operator_id=None,
|
||||
transaction_type="赎回",
|
||||
amount=credited,
|
||||
shares=shares,
|
||||
nav=product.nav,
|
||||
fee=Decimal("0"),
|
||||
status="已确认",
|
||||
create_time=now,
|
||||
)
|
||||
)
|
||||
await TradeOrderRepo(db).update_status(
|
||||
order.id, status="已确认", confirm_time=now
|
||||
)
|
||||
await db.commit()
|
||||
account = await account_repo.get_by_customer_id(user.id)
|
||||
current_holding = await holdings_repo.get_by_customer_product(
|
||||
user.id, product_id
|
||||
)
|
||||
return RedeemResp(
|
||||
order_no=order.order_no,
|
||||
status="已确认",
|
||||
balance=account.balance,
|
||||
holding=_holding_resp(current_holding),
|
||||
)
|
||||
|
||||
# 命中:生成预警 + 订单挂起
|
||||
summary = summarize(hits)
|
||||
alert = FinRiskAlert(
|
||||
customer_id=user.id,
|
||||
order_id=order.id,
|
||||
alert_type=summary.alert_type,
|
||||
alert_level=summary.alert_level,
|
||||
trigger_detail=summary.trigger_detail,
|
||||
transaction_ids=summary.transaction_ids or None,
|
||||
confidence=summary.confidence,
|
||||
status="未处理",
|
||||
create_time=now,
|
||||
)
|
||||
db.add(alert)
|
||||
await db.flush() # 生成 alert.id,供订单回填 risk_alert_id
|
||||
await TradeOrderRepo(db).update_status(
|
||||
order.id, status="风控挂起", risk_alert_id=alert.id
|
||||
)
|
||||
await db.commit()
|
||||
return RedeemResp(
|
||||
order_no=order.order_no, status="风控挂起", alert_id=alert.id
|
||||
)
|
||||
except Exception:
|
||||
await db.rollback()
|
||||
raise
|
||||
|
||||
account = await account_repo.get_by_customer_id(user.id)
|
||||
holding = await holdings_repo.get_by_customer_product(user.id, product_id)
|
||||
return RedeemResp(
|
||||
balance=account.balance,
|
||||
holding=HoldingResp(
|
||||
id=holding.id,
|
||||
customer_id=holding.customer_id,
|
||||
product_id=holding.product_id,
|
||||
shares=holding.shares,
|
||||
cost_amount=holding.cost_amount,
|
||||
current_value=holding.current_value,
|
||||
profit_loss=holding.profit_loss,
|
||||
profit_ratio=holding.profit_ratio,
|
||||
status=holding.status,
|
||||
),
|
||||
)
|
||||
|
||||
Reference in New Issue
Block a user