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