feat(expense): add persistent zero-entry receipt association

This commit is contained in:
caoxiaozhu
2026-07-16 10:23:23 +08:00
parent 54754b5502
commit ae3f02c35a
30 changed files with 4450 additions and 810 deletions

View File

@@ -1,7 +1,7 @@
from __future__ import annotations
import json
import hashlib
import json
import mimetypes
import re
import shutil
@@ -20,8 +20,8 @@ from app.schemas.receipt_folder import (
ReceiptFolderItemRead,
ReceiptFolderUpdate,
)
from app.services.document_preview import DocumentPreviewAssets
from app.services.document_intelligence import build_document_insight
from app.services.document_preview import DocumentPreviewAssets
from app.services.ocr import SUPPORTED_SUFFIXES
RECEIPT_DATE_PATTERN = re.compile(
@@ -116,9 +116,16 @@ class ReceiptFolderStorageMixin:
@staticmethod
def _owner_key(current_user: CurrentUserContext) -> str:
raw = str(current_user.username or current_user.name or "anonymous").strip().lower()
normalized = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", raw).strip("._")
return normalized or "anonymous"
raw_owner = str(current_user.username or current_user.name or "anonymous").strip().lower()
owner = re.sub(r"[^\w.\-\u4e00-\u9fff]+", "_", raw_owner).strip("._") or "anonymous"
raw_tenant = str(getattr(current_user, "tenant_id", "default") or "default").strip().lower()
tenant = re.sub(r"[^\w.\-]+", "_", raw_tenant).strip("._") or "default"
# 默认租户继续读取历史目录;其他租户必须使用独立命名空间。
if raw_tenant == "default":
return owner
tenant_digest = hashlib.sha256(raw_tenant.encode("utf-8")).hexdigest()[:12]
owner_digest = hashlib.sha256(raw_owner.encode("utf-8")).hexdigest()[:12]
return f"{tenant[:48]}-{tenant_digest}__{owner[:64]}-{owner_digest}"
@staticmethod
def _should_persist_source(filename: str, content: bytes) -> bool:
@@ -1180,6 +1187,8 @@ class ReceiptFolderService(ReceiptFolderStorageMixin, ReceiptFolderItemMixin, Re
meta = {
"id": receipt_id,
"owner_key": owner_key,
"tenant_id": str(getattr(current_user, "tenant_id", "default") or "default").strip()
or "default",
"file_name": normalized_name,
"source_file_name": normalized_name,
"media_type": resolved_media_type,
@@ -1258,6 +1267,23 @@ class ReceiptFolderService(ReceiptFolderStorageMixin, ReceiptFolderItemMixin, Re
self._write_meta(receipt_dir, meta)
return self._build_item(meta)
def restore_receipt_meta(
self,
*,
receipt_id: str,
current_user: CurrentUserContext,
meta: dict[str, Any],
) -> None:
"""仅供跨数据库/文件事务补偿使用,恢复调用前的票据元数据。"""
receipt_dir = self._receipt_dir(self._owner_key(current_user), receipt_id)
restored = json.loads(json.dumps(meta, ensure_ascii=False))
restored["id"] = str(receipt_id or "").strip()
restored["owner_key"] = self._owner_key(current_user)
restored["tenant_id"] = str(
getattr(current_user, "tenant_id", "default") or "default"
).strip() or "default"
self._write_meta(receipt_dir, restored)
def list_receipts(
self,
*,