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