feat(server): 新增附件关联/关联报销草稿后台任务与申请位置语义

- attachment_association_jobs:从票据夹批量关联附件到报销单,识别城市/日期并创建明细项,内存态 job 跟踪
- linked_reimbursement_draft_jobs:基于申请单异步生成关联报销草稿,调用 Orchestrator 编排,区分 succeeded/failed 终态
- application_location_semantics:抽取差旅出发/到达城市、判断具体地址/业务动作等位置语义,供申请单校验复用
- router 注册两个 job 端点,新增对应 job/语义单元测试
This commit is contained in:
caoxiaozhu
2026-06-24 10:42:05 +08:00
parent d4ff79f326
commit 332f77389d
10 changed files with 1830 additions and 0 deletions

View File

@@ -0,0 +1,75 @@
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, BackgroundTasks, Depends, HTTPException, status
from app.api.deps import CurrentUserContext, get_current_user
from app.db.session import get_session_factory
from app.schemas.attachment_association_job import (
AttachmentAssociationJobCreate,
AttachmentAssociationJobRead,
)
from app.schemas.common import ErrorResponse
from app.services.attachment_association_jobs import (
create_attachment_association_job,
get_attachment_association_job,
run_attachment_association_job,
)
router = APIRouter(prefix="/reimbursements/attachment-association-jobs")
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
@router.post(
"",
response_model=AttachmentAssociationJobRead,
status_code=status.HTTP_202_ACCEPTED,
summary="创建附件自动关联后台任务",
description="根据已 OCR 入票据夹的 receipt_id在后台自动匹配并归集到报销草稿。",
responses={
status.HTTP_400_BAD_REQUEST: {
"model": ErrorResponse,
"description": "请求缺少可关联票据。",
},
},
)
def create_attachment_association_job_endpoint(
payload: AttachmentAssociationJobCreate,
background_tasks: BackgroundTasks,
current_user: CurrentUser,
) -> AttachmentAssociationJobRead:
try:
job = create_attachment_association_job(payload, current_user)
except ValueError as exc:
raise HTTPException(status_code=status.HTTP_400_BAD_REQUEST, detail=str(exc)) from exc
background_tasks.add_task(
run_attachment_association_job,
job.job_id,
current_user,
get_session_factory(),
)
return job
@router.get(
"/{job_id}",
response_model=AttachmentAssociationJobRead,
summary="查询附件自动关联后台任务",
description="用于前端会话恢复后按 job_id 查询任务状态。",
responses={
status.HTTP_404_NOT_FOUND: {
"model": ErrorResponse,
"description": "任务不存在或当前用户无权查看。",
},
},
)
def get_attachment_association_job_endpoint(
job_id: str,
current_user: CurrentUser,
) -> AttachmentAssociationJobRead:
job = get_attachment_association_job(job_id, current_user)
if job is None:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="附件关联任务不存在或已失效。")
return job