2026-09-06 18:05:44 +08:00
|
|
|
|
"""模拟交易网关路由(PRD FR-1 · 薄路由,不含业务)。
|
|
|
|
|
|
|
2026-09-06 18:58:48 +08:00
|
|
|
|
鉴权:`Depends(get_auth_context)`(B6 回挂,评审 P2-2)——一期接受
|
|
|
|
|
|
risk_demo 演示账号或客户本人(auth.customer_id == 请求 customer_id,
|
|
|
|
|
|
PRD FR-1 §鉴权);越权经 deps.deny 审计后 403。T-01 后工厂内部换 JWT。
|
2026-09-06 20:48:50 +08:00
|
|
|
|
trace:main 中间件贯通(B7),响应头 X-Trace-Id 回写;service 层 ensure_trace
|
|
|
|
|
|
仍兜底脚本/测试直调场景。
|
|
|
|
|
|
挂载:main.py include(B7)。错误体统一 ApiError → 手册 §10 结构(挂账④),
|
|
|
|
|
|
convert 400 / 资源 404 不变 HTTP 语义。
|
2026-09-06 18:05:44 +08:00
|
|
|
|
"""
|
|
|
|
|
|
|
|
|
|
|
|
from __future__ import annotations
|
|
|
|
|
|
|
|
|
|
|
|
from decimal import Decimal
|
|
|
|
|
|
|
2026-09-06 20:48:50 +08:00
|
|
|
|
from fastapi import APIRouter, Depends
|
2026-09-06 18:05:44 +08:00
|
|
|
|
from pydantic import BaseModel, Field
|
|
|
|
|
|
|
2026-09-06 18:58:48 +08:00
|
|
|
|
from app.api.deps import AuthContext, deny, get_auth_context
|
2026-09-06 18:05:44 +08:00
|
|
|
|
from app.gateway.trade_gateway import UnsupportedTradeType, submit_trade
|
2026-09-06 18:58:48 +08:00
|
|
|
|
from app.repository.risk_repository import RiskRepository
|
2026-09-06 20:48:50 +08:00
|
|
|
|
from app.utils.exceptions import ApiError
|
2026-09-06 18:05:44 +08:00
|
|
|
|
|
|
|
|
|
|
router = APIRouter(prefix="/api/simulate", tags=["simulate"])
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-06 18:58:48 +08:00
|
|
|
|
def _repo() -> RiskRepository:
|
|
|
|
|
|
"""审计仓储(deny 留痕用;测试 monkeypatch 点)。"""
|
|
|
|
|
|
return RiskRepository()
|
|
|
|
|
|
|
|
|
|
|
|
|
2026-09-06 18:05:44 +08:00
|
|
|
|
class TradeRequest(BaseModel):
|
|
|
|
|
|
customer_id: str = Field(..., min_length=1)
|
|
|
|
|
|
product_id: str = Field(..., min_length=1)
|
2026-09-06 18:17:44 +08:00
|
|
|
|
trade_type: str = Field(..., max_length=16, description="subscribe | redeem;convert 显式拒绝")
|
2026-09-06 18:05:44 +08:00
|
|
|
|
amount: Decimal = Field(..., gt=0, description="交易金额(元),必须为正数")
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
|
@router.post("/trade")
|
2026-09-06 18:58:48 +08:00
|
|
|
|
def submit_trade_api(req: TradeRequest, auth: AuthContext = Depends(get_auth_context)) -> dict:
|
2026-09-06 18:05:44 +08:00
|
|
|
|
"""模拟交易(FR-1):适当性阻断或放行+引擎判定,返回 blocked + trade_id。"""
|
2026-09-06 18:58:48 +08:00
|
|
|
|
if not (auth.has_role("risk_demo") or (auth.is_customer() and auth.customer_id == req.customer_id)):
|
|
|
|
|
|
deny(
|
|
|
|
|
|
auth, "AUTH_403_ROLE", _repo(),
|
|
|
|
|
|
customer_id=req.customer_id, message="risk_demo or owner customer only",
|
2026-09-06 19:21:15 +08:00
|
|
|
|
agent_type="platform", # 网关越权与放行审计同口径(复审 P3)
|
2026-09-06 18:58:48 +08:00
|
|
|
|
)
|
2026-09-06 18:05:44 +08:00
|
|
|
|
try:
|
2026-09-07 19:53:39 +08:00
|
|
|
|
return submit_trade(req.model_dump(), actor_id=auth.actor_id)
|
2026-09-06 18:05:44 +08:00
|
|
|
|
except UnsupportedTradeType as exc:
|
2026-09-06 20:48:50 +08:00
|
|
|
|
raise ApiError(400, "BAD_REQUEST", str(exc)) from exc
|
2026-09-06 18:58:48 +08:00
|
|
|
|
except LookupError as exc: # NotFoundError 亦为其子类;已统一(B6 评审 P3-5)
|
2026-09-06 20:48:50 +08:00
|
|
|
|
raise ApiError(404, "NOT_FOUND", str(exc)) from exc
|