合并yy并同步远程qyqy_develop

This commit is contained in:
2026-09-14 01:11:07 +08:00
102 changed files with 7281 additions and 503 deletions
+33 -1
View File
@@ -1,6 +1,6 @@
from typing import Any, Literal
from fastapi import APIRouter, Depends, Header
from fastapi import APIRouter, Depends, Header, Query
from pydantic import Field
from app.api.dependencies.auth import build_request_context
@@ -9,6 +9,7 @@ from app.api.schemas.admin import StrictPayload
from app.api.schemas.conversations import HandoverRequest
from app.core.contracts import RequestContext
from app.service.public_platform_service import PublicPlatformService
from app.service.public_product_service import PublicProductService
router = APIRouter(prefix="/api/v1", tags=["public-platform"],
dependencies=[Depends(enforce_rate_limit)])
@@ -118,3 +119,34 @@ async def decide_memory_candidate(
return await CustomerProfileCandidateService().decide_by_customer(
candidate_id, payload.decision, context
)
@router.get("/products")
async def list_products(
context: RequestContext = Depends(build_request_context), # noqa: B008
) -> dict[str, Any]:
"""公开产品列表:在售场内基金 + 各自最新行情(访客令牌即可访问)。
这是访客三个页面(首页推荐 / 产品列表 / 产品详情)的数据源,
替代原先前端手写的 `common/mock-data.js`。
只要求**有效令牌**、不检查权限码:访客令牌的上下文只有 `roles=("visitor",)`
且不带权限,与 `/api/v1/conversations`、`/api/v1/agent-runs` 的访客口径一致。
"""
return await PublicProductService().list_products(context)
@router.get("/products/{product_code}/nav-history")
async def product_nav_history(
product_code: str,
days: int = Query(default=90, ge=1, le=365),
context: RequestContext = Depends(build_request_context), # noqa: B008
) -> dict[str, Any]:
"""产品历史净值序列(编号 `P002`):产品详情页净值走势图的数据源。
数据来自 `fin_nav_history`,由 `tools/sync_nav_history.py` 从东财净值接口同步。
鉴权口径与 P001 相同:**要求有效令牌但不校验权限码**(访客令牌可用)。
表为空时返回 `count=0` 与空数组,**不是错误** —— 前端据此显示"尚未接入"。
"""
return await PublicProductService().nav_history(product_code, context, days=days)
+16
View File
@@ -41,6 +41,22 @@ async def published_recommendations(
return await ProductRecommendationService().published(context)
@admin_router.get(
"/advisor/pending-contents",
dependencies=[Depends(enforce_advisor_rollout)],
)
async def pending_advisor_contents(
context: RequestContext = Depends(build_request_context), # noqa: B008
) -> dict[str, object]:
"""待审核的投顾内容(推荐方案 + 投资方案书)。编号 `A047`。
补这个入口的原因:审核/发布端点都要求先拿到 `content_id`,而此前**没有**任何
端点能列出待审内容,管理员拿不到 id ⇒ 审核链路不可达。返回体里的
`content_type` 用于前端区分两类内容。
"""
return await ProductRecommendationService().pending_reviews(context)
@admin_router.post(
"/advisor/recommendations/{content_id}/reviews",
dependencies=[Depends(enforce_advisor_rollout)],
+13 -3
View File
@@ -65,6 +65,16 @@ async def _idempotent_write(
return await ApiTransactionService().execute_in(session, context, scope, key, body, action)
def _risk_list_envelope(page: dict[str, Any], context: RequestContext) -> dict[str, object]:
"""在统一列表信封上补充风控列表总数和页大小。"""
response = _list_envelope(page, context)
meta = response["meta"]
if isinstance(meta, dict):
meta["total"] = int(page.get("total") or 0)
meta["page_size"] = int(page.get("page_size") or 0)
return response
@router.get("/overview")
async def risk_overview(
context: RequestContext = Depends(build_request_context), # noqa: B008
@@ -81,7 +91,7 @@ async def list_risk_alerts(
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
data = await RiskQueryService(session).list_alerts(context, query)
return _list_envelope(data, context)
return _risk_list_envelope(data, context)
@router.post("/alerts/scan")
@@ -231,7 +241,7 @@ async def list_risk_evidence(
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
data = await RiskQueryService(session).list_evidence(context, source, query)
return _list_envelope(data, context)
return _risk_list_envelope(data, context)
@router.get("/notifications")
@@ -241,7 +251,7 @@ async def list_risk_notifications(
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
data = await RiskNotificationService(session).list_notifications(context, query)
return _list_envelope(data, context)
return _risk_list_envelope(data, context)
@router.post("/daily-report")
+18 -2
View File
@@ -30,13 +30,20 @@ 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.authorization_service import AuthorizationService
from app.service.suitability_service import SuitabilityService
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)
return TradeService(session, suitability_evaluator=SuitabilityService())
async def _authorize(context: RequestContext, permission: str) -> None:
"""Enforce the endpoint permission declared in docs/05 before DB work."""
await AuthorizationService.require(context, permission)
# T001 账户看板
@@ -45,6 +52,7 @@ async def get_account_dashboard(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "account:read:self")
data = await _service(session, context).get_account_dashboard(context)
return envelope(data, context)
@@ -56,6 +64,7 @@ async def submit_order(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:create")
data = await _service(session, context).submit_order(payload, context)
return envelope(data, context)
@@ -68,6 +77,7 @@ async def list_orders(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:read")
cursor_id = int(cursor) if cursor else None
items, next_cursor = await _service(session, context).list_orders(
context, limit=limit, cursor=cursor_id
@@ -85,6 +95,7 @@ async def get_order(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:read")
data = await _service(session, context).get_order(order_no, context)
return envelope(data, context)
@@ -96,6 +107,7 @@ async def cancel_order(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:order:cancel")
order = await _service(session, context).cancel_order(order_no, context)
return envelope(order, context)
@@ -106,6 +118,7 @@ async def list_holdings(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "holding:read:self")
data = await _service(session, context).list_holdings(context)
return envelope(data, context)
@@ -118,6 +131,7 @@ async def list_transactions(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:txn:read")
cursor_id = int(cursor) if cursor else None
data = await _service(session, context).list_transactions(
context, limit=limit, cursor=cursor_id
@@ -132,6 +146,7 @@ async def get_transaction(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "trade:txn:read")
item = await _service(session, context).get_transaction(txn_no, context)
return envelope(item, context)
@@ -144,8 +159,9 @@ async def list_cash_ledger(
context: RequestContext = Depends(build_request_context), # noqa: B008
session: AsyncSession = Depends(get_session), # noqa: B008
) -> dict[str, object]:
await _authorize(context, "account:read:self")
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)
return envelope(data, context)