feat(server): 新增附件关联/关联报销草稿后台任务与申请位置语义
- attachment_association_jobs:从票据夹批量关联附件到报销单,识别城市/日期并创建明细项,内存态 job 跟踪 - linked_reimbursement_draft_jobs:基于申请单异步生成关联报销草稿,调用 Orchestrator 编排,区分 succeeded/failed 终态 - application_location_semantics:抽取差旅出发/到达城市、判断具体地址/业务动作等位置语义,供申请单校验复用 - router 注册两个 job 端点,新增对应 job/语义单元测试
This commit is contained in:
@@ -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
|
||||
|
||||
@@ -0,0 +1,74 @@
|
||||
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.common import ErrorResponse
|
||||
from app.schemas.linked_reimbursement_draft_job import (
|
||||
LinkedReimbursementDraftJobCreate,
|
||||
LinkedReimbursementDraftJobRead,
|
||||
)
|
||||
from app.services.linked_reimbursement_draft_jobs import (
|
||||
create_linked_reimbursement_draft_job,
|
||||
get_linked_reimbursement_draft_job,
|
||||
run_linked_reimbursement_draft_job,
|
||||
)
|
||||
|
||||
router = APIRouter(prefix="/reimbursements/linked-reimbursement-draft-jobs")
|
||||
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
|
||||
|
||||
|
||||
@router.post(
|
||||
"",
|
||||
response_model=LinkedReimbursementDraftJobRead,
|
||||
status_code=status.HTTP_202_ACCEPTED,
|
||||
summary="创建关联申请单生成报销草稿后台任务",
|
||||
description="用户选择关联申请单后,后台继续生成报销草稿,避免当前会话长时间同步等待。",
|
||||
responses={
|
||||
status.HTTP_400_BAD_REQUEST: {
|
||||
"model": ErrorResponse,
|
||||
"description": "请求缺少申请单关联上下文。",
|
||||
},
|
||||
},
|
||||
)
|
||||
def create_linked_reimbursement_draft_job_endpoint(
|
||||
payload: LinkedReimbursementDraftJobCreate,
|
||||
background_tasks: BackgroundTasks,
|
||||
current_user: CurrentUser,
|
||||
) -> LinkedReimbursementDraftJobRead:
|
||||
try:
|
||||
job = create_linked_reimbursement_draft_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_linked_reimbursement_draft_job,
|
||||
job.job_id,
|
||||
current_user,
|
||||
get_session_factory(),
|
||||
)
|
||||
return job
|
||||
|
||||
|
||||
@router.get(
|
||||
"/{job_id}",
|
||||
response_model=LinkedReimbursementDraftJobRead,
|
||||
summary="查询关联申请单生成报销草稿后台任务",
|
||||
description="用于前端按 job_id 查询草稿生成状态。",
|
||||
responses={
|
||||
status.HTTP_404_NOT_FOUND: {
|
||||
"model": ErrorResponse,
|
||||
"description": "任务不存在或当前用户无权查看。",
|
||||
},
|
||||
},
|
||||
)
|
||||
def get_linked_reimbursement_draft_job_endpoint(
|
||||
job_id: str,
|
||||
current_user: CurrentUser,
|
||||
) -> LinkedReimbursementDraftJobRead:
|
||||
job = get_linked_reimbursement_draft_job(job_id, current_user)
|
||||
if job is None:
|
||||
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="报销草稿生成任务不存在或已失效。")
|
||||
return job
|
||||
@@ -6,6 +6,7 @@ from app.api.v1.endpoints.agent_feedback import router as agent_feedback_router
|
||||
from app.api.v1.endpoints.agent_runs import router as agent_runs_router
|
||||
from app.api.v1.endpoints.agent_traces import router as agent_traces_router
|
||||
from app.api.v1.endpoints.analytics import router as analytics_router
|
||||
from app.api.v1.endpoints.attachment_association_jobs import router as attachment_association_jobs_router
|
||||
from app.api.v1.endpoints.audit_logs import router as audit_logs_router
|
||||
from app.api.v1.endpoints.auth import router as auth_router
|
||||
from app.api.v1.endpoints.bootstrap import router as bootstrap_router
|
||||
@@ -14,6 +15,7 @@ from app.api.v1.endpoints.employees import router as employees_router
|
||||
from app.api.v1.endpoints.employee_profiles import router as employee_profiles_router
|
||||
from app.api.v1.endpoints.health import router as health_router
|
||||
from app.api.v1.endpoints.knowledge import router as knowledge_router
|
||||
from app.api.v1.endpoints.linked_reimbursement_draft_jobs import router as linked_reimbursement_draft_jobs_router
|
||||
from app.api.v1.endpoints.notification_states import router as notification_states_router
|
||||
from app.api.v1.endpoints.ocr import router as ocr_router
|
||||
from app.api.v1.endpoints.ontology import router as ontology_router
|
||||
@@ -36,8 +38,10 @@ router.include_router(agent_feedback_router, tags=["agent-feedback"])
|
||||
router.include_router(agent_runs_router, tags=["agent-runs"])
|
||||
router.include_router(agent_traces_router, tags=["agent-traces"])
|
||||
router.include_router(analytics_router, tags=["analytics"])
|
||||
router.include_router(attachment_association_jobs_router, tags=["attachment-association-jobs"])
|
||||
router.include_router(audit_logs_router, tags=["audit-logs"])
|
||||
router.include_router(knowledge_router, tags=["knowledge"])
|
||||
router.include_router(linked_reimbursement_draft_jobs_router, tags=["linked-reimbursement-draft-jobs"])
|
||||
router.include_router(notification_states_router, tags=["notification-states"])
|
||||
router.include_router(ocr_router, tags=["ocr"])
|
||||
router.include_router(ontology_router, tags=["ontology"])
|
||||
|
||||
Reference in New Issue
Block a user