feat: 新增票据夹模块并优化 OCR 与员工画像服务

后端新增票据夹端点、数据模型和服务模块,优化 OCR 端点
Schema 和附件操作逻辑,完善员工行为画像服务和辅助函数,
前端新增票据夹视图和服务层,优化文档中心样式和侧边栏导
航,完善员工画像详情弹窗和权限控制,补充单元测试。
This commit is contained in:
caoxiaozhu
2026-05-29 14:51:18 +08:00
parent 678f64d772
commit 4c59941ec6
33 changed files with 2855 additions and 551 deletions

View File

@@ -0,0 +1,108 @@
from __future__ import annotations
from typing import Annotated
from fastapi import APIRouter, Depends, HTTPException, Query, status
from fastapi.responses import FileResponse
from app.api.deps import CurrentUserContext, get_current_user
from app.schemas.common import ErrorResponse
from app.schemas.receipt_folder import (
ReceiptFolderDeleteResponse,
ReceiptFolderDetailRead,
ReceiptFolderItemRead,
ReceiptFolderUpdate,
)
from app.services.receipt_folder import ReceiptFolderService
router = APIRouter(prefix="/receipt-folder")
CurrentUser = Annotated[CurrentUserContext, Depends(get_current_user)]
@router.get(
"",
response_model=list[ReceiptFolderItemRead],
summary="查询票据夹列表",
description="返回当前登录用户上传并持久化的票据列表。",
)
def list_receipts(
current_user: CurrentUser,
status_filter: Annotated[str, Query(alias="status")] = "all",
) -> list[ReceiptFolderItemRead]:
return ReceiptFolderService().list_receipts(
current_user=current_user,
status_filter=status_filter,
)
@router.get(
"/{receipt_id}",
response_model=ReceiptFolderDetailRead,
summary="读取票据详情",
responses={status.HTTP_404_NOT_FOUND: {"model": ErrorResponse, "description": "票据不存在。"}},
)
def get_receipt(receipt_id: str, current_user: CurrentUser) -> ReceiptFolderDetailRead:
try:
return ReceiptFolderService().get_receipt(receipt_id, current_user)
except FileNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Receipt not found") from exc
@router.patch(
"/{receipt_id}",
response_model=ReceiptFolderDetailRead,
summary="更新票据基础识别信息",
responses={status.HTTP_404_NOT_FOUND: {"model": ErrorResponse, "description": "票据不存在。"}},
)
def update_receipt(
receipt_id: str,
payload: ReceiptFolderUpdate,
current_user: CurrentUser,
) -> ReceiptFolderDetailRead:
try:
return ReceiptFolderService().update_receipt(
receipt_id=receipt_id,
payload=payload,
current_user=current_user,
)
except FileNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Receipt not found") from exc
@router.delete(
"/{receipt_id}",
response_model=ReceiptFolderDeleteResponse,
summary="删除票据",
responses={status.HTTP_404_NOT_FOUND: {"model": ErrorResponse, "description": "票据不存在。"}},
)
def delete_receipt(receipt_id: str, current_user: CurrentUser) -> ReceiptFolderDeleteResponse:
try:
return ReceiptFolderService().delete_receipt(receipt_id=receipt_id, current_user=current_user)
except FileNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Receipt not found") from exc
@router.get(
"/{receipt_id}/preview",
summary="预览票据原始文件",
responses={status.HTTP_404_NOT_FOUND: {"model": ErrorResponse, "description": "票据预览不存在。"}},
)
def preview_receipt(receipt_id: str, current_user: CurrentUser) -> FileResponse:
try:
file_path, media_type, file_name = ReceiptFolderService().resolve_preview(receipt_id, current_user)
except FileNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Receipt preview not found") from exc
return FileResponse(file_path, media_type=media_type, filename=file_name)
@router.get(
"/{receipt_id}/source",
summary="读取票据源文件",
responses={status.HTTP_404_NOT_FOUND: {"model": ErrorResponse, "description": "票据源文件不存在。"}},
)
def source_receipt(receipt_id: str, current_user: CurrentUser) -> FileResponse:
try:
file_path, media_type, file_name = ReceiptFolderService().resolve_source(receipt_id, current_user)
except FileNotFoundError as exc:
raise HTTPException(status_code=status.HTTP_404_NOT_FOUND, detail="Receipt source not found") from exc
return FileResponse(file_path, media_type=media_type, filename=file_name)