feat: 新增票据夹模块并优化 OCR 与员工画像服务
后端新增票据夹端点、数据模型和服务模块,优化 OCR 端点 Schema 和附件操作逻辑,完善员工行为画像服务和辅助函数, 前端新增票据夹视图和服务层,优化文档中心样式和侧边栏导 航,完善员工画像详情弹窗和权限控制,补充单元测试。
This commit is contained in:
@@ -264,6 +264,74 @@ def test_current_employee_profile_endpoint_resolves_login_user() -> None:
|
||||
payload = response.json()
|
||||
assert payload["employee_id"] == "emp-main"
|
||||
assert {item["profile_type"] for item in payload["profiles"]} >= {"expense", "ai_usage"}
|
||||
ai_profile = next(item for item in payload["profiles"] if item["profile_type"] == "ai_usage")
|
||||
assert ai_profile["metrics"]["ai_run_duration_ms"] == 120
|
||||
assert payload["profile_tags"]
|
||||
assert payload["radar"]["dimensions"]
|
||||
|
||||
|
||||
def test_current_admin_profile_endpoint_returns_account_usage_profile() -> None:
|
||||
session_factory = build_session_factory()
|
||||
with session_factory() as db:
|
||||
seed_profile_data(db)
|
||||
now = datetime.now(UTC)
|
||||
for index in range(12):
|
||||
run_id = f"run-admin-usage-{index}"
|
||||
started_at = now - timedelta(days=1, minutes=index)
|
||||
db.add(
|
||||
AgentRun(
|
||||
run_id=run_id,
|
||||
agent="user_agent",
|
||||
source="user_message",
|
||||
user_id="admin",
|
||||
status="success",
|
||||
result_summary="管理员查看运行概览。",
|
||||
started_at=started_at,
|
||||
finished_at=started_at + timedelta(seconds=2),
|
||||
tool_calls=[
|
||||
AgentToolCall(
|
||||
run_id=run_id,
|
||||
tool_type="database",
|
||||
tool_name="agent_runs.list",
|
||||
request_json={"limit": 20},
|
||||
response_json={"ok": True},
|
||||
status="success",
|
||||
duration_ms=120,
|
||||
)
|
||||
],
|
||||
)
|
||||
)
|
||||
db.commit()
|
||||
|
||||
app = create_app()
|
||||
|
||||
def override_db() -> Generator[Session, None, None]:
|
||||
db = session_factory()
|
||||
try:
|
||||
yield db
|
||||
finally:
|
||||
db.close()
|
||||
|
||||
app.dependency_overrides[get_db] = override_db
|
||||
client = TestClient(app)
|
||||
response = client.get(
|
||||
"/api/v1/employee-profiles/me/latest",
|
||||
params={
|
||||
"scene": "operations",
|
||||
"window_days": 90,
|
||||
"expense_type_scope": "overall",
|
||||
},
|
||||
headers={"x-auth-username": "admin", "x-auth-name": "admin", "x-auth-is-admin": "true"},
|
||||
)
|
||||
|
||||
assert response.status_code == 200
|
||||
payload = response.json()
|
||||
assert payload["employee_id"] == "admin"
|
||||
assert payload["empty_reason"] == ""
|
||||
assert [item["profile_type"] for item in payload["profiles"]] == ["ai_usage"]
|
||||
metrics = payload["profiles"][0]["metrics"]
|
||||
assert metrics["ai_run_count"] == 12
|
||||
assert metrics["ai_run_duration_ms"] == 24000
|
||||
assert payload["profile_tags"]
|
||||
assert payload["radar"]["dimensions"]
|
||||
|
||||
|
||||
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user