"""Signed external assessments and report lookup; no browser JWT or conversation required.""" import asyncio import logging from datetime import datetime, timedelta, timezone from typing import Annotated from fastapi import APIRouter, Depends, Request from fastapi.exceptions import RequestValidationError from fastapi.responses import JSONResponse from fastapi.routing import APIRoute from sqlalchemy.exc import SQLAlchemyError from app.api.dependencies import DatabaseSession from app.core.config import get_settings from app.core.exceptions import AppException from app.models.entities import IntegrationAssessment, OperationAudit from app.repositories.integration_risk import IntegrationRiskRepository from app.schemas.integration_risk import IntegrationRiskRequest from app.services.external_risk_report import ExternalRiskReportService from app.services.integration_auth import authenticate_integration, authorize_operator from app.services.markdown_reports import render_markdown_report logger = logging.getLogger(__name__) class IntegrationRoute(APIRoute): def get_route_handler(self): handler = super().get_route_handler() async def wrapped(request: Request): try: return await handler(request) except AppException as exc: headers = {"Retry-After": "60"} if exc.status_code == 429 else {} return JSONResponse( status_code=exc.status_code, headers=headers, content={ "code": exc.code, "message": exc.message, "data": exc.details, }, ) except SQLAlchemyError: logger.error("External report database operation failed") return JSONResponse( status_code=503, content={ "code": 503, "message": "风险数据库暂不可用,请使用原请求编号重试", "data": None, }, ) except RequestValidationError as exc: invalid_json = any(error["type"] == "json_invalid" for error in exc.errors()) status = 400 if invalid_json else 422 locations = [".".join(map(str, error["loc"])) for error in exc.errors()] return JSONResponse( status_code=status, content={ "code": status, "message": "请求体必须是合法 JSON" if invalid_json else "请求参数不符合约束:" + "、".join(locations), "data": None, }, ) except Exception as exc: # noqa: BLE001 - external callers receive no internal traceback logger.error("External interface failed: %s", exc.__class__.__name__) return JSONResponse( status_code=500, content={ "code": 500, "message": "平台内部处理异常,请使用原请求编号重试", "data": None, }, ) return wrapped router = APIRouter(prefix="/integrations/risk-assessments", tags=["integrations"], route_class=IntegrationRoute) async def integration_client(request: Request, session: DatabaseSession) -> str: if ( request.method == "POST" and request.headers.get("Content-Type", "").split(";", 1)[0].strip().lower() != "application/json" ): raise AppException("Content-Type 必须为 application/json", code=400, status_code=400) if len(await request.body()) > 65536: raise AppException("请求体超过 64KB", code=422, status_code=422) return await authenticate_integration(request, session) ClientId = Annotated[str, Depends(integration_client)] def report_response(record: IntegrationAssessment) -> JSONResponse: if record.status == "processing": return JSONResponse( status_code=202, headers={"Retry-After": "5"}, content={ "code": 202, "message": "报告正在生成,请稍后使用原参数重试本接口", "data": {"requestId": record.request_id, "status": "processing"}, }, ) if record.status == "failed": raise AppException("报告生成失败,请使用原请求编号和内容重新提交", code=503, status_code=503) result = record.result_data return JSONResponse( content={ "code": 200, "message": "success", "data": { "requestId": record.request_id, "status": "completed", "assessmentId": record.id, "score": result["score"], "level": result["level"], "modelVersion": result["modelVersion"], "asOfDate": record.as_of_date.isoformat(), "reportMarkdown": record.report_markdown, }, }, headers={ "X-Request-Id": record.request_id, "X-Assessment-Id": str(record.id), "X-Model-Version": result["modelVersion"], "X-Risk-Score": f"{result['score']:.2f}", "X-Risk-Level": result["level"], "Cache-Control": "no-store", }, ) @router.post( "/report", response_class=JSONResponse, responses={ 200: {"description": "JSON 结果,reportMarkdown 字段包含完整报告正文"}, 202: {"description": "同一请求正在生成"}, }, ) async def create_external_report( request: Request, payload: IntegrationRiskRequest, session: DatabaseSession, client_id: ClientId, ) -> JSONResponse: if payload.source_system != client_id: raise AppException("sourceSystem 必须与 X-Client-Id 一致", code=403, status_code=403) if request.headers.get("Idempotency-Key") != payload.request_id: raise AppException("Idempotency-Key 必须与 requestId 一致", code=422, status_code=422) today = datetime.now(timezone(timedelta(hours=8))).date() if payload.assessment.as_of_date and payload.assessment.as_of_date > today: raise AppException("asOfDate 不允许使用未来日期", code=422, status_code=422) user = await authorize_operator(session, payload.operator, client_id) user_id, operator_name, emp_no = user.id, user.name, user.emp_no repository = IntegrationRiskRepository(session) record, owned = await repository.claim(payload, user_id, today) if not owned: return report_response(record) record_id, token, as_of = record.id, record.owner_token, record.as_of_date try: async with asyncio.timeout(get_settings().integration_report_timeout_seconds): result = await ExternalRiskReportService(session).assess(payload, as_of) markdown = render_markdown_report(payload, result, as_of, operator_name) session.add( OperationAudit( actor_user_id=user_id, actor_emp_no=emp_no, action="integration.risk_assess", resource_type="integration_assessment", resource_id=str(record_id), audit_metadata={ "clientId": client_id, "requestId": payload.request_id, "modelVersion": result.model_version, }, ) ) if not await repository.finish( record_id, token, result=result.model_dump(mode="json", by_alias=True), markdown=markdown ): raise AppException("评估处理权已变更,请查询原请求", code=503, status_code=503) except asyncio.CancelledError: await session.rollback() await repository.finish(record_id, token, error="请求处理被中断") raise except Exception as exc: # noqa: BLE001 - preserve retryable failure without exposing database/provider errors logger.warning("External assessment failed request=%s error=%s", payload.request_id, exc.__class__.__name__) await session.rollback() await repository.finish(record_id, token, error="报告生成失败") raise AppException("报告生成失败,请使用原请求编号和内容重试", code=503, status_code=503) from exc record = await repository.get(client_id, payload.request_id) return report_response(record)