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

@@ -8,6 +8,7 @@ from sqlalchemy.orm import Session, sessionmaker
from sqlalchemy.pool import StaticPool
from app.api.deps import get_db
from app.core.config import get_settings
from app.db.base import Base
from app.main import create_app
from app.schemas.ocr import OcrRecognizeBatchRead, OcrRecognizeDocumentRead, OcrRecognizeFieldRead, OcrRecognizeLineRead
@@ -35,7 +36,7 @@ def build_client() -> TestClient:
return TestClient(app)
def test_ocr_recognize_endpoint_returns_structured_payload(monkeypatch) -> None:
def test_ocr_recognize_endpoint_returns_structured_payload(monkeypatch, tmp_path) -> None:
def fake_recognize(
self,
files: list[tuple[str, bytes, str | None]],
@@ -76,21 +77,84 @@ def test_ocr_recognize_endpoint_returns_structured_payload(monkeypatch) -> None:
],
)
monkeypatch.setenv("STORAGE_ROOT_DIR", str(tmp_path / "storage"))
get_settings.cache_clear()
monkeypatch.setattr(OcrService, "recognize_files", fake_recognize)
client = build_client()
try:
client = build_client()
auth_headers = {"x-auth-username": "pytest", "x-auth-name": "Py Test"}
response = client.post(
"/api/v1/ocr/recognize",
headers={"x-auth-username": "pytest", "x-auth-name": "Py Test"},
files=[("files", ("invoice.png", b"fake-image", "image/png"))],
)
response = client.post(
"/api/v1/ocr/recognize",
headers=auth_headers,
files=[("files", ("invoice.png", b"fake-image", "image/png"))],
)
assert response.status_code == 200
payload = response.json()
assert payload["engine"] == "paddleocr_mobile"
assert payload["success_count"] == 1
assert payload["documents"][0]["filename"] == "invoice.png"
assert payload["documents"][0]["summary"] == "增值税电子发票,金额 100 元。"
assert payload["documents"][0]["document_type"] == "vat_invoice"
assert payload["documents"][0]["document_type_label"] == "增值税发票"
assert payload["documents"][0]["document_fields"][0]["label"] == "金额"
assert response.status_code == 200
payload = response.json()
document = payload["documents"][0]
assert payload["engine"] == "paddleocr_mobile"
assert payload["success_count"] == 1
assert document["filename"] == "invoice.png"
assert document["summary"] == "增值税电子发票,金额 100 元。"
assert document["document_type"] == "vat_invoice"
assert document["document_type_label"] == "增值税发票"
assert document["document_fields"][0]["label"] == "金额"
assert document["receipt_id"]
assert document["receipt_status"] == "unlinked"
assert document["receipt_preview_url"].endswith(f"/receipt-folder/{document['receipt_id']}/preview")
assert document["receipt_source_url"].endswith(f"/receipt-folder/{document['receipt_id']}/source")
receipt_id = document["receipt_id"]
list_response = client.get("/api/v1/receipt-folder?status=unlinked", headers=auth_headers)
assert list_response.status_code == 200
receipt_list = list_response.json()
assert len(receipt_list) == 1
assert receipt_list[0]["id"] == receipt_id
assert receipt_list[0]["amount"] == "100元"
repeated_response = client.post(
"/api/v1/ocr/recognize",
headers=auth_headers,
data={"receipt_ids": receipt_id},
files=[("files", ("invoice.png", b"fake-image", "image/png"))],
)
assert repeated_response.status_code == 200
repeated_document = repeated_response.json()["documents"][0]
assert repeated_document["receipt_id"] == receipt_id
all_receipts_response = client.get("/api/v1/receipt-folder?status=all", headers=auth_headers)
assert all_receipts_response.status_code == 200
assert len(all_receipts_response.json()) == 1
detail_response = client.get(f"/api/v1/receipt-folder/{receipt_id}", headers=auth_headers)
assert detail_response.status_code == 200
detail_payload = detail_response.json()
assert detail_payload["file_name"] == "invoice.png"
assert detail_payload["fields"][0]["label"] == "金额"
update_response = client.patch(
f"/api/v1/receipt-folder/{receipt_id}",
headers=auth_headers,
json={
"document_type_label": "电子发票",
"amount": "108元",
"fields": [{"key": "amount", "label": "金额", "value": "108元"}],
},
)
assert update_response.status_code == 200
assert update_response.json()["document_type_label"] == "电子发票"
assert update_response.json()["amount"] == "108元"
preview_response = client.get(f"/api/v1/receipt-folder/{receipt_id}/preview", headers=auth_headers)
assert preview_response.status_code == 200
assert preview_response.content == b"fake-image"
delete_response = client.delete(f"/api/v1/receipt-folder/{receipt_id}", headers=auth_headers)
assert delete_response.status_code == 200
assert delete_response.json()["receipt_id"] == receipt_id
deleted_response = client.get(f"/api/v1/receipt-folder/{receipt_id}", headers=auth_headers)
assert deleted_response.status_code == 404
finally:
get_settings.cache_clear()