From e65e22b0ce09b0e5746116c6cad0401e706024ea Mon Sep 17 00:00:00 2001
From: liu <2021943741@qq.com>
Date: 星期三, 26 八月 2026 10:08:00 +0800
Subject: [PATCH] feat(erp): 采购来票新增发票OCR识别接口

---
 yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrRespVO.java |   40 +++++
 yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/ErpPurchaseInvoiceAiController.java |   36 +++++
 yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiServiceImpl.java         |  252 ++++++++++++++++++++++++++++++++++++
 yudao-module-erp/pom.xml                                                                                                    |   14 ++
 yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrReqVO.java  |   15 ++
 yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiService.java             |   19 ++
 6 files changed, 376 insertions(+), 0 deletions(-)

diff --git a/yudao-module-erp/pom.xml b/yudao-module-erp/pom.xml
index 6d0e0fa..12adb08 100644
--- a/yudao-module-erp/pom.xml
+++ b/yudao-module-erp/pom.xml
@@ -66,6 +66,13 @@
             <version>${revision}</version>
         </dependency>
 
+        <!-- AI 澶фā鍨� API -->
+        <dependency>
+            <groupId>cn.iocoder.boot</groupId>
+            <artifactId>yudao-module-ai-api</artifactId>
+            <version>${revision}</version>
+        </dependency>
+
         <!-- 涓氬姟缁勪欢 -->
 
         <!-- Web 鐩稿叧 -->
@@ -102,6 +109,13 @@
             <artifactId>yudao-spring-boot-starter-test</artifactId>
         </dependency>
 
+        <!-- PDF 瑙f瀽 -->
+        <dependency>
+            <groupId>org.apache.pdfbox</groupId>
+            <artifactId>pdfbox</artifactId>
+            <version>3.0.4</version>
+        </dependency>
+
     </dependencies>
 
 </project>
