"""场外基金运营接口。""" from typing import Any from fastapi import APIRouter, Depends, Query, Response from fastapi.responses import FileResponse 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.offsite_fund import ( OffsiteConfirmRequest, OffsiteMailboxRecoveryRequest, OffsiteMailDeletionRequest, OffsiteNl2SqlCorrectionRequest, OffsiteNotificationRequest, OffsiteNotificationSendRequest, OffsiteRecalculateRequest, OffsiteRecognitionCorrectionRequest, OffsiteRecognitionRetryRequest, OffsiteRuleRecalculationRequest, OffsiteTriggerNl2SqlRequest, ) from app.core.contracts import RequestContext from app.service.offsite_fund_service import OffsiteFundService router = APIRouter(prefix="/api/v1/offsite-fund", tags=["offsite-fund"]) operation_router = APIRouter(prefix="/api", tags=["offsite-fund"]) @router.get("/mails") async def list_mails( page: int = Query(default=1, ge=1), page_size: int = Query(default=20, ge=1, le=100), sender: str | None = Query(default=None, min_length=3, max_length=255), status: str | None = Query(default=None, min_length=1, max_length=32), context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).list_mails( context, page, page_size, sender, status ) @router.get("/mails/{mail_id}") async def get_mail( mail_id: str, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).get_mail(mail_id, context) @router.post("/mails/{mail_id}/deletions") async def delete_mail( mail_id: str, payload: OffsiteMailDeletionRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """物理删除邮件及其关联业务数据,保留删除审计。""" return await OffsiteFundService(session).delete_mail( mail_id, payload.operator_id, context ) @router.get("/mails/{mail_id}/recognition-fields") async def get_mail_recognition_fields( mail_id: str, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """读取邮件内全部附件的 OCR 识别字段:只读查询,不触发重新识别。""" return await OffsiteFundService(session).mail_recognition_fields(mail_id, context) @router.put("/mails/{mail_id}/recognition-fields") async def save_mail_recognition_fields( mail_id: str, payload: OffsiteRecognitionCorrectionRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """保存 OCR 识别字段的人工修正值:只新增修正记录,不覆盖 Agent 原始识别值。""" return await OffsiteFundService(session).save_recognition_corrections( mail_id, [(item.attachment_id, item.fields) for item in payload.attachments], payload.operator_id, context, ) @router.get("/documents/{task_id}/nl2sql-fields") async def get_document_nl2sql_fields( task_id: str, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """读取单据核对使用的 NL2SQL 返回字段:只读查询,不触发新的查询。""" return await OffsiteFundService(session).nl2sql_fields(task_id, context) @router.put("/documents/{task_id}/nl2sql-fields") async def save_document_nl2sql_fields( task_id: str, payload: OffsiteNl2SqlCorrectionRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """保存 NL2SQL 返回字段的人工修正值:只新增修正记录,不改写查询记录。""" return await OffsiteFundService(session).save_nl2sql_corrections( task_id, payload.fields, payload.operator_id, context ) @router.get("/documents/{task_id}/rule-results") async def get_document_rule_results( task_id: str, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """读取单据规则判定结果:只读查询,不触发查询或重新判定。""" return await OffsiteFundService(session).rule_results(task_id, context) @router.post("/documents/{task_id}/rule-results/recalculations") async def recalculate_document_rule_results( task_id: str, payload: OffsiteRuleRecalculationRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """用已落库的识别字段和查询结果重新判定规则:不重新识别、不重新调用 NL2SQL。""" return await OffsiteFundService(session).recalculate_rule_results( task_id, payload.operator_id, context ) @router.get("/mailbox-status") async def mailbox_status( context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """收件箱状态:游标锁死时前端据此弹出告警。""" return await OffsiteFundService(session).mailbox_status(context) @router.post("/mailbox-status/recoveries") async def recover_mailbox( payload: OffsiteMailboxRecoveryRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: """解除收件游标阻塞,保留失败 UID 并由 Worker 重新处理。""" return await OffsiteFundService(session).recover_mailbox( payload.operator_id, context ) @router.get("/attachments/{attachment_id}/file", response_model=None) async def open_attachment_file( attachment_id: str, disposition: str = Query(default="inline", pattern="^(inline|attachment)$"), context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> Response | dict[str, Any]: """按附件编号返回原始文件:PDF/图片默认内联预览,其余类型转为下载。""" result = await OffsiteFundService(session).open_attachment_file( attachment_id, disposition, context ) if isinstance(result, dict): return result return FileResponse( result.path, media_type=result.media_type, filename=result.filename, content_disposition_type=result.disposition, headers={"X-Content-Type-Options": "nosniff"}, ) @router.post("/documents/{task_id}/confirmations") async def confirm_document( task_id: str, payload: OffsiteConfirmRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).confirm_document( task_id, payload.decision, payload.operator_id, context) @router.post("/documents/{task_id}/recognition-retries") async def retry_document_recognition( task_id: str, payload: OffsiteRecognitionRetryRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).retry_document_recognition( task_id, payload.operator_id, context ) @router.post("/documents/{task_id}/notifications") async def create_notification( task_id: str, payload: OffsiteNotificationRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).create_notification( task_id, payload.notification_type, payload.operator_id, context) @router.post("/notifications/{notification_id}/send") async def send_notification( notification_id: int, payload: OffsiteNotificationSendRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).send_notification( notification_id, payload.operator_id, payload.operator_confirmed, payload.final_content, context, ) @router.post("/settlement-statistics/recalculate") async def recalculate_statistics( payload: OffsiteRecalculateRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).recalculate_statistics( payload.fund_code, payload.application_date, context) @operation_router.post("/tasks/{task_id}/trigger-agent-nl2sql") async def trigger_agent_nl2sql( task_id: str, payload: OffsiteTriggerNl2SqlRequest, context: RequestContext = Depends(build_request_context), # noqa: B008 session: AsyncSession = Depends(get_session), # noqa: B008 ) -> dict[str, Any]: return await OffsiteFundService(session).trigger_agent_nl2sql( task_id, payload.operator_id, payload.manual_confirmed, context)