"""Recommendation generation and reviewed publication endpoints.""" from typing import Any from fastapi import APIRouter, Depends, Header, Path from app.api.dependencies.auth import build_request_context from app.api.dependencies.rate_limit import enforce_rate_limit from app.core.contracts import RequestContext from app.core.product_recommendation_contracts import ProductRecommendationQuery from app.service.advisor_rollout_service import enforce_advisor_rollout from app.service.product_recommendation_service import ProductRecommendationService advisor_router = APIRouter( prefix="/api/v1/advisor", tags=["advisor-recommendations"], dependencies=[Depends(enforce_rate_limit), Depends(enforce_advisor_rollout)], ) admin_router = APIRouter( prefix="/api/v1/admin", tags=["platform-admin"], dependencies=[Depends(enforce_rate_limit)], ) @advisor_router.post("/recommendations") async def generate_recommendation( payload: ProductRecommendationQuery, context: RequestContext = Depends(build_request_context), # noqa: B008 key: str | None = Header(default=None, alias="Idempotency-Key"), ) -> dict[str, object]: return await ProductRecommendationService(enforce_profile_governance=True).generate( payload, context, key ) @advisor_router.get("/recommendations/published") async def published_recommendations( context: RequestContext = Depends(build_request_context), # noqa: B008 ) -> dict[str, object]: return await ProductRecommendationService().published(context) # ---- 投顾自助审核/发布(2026-09-14 新增)------------------------------------ # # 为什么要有这两个**投顾侧**路由:审核/发布原先只在 `/api/v1/admin/advisor/...` # 下、且服务层还有 `admin=True` 角色闸门 —— 于是投顾生成完草案后**无法自行推进**, # 草案永远停在 `pending_review`,必须切到管理员账号才能审。业务要求投顾能审自己的方案。 # # 与 admin 路由的关系:两者调用**同一个服务方法**,管理面复核队列 # (`GET /api/v1/admin/advisor/pending-contents`)仍保持 admin 专属、未放宽。 @advisor_router.post("/recommendations/{content_id}/reviews") async def advisor_review_recommendation( payload: dict[str, Any], content_id: int = Path(gt=0), context: RequestContext = Depends(build_request_context), # noqa: B008 key: str | None = Header(default=None, alias="Idempotency-Key"), ) -> dict[str, object]: decision = payload.get("decision") if decision not in {"approved", "rejected"}: from app.core.errors import ValidationAgentError raise ValidationAgentError("decision 必须为 approved 或 rejected") comment = payload.get("comment", "") if not isinstance(comment, str): raise ValueError("comment must be a string") return await ProductRecommendationService().review(content_id, decision, comment, context, key) @advisor_router.post("/recommendations/{content_id}/publications") async def advisor_publish_recommendation( content_id: int = Path(gt=0), context: RequestContext = Depends(build_request_context), # noqa: B008 key: str | None = Header(default=None, alias="Idempotency-Key"), ) -> dict[str, object]: return await ProductRecommendationService().publish(content_id, context, key) @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)], ) async def review_recommendation( payload: dict[str, Any], content_id: int = Path(gt=0), context: RequestContext = Depends(build_request_context), # noqa: B008 key: str | None = Header(default=None, alias="Idempotency-Key"), ) -> dict[str, object]: decision = payload.get("decision") if decision not in {"approved", "rejected"}: from app.core.errors import ValidationAgentError raise ValidationAgentError("decision 必须为 approved 或 rejected") comment = payload.get("comment", "") if not isinstance(comment, str): raise ValueError("comment must be a string") return await ProductRecommendationService().review(content_id, decision, comment, context, key) @admin_router.post( "/advisor/recommendations/{content_id}/publications", dependencies=[Depends(enforce_advisor_rollout)], ) async def publish_recommendation( content_id: int = Path(gt=0), context: RequestContext = Depends(build_request_context), # noqa: B008 key: str | None = Header(default=None, alias="Idempotency-Key"), ) -> dict[str, object]: return await ProductRecommendationService().publish(content_id, context, key)