"""§T 用户自助场内基金模拟交易 controller(`docs/05` §19 T 段)。 端点与权限码(9 个端点 / 5 个权限码): | § | 端点 | 权限码 | 摘要 | |---|---|---|---| | T001 | `GET /api/v1/users/me/account/dashboard` | `account:read:self` | 我的账户看板 | | T002 | `POST /api/v1/users/me/orders` | `trade:order:create` | 提交委托(首版市价立即成交) | | T003 | `GET /api/v1/users/me/orders` | `trade:order:read` | 委托列表 | | T004 | `GET /api/v1/users/me/orders/{order_no}` | `trade:order:read` | 委托详情 | | T005 | `POST /api/v1/users/me/orders/{order_no}/cancellations` | `trade:order:cancel` | 撤单 | | T006 | `GET /api/v1/users/me/holdings` | `holding:read:self` | 持仓列表 | | T007 | `GET /api/v1/users/me/transactions` | `trade:txn:read` | 成交记录列表 | | T008 | `GET /api/v1/users/me/transactions/{txn_no}` | `trade:txn:read` | 成交详情 | | T009 | `GET /api/v1/users/me/cash-ledger` | `account:read:self` | 资金明细 | 设计要点: - 全部走 `build_request_context`(与 memory / portfolio 一致),数据范围 `self`。 - 不走限流依赖(`enforce_rate_limit`)——场内交易为低频,由底座网关层限流。 - 信封用 `envelope` / `list_envelope`,与 §3.3 一致。 """ from __future__ import annotations from fastapi import APIRouter, Depends, Query, status from sqlalchemy.ext.asyncio import AsyncSession from app.api.dependencies.auth import build_request_context from app.api.dependencies.database import get_session from app.api.schemas.trading import OrderCreateRequest from app.api.views.envelope import envelope, list_envelope from app.core.contracts import RequestContext from app.service.trade_service import TradeService router = APIRouter(prefix="/api/v1/users/me", tags=["trading"]) def _service(session: AsyncSession, context: RequestContext) -> TradeService: return TradeService(session) # T001 账户看板 @router.get("/account/dashboard") async def get_account_dashboard( context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: data = await _service(session, context).get_account_dashboard(context) return envelope(data, context) # T002 提交委托(市价立即成交) @router.post("/orders", status_code=status.HTTP_201_CREATED) async def submit_order( payload: OrderCreateRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: data = await _service(session, context).submit_order(payload, context) return envelope(data, context) # T003 委托列表 @router.get("/orders") async def list_orders( limit: int = Query(default=20, ge=1, le=100), cursor: str | None = Query(default=None), context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: cursor_id = int(cursor) if cursor else None items, next_cursor = await _service(session, context).list_orders( context, limit=limit, cursor=cursor_id ) return list_envelope( {"items": items, "next_cursor": next_cursor, "has_more": next_cursor is not None}, context, ) # T004 委托详情 @router.get("/orders/{order_no}") async def get_order( order_no: str, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: data = await _service(session, context).get_order(order_no, context) return envelope(data, context) # T005 撤单 @router.post("/orders/{order_no}/cancellations", status_code=status.HTTP_200_OK) async def cancel_order( order_no: str, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: order = await _service(session, context).cancel_order(order_no, context) return envelope(order, context) # T006 持仓列表 @router.get("/holdings") async def list_holdings( context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: data = await _service(session, context).list_holdings(context) return envelope(data, context) # T007 成交记录列表 @router.get("/transactions") async def list_transactions( limit: int = Query(default=20, ge=1, le=100), cursor: str | None = Query(default=None), context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: cursor_id = int(cursor) if cursor else None data = await _service(session, context).list_transactions( context, limit=limit, cursor=cursor_id ) return envelope(data, context) # T008 成交详情 @router.get("/transactions/{txn_no}") async def get_transaction( txn_no: str, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: item = await _service(session, context).get_transaction(txn_no, context) return envelope(item, context) # T009 资金明细 @router.get("/cash-ledger") async def list_cash_ledger( limit: int = Query(default=20, ge=1, le=100), cursor: str | None = Query(default=None), context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, object]: cursor_id = int(cursor) if cursor else None data = await _service(session, context).list_cash_ledger( context, limit=limit, cursor=cursor_id ) return envelope(data, context)