\ No newline at end of file
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/ErpPurchaseInvoiceAiController.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/ErpPurchaseInvoiceAiController.java
new file mode 100644
index 0000000..598c291
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/ErpPurchaseInvoiceAiController.java
@@ -0,0 +1,36 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.ai;
+
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo.ErpPurchaseInvoiceOcrReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo.ErpPurchaseInvoiceOcrRespVO;
+import cn.iocoder.yudao.module.erp.service.purchase.ai.ErpPurchaseInvoiceAiService;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+
+@Tag(name = "绠$悊鍚庡彴 - ERP 閲囪喘鏉ョエ AI OCR")
+@RestController
+@RequestMapping("/erp/purchase-invoice/ai")
+@Validated
+public class ErpPurchaseInvoiceAiController {
+
+    @Resource
+    private ErpPurchaseInvoiceAiService invoiceAiService;
+
+    @PostMapping("/ocr")
+    @Operation(summary = "AI OCR 璇嗗埆鍙戠エ鏂囦欢")
+    @PreAuthorize("@ss.hasPermission('erp:purchase-invoice:create')")
+    public CommonResult<ErpPurchaseInvoiceOcrRespVO> ocrInvoice(@Valid @RequestBody ErpPurchaseInvoiceOcrReqVO reqVO) {
+        return success(invoiceAiService.ocrInvoice(reqVO));
+    }
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrReqVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrReqVO.java
new file mode 100644
index 0000000..e30cc27
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrReqVO.java
@@ -0,0 +1,15 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+@Schema(description = "绠$悊鍚庡彴 - AI OCR 璇嗗埆閲囪喘鏉ョエ鍙戠エ璇锋眰 VO")
+@Data
+public class ErpPurchaseInvoiceOcrReqVO {
+
+    @Schema(description = "宸蹭笂浼犳枃浠剁殑 blob ID", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+    @NotNull(message = "blob ID 涓嶈兘涓虹┖")
+    private Long blobId;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrRespVO.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrRespVO.java
new file mode 100644
index 0000000..c4507a6
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/controller/admin/purchase/ai/vo/ErpPurchaseInvoiceOcrRespVO.java
@@ -0,0 +1,40 @@
+package cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.math.BigDecimal;
+import java.time.LocalDate;
+
+@Schema(description = "绠$悊鍚庡彴 - AI OCR 璇嗗埆閲囪喘鏉ョエ鍙戠エ鍝嶅簲 VO")
+@Data
+public class ErpPurchaseInvoiceOcrRespVO {
+
+    @Schema(description = "鍙戠エ鍙风爜", example = "02564178")
+    private String invoiceNo;
+
+    @Schema(description = "鍙戠エ鎶ご锛堣喘涔版柟鍚嶇О锛�", example = "XX绉戞妧鏈夐檺鍏徃")
+    private String invoiceTitle;
+
+    @Schema(description = "閿�鍞柟鍚嶇О锛堟枃鏈紝渚涙牳瀵逛緵搴斿晢鏄惁鍖归厤锛�", example = "灏忕暘鑼勫叕鍙�")
+    private String supplierName;
+
+    @Schema(description = "鏉ョエ閲戦锛堜环绋庡悎璁★級锛屽崟浣嶏細鍏�", example = "9000.00")
+    private BigDecimal price;
+
+    @Schema(description = "寮�绁ㄦ棩鏈�", example = "2026-08-01")
+    private LocalDate invoiceTime;
+
+    @Schema(description = "鍙戠エ绫诲瀷锛堝澧炲�肩◣涓撶敤鍙戠エ銆佸鍊肩◣鏅�氬彂绁級", example = "澧炲�肩◣涓撶敤鍙戠エ")
+    private String invoiceType;
+
+    @Schema(description = "绋庣巼锛堢櫨鍒嗘瘮锛夛紝濡傚浘涓湭鏍囨敞鍒欎负绌�", example = "13.0")
+    private BigDecimal taxRate;
+
+    @Schema(description = "澶囨敞", example = "鍚◣鍚繍璐�")
+    private String remark;
+
+    @Schema(description = "鍘熷璇嗗埆鏂囨湰锛堝墠绔彲閫夊睍绀虹敤浜庤皟璇�/鍙傝�冿紱璇嗗埆澶辫触鏃朵负閿欒鎻愮ず锛�")
+    private String rawText;
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiService.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiService.java
new file mode 100644
index 0000000..3f2e0b4
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiService.java
@@ -0,0 +1,19 @@
+package cn.iocoder.yudao.module.erp.service.purchase.ai;
+
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo.ErpPurchaseInvoiceOcrReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo.ErpPurchaseInvoiceOcrRespVO;
+
+/**
+ * ERP 閲囪喘鏉ョエ AI OCR Service 鎺ュ彛
+ */
+public interface ErpPurchaseInvoiceAiService {
+
+    /**
+     * AI OCR 璇嗗埆鍙戠エ鏂囦欢锛岃繑鍥炵粨鏋勫寲淇℃伅
+     *
+     * @param reqVO 璇嗗埆璇锋眰
+     * @return 璇嗗埆缁撴灉
+     */
+    ErpPurchaseInvoiceOcrRespVO ocrInvoice(ErpPurchaseInvoiceOcrReqVO reqVO);
+
+}
diff --git a/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiServiceImpl.java b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiServiceImpl.java
new file mode 100644
index 0000000..b70ebec
--- /dev/null
+++ b/yudao-module-erp/src/main/java/cn/iocoder/yudao/module/erp/service/purchase/ai/ErpPurchaseInvoiceAiServiceImpl.java
@@ -0,0 +1,252 @@
+package cn.iocoder.yudao.module.erp.service.purchase.ai;
+
+import cn.hutool.core.io.FileUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.hutool.json.JSONUtil;
+import cn.iocoder.yudao.module.ai.api.chat.AiChatApi;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo.ErpPurchaseInvoiceOcrReqVO;
+import cn.iocoder.yudao.module.erp.controller.admin.purchase.ai.vo.ErpPurchaseInvoiceOcrRespVO;
+import cn.iocoder.yudao.module.system.dal.dataobject.storage.SystemStorageBlobDO;
+import cn.iocoder.yudao.module.system.service.storage.SystemStorageBlobService;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.apache.pdfbox.Loader;
+import org.apache.pdfbox.pdmodel.PDDocument;
+import org.apache.pdfbox.text.PDFTextStripper;
+import org.springframework.stereotype.Service;
+
+import java.io.File;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.nio.charset.StandardCharsets;
+import java.nio.file.Files;
+import java.time.LocalDate;
+import java.util.Base64;
+import java.util.Map;
+import java.util.Set;
+
+/**
+ * ERP 閲囪喘鏉ョエ AI OCR Service 瀹炵幇绫�
+ *
+ * <p>
+ * 鏀寔閫氳繃 AI 璇嗗埆鍙戠エ鍥剧墖 / PDF / 鏂囨湰鏂囦欢锛屾彁鍙栧彂绁ㄥ彿鐮併�佹姮澶淬�侀噾棰濄�佸紑绁ㄦ棩鏈熺瓑缁撴瀯鍖栦俊鎭紝
+ * 渚涘墠绔濉�"閲囪喘鏉ョエ"鏂板琛ㄥ崟銆傚浘鐗囪蛋澶氭ā鎬佽瑙夎瘑鍒紝鍏朵綑鏍煎紡鍏堟娊鍙栨枃鏈啀璧扮函鏂囨湰璇嗗埆銆�
+ */
+@Slf4j
+@Service
+public class ErpPurchaseInvoiceAiServiceImpl implements ErpPurchaseInvoiceAiService {
+
+    private static final Set<String> SUPPORTED_EXTENSIONS = Set.of(
+            "pdf", "png", "jpg", "jpeg", "gif", "bmp", "txt", "csv"
+    );
+
+    private static final Set<String> IMAGE_EXTENSIONS = Set.of("png", "jpg", "jpeg", "gif", "bmp");
+
+    private static final String SYSTEM_PROMPT = """
+            浣犳槸涓�浣嶄笓涓氱殑鍙戠エ淇℃伅鎻愬彇涓撳銆傝浠庡彂绁ㄦ枃浠朵腑鎻愬彇缁撴瀯鍖栦俊鎭��
+
+            ## 鎻愬彇瑙勫垯
+            1. 浠旂粏璇嗗埆鍙戠エ涓殑淇℃伅锛氬彂绁ㄧ被鍨嬨�佸彂绁ㄥ彿鐮併�佽喘涔版柟鍚嶇О銆侀攢鍞柟鍚嶇О銆佷环绋庡悎璁¢噾棰濄�佸紑绁ㄦ棩鏈熴�佺◣鐜囥�佸娉ㄧ瓑
+            2. 鍙戠エ鍙风爜涓哄彂绁ㄤ笂"鍙戠エ鍙风爜"鏍忕殑鏁板瓧锛屽師鏍疯繑鍥烇紙涓嶈鍘绘帀鍓嶅 0锛�
+            3. invoiceTitle 涓�"璐拱鏂�"鍚嶇О锛堝彂绁ㄦ姮澶达級
+            4. price 涓�"浠风◣鍚堣锛堝皬鍐欙級"閲戦锛屽崟浣嶄负鍏冿紝淇濈暀涓や綅灏忔暟
+            5. 鏃ユ湡缁熶竴杞崲涓� yyyy-MM-dd 鏍煎紡
+            6. 鏁板瓧瀛楁浠呬繚鐣欐暟瀛楋紝鍘绘帀璐у竵绗﹀彿鍜屽崈鍒嗕綅閫楀彿
+            7. 绋庣巼锛堝 13%銆�9%銆�6%锛夊鍙戠エ涓湭鏄庣‘鏍囨敞锛屼笉濉�
+
+            ## 杈撳嚭鏍煎紡
+            璇锋寜浠ヤ笅JSON鏍煎紡杩斿洖锛堜笉瑕佸寘鍚叾浠栧唴瀹癸級锛�
+            {"invoiceNo": "02564178", "invoiceTitle": "璐拱鏂瑰悕绉�", "supplierName": "閿�鍞柟鍚嶇О", "price": 9000.00, "invoiceTime": "2026-08-01", "invoiceType": "澧炲�肩◣涓撶敤鍙戠エ", "taxRate": 13.0, "remark": "澶囨敞淇℃伅"}
+
+            鎵�鏈夊瓧娈靛潎涓哄彲閫夛紝鏈彁鍙栧埌鐨勫瓧娈典笉瑕佽繑鍥炪��
+            濡傛灉鏂囦欢鍐呭鏃犳硶璇嗗埆涓哄彂绁紝杩斿洖绌篔SON锛歿}
+            """;
+
+    private static final String SYSTEM_PROMPT_IMAGE = """
+            浣犳槸涓�浣嶄笓涓氱殑鍙戠エOCR璇嗗埆涓撳銆傝浠庡彂绁ㄥ浘鐗囦腑鎻愬彇缁撴瀯鍖栦俊鎭��
+
+            ## 鎻愬彇瑙勫垯
+            1. 浠旂粏璇嗗埆鍥剧墖涓殑鍙戠エ鏂囧瓧淇℃伅
+            2. 鎻愬彇鍙戠エ鐩稿叧淇℃伅锛氬彂绁ㄧ被鍨嬨�佸彂绁ㄥ彿鐮併�佽喘涔版柟鍚嶇О銆侀攢鍞柟鍚嶇О銆佷环绋庡悎璁¢噾棰濄�佸紑绁ㄦ棩鏈熴�佺◣鐜囥�佸娉ㄧ瓑
+            3. 鍙戠エ鍙风爜涓哄彂绁ㄤ笂"鍙戠エ鍙风爜"鏍忕殑鏁板瓧锛屽師鏍疯繑鍥烇紙涓嶈鍘绘帀鍓嶅 0锛�
+            4. invoiceTitle 涓�"璐拱鏂�"鍚嶇О锛堝彂绁ㄦ姮澶达級
+            5. price 涓�"浠风◣鍚堣锛堝皬鍐欙級"閲戦锛屽崟浣嶄负鍏冿紝淇濈暀涓や綅灏忔暟
+            6. 鏃ユ湡缁熶竴杞崲涓� yyyy-MM-dd 鏍煎紡
+            7. 绋庣巼濡傚彂绁ㄤ腑鏈槑纭爣娉紝涓嶅~
+
+            ## 杈撳嚭鏍煎紡
+            璇锋寜浠ヤ笅JSON鏍煎紡杩斿洖锛堜笉瑕佸寘鍚叾浠栧唴瀹癸級锛�
+            {"invoiceNo": "02564178", "invoiceTitle": "璐拱鏂瑰悕绉�", "supplierName": "閿�鍞柟鍚嶇О", "price": 9000.00, "invoiceTime": "2026-08-01", "invoiceType": "澧炲�肩◣涓撶敤鍙戠エ", "taxRate": 13.0, "remark": "澶囨敞淇℃伅"}
+
+            鎵�鏈夊瓧娈靛潎涓哄彲閫夛紝鏈彁鍙栧埌鐨勫瓧娈典笉瑕佽繑鍥炪��
+            濡傛灉鍥剧墖鍐呭鏃犳硶璇嗗埆涓哄彂绁紝杩斿洖绌篔SON锛歿}
+            """;
+
+    @Resource
+    private SystemStorageBlobService blobService;
+
+    @Resource
+    private AiChatApi aiChatApi;
+
+    @Override
+    public ErpPurchaseInvoiceOcrRespVO ocrInvoice(ErpPurchaseInvoiceOcrReqVO reqVO) {
+        File tempFile = null;
+        try {
+            // 1. 涓嬭浇鏂囦欢
+            SystemStorageBlobDO blob = blobService.getStorageBlob(reqVO.getBlobId());
+            if (blob == null) {
+                return errorResp("鏂囦欢涓嶅瓨鍦�");
+            }
+
+            // 2. 鏍¢獙鏂囦欢绫诲瀷
+            String ext = StrUtil.isBlank(blob.getOriginalFilename()) ? ""
+                    : FileUtil.extName(blob.getOriginalFilename()).toLowerCase();
+            if (!SUPPORTED_EXTENSIONS.contains(ext)) {
+                return errorResp("涓嶆敮鎸佺殑鏂囦欢绫诲瀷锛岃涓婁紶 PDF/鍥剧墖鏂囦欢");
+            }
+
+            // 3. 鑾峰彇鏂囦欢瀛楄妭
+            tempFile = blobService.getPublicFile(blob.getUidFilename(), blob.getResourceKey());
+            if (tempFile == null || !tempFile.exists()) {
+                return errorResp("鏂囦欢璇诲彇澶辫触");
+            }
+            byte[] fileBytes = Files.readAllBytes(tempFile.toPath());
+            if (fileBytes.length == 0) {
+                return errorResp("鏂囦欢鍐呭涓虹┖锛屾棤娉曡瘑鍒�");
+            }
+
+            // 4. AI 璇嗗埆锛堝浘鐗囪蛋澶氭ā鎬侊紝鏂囨湰璧扮函鏂囨湰锛�
+            boolean isImage = IMAGE_EXTENSIONS.contains(ext);
+            String systemPrompt = isImage ? SYSTEM_PROMPT_IMAGE : SYSTEM_PROMPT;
+            String aiResponse;
+            if (isImage) {
+                String imageBase64 = Base64.getEncoder().encodeToString(fileBytes);
+                String mimeType = getMimeType(ext);
+                aiResponse = aiChatApi.chatWithImage(systemPrompt, "璇疯瘑鍒浘鐗囦腑鐨勫彂绁ㄤ俊鎭�", imageBase64, mimeType);
+            } else {
+                String extractedText = extractTextByType(ext, fileBytes);
+                if (StrUtil.isEmpty(extractedText)) {
+                    return errorResp("鏂囦欢鍐呭涓虹┖锛屾棤娉曡瘑鍒�");
+                }
+                aiResponse = aiChatApi.chat(systemPrompt, extractedText);
+            }
+            log.info("AI OCR 璇嗗埆鍙戠エ缁撴灉: blobId={}, file={}, response={}",
+                    reqVO.getBlobId(), blob.getOriginalFilename(), aiResponse);
+
+            // 5. 瑙f瀽鍝嶅簲
+            return parseOcrResponse(aiResponse);
+
+        } catch (Exception e) {
+            log.warn("AI OCR 璇嗗埆鍙戠エ澶辫触锛宐lobId={}", reqVO.getBlobId(), e);
+            return errorResp("AI 璇嗗埆鏆傛椂涓嶅彲鐢紝璇锋墜鍔ㄥ綍鍏�");
+        } finally {
+            if (tempFile != null && tempFile.exists()) {
+                tempFile.delete();
+            }
+        }
+    }
+
+    private ErpPurchaseInvoiceOcrRespVO errorResp(String message) {
+        ErpPurchaseInvoiceOcrRespVO error = new ErpPurchaseInvoiceOcrRespVO();
+        error.setRawText(message);
+        return error;
+    }
+
+    private String extractTextByType(String ext, byte[] bytes) throws IOException {
+        return switch (ext) {
+            case "pdf" -> extractPdfText(bytes);
+            case "txt", "csv" -> new String(bytes, StandardCharsets.UTF_8);
+            default -> "";
+        };
+    }
+
+    private String extractPdfText(byte[] bytes) throws IOException {
+        try (PDDocument doc = Loader.loadPDF(bytes)) {
+            PDFTextStripper stripper = new PDFTextStripper();
+            stripper.setSortByPosition(true);
+            String text = stripper.getText(doc);
+            return StrUtil.isNotBlank(text) ? text.trim() : "";
+        }
+    }
+
+    private String getMimeType(String ext) {
+        return switch (ext) {
+            case "jpg", "jpeg" -> "image/jpeg";
+            case "gif" -> "image/gif";
+            case "bmp" -> "image/bmp";
+            default -> "image/png";
+        };
+    }
+
+    private ErpPurchaseInvoiceOcrRespVO parseOcrResponse(String aiResponse) {
+        ErpPurchaseInvoiceOcrRespVO respVO = new ErpPurchaseInvoiceOcrRespVO();
+        try {
+            String json = aiResponse;
+            int start = json.indexOf("{");
+            int end = json.lastIndexOf("}");
+            if (start >= 0 && end > start) {
+                json = json.substring(start, end + 1);
+            }
+            Map<String, Object> parsed = JSONUtil.toBean(json, Map.class);
+
+            respVO.setInvoiceNo((String) parsed.get("invoiceNo"));
+            respVO.setInvoiceTitle((String) parsed.get("invoiceTitle"));
+            respVO.setSupplierName((String) parsed.get("supplierName"));
+            respVO.setInvoiceType((String) parsed.get("invoiceType"));
+            respVO.setRemark((String) parsed.get("remark"));
+
+            respVO.setPrice(toBigDecimal(parsed.get("price")));
+            respVO.setTaxRate(toBigDecimal(parsed.get("taxRate")));
+
+            Object it = parsed.get("invoiceTime");
+            if (it instanceof String s && StrUtil.isNotBlank(s)) {
+                respVO.setInvoiceTime(parseDate(s));
+            }
+        } catch (Exception e) {
+            log.warn("瑙f瀽 AI OCR 鍝嶅簲澶辫触: {}", aiResponse, e);
+            respVO.setRawText(aiResponse);
+        }
+        return respVO;
+    }
+
+    /**
+     * 灏� AI 杩斿洖鐨勬暟瀛楋紙鍙兘鏄� Number 鎴栨暟瀛楀瓧绗︿覆锛夎浆鎹负 BigDecimal
+     */
+    private BigDecimal toBigDecimal(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof Number n) {
+            return BigDecimal.valueOf(n.doubleValue());
+        }
+        if (value instanceof String s) {
+            s = s.replace(",", "").trim();
+            if (StrUtil.isBlank(s)) {
+                return null;
+            }
+            try {
+                return new BigDecimal(s);
+            } catch (NumberFormatException ignored) {
+                return null;
+            }
+        }
+        return null;
+    }
+
+    /**
+     * 瑙f瀽鏃ユ湡锛屽吋瀹� yyyy-MM-dd銆亂yyy/MM/dd銆亂yyy骞碝M鏈坉d鏃� 绛夊父瑙佹牸寮�
+     */
+    private LocalDate parseDate(String value) {
+        String s = value.trim()
+                .replace("骞�", "-")
+                .replace("鏈�", "-")
+                .replace("鏃�", "")
+                .replace("/", "-");
+        try {
+            return LocalDate.parse(s);
+        } catch (Exception ignored) {
+            return null;
+        }
+    }
+
+}

--
Gitblit v1.9.3