File size: 6,824 Bytes
b4f2238 | 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 100 101 102 103 104 105 106 107 108 109 110 111 112 113 114 115 116 117 118 119 120 121 122 123 124 125 126 127 128 129 130 131 132 133 134 135 136 137 138 139 140 141 142 143 144 145 146 147 148 149 150 151 152 153 154 155 156 157 158 159 160 161 162 163 164 165 166 167 168 169 170 171 172 173 174 175 176 177 178 179 180 181 182 183 184 185 186 187 188 189 190 191 192 193 194 195 196 197 198 199 200 201 202 203 204 205 | """
发票识别 API
提供发票文件上传、识别状态查询和下载接口。
"""
import logging
from pathlib import Path
from typing import Any
from django.http import HttpResponse
from django.utils.translation import gettext as _
from ninja import File, Router
from ninja.errors import HttpError
from ninja.files import UploadedFile
from apps.core.infrastructure.throttling import rate_limit_from_settings
logger = logging.getLogger("apps.invoice_recognition")
router = Router(tags=["发票识别"])
@router.post("/quick-recognize")
def quick_recognize(
request: Any,
files: list[UploadedFile] = File(...), # type: ignore[type-arg]
) -> dict[str, Any]:
"""快速识别发票文件(不创建任务)"""
if not files:
raise HttpError(400, _("未提供文件"))
service = _get_quick_recognition_service()
try:
results = service.recognize_files(files)
except Exception as exc:
logger.error("快速识别失败: %s", exc, exc_info=True)
raise HttpError(500, _("服务器内部错误"))
results_data: list[dict[str, Any]] = []
for result in results:
result_dict: dict[str, Any] = {
"filename": result.filename,
"success": result.success,
}
if result.success and result.data:
import datetime
from decimal import Decimal
def _serialize_value(value: Any) -> Any:
if isinstance(value, (datetime.datetime, datetime.date)):
return str(value)
if isinstance(value, Decimal):
return str(value)
return value
result_dict["data"] = {
"invoice_code": _serialize_value(result.data.invoice_code),
"invoice_number": _serialize_value(result.data.invoice_number),
"invoice_date": _serialize_value(result.data.invoice_date),
"amount": _serialize_value(result.data.amount),
"tax_amount": _serialize_value(result.data.tax_amount),
"total_amount": _serialize_value(result.data.total_amount),
"buyer_name": _serialize_value(result.data.buyer_name),
"seller_name": _serialize_value(result.data.seller_name),
"project_name": _serialize_value(result.data.project_name),
"category": _serialize_value(result.data.category),
}
else:
result_dict["error"] = result.error
results_data.append(result_dict)
return {"results": results_data}
def _get_recognition_service() -> Any:
from apps.invoice_recognition.services.wiring import get_invoice_recognition_service
return get_invoice_recognition_service()
def _get_download_service() -> Any:
from apps.invoice_recognition.services.wiring import get_invoice_download_service
return get_invoice_download_service()
def _get_quick_recognition_service() -> Any:
from apps.invoice_recognition.services.wiring import get_quick_recognition_service
return get_quick_recognition_service()
@router.post("/{task_id}/upload")
@rate_limit_from_settings("UPLOAD", by_user=True)
def upload_invoices(
request: Any,
task_id: int,
files: list[UploadedFile] = File(...), # type: ignore[type-arg]
) -> dict[str, Any]:
"""多文件上传 + 自动识别"""
from django.core.exceptions import ObjectDoesNotExist, ValidationError
service = _get_recognition_service()
try:
records = service.upload_and_recognize(task_id, files)
except ObjectDoesNotExist:
raise HttpError(404, _("任务不存在"))
except ValidationError as exc:
raise HttpError(400, str(exc.message if hasattr(exc, "message") else exc))
record_list: list[dict[str, Any]] = [
{
"id": r.id,
"original_filename": r.original_filename,
"status": r.status,
"invoice_code": r.invoice_code,
"invoice_number": r.invoice_number,
"is_duplicate": r.is_duplicate,
"category": r.category,
}
for r in records
]
return {"success": True, "count": len(records), "records": record_list}
@router.get("/{task_id}/status")
def get_task_status(
request: Any,
task_id: int,
) -> dict[str, Any]:
"""任务状态 + 发票记录列表"""
from django.core.exceptions import ObjectDoesNotExist
service = _get_recognition_service()
try:
data = service.get_task_status(task_id)
except ObjectDoesNotExist:
raise HttpError(404, _("任务不存在"))
def _serialize(obj: Any) -> Any:
import datetime
from decimal import Decimal
if isinstance(obj, (datetime.datetime, datetime.date)):
return str(obj)
if isinstance(obj, Decimal):
return str(obj)
if isinstance(obj, dict):
return {k: _serialize(v) for k, v in obj.items()}
if isinstance(obj, list):
return [_serialize(item) for item in obj]
return obj
serialized = _serialize(data)
assert isinstance(serialized, dict)
return serialized
@router.get("/{task_id}/download")
@rate_limit_from_settings("EXPORT", by_user=True)
def download_invoices(
request: Any,
task_id: int,
scope: str,
fmt: str = "zip",
invoice_id: int | None = None,
category: str | None = None,
) -> HttpResponse:
"""灵活下载:scope=single/category/all,fmt=pdf/zip"""
from django.core.exceptions import ObjectDoesNotExist
content_type_map: dict[str, str] = {
"pdf": "application/pdf",
"zip": "application/zip",
}
content_type = content_type_map.get(fmt, "application/octet-stream")
download_service = _get_download_service()
try:
if scope == "single":
if invoice_id is None:
raise HttpError(400, _("scope=single 时必须提供 invoice_id"))
file_path, filename = download_service.download_single(invoice_id)
with Path(file_path).open("rb") as f:
data = f.read()
elif scope == "category":
if category is None:
raise HttpError(400, _("scope=category 时必须提供 category"))
data, filename = download_service.download_by_category(task_id, category, fmt)
elif scope == "all":
data, filename = download_service.download_all(task_id, fmt)
else:
raise HttpError(400, _("无效的 scope 参数,允许值:single/category/all"))
except ObjectDoesNotExist:
raise HttpError(404, _("任务或发票不存在"))
response = HttpResponse(data, content_type=content_type)
response["Content-Disposition"] = f'attachment; filename="{filename}"'
return response
|