From d640da3dac5b5f811284ab9a7c386da1e7ab6739 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 30 四月 2026 17:30:57 +0800
Subject: [PATCH] feat(ai): 增强AI文件提取和审批待办功能
---
src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java | 16
src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java | 12
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java | 54 +
src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java | 803 +++++++++++++++++++++++++++++
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java | 44 +
src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java | 35
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java | 334 ++++++++++++
src/main/resources/purchase-agent-prompt.txt | 11
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java | 55 +
doc/采购智能体多文件分析前端联调说明.md | 183 ++++++
src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java | 26
src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java | 14
src/main/resources/approve-todo-agent-prompt.txt | 17
13 files changed, 1,550 insertions(+), 54 deletions(-)
diff --git "a/doc/\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md" "b/doc/\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..cb9e3cf
--- /dev/null
+++ "b/doc/\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md"
@@ -0,0 +1,183 @@
+# 閲囪喘鏅鸿兘浣撳鏂囦欢鍒嗘瀽鍓嶇鑱旇皟璇存槑
+
+## 娴佺▼璇存槑
+
+鍚庣宸叉柊澧為噰璐櫤鑳戒綋澶氭枃浠跺垎鏋愮‘璁ゆ祦绋嬶細
+
+1. 鍓嶇涓婁紶澶氫釜閲囪喘鐩稿叧鏂囦欢锛屽苟闄勫甫鐢ㄦ埛瑕佹眰銆�
+2. 鍚庣鎻愬彇鏂囦欢鍐呭锛屼氦缁欓噰璐櫤鑳戒綋鍒嗘瀽銆�
+3. 鏅鸿兘浣撹繑鍥炲緟瀹㈡埛纭鐨勭粨鏋勫寲 JSON銆�
+4. 鍓嶇灞曠ず鎽樿銆侀闄┿�佺己澶卞瓧娈靛拰寰呭鐞嗘暟鎹��
+5. 瀹㈡埛纭鎴栬ˉ鍏呮暟鎹悗锛屽墠绔皟鐢ㄧ‘璁ゆ帴鍙c��
+6. 鍚庣鏍规嵁纭鍚庣殑鏁版嵁鎵ц瀵瑰簲閲囪喘涓氬姟澶勭悊銆�
+
+鍒嗘瀽鎺ュ彛涓嶄細钀藉簱锛屽彧鏈夌‘璁ゆ帴鍙d細鎵ц涓氬姟澶勭悊銆�
+
+## 鎺ュ彛 1锛氶噰璐鏂囦欢鍒嗘瀽
+
+```http
+POST /purchase-ai/analyze-files
+Content-Type: multipart/form-data
+```
+
+璇锋眰鍙傛暟锛�
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+| --- | --- | --- | --- |
+| files | file[] | 鏄� | 澶氭枃浠朵笂浼犲瓧娈碉紝瀛楁鍚嶅繀椤绘槸 `files` |
+| message | string | 鍚� | 鐢ㄦ埛瑕佹眰锛屼緥濡傦細璇锋牴鎹繖浜涢噰璐悎鍚屽拰鏄庣粏鏁寸悊閲囪喘鍙拌处鏁版嵁 |
+| memoryId | string | 鍚� | 浼氳瘽 ID锛屼笉浼犳椂鍚庣浼氳嚜鍔ㄧ敓鎴愬唴閮ㄤ細璇� |
+
+杩斿洖锛�
+
+```http
+Content-Type: text/stream;charset=utf-8
+```
+
+鍓嶇闇�瑕佹嫾鎺ュ畬鏁存祦寮忔枃鏈悗鍐嶆墽琛� `JSON.parse`銆�
+
+杩斿洖 JSON 缁撴瀯绀轰緥锛�
+
+```json
+{
+ "success": true,
+ "businessType": "purchase_ledger",
+ "action": "confirm_required",
+ "description": "宸叉牴鎹枃浠舵暣鐞嗗嚭閲囪喘鍙拌处鑽夌锛岃纭銆�",
+ "confidence": 0.86,
+ "missingFields": [],
+ "warnings": [],
+ "payload": {},
+ "preview": []
+}
+```
+
+瀛楁璇存槑锛�
+
+| 瀛楁 | 璇存槑 |
+| --- | --- |
+| success | 鏄惁鍒嗘瀽鎴愬姛 |
+| businessType | 涓氬姟绫诲瀷锛歚purchase_ledger`銆乣payment_registration`銆乣purchase_return_order`銆乣unknown` |
+| action | 鍥哄畾涓� `confirm_required` |
+| description | 涓枃璇存槑 |
+| confidence | 缃俊搴︼紝0 鍒� 1 |
+| missingFields | 缂哄け瀛楁锛屽墠绔渶瑕佹彁绀虹敤鎴疯ˉ鍏� |
+| warnings | 椋庨櫓鎻愮ず |
+| payload | 寰呭鎴风‘璁ゅ苟鎻愪氦缁欑‘璁ゆ帴鍙g殑鏁版嵁 |
+| preview | 缁欏鎴风‘璁ょ敤鐨勪腑鏂囨憳瑕� |
+
+## 鎺ュ彛 2锛氱‘璁ゅ苟鎵ц涓氬姟澶勭悊
+
+```http
+POST /purchase-ai/analyze-files/confirm
+Content-Type: application/json
+```
+
+璇锋眰浣擄細
+
+```json
+{
+ "businessType": "purchase_ledger",
+ "payload": {
+ }
+}
+```
+
+褰撳墠鏀寔鐨� `businessType`锛�
+
+| businessType | 璇存槑 | 鍚庣澶勭悊 |
+| --- | --- | --- |
+| purchase_ledger | 閲囪喘鍙拌处 | 璋冪敤閲囪喘鍙拌处鏂板/缂栬緫 |
+| payment_registration | 浠樻鐧昏 | 璋冪敤浠樻鐧昏鏂板 |
+| purchase_return_order | 閲囪喘閫�璐у崟 | 璋冪敤閲囪喘閫�璐у崟鏂板 |
+
+纭鎺ュ彛杩斿洖鏅�� `AjaxResult`銆�
+
+## 閲囪喘鍙拌处 Payload 绾﹀畾
+
+閲囪喘鍙拌处纭鎺ㄨ崘浣跨敤涓や釜闆嗗悎锛�
+
+```json
+{
+ "businessType": "purchase_ledger",
+ "payload": {
+ "purchaseLedgers": []
+ }
+}
+```
+
+瀛楁绾﹀畾锛�
+
+- `purchaseLedgers` 鏀鹃噰璐鍗�/閲囪喘鍙拌处涓昏〃鏁版嵁锛屽瓧娈靛悕蹇呴』涓� `PurchaseLedgerDto` 淇濇寔涓�鑷淬��
+- 浜у搧鏄庣粏鏀惧湪姣忔潯 `purchaseLedgers[i].productData` 涓紝瀵瑰簲 `PurchaseLedgerDto` 鐨� `private List<SalesLedgerProduct> productData;`銆�
+- 椤跺眰 `payload.productData` 浠呬綔涓烘棫鏍煎紡鍏煎锛屼笉寤鸿鍓嶇缁х画浣跨敤銆�
+- 鏂囦欢涓殑鈥滈噰璐崟鍙封�濆氨鏄�滈噰璐悎鍚屽彿鈥濓紝鍓嶇鍙互缁熶竴鏄犲皠鎴� `purchaseContractNumber`銆�
+- 鏂囦欢涓殑鈥滈攢鍞崟鍙封�濆氨鏄�滈攢鍞悎鍚屽彿鈥濓紝鍓嶇鍙互缁熶竴鏄犲皠鎴� `salesContractNo`銆�
+- 鏃ユ湡瀛楁缁熶竴浣跨敤 `yyyy-MM-dd`锛屼緥濡� `2026-04-30`锛涗笉瑕佹彁浜� `4/30/26`銆乣2026/4/30`銆乣2026骞�4鏈�30鏃 鎴栧甫鏃跺垎绉掔殑鏍煎紡銆�
+- 閲囪喘鍙拌处涓嶉渶瑕佸墠绔紶瀹℃壒浜猴紝涓嶈鎻愪氦 `approveUserIds`銆乣approverId`銆�
+- `missingFields` 闈㈠悜瀹㈡埛灞曠ず锛屽彧鏀句腑鏂囩己澶遍」锛屼緥濡� `渚涘簲鍟嗗悕绉癭銆乣鍚◣鍗曚环`锛屼笉瑕佸睍绀鸿嫳鏂囧瓧娈靛悕銆�
+- 閲囪喘鍙拌处涓氬姟蹇呭~锛氶噰璐悎鍚屽彿銆佷緵搴斿晢鍚嶇О鎴栦緵搴斿晢ID銆�
+- 浜у搧鏄庣粏涓氬姟蹇呭~锛氫骇鍝佸悕绉般�佽鏍煎瀷鍙枫�佸崟浣嶃�佹暟閲忋�佸惈绋庡崟浠枫�佸惈绋庢�讳环锛涘鏋滃彧鏈夊惈绋庢�讳环鍜屾暟閲忥紝鍚庣浼氳嚜鍔ㄨ绠楀惈绋庡崟浠凤紱濡傛灉鍙湁鍚◣鍗曚环鍜屾暟閲忥紝鍚庣浼氳嚜鍔ㄨ绠楀惈绋庢�讳环銆�
+- 浜у搧鏄庣粏鍙�氳繃 `purchaseContractNumber`銆乣purchaseContractNo`銆乣閲囪喘鍚堝悓鍙穈銆乣閲囪喘鍗曞彿`銆乣閲囪喘璁㈠崟鍙穈 鍏宠仈瀵瑰簲閲囪喘璁㈠崟锛涗篃鍙�氳繃 `salesContractNo`銆乣salesContractNumber`銆乣閿�鍞悎鍚屽彿`銆乣閿�鍞崟鍙穈銆乣閿�鍞鍗曞彿` 杈呭姪鍖归厤銆�
+
+`purchaseLedgers` 鍗曟潯璁板綍鍏佽浣跨敤鐨� `PurchaseLedgerDto` 瀛楁锛�
+
+```text
+entryDateStart, entryDateEnd, id, purchaseContractNumber,
+supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo,
+salesContractNoId, projectName, entryDate, executionDate, remarks,
+attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type,
+productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId,
+productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId,
+contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type,
+paymentMethod, approvalStatus, templateName
+```
+
+绀轰緥锛�
+
+```json
+{
+ "purchaseLedgers": [
+ {
+ "purchaseContractNumber": "CG-2026-001",
+ "supplierName": "鍗楅�氱ず渚嬩緵搴斿晢",
+ "salesContractNo": "XS-2026-001",
+ "projectName": "绀轰緥椤圭洰",
+ "entryDate": "2026-04-30",
+ "executionDate": "2026-04-30",
+ "contractAmount": 120000,
+ "remarks": "鐢辨枃浠跺垎鏋愮敓鎴愶紝寰呯‘璁�",
+ "productData": [
+ {
+ "productCategory": "绀轰緥浜у搧",
+ "specificationModel": "鍨嬪彿A",
+ "unit": "浠�",
+ "quantity": 10,
+ "taxInclusiveUnitPrice": 12000,
+ "taxInclusiveTotalPrice": 120000,
+ "type": 2
+ }
+ ]
+ }
+ ]
+}
+```
+
+## 鍓嶇澶勭悊寤鸿
+
+1. 鐢ㄦ埛閫夋嫨澶氫釜鏂囦欢锛屽~鍐欏垎鏋愯姹傘��
+2. 浣跨敤 `multipart/form-data` 璋冪敤 `/purchase-ai/analyze-files`銆�
+3. 鎷兼帴娴佸紡杩斿洖鏂囨湰銆�
+4. 瀵瑰畬鏁存枃鏈墽琛� `JSON.parse`銆�
+5. 灞曠ず `preview`銆乣warnings`銆乣missingFields` 鍜� `payload`銆�
+6. 濡傛灉 `missingFields` 涓嶄负绌猴紝寮曞鐢ㄦ埛琛ュ厖鎴栫紪杈� `payload`銆�
+7. 鐢ㄦ埛纭鍚庯紝灏� `businessType` 鍜岀‘璁ゅ悗鐨� `payload` 鎻愪氦鍒� `/purchase-ai/analyze-files/confirm`銆�
+
+## 娉ㄦ剰浜嬮」
+
+- 鏂囦欢涓婁紶瀛楁鍚嶅繀椤绘槸 `files`銆�
+- 鍒嗘瀽鎺ュ彛鍙敓鎴愬緟纭鏁版嵁锛屼笉浼氭墽琛屼笟鍔¤惤搴撱��
+- 纭鎺ュ彛鎵嶄細鎵ц涓氬姟澶勭悊銆�
+- 濡傛灉 `payload` 缂哄皯蹇呰涓氬姟 ID锛岀‘璁ゆ帴鍙e彲鑳借繑鍥炰笟鍔℃牎楠岄敊璇��
+- 鍓嶇闇�瑕佹妸 `missingFields` 鏄庣‘灞曠ず缁欑敤鎴枫��
+- AI 杩斿洖鍐呭鎸夊悎娉� JSON 澶勭悊锛屼笉瑕佹寜鏅�氳嚜鐒惰瑷�灞曠ず銆�
diff --git a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
index 5911521..9f8499d 100644
--- a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
@@ -44,7 +44,7 @@
extractTimeRange(text)
);
}
- if (containsAny(text, "娴佽浆", "杩涘害", "鑺傜偣", "鏃ュ織")) {
+ if (containsAny(text, "娴佽浆", "杩涘害", "鑺傜偣", "鏃ュ織", "鍗″湪", "鍗″埌", "褰撳墠瀹℃壒浜�", "澶勭悊璁板綍")) {
return StringUtils.hasText(approveId)
? approveTodoTools.getTodoProgress(memoryId, approveId)
: missingApproveId("todo_progress", "鏌ヨ瀹℃壒杩涘害闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
@@ -54,19 +54,20 @@
? approveTodoTools.getTodoDetail(memoryId, approveId)
: missingApproveId("todo_detail", "鏌ヨ瀹℃壒璇︽儏闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
- if (containsAny(text, "鍙栨秷瀹℃牳", "鎾ら攢瀹℃牳", "鍥為��瀹℃牳")) {
+ if (containsAny(text, "鍙栨秷瀹℃牳", "鎾ら攢瀹℃牳", "鍥為��瀹℃牳", "鎾ら攢瀹℃壒", "鎾ゅ洖瀹℃壒")
+ || (containsAny(text, "鎾ら攢", "鎾ゅ洖") && containsAny(text, "瀹℃壒鎿嶄綔", "瀹℃牳鎿嶄綔"))) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractTail(text, "鍘熷洜"))
+ ? approveTodoTools.cancelReviewTodo(memoryId, approveId, firstNonBlank(extractTail(text, "鍘熷洜"), extractTail(text, "澶囨敞")))
: missingApproveId("cancel_review_action", "鍙栨秷瀹℃牳闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
- if (containsAny(text, "鍒犻櫎")) {
+ if (containsAny(text, "鍒犻櫎", "绉婚櫎")) {
return StringUtils.hasText(approveId)
? approveTodoTools.deleteTodo(memoryId, approveId)
: missingApproveId("delete_action", "鍒犻櫎瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (containsAny(text, "椹冲洖", "鎷掔粷")) {
return StringUtils.hasText(approveId)
- ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractTail(text, "鍘熷洜"))
+ ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", firstNonBlank(extractTail(text, "鍘熷洜"), extractTail(text, "澶囨敞")))
: missingApproveId("review_action", "椹冲洖瀹℃壒闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
if (containsAny(text, "瀹℃牳閫氳繃", "瀹℃壒閫氳繃", "閫氳繃瀹℃壒", "鍚屾剰瀹℃壒", "瀹℃壒鍚屾剰")) {
@@ -79,7 +80,7 @@
&& !containsAny(text, "鏈�氳繃", "閫氳繃鐜�", "瀹℃壒閫氳繃鐜�", "瀹℃牳閫氳繃鐜�")) {
return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "澶囨敞"));
}
- if (containsAny(text, "淇敼")) {
+ if (containsAny(text, "淇敼", "鏇存柊", "鍙樻洿")) {
return StringUtils.hasText(approveId)
? approveTodoTools.updateTodo(
memoryId,
@@ -93,19 +94,20 @@
extractValue(text, "澶囨敞"))
: missingApproveId("update_action", "淇敼瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
}
- if (containsAny(text, "鍒楄〃", "寰呭姙", "鏌ヨ瀹℃壒")) {
+ if (containsAny(text, "鍒楄〃", "寰呭姙", "鏌ヨ瀹℃壒", "鍗曟嵁", "娴佺▼", "瀹℃壒鎵�")) {
return approveTodoTools.listTodos(
memoryId,
extractStatus(text),
extractApproveType(text),
extractKeyword(text),
- extractLimit(text));
+ extractLimit(text),
+ extractScope(text));
}
return null;
}
private boolean isStatsIntent(String text) {
- if (containsAny(text, "缁熻", "鍒嗘瀽", "鍥捐〃", "瓒嬪娍", "鍗犳瘮", "姹囨��", "鎬婚噺")) {
+ if (containsAny(text, "缁熻", "鍒嗘瀽", "鍥捐〃", "瓒嬪娍", "鍗犳瘮", "姹囨��", "鎬婚噺", "鍒嗗竷", "鍚勬湁澶氬皯", "鏈夊灏�")) {
return true;
}
boolean hasQueryWord = containsAny(text, "鏌ヨ", "鏌ョ湅", "鐪嬩笅", "鐪嬬湅", "鑾峰彇");
@@ -141,13 +143,13 @@
if (containsAny(text, "寰呭鏍�", "寰呭鎵�")) {
return "pending";
}
- if (containsAny(text, "瀹℃牳涓�")) {
+ if (containsAny(text, "瀹℃牳涓�", "澶勭悊涓�", "澶勭悊涓殑", "鍔炵悊涓�")) {
return "processing";
}
- if (containsAny(text, "宸查�氳繃", "瀹℃牳瀹屾垚")) {
+ if (containsAny(text, "宸查�氳繃", "閫氳繃", "瀹℃牳瀹屾垚", "瀹℃壒瀹屾垚")) {
return "approved";
}
- if (containsAny(text, "鏈�氳繃", "椹冲洖")) {
+ if (containsAny(text, "鏈�氳繃", "椹冲洖", "宸查┏鍥�", "鎷掔粷")) {
return "rejected";
}
if (containsAny(text, "閲嶆柊鎻愪氦")) {
@@ -187,7 +189,11 @@
private String extractKeyword(String text) {
String cleaned = text
.replace("鏌ヨ", "")
+ .replace("鏌ョ湅", "")
+ .replace("鍒楀嚭", "")
+ .replace("甯垜", "")
.replace("瀹℃壒", "")
+ .replace("鍗曟嵁", "")
.replace("寰呭姙", "")
.replace("鍒楄〃", "")
.replace("鍓�10鏉�", "")
@@ -263,6 +269,20 @@
return matcher.find() ? matcher.group(2).trim() : null;
}
+ private String extractScope(String text) {
+ if (containsAny(text, "鎴戝彂璧�", "鎴戞彁浜�", "鎴戠敵璇�", "鐢宠浜烘槸鎴�")) {
+ return "applicant";
+ }
+ if (containsAny(text, "寰呮垜瀹℃壒", "寰呮垜瀹℃牳", "鎴戝鐞�", "鎴戝鎵�", "褰撳墠寰呮垜", "闇�瑕佹垜澶勭悊")) {
+ return "approver";
+ }
+ return "related";
+ }
+
+ private String firstNonBlank(String first, String second) {
+ return StringUtils.hasText(first) ? first : second;
+ }
+
private String missingApproveId(String type, String description) {
Map<String, Object> result = new LinkedHashMap<>();
result.put("success", false);
diff --git a/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
index 2d4e6b8..5991fc3 100644
--- a/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
@@ -26,6 +26,51 @@
}
String text = message.trim();
+ if (containsAny(text, "鎺掕", "鎺掑悕", "鍓嶅嚑", "鍓嶄簲", "鍓嶅崄") && containsAny(text, "鐗╂枡", "浜у搧", "鍘熸潗鏂�", "閲囪喘閲戦", "閲戦")) {
+ return purchaseAgentTools.rankPurchaseMaterials(
+ memoryId,
+ extractStartDate(text),
+ extractEndDate(text),
+ text,
+ extractLimit(text)
+ );
+ }
+ if (containsAny(text, "鏈叆搴�", "寰呭叆搴�", "娌℃湁鍏ュ簱", "杩樻湭鍏ュ簱")) {
+ return purchaseAgentTools.listUnstockedPurchaseOrders(
+ memoryId,
+ extractStartDate(text),
+ extractEndDate(text),
+ extractKeyword(text),
+ extractLimit(text)
+ );
+ }
+ if (containsAny(text, "鍒拌揣寮傚父", "鍒拌揣鏈夊紓甯�", "寮傚父鍒拌揣", "鍒拌揣闂", "渚涘簲鍟嗗埌璐у紓甯�")) {
+ return purchaseAgentTools.listArrivalExceptions(
+ memoryId,
+ extractStartDate(text),
+ extractEndDate(text),
+ text,
+ extractLimit(text)
+ );
+ }
+ if (containsAny(text, "寰呬粯娆�", "鏈粯娆�", "鏈粯娓�", "寰呮敮浠�", "搴斾粯")) {
+ return purchaseAgentTools.listPendingPaymentOrders(
+ memoryId,
+ extractStartDate(text),
+ extractEndDate(text),
+ extractKeyword(text),
+ extractLimit(text)
+ );
+ }
+ if (containsAny(text, "閫�璐�", "閫�鏂�", "鎷掓敹")) {
+ return purchaseAgentTools.listPurchaseReturns(
+ memoryId,
+ extractStartDate(text),
+ extractEndDate(text),
+ extractKeyword(text),
+ extractLimit(text)
+ );
+ }
if (isStatsIntent(text)) {
return purchaseAgentTools.getPurchaseStats(
memoryId,
@@ -37,7 +82,7 @@
if (containsAny(text, "璇︽儏", "鏄庣粏") && extractId(text) != null) {
return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, extractId(text));
}
- if (containsAny(text, "鍙拌处", "閲囪喘鍗�", "鍚堝悓", "鍒楄〃", "鏌ヨ")) {
+ if (containsAny(text, "鍙拌处", "閲囪喘鍗�", "閲囪喘璁㈠崟", "璁㈠崟", "鍚堝悓", "鍒楄〃", "鏌ヨ")) {
return purchaseAgentTools.listPurchaseLedgers(
memoryId,
extractKeyword(text),
@@ -50,7 +95,7 @@
}
private boolean isStatsIntent(String text) {
- if (containsAny(text, "缁熻", "鍒嗘瀽", "鎶ヨ〃", "姹囨��", "瓒嬪娍", "鏁版嵁鐪嬫澘")) {
+ if (containsAny(text, "缁熻", "鍒嗘瀽", "鎶ヨ〃", "姹囨��", "瓒嬪娍", "鏁版嵁鐪嬫澘", "鎯呭喌", "鏈夊灏�")) {
return true;
}
boolean queryWord = containsAny(text, "鏌ヨ", "鏌ョ湅", "鐪嬩笅", "鐪嬬湅", "鑾峰彇");
@@ -100,8 +145,14 @@
.replace("鏌ヨ", "")
.replace("鏌ョ湅", "")
.replace("閲囪喘", "")
+ .replace("閲囪喘鍗�", "")
+ .replace("閲囪喘璁㈠崟", "")
+ .replace("璁㈠崟", "")
.replace("鍙拌处", "")
.replace("鍒楄〃", "")
+ .replace("鍝簺", "")
+ .replace("鍒楀嚭", "")
+ .replace("甯垜", "")
.replace("鏈�杩�10鏉�", "")
.replace("鍓�10鏉�", "")
.trim();
diff --git a/src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java b/src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java
new file mode 100644
index 0000000..f727981
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java
@@ -0,0 +1,26 @@
+package com.ruoyi.ai.bean;
+
+import java.util.Map;
+
+public class PurchaseAiConfirmRequest {
+
+ private String businessType;
+
+ private Map<String, Object> payload;
+
+ public String getBusinessType() {
+ return businessType;
+ }
+
+ public void setBusinessType(String businessType) {
+ this.businessType = businessType;
+ }
+
+ public Map<String, Object> getPayload() {
+ return payload;
+ }
+
+ public void setPayload(Map<String, Object> payload) {
+ this.payload = payload;
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java b/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
index 6ddf304..62061ee 100644
--- a/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
+++ b/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
@@ -1,8 +1,10 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
+import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@@ -17,4 +19,14 @@
.chatMemoryStore(mongoChatMemoryStore)
.build();
}
+
+ @Bean("purchaseVisionStreamingChatModel")
+ QwenStreamingChatModel purchaseVisionStreamingChatModel(
+ @Value("${langchain4j.community.dashscope.streaming-chat-model.api-key}") String apiKey) {
+ return QwenStreamingChatModel.builder()
+ .apiKey(apiKey)
+ .modelName("qwen-vl-max")
+ .isMultimodalModel(true)
+ .build();
+ }
}
diff --git a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
index f3e5aec..2bb0625 100644
--- a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -1,20 +1,45 @@
package com.ruoyi.ai.controller;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
import com.ruoyi.ai.assistant.PurchaseAgent;
import com.ruoyi.ai.assistant.PurchaseIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
+import com.ruoyi.ai.bean.PurchaseAiConfirmRequest;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
+import com.ruoyi.ai.service.AiFileTextExtractor;
import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.basic.mapper.SupplierManageMapper;
+import com.ruoyi.basic.pojo.SupplierManage;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
+import com.ruoyi.purchase.dto.PurchaseLedgerDto;
+import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
+import com.ruoyi.purchase.pojo.PaymentRegistration;
+import com.ruoyi.purchase.service.IPaymentRegistrationService;
+import com.ruoyi.purchase.service.IPurchaseLedgerService;
+import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
+import dev.langchain4j.data.image.Image;
import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.Content;
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.TextContent;
import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.StreamingChatLanguageModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
@@ -22,31 +47,73 @@
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
import reactor.core.publisher.Flux;
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Base64;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.UUID;
@Tag(name = "閲囪喘鏅鸿兘浣�")
@RestController
@RequestMapping("/purchase-ai")
public class PurchaseAiController extends BaseController {
+ private static final String PURCHASE_FILE_ANALYZE_MEMORY_PREFIX = "purchase-file-analyze::";
+ private static final int MAX_FILE_COUNT = 10;
+ private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000;
+ private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000;
+
private final PurchaseAgent purchaseAgent;
private final PurchaseIntentExecutor purchaseIntentExecutor;
private final AiSessionUserContext aiSessionUserContext;
private final MongoChatMemoryStore mongoChatMemoryStore;
private final AiChatSessionService aiChatSessionService;
+ private final AiFileTextExtractor aiFileTextExtractor;
+ private final ObjectMapper objectMapper;
+ private final IPurchaseLedgerService purchaseLedgerService;
+ private final IPaymentRegistrationService paymentRegistrationService;
+ private final PurchaseReturnOrdersService purchaseReturnOrdersService;
+ private final SupplierManageMapper supplierManageMapper;
+ private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
public PurchaseAiController(PurchaseAgent purchaseAgent,
PurchaseIntentExecutor purchaseIntentExecutor,
AiSessionUserContext aiSessionUserContext,
MongoChatMemoryStore mongoChatMemoryStore,
- AiChatSessionService aiChatSessionService) {
+ AiChatSessionService aiChatSessionService,
+ AiFileTextExtractor aiFileTextExtractor,
+ ObjectMapper objectMapper,
+ IPurchaseLedgerService purchaseLedgerService,
+ IPaymentRegistrationService paymentRegistrationService,
+ PurchaseReturnOrdersService purchaseReturnOrdersService,
+ SupplierManageMapper supplierManageMapper,
+ @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
this.purchaseAgent = purchaseAgent;
this.purchaseIntentExecutor = purchaseIntentExecutor;
this.aiSessionUserContext = aiSessionUserContext;
this.mongoChatMemoryStore = mongoChatMemoryStore;
this.aiChatSessionService = aiChatSessionService;
+ this.aiFileTextExtractor = aiFileTextExtractor;
+ this.objectMapper = objectMapper;
+ this.purchaseLedgerService = purchaseLedgerService;
+ this.paymentRegistrationService = paymentRegistrationService;
+ this.purchaseReturnOrdersService = purchaseReturnOrdersService;
+ this.supplierManageMapper = supplierManageMapper;
+ this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
}
@Operation(summary = "閲囪喘瀵硅瘽")
@@ -81,6 +148,84 @@
.doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
}
+ @Operation(summary = "閲囪喘澶氭枃浠跺垎鏋�")
+ @PostMapping(value = "/analyze-files", consumes = "multipart/form-data", produces = "text/stream;charset=utf-8")
+ public Flux<String> analyzeFiles(@RequestParam("files") MultipartFile[] files,
+ @RequestParam(value = "message", required = false) String message,
+ @RequestParam(value = "memoryId", required = false) String memoryId) {
+ if (files == null || files.length == 0) {
+ return Flux.just("files涓嶈兘涓虹┖");
+ }
+ if (files.length > MAX_FILE_COUNT) {
+ return Flux.just("涓�娆℃渶澶氬垎鏋�" + MAX_FILE_COUNT + "涓枃浠�");
+ }
+
+ String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
+ String finalMemoryId = rawMemoryId.startsWith(PURCHASE_FILE_ANALYZE_MEMORY_PREFIX)
+ ? rawMemoryId
+ : PURCHASE_FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
+
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ aiSessionUserContext.bind(finalMemoryId, loginUser);
+
+ String finalMessage = StringUtils.hasText(message)
+ ? message
+ : "璇峰垎鏋愯繖浜涢噰璐枃浠讹紝鎻愬彇鍙敤浜庝笟鍔″鐞嗙殑鏁版嵁锛屽苟鏁寸悊鎴愬緟瀹㈡埛纭鐨勬牸寮�";
+
+ String fileContent;
+ try {
+ fileContent = buildMultiFileContent(files);
+ } catch (IllegalArgumentException ex) {
+ return Flux.just(ex.getMessage());
+ } catch (IOException ex) {
+ return Flux.just("鏂囦欢璇诲彇澶辫触");
+ }
+
+ if (!StringUtils.hasText(fileContent)) {
+ return Flux.just("鏈彁鍙栧埌鏈夋晥鏂囦欢鍐呭");
+ }
+
+ String userPrompt = buildPurchaseFileAnalyzePrompt(finalMessage, fileContent);
+ aiChatSessionService.touchSession(finalMemoryId, loginUser, "閲囪喘澶氭枃浠跺垎鏋�: " + finalMessage);
+
+ if (containsImageFile(files)) {
+ return chatWithPurchaseVisionModel(finalMemoryId, userPrompt, files)
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+ }
+
+ return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt))
+ .onErrorResume(NoSuchElementException.class, ex -> {
+ mongoChatMemoryStore.deleteMessages(finalMemoryId);
+ return purchaseAgent.chat(finalMemoryId, userPrompt);
+ })
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+ }
+
+ @Operation(summary = "閲囪喘澶氭枃浠跺垎鏋愮‘璁ゅ鐞�")
+ @PostMapping("/analyze-files/confirm")
+ public AjaxResult confirmAnalyzeResult(@RequestBody PurchaseAiConfirmRequest request) {
+ if (request == null || !StringUtils.hasText(request.getBusinessType())) {
+ return AjaxResult.error("businessType涓嶈兘涓虹┖");
+ }
+ if (request.getPayload() == null || request.getPayload().isEmpty()) {
+ return AjaxResult.error("payload涓嶈兘涓虹┖");
+ }
+
+ try {
+ String businessType = request.getBusinessType().trim();
+ return switch (businessType) {
+ case "purchase_ledger" -> processPurchaseLedger(request.getPayload());
+ case "payment_registration" -> processPaymentRegistration(request.getPayload());
+ case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload());
+ default -> AjaxResult.error("鏆備笉鏀寔璇ヤ笟鍔$被鍨�: " + businessType);
+ };
+ } catch (Exception ex) {
+ return AjaxResult.error(toCustomerMessage(ex));
+ }
+ }
+
@Operation(summary = "閲囪喘浼氳瘽鍒楄〃")
@GetMapping("/history/sessions")
public AjaxResult listSessions() {
@@ -99,4 +244,660 @@
aiSessionUserContext.remove(memoryId);
return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
}
+
+ private String buildMultiFileContent(MultipartFile[] files) throws IOException {
+ StringBuilder builder = new StringBuilder();
+ int totalLength = 0;
+ for (MultipartFile file : files) {
+ String text = aiFileTextExtractor.extractText(file);
+ if (!StringUtils.hasText(text)) {
+ continue;
+ }
+ String limitedText = text.length() > MAX_SINGLE_FILE_TEXT_LENGTH
+ ? text.substring(0, MAX_SINGLE_FILE_TEXT_LENGTH)
+ : text;
+ if (totalLength + limitedText.length() > MAX_TOTAL_FILE_TEXT_LENGTH) {
+ int remain = MAX_TOTAL_FILE_TEXT_LENGTH - totalLength;
+ if (remain <= 0) {
+ break;
+ }
+ limitedText = limitedText.substring(0, remain);
+ }
+ builder.append("\n--- 鏂囦欢: ")
+ .append(file.getOriginalFilename())
+ .append(" ---\n")
+ .append(limitedText)
+ .append('\n');
+ totalLength += limitedText.length();
+ }
+ return builder.toString();
+ }
+
+ private boolean containsImageFile(MultipartFile[] files) {
+ for (MultipartFile file : files) {
+ if (aiFileTextExtractor.isImageFile(file)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) {
+ return Flux.create(sink -> {
+ try {
+ List<Content> contents = new ArrayList<>();
+ contents.add(TextContent.from(userPrompt));
+ for (MultipartFile file : files) {
+ if (!aiFileTextExtractor.isImageFile(file)) {
+ continue;
+ }
+ contents.add(TextContent.from("涓嬮潰杩欏紶鍥剧墖鏂囦欢鍚嶏細" + file.getOriginalFilename()));
+ contents.add(ImageContent.from(Image.builder()
+ .base64Data(Base64.getEncoder().encodeToString(file.getBytes()))
+ .mimeType(resolveImageMimeType(file))
+ .build()));
+ }
+
+ List<ChatMessage> messages = List.of(
+ SystemMessage.from("浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝浠庢枃鏈拰鍥剧墖涓瘑鍒噰璐彴璐︺�侀噰璐骇鍝佹槑缁嗐�佷粯娆炬垨閫�璐т俊鎭紝鍙緭鍑哄悎娉� JSON銆�"),
+ UserMessage.from(contents)
+ );
+ purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() {
+ @Override
+ public void onPartialResponse(String partialResponse) {
+ sink.next(partialResponse);
+ }
+
+ @Override
+ public void onCompleteResponse(ChatResponse completeResponse) {
+ sink.complete();
+ }
+
+ @Override
+ public void onError(Throwable error) {
+ sink.error(error);
+ }
+ });
+ } catch (Exception ex) {
+ sink.next("鍥剧墖鏂囦欢璇诲彇澶辫触锛岃纭鍥剧墖鏍煎紡涓� png銆乯pg銆乯peg銆亀ebp 鎴� bmp锛屼笖澶у皬涓嶈秴杩�10MB");
+ sink.complete();
+ }
+ });
+ }
+
+ private String resolveImageMimeType(MultipartFile file) {
+ String contentType = file.getContentType();
+ if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) {
+ return contentType;
+ }
+ String filename = file.getOriginalFilename();
+ String ext = "";
+ if (StringUtils.hasText(filename) && filename.contains(".")) {
+ ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
+ }
+ return switch (ext) {
+ case "jpg", "jpeg" -> "image/jpeg";
+ case "webp" -> "image/webp";
+ case "bmp" -> "image/bmp";
+ default -> "image/png";
+ };
+ }
+
+ private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) {
+ return """
+ 浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝涓ユ牸鏍规嵁鐢ㄦ埛涓婁紶鐨勫涓枃浠跺拰鐢ㄦ埛瑕佹眰鎻愬彇閲囪喘涓氬姟鏁版嵁銆�
+
+ 鐢ㄦ埛瑕佹眰:
+ %s
+
+ 杈撳嚭瑕佹眰:
+ 1. 鍙緭鍑哄悎娉� JSON锛屼笉瑕� Markdown锛屼笉瑕侀澶栬В閲娿��
+ 2. JSON 椤跺眰瀛楁鍥哄畾涓�:
+ - success: boolean
+ - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown
+ - action: confirm_required
+ - description: 涓枃璇存槑
+ - confidence: 0鍒�1鐨勫皬鏁�
+ - missingFields: 缂哄け瀛楁涓枃鍚嶇О鏁扮粍锛岄潰鍚戝鎴峰睍绀猴紝涓嶈杈撳嚭鑻辨枃瀛楁鍚�
+ - warnings: 椋庨櫓鎻愮ず鏁扮粍
+ - payload: 寰呭鎴风‘璁ょ殑鏁版嵁锛屽瓧娈靛悕蹇呴』浣跨敤鍚庣 DTO 瀛楁鍚�
+ - preview: 缁欏鎴风‘璁ょ敤鐨勪腑鏂囨憳瑕佹暟缁�
+ 3. 濡傛灉鍙垽鏂负閲囪喘鍙拌处锛宐usinessType 浣跨敤 purchase_ledger锛宲ayload.purchaseLedgers 涓洪噰璐鍗�/閲囪喘鍙拌处鏁扮粍:
+ - purchaseLedgers: 閲囪喘璁㈠崟/閲囪喘鍙拌处鏁扮粍锛屾瘡鏉¤褰曞瓧娈靛悕蹇呴』涓� PurchaseLedgerDto 淇濇寔涓�鑷�
+ - 浜у搧鏄庣粏蹇呴』鏀惧湪姣忔潯閲囪喘鍙拌处璁板綍鐨� productData 瀛楁涓紝productData 绫诲瀷涓� List<SalesLedgerProduct>
+ - 涓嶈浼樺厛浣跨敤 payload 椤跺眰 productData锛涢《灞� productData 浠呬綔涓烘棫鏍煎紡鍏煎
+ - 鏂囦欢閲岀殑鈥滈噰璐崟鍙封�濆氨鏄�滈噰璐悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� purchaseContractNumber
+ - 鏂囦欢閲岀殑鈥滈攢鍞崟鍙封�濆氨鏄�滈攢鍞悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� salesContractNo
+ - 鎵�鏈夋棩鏈熷瓧娈靛繀椤讳娇鐢� yyyy-MM-dd锛屼緥濡� 2026-04-30锛涗笉瑕佽緭鍑� 4/30/26銆�2026/4/30銆�2026骞�4鏈�30鏃� 鎴栧甫鏃跺垎绉掔殑鏍煎紡
+ - 閲囪喘鍙拌处涓嶉渶瑕佸湪 payload 涓紶瀹℃壒浜猴紝涓嶈杈撳嚭 approveUserIds銆乤pproverId
+ - missingFields 鍙~鍐欎笟鍔″繀濉絾鏃犳硶璇嗗埆鐨勫瓧娈碉紝涓嶈鎶� PurchaseLedgerDto 鐨勬墍鏈夌┖瀛楁閮藉垪涓虹己澶憋紱缂哄け椤瑰繀椤诲啓涓枃锛屼緥濡傗�滀緵搴斿晢鍚嶇О鈥濃�滃惈绋庡崟浠封�濓紝涓嶈鍐� supplierId銆乼axInclusiveUnitPrice
+ - 閲囪喘鍙拌处涓昏〃蹇呭~瀛楁浠呮寜杩欎簺鍒ゆ柇: purchaseContractNumber銆乻upplierName 鎴� supplierId
+ - productData 姣忔潯浜у搧蹇呭~瀛楁: productCategory銆乻pecificationModel銆乽nit銆乹uantity銆乼axInclusiveUnitPrice 鎴� taxInclusiveTotalPrice锛涘鏋滃彧鏈夊惈绋庢�讳环鍜屾暟閲忥紝蹇呴』璁$畻 taxInclusiveUnitPrice锛涘鏋滃彧鏈夊惈绋庡崟浠峰拰鏁伴噺锛屽繀椤昏绠� taxInclusiveTotalPrice
+ - 浜у搧瀛楁鎸夐噰璐鍏ユ帴鍙� PurchaseLedgerProductImportDto 瀵归綈: 閲囪喘鍗曞彿銆佷骇鍝佸ぇ绫汇�佽鏍煎瀷鍙枫�佸崟浣嶃�佹暟閲忋�佺◣鐜囥�佸惈绋庡崟浠枫�佸惈绋庢�讳环銆佸彂绁ㄧ被鍨嬨�佹槸鍚﹁川妫�
+ - 閲囪喘浜у搧 type 鍥哄畾涓� 2
+ - purchaseLedgers 姣忔潯璁板綍鍙娇鐢ㄨ繖浜� PurchaseLedgerDto 瀛楁鍚�:
+ entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName
+ - productData 姣忔潯浜у搧鍙娇鐢ㄨ繖浜� SalesLedgerProduct 瀛楁鍚�:
+ productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type
+ 4. 濡傛灉鍙垽鏂负浠樻鐧昏锛宐usinessType 浣跨敤 payment_registration锛宲ayload.records 涓轰粯娆剧櫥璁版暟缁勶紝瀛楁灏介噺鍖呭惈 purchaseLedgerId銆乻alesLedgerProductId銆乧urrentPaymentAmount銆乸aymentMethod銆乸aymentDate銆�
+ 5. 濡傛灉鍙垽鏂负閲囪喘閫�璐э紝businessType 浣跨敤 purchase_return_order锛宲ayload 鎸� PurchaseReturnOrderDto 缁勭粐锛屾槑缁嗘斁 purchaseReturnOrderProductsDtos銆�
+ 6. 缂哄皯涓氬姟澶勭悊蹇呴』瀛楁鏃讹紝涓嶈缂栭�� ID锛屾妸瀛楁鏀惧叆 missingFields锛屽苟浠嶈繑鍥炲彲纭鐨勮崏绋挎暟鎹��
+ 7. 鎵�鏈変腑鏂囧唴瀹圭洿鎺ヤ繚鐣欙紝涓嶈杞箟鎴� Unicode銆�
+
+ 鏂囦欢鍐呭:
+ %s
+ """.formatted(message, fileContent);
+ }
+
+ private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception {
+ if (payload.containsKey("purchaseLedgers")) {
+ return processPurchaseLedgerBatch(payload);
+ }
+
+ Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload);
+ PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class);
+ AjaxResult ledgerResult = validatePurchaseLedger(dto, 0);
+ if (ledgerResult != null) {
+ return ledgerResult;
+ }
+ AjaxResult supplierResult = fillSupplierIdByName(dto);
+ if (supplierResult != null) {
+ return supplierResult;
+ }
+ AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0);
+ if (productResult != null) {
+ return productResult;
+ }
+ int result = purchaseLedgerService.addOrEditPurchase(dto);
+ return AjaxResult.success("閲囪喘鍙拌处宸插鐞�", result);
+ }
+
+ private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception {
+ List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers"));
+ if (purchaseLedgers.isEmpty()) {
+ return AjaxResult.error("purchaseLedgers涓嶈兘涓虹┖");
+ }
+
+ List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData"));
+ List<Map<String, Object>> results = new ArrayList<>();
+ for (int i = 0; i < purchaseLedgers.size(); i++) {
+ Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i));
+ PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class);
+ AjaxResult ledgerResult = validatePurchaseLedger(dto, i);
+ if (ledgerResult != null) {
+ return ledgerResult;
+ }
+ AjaxResult supplierResult = fillSupplierIdByName(dto);
+ if (supplierResult != null) {
+ return supplierResult;
+ }
+
+ List<SalesLedgerProduct> products = dto.getProductData();
+ if (products == null || products.isEmpty()) {
+ products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1);
+ dto.setProductData(products);
+ }
+ AjaxResult productResult = validatePurchaseProducts(products, i);
+ if (productResult != null) {
+ return productResult;
+ }
+ int result = purchaseLedgerService.addOrEditPurchase(dto);
+
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("index", i);
+ item.put("purchaseContractNumber", dto.getPurchaseContractNumber());
+ item.put("supplierId", dto.getSupplierId());
+ item.put("supplierName", dto.getSupplierName());
+ item.put("productCount", products.size());
+ item.put("result", result);
+ results.add(item);
+ }
+ return AjaxResult.success("閲囪喘鍙拌处宸叉壒閲忓鐞�", results);
+ }
+
+ private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap,
+ PurchaseLedgerDto dto,
+ List<Map<String, Object>> productData,
+ boolean onlyOneLedger) {
+ List<SalesLedgerProduct> products = new ArrayList<>();
+ for (Map<String, Object> productMap : productData) {
+ if (onlyOneLedger || productBelongsToLedger(productMap, ledgerMap, dto)) {
+ products.add(objectMapper.convertValue(normalizeSalesLedgerProductMap(productMap), SalesLedgerProduct.class));
+ }
+ }
+ return products;
+ }
+
+ private boolean productBelongsToLedger(Map<String, Object> productMap, Map<String, Object> ledgerMap, PurchaseLedgerDto dto) {
+ Long productPurchaseLedgerId = longValue(productMap, "purchaseLedgerId", "purchaseId", "閲囪喘璁㈠崟id", "閲囪喘鍙拌处id");
+ if (productPurchaseLedgerId != null && dto.getId() != null && productPurchaseLedgerId.equals(dto.getId())) {
+ return true;
+ }
+
+ Long productSalesLedgerId = longValue(productMap, "salesLedgerId");
+ if (productSalesLedgerId != null && dto.getId() != null && productSalesLedgerId.equals(dto.getId())) {
+ return true;
+ }
+
+ String productContractNo = stringValue(productMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+ if (StringUtils.hasText(productContractNo)
+ && StringUtils.hasText(dto.getPurchaseContractNumber())
+ && productContractNo.trim().equals(dto.getPurchaseContractNumber().trim())) {
+ return true;
+ }
+
+ String ledgerContractNo = stringValue(ledgerMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+ if (StringUtils.hasText(productContractNo)
+ && StringUtils.hasText(ledgerContractNo)
+ && productContractNo.trim().equals(ledgerContractNo.trim())) {
+ return true;
+ }
+
+ String productSalesContractNo = stringValue(productMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+ if (StringUtils.hasText(productSalesContractNo)
+ && StringUtils.hasText(dto.getSalesContractNo())
+ && productSalesContractNo.trim().equals(dto.getSalesContractNo().trim())) {
+ return true;
+ }
+
+ String ledgerSalesContractNo = stringValue(ledgerMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+ if (StringUtils.hasText(productSalesContractNo)
+ && StringUtils.hasText(ledgerSalesContractNo)
+ && productSalesContractNo.trim().equals(ledgerSalesContractNo.trim())) {
+ return true;
+ }
+
+ String productSupplierName = stringValue(productMap, "supplierName", "渚涘簲鍟嗗悕绉�");
+ return StringUtils.hasText(productSupplierName)
+ && StringUtils.hasText(dto.getSupplierName())
+ && productSupplierName.trim().equals(dto.getSupplierName().trim());
+ }
+
+ private Map<String, Object> normalizePurchaseLedgerMap(Map<String, Object> source) {
+ Map<String, Object> target = new LinkedHashMap<>();
+ copyPurchaseLedgerDtoFields(source, target);
+ putDtoFieldIfPresent(source, target, "entryDateStart", "褰曞叆寮�濮嬫棩鏈�", "褰曞叆鏃ユ湡寮�濮�");
+ putDtoFieldIfPresent(source, target, "entryDateEnd", "褰曞叆缁撴潫鏃ユ湡", "褰曞叆鏃ユ湡缁撴潫");
+ putDtoFieldIfPresent(source, target, "id", "閲囪喘鍙拌处id", "閲囪喘璁㈠崟id", "涓婚敭");
+ putDtoFieldIfPresent(source, target, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+ putDtoFieldIfPresent(source, target, "supplierId", "渚涘簲鍟唅d", "渚涘簲鍟咺D", "渚涘簲鍟嗗悕绉癷d", "渚涘簲鍟嗗悕绉癐D");
+ putDtoFieldIfPresent(source, target, "supplierName", "渚涘簲鍟�", "渚涘簲鍟嗗悕绉�");
+ putDtoFieldIfPresent(source, target, "isWhite", "鏄惁鐧藉悕鍗�");
+ putDtoFieldIfPresent(source, target, "recorderId", "褰曞叆浜篿d", "褰曞叆浜篒D", "褰曞叆浜哄鍚峣d", "褰曞叆浜哄鍚岻D");
+ putDtoFieldIfPresent(source, target, "recorderName", "褰曞叆浜�", "褰曞叆浜哄鍚�");
+ putDtoFieldIfPresent(source, target, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+ putDtoFieldIfPresent(source, target, "salesContractNoId", "閿�鍞悎鍚屽彿id", "閿�鍞悎鍚屽彿ID", "閿�鍞崟鍙穒d", "閿�鍞崟鍙稩D");
+ putDtoFieldIfPresent(source, target, "projectName", "椤圭洰", "椤圭洰鍚嶇О");
+ putDtoFieldIfPresent(source, target, "entryDate", "褰曞叆鏃ユ湡");
+ putDtoFieldIfPresent(source, target, "executionDate", "绛捐鏃ユ湡", "鍚堝悓绛捐鏃ユ湡");
+ putDtoFieldIfPresent(source, target, "remarks", "澶囨敞", "璇存槑");
+ putDtoFieldIfPresent(source, target, "attachmentMaterials", "闄勪欢鏉愭枡", "闄勪欢鏉愭枡璺緞鎴栧悕绉�");
+ putDtoFieldIfPresent(source, target, "createdAt", "鍒涘缓鏃堕棿", "璁板綍鍒涘缓鏃堕棿");
+ putDtoFieldIfPresent(source, target, "updatedAt", "鏇存柊鏃堕棿", "璁板綍鏈�鍚庢洿鏂版椂闂�");
+ putDtoFieldIfPresent(source, target, "salesLedgerId", "閿�鍞彴璐d", "閿�鍞彴璐D", "鍏宠仈閿�鍞彴璐︿富琛ㄤ富閿�");
+ putDtoFieldIfPresent(source, target, "hasChildren", "鏄惁鏈夊瓙绾�", "鏄惁鏈夋槑缁�");
+ putDtoFieldIfPresent(source, target, "Type", "鍙拌处绫诲瀷", "涓氬姟绫诲瀷");
+ putDtoFieldIfPresent(source, target, "productData", "products", "浜у搧鏄庣粏", "閲囪喘浜у搧鏄庣粏");
+ putDtoFieldIfPresent(source, target, "tempFileIds", "涓存椂鏂囦欢id", "涓存椂鏂囦欢ID", "涓存椂鏂囦欢ids");
+ putDtoFieldIfPresent(source, target, "SalesLedgerFiles", "闄勪欢鍒楄〃", "閿�鍞彴璐﹂檮浠�");
+ putDtoFieldIfPresent(source, target, "phoneNumber", "涓氬姟鍛樻墜鏈哄彿", "鎵嬫満鍙�");
+ putDtoFieldIfPresent(source, target, "businessPersonId", "涓氬姟鍛榠d", "涓氬姟鍛業D");
+ putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
+ putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID");
+ putDtoFieldIfPresent(source, target, "invoiceNumber", "鍙戠エ鍙�", "鍙戠エ鍙风爜");
+ putDtoFieldIfPresent(source, target, "invoiceAmount", "鍙戠エ閲戦", "鍙戠エ閲戦锛堝厓锛�");
+ putDtoFieldIfPresent(source, target, "ticketRegistrationId", "鏉ョエ鐧昏id", "鏉ョエ鐧昏ID");
+ putDtoFieldIfPresent(source, target, "contractAmount", "鍚堝悓閲戦", "鍚堝悓閲戦锛堜骇鍝佸惈绋庢�讳环锛�");
+ putDtoFieldIfPresent(source, target, "receiptPaymentAmount", "鏉ョエ閲戦", "宸叉潵绁ㄩ噾棰�", "宸叉潵绁ㄩ噾棰�(鍏�)");
+ putDtoFieldIfPresent(source, target, "unReceiptPaymentAmount", "鏈潵绁ㄩ噾棰�", "鏈潵绁ㄩ噾棰�(鍏�)");
+ putDtoFieldIfPresent(source, target, "type", "鏂囦欢绫诲瀷");
+ putDtoFieldIfPresent(source, target, "paymentMethod", "浠樻鏂瑰紡");
+ putDtoFieldIfPresent(source, target, "approvalStatus", "瀹℃壒鐘舵��");
+ putDtoFieldIfPresent(source, target, "templateName", "妯℃澘鍚嶇О");
+ target.remove("approveUserIds");
+ target.remove("approverId");
+ normalizeNestedProductData(target);
+ attachImportStyleProductData(source, target);
+ if (target.get("type") == null) {
+ target.put("type", 2);
+ }
+ target.putIfAbsent("entryDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
+ normalizePurchaseLedgerDateFields(target);
+ return target;
+ }
+
+ private void attachImportStyleProductData(Map<String, Object> source, Map<String, Object> target) {
+ if (target.get("productData") != null) {
+ return;
+ }
+ Map<String, Object> productMap = normalizeSalesLedgerProductMap(source);
+ if (hasImportStyleProductData(productMap)) {
+ target.put("productData", List.of(productMap));
+ }
+ }
+
+ private boolean hasImportStyleProductData(Map<String, Object> productMap) {
+ return hasMapText(productMap, "productCategory")
+ || hasMapText(productMap, "specificationModel")
+ || productMap.get("quantity") != null
+ || productMap.get("taxInclusiveUnitPrice") != null
+ || productMap.get("taxInclusiveTotalPrice") != null;
+ }
+
+ private boolean hasMapText(Map<String, Object> map, String key) {
+ Object value = map.get(key);
+ return value != null && StringUtils.hasText(String.valueOf(value));
+ }
+
+ private void normalizeNestedProductData(Map<String, Object> target) {
+ Object productDataValue = target.get("productData");
+ if (productDataValue == null) {
+ return;
+ }
+ List<Map<String, Object>> productMaps = toMapList(productDataValue);
+ List<Map<String, Object>> normalizedProducts = new ArrayList<>();
+ for (Map<String, Object> productMap : productMaps) {
+ normalizedProducts.add(normalizeSalesLedgerProductMap(productMap));
+ }
+ target.put("productData", normalizedProducts);
+ }
+
+ private Map<String, Object> normalizeSalesLedgerProductMap(Map<String, Object> source) {
+ Map<String, Object> target = new LinkedHashMap<>();
+ copySalesLedgerProductFields(source, target);
+ putDtoFieldIfPresent(source, target, "productCategory", "浜у搧澶х被", "浜у搧鍚嶇О", "浜у搧", "鍝佸悕", "鐗╂枡鍚嶇О");
+ putDtoFieldIfPresent(source, target, "specificationModel", "瑙勬牸鍨嬪彿", "鍨嬪彿", "瑙勬牸", "浜у搧瑙勬牸");
+ putDtoFieldIfPresent(source, target, "unit", "鍗曚綅");
+ putDtoFieldIfPresent(source, target, "quantity", "鏁伴噺", "閲囪喘鏁伴噺");
+ putDtoFieldIfPresent(source, target, "taxRate", "绋庣巼");
+ putDtoFieldIfPresent(source, target, "taxInclusiveUnitPrice", "鍚◣鍗曚环", "鍗曚环", "閲囪喘鍗曚环", "鍚◣浠锋牸");
+ putDtoFieldIfPresent(source, target, "taxInclusiveTotalPrice", "鍚◣鎬讳环", "鎬讳环", "閲囪喘閲戦", "閲戦", "鍚堝悓閲戦");
+ putDtoFieldIfPresent(source, target, "taxExclusiveTotalPrice", "涓嶅惈绋庢�讳环");
+ putDtoFieldIfPresent(source, target, "invoiceType", "鍙戠エ绫诲瀷", "鍙戠エ绫诲埆");
+ putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
+ putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID", "鍨嬪彿id", "鍨嬪彿ID");
+ putDtoFieldIfPresent(source, target, "isChecked", "鏄惁璐ㄦ", "鏄惁璐ㄦ楠�", "璐ㄦ");
+ putDtoFieldIfPresent(source, target, "type", "鍙拌处绫诲瀷");
+ normalizeProductAmounts(target);
+ target.putIfAbsent("type", 2);
+ return target;
+ }
+
+ private void copySalesLedgerProductFields(Map<String, Object> source, Map<String, Object> target) {
+ String[] productFields = {
+ "id", "salesLedgerId", "warnNum", "productCategory", "specificationModel", "unit",
+ "speculativeTradingName", "quantity", "minStock", "taxRate", "taxInclusiveUnitPrice",
+ "taxInclusiveTotalPrice", "taxExclusiveTotalPrice", "invoiceType", "type", "ticketsNum",
+ "ticketsAmount", "futureTickets", "futureTicketsAmount", "invoiceNum", "noInvoiceNum",
+ "invoiceAmount", "noInvoiceAmount", "productId", "productModelId", "register", "registerDate",
+ "approveStatus", "pendingInvoiceTotal", "invoiceTotal", "pendingTicketsTotal", "ticketsTotal",
+ "isChecked", "isProduction"
+ };
+ for (String field : productFields) {
+ if (source.containsKey(field)) {
+ target.put(field, source.get(field));
+ }
+ }
+ }
+
+ private void normalizeProductAmounts(Map<String, Object> target) {
+ BigDecimal quantity = decimalValue(target.get("quantity"));
+ BigDecimal unitPrice = decimalValue(target.get("taxInclusiveUnitPrice"));
+ BigDecimal totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
+ if (unitPrice == null && totalPrice != null && quantity != null && quantity.compareTo(BigDecimal.ZERO) != 0) {
+ target.put("taxInclusiveUnitPrice", totalPrice.divide(quantity, 6, RoundingMode.HALF_UP));
+ }
+ if (totalPrice == null && unitPrice != null && quantity != null) {
+ target.put("taxInclusiveTotalPrice", unitPrice.multiply(quantity));
+ }
+ BigDecimal taxRate = decimalValue(target.get("taxRate"));
+ totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
+ if (target.get("taxExclusiveTotalPrice") == null && totalPrice != null && taxRate != null) {
+ BigDecimal divisor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
+ target.put("taxExclusiveTotalPrice", totalPrice.divide(divisor, 2, RoundingMode.HALF_UP));
+ }
+ }
+
+ private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) {
+ if (products == null || products.isEmpty()) {
+ return null;
+ }
+ for (int i = 0; i < products.size(); i++) {
+ SalesLedgerProduct product = products.get(i);
+ String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐︾殑绗�" + (i + 1) + "鏉′骇鍝�";
+ if (!StringUtils.hasText(product.getProductCategory())) {
+ return AjaxResult.error(prefix + "缂哄皯浜у搧鍚嶇О锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (!StringUtils.hasText(product.getSpecificationModel())) {
+ return AjaxResult.error(prefix + "缂哄皯瑙勬牸鍨嬪彿锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (!StringUtils.hasText(product.getUnit())) {
+ return AjaxResult.error(prefix + "缂哄皯鍗曚綅锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (product.getQuantity() == null) {
+ return AjaxResult.error(prefix + "缂哄皯鏁伴噺");
+ }
+ if (product.getTaxInclusiveUnitPrice() == null) {
+ return AjaxResult.error(prefix + "缂哄皯鍚◣鍗曚环锛岃琛ュ厖鍚庡啀纭");
+ }
+ if (product.getTaxInclusiveTotalPrice() == null) {
+ return AjaxResult.error(prefix + "缂哄皯鍚◣鎬讳环锛岃琛ュ厖鍚庡啀纭");
+ }
+ }
+ return null;
+ }
+
+ private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) {
+ String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐�";
+ if (!StringUtils.hasText(dto.getPurchaseContractNumber())) {
+ return AjaxResult.error(prefix + "缂哄皯閲囪喘鍚堝悓鍙凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
+ }
+ if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) {
+ return AjaxResult.error(prefix + "缂哄皯渚涘簲鍟嗗悕绉帮紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
+ }
+ return null;
+ }
+
+ private void normalizePurchaseLedgerDateFields(Map<String, Object> target) {
+ normalizeDateField(target, "entryDate");
+ normalizeDateField(target, "executionDate");
+ normalizeDateField(target, "createdAt");
+ normalizeDateField(target, "updatedAt");
+ }
+
+ private void normalizeDateField(Map<String, Object> target, String fieldName) {
+ Object value = target.get(fieldName);
+ if (value == null) {
+ return;
+ }
+ String normalizedDate = normalizeDateValue(value);
+ if (StringUtils.hasText(normalizedDate)) {
+ target.put(fieldName, normalizedDate);
+ }
+ }
+
+ private String normalizeDateValue(Object value) {
+ if (value instanceof Date date) {
+ return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+ if (value instanceof Number number) {
+ return LocalDate.of(1899, 12, 30)
+ .plusDays(number.longValue())
+ .format(DateTimeFormatter.ISO_LOCAL_DATE);
+ }
+
+ String text = String.valueOf(value).trim();
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ if (text.length() >= 10 && text.charAt(4) == '-' && text.charAt(7) == '-') {
+ return text.substring(0, 10);
+ }
+
+ String normalizedText = text.replace("骞�", "-")
+ .replace("鏈�", "-")
+ .replace("鏃�", "")
+ .replace(".", "-")
+ .replace("/", "-")
+ .trim();
+ DateTimeFormatter[] formatters = {
+ DateTimeFormatter.ofPattern("yyyy-M-d"),
+ DateTimeFormatter.ofPattern("M-d-yyyy"),
+ DateTimeFormatter.ofPattern("M-d-yy")
+ };
+ for (DateTimeFormatter formatter : formatters) {
+ try {
+ return LocalDate.parse(normalizedText, formatter).format(DateTimeFormatter.ISO_LOCAL_DATE);
+ } catch (DateTimeParseException ignored) {
+ // Try the next supported input pattern.
+ }
+ }
+ return text;
+ }
+
+ private void copyPurchaseLedgerDtoFields(Map<String, Object> source, Map<String, Object> target) {
+ String[] dtoFields = {
+ "entryDateStart", "entryDateEnd", "id", "purchaseContractNumber",
+ "supplierId", "supplierName", "isWhite", "recorderId", "recorderName", "salesContractNo",
+ "salesContractNoId", "projectName", "entryDate", "executionDate", "remarks", "attachmentMaterials",
+ "createdAt", "updatedAt", "salesLedgerId", "hasChildren", "Type", "productData", "tempFileIds",
+ "SalesLedgerFiles", "phoneNumber", "businessPersonId", "productId", "productModelId", "invoiceNumber",
+ "invoiceAmount", "ticketRegistrationId", "contractAmount", "receiptPaymentAmount",
+ "unReceiptPaymentAmount", "type", "paymentMethod", "approvalStatus", "templateName"
+ };
+ for (String field : dtoFields) {
+ if (source.containsKey(field)) {
+ target.put(field, source.get(field));
+ }
+ }
+ }
+
+ private void putDtoFieldIfPresent(Map<String, Object> source, Map<String, Object> target, String dtoField, String... aliases) {
+ if (target.containsKey(dtoField) && target.get(dtoField) != null) {
+ return;
+ }
+ for (String alias : aliases) {
+ Object value = source.get(alias);
+ if (value != null && StringUtils.hasText(String.valueOf(value))) {
+ target.put(dtoField, value);
+ return;
+ }
+ }
+ }
+
+ private List<Map<String, Object>> toMapList(Object value) {
+ if (value == null) {
+ return List.of();
+ }
+ return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>() {
+ });
+ }
+
+ private String stringValue(Map<String, Object> map, String... keys) {
+ for (String key : keys) {
+ Object value = map.get(key);
+ if (value != null && StringUtils.hasText(String.valueOf(value))) {
+ return String.valueOf(value);
+ }
+ }
+ return null;
+ }
+
+ private Long longValue(Map<String, Object> map, String... keys) {
+ String value = stringValue(map, keys);
+ if (!StringUtils.hasText(value)) {
+ return null;
+ }
+ try {
+ return Long.parseLong(value.trim());
+ } catch (NumberFormatException ignored) {
+ return null;
+ }
+ }
+
+ private BigDecimal decimalValue(Object value) {
+ if (value == null) {
+ return null;
+ }
+ if (value instanceof BigDecimal decimal) {
+ return decimal;
+ }
+ if (value instanceof Number number) {
+ return new BigDecimal(String.valueOf(number));
+ }
+ String text = String.valueOf(value)
+ .replace(",", "")
+ .replace("锛�", "")
+ .replace("鍏�", "")
+ .replace("锟�", "")
+ .trim();
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ try {
+ return new BigDecimal(text);
+ } catch (NumberFormatException ignored) {
+ return null;
+ }
+ }
+
+ private String toCustomerMessage(Exception ex) {
+ String message = ex.getMessage();
+ if (!StringUtils.hasText(message)) {
+ return "澶勭悊澶辫触锛岃妫�鏌ョ‘璁ゆ暟鎹悗閲嶈瘯";
+ }
+ if (message.contains("tax_inclusive_unit_price")) {
+ return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庡崟浠凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�";
+ }
+ if (message.contains("tax_inclusive_total_price")) {
+ return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庢�讳环锛岃琛ュ厖鍚庡啀纭";
+ }
+ if (message.contains("entryDate")) {
+ return "澶勭悊澶辫触锛氬綍鍏ユ棩鏈熸牸寮忎笉姝g‘锛岃浣跨敤 yyyy-MM-dd锛屼緥濡� 2026-04-30";
+ }
+ if (message.contains("supplier")) {
+ return "澶勭悊澶辫触锛氫緵搴斿晢淇℃伅涓嶅畬鏁达紝璇风‘璁や緵搴斿晢鍚嶇О鎴栦緵搴斿晢ID";
+ }
+ if (message.contains("SQL") || message.contains("java.") || message.contains("Exception")) {
+ return "澶勭悊澶辫触锛氱‘璁ゆ暟鎹笉瀹屾暣鎴栨牸寮忎笉姝g‘锛岃妫�鏌ュ繀濉瓧娈靛悗閲嶈瘯";
+ }
+ return "澶勭悊澶辫触锛�" + message;
+ }
+
+ private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) {
+ if (dto.getSupplierId() != null) {
+ return null;
+ }
+ if (!StringUtils.hasText(dto.getSupplierName())) {
+ return AjaxResult.error("渚涘簲鍟咺D涓嶈兘涓虹┖锛涙湭璇嗗埆鍒颁緵搴斿晢鍚嶇О锛屾棤娉曡嚜鍔ㄥ尮閰嶄緵搴斿晢ID");
+ }
+
+ SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>()
+ .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim())
+ .last("limit 1"));
+ if (supplier == null) {
+ return AjaxResult.error("鏈壘鍒颁緵搴斿晢锛�" + dto.getSupplierName() + "锛岃鍏堢淮鎶や緵搴斿晢鎴栨墜鍔ㄩ�夋嫨渚涘簲鍟咺D");
+ }
+ dto.setSupplierId(supplier.getId());
+ return null;
+ }
+
+ private AjaxResult processPaymentRegistration(Map<String, Object> payload) {
+ Object recordsValue = payload.get("records");
+ List<PaymentRegistration> records;
+ if (recordsValue == null) {
+ records = Collections.singletonList(objectMapper.convertValue(payload, PaymentRegistration.class));
+ } else {
+ records = objectMapper.convertValue(recordsValue, new TypeReference<List<PaymentRegistration>>() {
+ });
+ }
+ int result = paymentRegistrationService.insertPaymentRegistration(records);
+ return AjaxResult.success("浠樻鐧昏宸插鐞�", result);
+ }
+
+ private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) {
+ PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class);
+ Boolean result = purchaseReturnOrdersService.add(dto);
+ return AjaxResult.success("閲囪喘閫�璐у崟宸插鐞�", result);
+ }
}
diff --git a/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
index 82cf1eb..6e37451 100644
--- a/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
+++ b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
@@ -44,7 +44,17 @@
if ("xls".equals(ext)) {
return extractXls(bytes);
}
+ if (isImage(ext)) {
+ return "鍥剧墖鏂囦欢锛�" + filename + "锛屽凡涓婁紶锛岃缁撳悎鍥剧墖鍐呭璇嗗埆閲囪喘鍗曟嵁銆佽〃鏍煎拰浜у搧鏄庣粏銆�";
+ }
throw new IllegalArgumentException("鏆備笉鏀寔璇ユ枃浠剁被鍨�: " + ext);
+ }
+
+ public boolean isImageFile(MultipartFile file) {
+ if (file == null) {
+ return false;
+ }
+ return isImage(getExtension(file.getOriginalFilename()));
}
private String extractDocx(byte[] bytes) throws IOException {
@@ -114,4 +124,8 @@
"txt", "md", "markdown", "json", "xml", "yaml", "yml", "csv", "log", "properties",
"java", "js", "ts", "vue", "html", "css", "sql", "py", "go", "sh", "bat");
}
+
+ private boolean isImage(String ext) {
+ return StringUtils.inStringIgnoreCase(ext, "png", "jpg", "jpeg", "webp", "bmp");
+ }
}
diff --git a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
index 8215ce8..233c459 100644
--- a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -70,36 +70,54 @@
this.aiSessionUserContext = aiSessionUserContext;
}
- @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ褰撳墠鐧诲綍浜虹浉鍏崇殑瀹℃壒寰呭姙锛屼紭鍏堣繑鍥炶嚜宸卞緟澶勭悊鐨勫鎵癸紝鏀寔鎸夌姸鎬併�佺被鍨嬨�佸叧閿瓧杩囨护銆�")
+ @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ褰撳墠鐧诲綍浜虹浉鍏崇殑瀹℃壒寰呭姙锛屼紭鍏堣繑鍥炶嚜宸卞緟澶勭悊鐨勫鎵癸紝鏀寔鎸夌姸鎬併�佺被鍨嬨�佸叧閿瓧鍜岃寖鍥磋繃婊ゃ��")
public String listTodos(@ToolMemoryId String memoryId,
@P(value = "瀹℃壒鐘舵�侊紝鍙�夊�硷細all銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", required = false) String status,
@P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
@P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
- @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit) {
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit,
+ @P(value = "鏌ヨ鑼冨洿锛屽彲閫夊�硷細related銆乤pplicant銆乤pprover锛況elated 琛ㄧず褰撳墠鐢ㄦ埛鐩稿叧锛宎pplicant 琛ㄧず鎴戝彂璧风殑锛宎pprover 琛ㄧず寰呮垜澶勭悊鐨�", required = false) String scope) {
LoginUser loginUser = currentLoginUser(memoryId);
Long userId = loginUser.getUserId();
Integer statusCode = parseStatus(status);
+ String normalizedScope = normalizeScope(scope);
LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>();
- wrapper.eq(ApproveProcess::getApproveDelete, 0)
- .ne(ApproveProcess::getApproveStatus, 2);
+ wrapper.eq(ApproveProcess::getApproveDelete, 0);
+ if (statusCode == null) {
+ wrapper.ne(ApproveProcess::getApproveStatus, 2);
+ }
if (approveType != null) {
wrapper.eq(ApproveProcess::getApproveType, approveType);
}
-// if (StringUtils.hasText(keyword)) {
-// wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
-// .or().like(ApproveProcess::getApproveReason, keyword)
-// .or().like(ApproveProcess::getApproveUserName, keyword)
-// .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
-// }
- if (statusCode != null && (statusCode == 0 || statusCode == 1)) {
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
+ .or().like(ApproveProcess::getApproveReason, keyword)
+ .or().like(ApproveProcess::getApproveUserName, keyword)
+ .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
+ }
+ if ("applicant".equals(normalizedScope)) {
+ wrapper.eq(ApproveProcess::getApproveUser, userId);
+ if (statusCode != null) {
+ wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
+ }
+ } else if ("approver".equals(normalizedScope)) {
wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
+ if (statusCode != null) {
+ wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
+ }
+ } else if (statusCode != null && (statusCode == 0 || statusCode == 1)) {
+ wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
+ wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
} else {
wrapper.and(w -> w.eq(ApproveProcess::getApproveUser, userId)
.or().eq(ApproveProcess::getApproveUserCurrentId, userId)
.or().apply("FIND_IN_SET({0}, approve_user_ids)", userId));
+ if (statusCode != null) {
+ wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
+ }
}
wrapper.orderByDesc(ApproveProcess::getCreateTime)
@@ -137,7 +155,8 @@
"count", items.size(),
"statusFilter", StringUtils.hasText(status) ? status : "all",
"approveType", approveType == null ? "" : approveType,
- "keyword", keyword == null ? "" : keyword
+ "keyword", keyword == null ? "" : keyword,
+ "scope", normalizedScope
),
Map.of("columns", todoColumns(), "items", items),
Map.of());
@@ -638,6 +657,17 @@
};
}
+ private String normalizeScope(String scope) {
+ if (!StringUtils.hasText(scope)) {
+ return "related";
+ }
+ return switch (scope.trim().toLowerCase()) {
+ case "applicant", "mine", "created", "initiated" -> "applicant";
+ case "approver", "handler", "todo", "pending" -> "approver";
+ default -> "related";
+ };
+ }
+
private String approveStatusName(Integer status) {
if (status == null) {
return "鏈煡";
diff --git a/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
index 92b739f..17b6868 100644
--- a/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -13,6 +13,12 @@
import com.ruoyi.purchase.pojo.PaymentRegistration;
import com.ruoyi.purchase.pojo.PurchaseLedger;
import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
+import com.ruoyi.procurementrecord.mapper.InboundManagementMapper;
+import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
+import com.ruoyi.procurementrecord.pojo.InboundManagement;
+import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
+import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
@@ -29,6 +35,7 @@
import java.util.List;
import java.util.Map;
import java.util.Objects;
+import java.util.Comparator;
import java.util.stream.Collectors;
@Component
@@ -42,17 +49,26 @@
private final PaymentRegistrationMapper paymentRegistrationMapper;
private final InvoicePurchaseMapper invoicePurchaseMapper;
private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
+ private final SalesLedgerProductMapper salesLedgerProductMapper;
+ private final ProcurementRecordMapper procurementRecordMapper;
+ private final InboundManagementMapper inboundManagementMapper;
private final AiSessionUserContext aiSessionUserContext;
public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper,
PaymentRegistrationMapper paymentRegistrationMapper,
InvoicePurchaseMapper invoicePurchaseMapper,
PurchaseReturnOrdersMapper purchaseReturnOrdersMapper,
+ SalesLedgerProductMapper salesLedgerProductMapper,
+ ProcurementRecordMapper procurementRecordMapper,
+ InboundManagementMapper inboundManagementMapper,
AiSessionUserContext aiSessionUserContext) {
this.purchaseLedgerMapper = purchaseLedgerMapper;
this.paymentRegistrationMapper = paymentRegistrationMapper;
this.invoicePurchaseMapper = invoicePurchaseMapper;
this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper;
+ this.salesLedgerProductMapper = salesLedgerProductMapper;
+ this.procurementRecordMapper = procurementRecordMapper;
+ this.inboundManagementMapper = inboundManagementMapper;
this.aiSessionUserContext = aiSessionUserContext;
}
@@ -78,7 +94,7 @@
wrapper.ge(PurchaseLedger::getEntryDate, toDate(start));
}
if (end != null) {
- wrapper.le(PurchaseLedger::getEntryDate, toDate(end));
+ wrapper.lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(end));
}
wrapper.orderByDesc(PurchaseLedger::getEntryDate, PurchaseLedger::getId).last("limit " + finalLimit);
@@ -151,19 +167,290 @@
return jsonResponse(true, "purchase_stats", "宸茶繑鍥為噰璐粺璁℃暟鎹�", summary, Map.of(), Map.of());
}
+ @Tool(name = "閲囪喘鐗╂枡閲戦鎺掕", value = "鎸夋椂闂磋寖鍥寸粺璁¢噰璐墿鏂欓噾棰濇帓琛岋紝鍙洖绛旀湰鏈堥噰璐噾棰濇帓鍚嶉潬鍓嶇殑鐗╂枡銆�")
+ public String rankPurchaseMaterials(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+ @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡傛湰鏈堛�佽繎7澶┿�佽繎30澶�", required = false) String timeRange,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ DateRange range = resolveDateRange(startDate, endDate, timeRange);
+ List<Long> ledgerIds = queryLedgers(loginUser, range).stream()
+ .map(PurchaseLedger::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ if (ledgerIds.isEmpty()) {
+ return jsonResponse(true, "purchase_material_rank", "褰撳墠鏃堕棿鑼冨洿鍐呮病鏈夐噰璐墿鏂欐暟鎹��",
+ rangeSummary(range, 0), Map.of("items", List.of()), Map.of());
+ }
+
+ List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>()
+ .eq(SalesLedgerProduct::getType, 2)
+ .in(SalesLedgerProduct::getSalesLedgerId, ledgerIds)));
+
+ Map<String, MaterialRankItem> grouped = new LinkedHashMap<>();
+ for (SalesLedgerProduct product : products) {
+ String name = safe(product.getProductCategory());
+ String model = safe(product.getSpecificationModel());
+ String key = name + "|" + model;
+ MaterialRankItem item = grouped.computeIfAbsent(key, ignored -> new MaterialRankItem(name, model, safe(product.getUnit())));
+ item.quantity = item.quantity.add(defaultDecimal(product.getQuantity()));
+ item.amount = item.amount.add(defaultDecimal(product.getTaxInclusiveTotalPrice()));
+ }
+
+ List<Map<String, Object>> items = grouped.values().stream()
+ .sorted(Comparator.comparing((MaterialRankItem item) -> item.amount).reversed())
+ .limit(normalizeLimit(limit))
+ .map(MaterialRankItem::toMap)
+ .collect(Collectors.toList());
+
+ return jsonResponse(true, "purchase_material_rank", "宸茶繑鍥為噰璐墿鏂欓噾棰濇帓琛屻��",
+ rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+ }
+
+ @Tool(name = "鏌ヨ鏈叆搴撻噰璐鍗�", value = "鏌ヨ閲囪喘璁㈠崟涓嬩粛鏈夊緟鍏ュ簱鏁伴噺鐨勭墿鏂欐槑缁嗐��")
+ public String listUnstockedPurchaseOrders(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+ @P(value = "鍏抽敭瀛楋紝鍙尮閰嶉噰璐悎鍚屽彿/渚涘簲鍟�/鐗╂枡", required = false) String keyword,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ DateRange range = resolveDateRange(startDate, endDate, null);
+ List<PurchaseLedger> ledgers = queryLedgers(loginUser, range).stream()
+ .filter(ledger -> matchLedgerKeyword(ledger, keyword))
+ .collect(Collectors.toList());
+ Map<Long, PurchaseLedger> ledgerMap = ledgers.stream()
+ .filter(ledger -> ledger.getId() != null)
+ .collect(Collectors.toMap(PurchaseLedger::getId, ledger -> ledger, (a, b) -> a, LinkedHashMap::new));
+ if (ledgerMap.isEmpty()) {
+ return jsonResponse(true, "purchase_unstocked_list", "鏈煡璇㈠埌绗﹀悎鏉′欢鐨勯噰璐鍗曘��",
+ rangeSummary(range, 0), Map.of("items", List.of()), Map.of());
+ }
+
+ List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>()
+ .eq(SalesLedgerProduct::getType, 2)
+ .in(SalesLedgerProduct::getSalesLedgerId, ledgerMap.keySet())));
+
+ List<Map<String, Object>> items = products.stream()
+ .filter(product -> matchProductKeyword(product, keyword))
+ .map(product -> toUnstockedItem(product, ledgerMap.get(product.getSalesLedgerId())))
+ .filter(Objects::nonNull)
+ .limit(normalizeLimit(limit))
+ .collect(Collectors.toList());
+
+ return jsonResponse(true, "purchase_unstocked_list", "宸茶繑鍥炴湭鍏ュ簱閲囪喘璁㈠崟銆�",
+ rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+ }
+
+ @Tool(name = "鏌ヨ閲囪喘鍒拌揣寮傚父", value = "鏌ヨ鍒拌揣鐘舵�佸紓甯告垨澶囨敞鍖呭惈寮傚父淇℃伅鐨勫埌璐ц褰曘��")
+ public String listArrivalExceptions(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+ @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡傝繎7澶┿�佹湰鏈�", required = false) String timeRange,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ DateRange range = resolveDateRange(startDate, endDate, timeRange);
+ LambdaQueryWrapper<InboundManagement> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), InboundManagement::getTenantId);
+ wrapper.ge(InboundManagement::getArrivalTime, toDate(range.start()))
+ .lt(InboundManagement::getArrivalTime, toExclusiveEndDate(range.end()))
+ .and(w -> w.notLike(InboundManagement::getStatus, "姝e父")
+ .notLike(InboundManagement::getStatus, "瀹屾垚")
+ .notLike(InboundManagement::getStatus, "宸插埌璐�")
+ .or().like(InboundManagement::getStatus, "寮傚父")
+ .or().like(InboundManagement::getRemark, "寮傚父")
+ .or().like(InboundManagement::getRemark, "闂")
+ .or().like(InboundManagement::getRemark, "寤惰繜")
+ .or().like(InboundManagement::getRemark, "鐭己"));
+ wrapper.orderByDesc(InboundManagement::getArrivalTime).last("limit " + normalizeLimit(limit));
+
+ List<Map<String, Object>> items = defaultList(inboundManagementMapper.selectList(wrapper)).stream()
+ .map(this::toArrivalItem)
+ .collect(Collectors.toList());
+ return jsonResponse(true, "purchase_arrival_exception_list", "宸茶繑鍥為噰璐埌璐у紓甯歌褰曘��",
+ rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+ }
+
+ @Tool(name = "鏌ヨ寰呬粯娆鹃噰璐崟", value = "鏌ヨ鍚堝悓閲戦澶т簬宸蹭粯娆鹃噾棰濈殑閲囪喘鍗曘��")
+ public String listPendingPaymentOrders(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+ @P(value = "鍏抽敭瀛楋紝鍙尮閰嶉噰璐悎鍚屽彿/渚涘簲鍟�/椤圭洰鍚�", required = false) String keyword,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ DateRange range = resolveDateRange(startDate, endDate, null);
+ List<Map<String, Object>> items = queryLedgers(loginUser, range).stream()
+ .filter(ledger -> matchLedgerKeyword(ledger, keyword))
+ .map(ledger -> toPendingPaymentItem(loginUser, ledger))
+ .filter(Objects::nonNull)
+ .sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder()))
+ .limit(normalizeLimit(limit))
+ .collect(Collectors.toList());
+ return jsonResponse(true, "purchase_pending_payment_list", "宸茶繑鍥炲緟浠樻閲囪喘鍗曘��",
+ rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+ }
+
+ @Tool(name = "鏌ヨ閲囪喘閫�璐ф儏鍐�", value = "鎸夋椂闂磋寖鍥存煡璇㈤噰璐��璐у崟鍒楄〃鍜岄��璐ч噾棰濄��")
+ public String listPurchaseReturns(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+ @P(value = "鍏抽敭瀛楋紝鍙尮閰嶉��璐у崟鍙�/澶囨敞", required = false) String keyword,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ DateRange range = resolveDateRange(startDate, endDate, null);
+ LambdaQueryWrapper<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), PurchaseReturnOrders::getDeptId);
+ wrapper.ge(PurchaseReturnOrders::getPreparedAt, range.start())
+ .le(PurchaseReturnOrders::getPreparedAt, range.end());
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(PurchaseReturnOrders::getNo, keyword)
+ .or().like(PurchaseReturnOrders::getRemark, keyword)
+ .or().like(PurchaseReturnOrders::getReturnUserName, keyword));
+ }
+ wrapper.orderByDesc(PurchaseReturnOrders::getPreparedAt).last("limit " + normalizeLimit(limit));
+
+ List<PurchaseReturnOrders> returns = defaultList(purchaseReturnOrdersMapper.selectList(wrapper));
+ BigDecimal totalAmount = returns.stream()
+ .map(PurchaseReturnOrders::getTotalAmount)
+ .filter(Objects::nonNull)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ Map<String, Object> summary = rangeSummary(range, returns.size());
+ summary.put("returnAmount", totalAmount);
+
+ return jsonResponse(true, "purchase_return_list", "宸茶繑鍥為噰璐��璐ф儏鍐点��",
+ summary,
+ Map.of("items", returns.stream().map(this::toReturnItem).collect(Collectors.toList())),
+ Map.of());
+ }
+
private List<PurchaseLedger> queryLedgers(LoginUser loginUser, DateRange range) {
LambdaQueryWrapper<PurchaseLedger> wrapper = new LambdaQueryWrapper<>();
applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId);
wrapper.ge(PurchaseLedger::getEntryDate, toDate(range.start()))
- .le(PurchaseLedger::getEntryDate, toDate(range.end()));
+ .lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(range.end()));
return defaultList(purchaseLedgerMapper.selectList(wrapper));
+ }
+
+ private Map<String, Object> rangeSummary(DateRange range, int count) {
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("timeRange", range.label());
+ summary.put("startDate", range.start().toString());
+ summary.put("endDate", range.end().toString());
+ summary.put("count", count);
+ return summary;
+ }
+
+ private boolean matchLedgerKeyword(PurchaseLedger ledger, String keyword) {
+ if (!StringUtils.hasText(keyword)) {
+ return true;
+ }
+ String text = keyword.trim();
+ return safe(ledger.getPurchaseContractNumber()).contains(text)
+ || safe(ledger.getSupplierName()).contains(text)
+ || safe(ledger.getProjectName()).contains(text);
+ }
+
+ private boolean matchProductKeyword(SalesLedgerProduct product, String keyword) {
+ if (!StringUtils.hasText(keyword)) {
+ return true;
+ }
+ String text = keyword.trim();
+ return safe(product.getProductCategory()).contains(text)
+ || safe(product.getSpecificationModel()).contains(text);
+ }
+
+ private Map<String, Object> toUnstockedItem(SalesLedgerProduct product, PurchaseLedger ledger) {
+ if (product == null || ledger == null || product.getId() == null) {
+ return null;
+ }
+ BigDecimal orderedQuantity = defaultDecimal(product.getQuantity());
+ BigDecimal inboundQuantity = sumInboundQuantity(product.getId());
+ BigDecimal pendingQuantity = orderedQuantity.subtract(inboundQuantity);
+ if (pendingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+ return null;
+ }
+ Map<String, Object> item = new LinkedHashMap<>();
+ item.put("purchaseLedgerId", ledger.getId());
+ item.put("purchaseContractNumber", safe(ledger.getPurchaseContractNumber()));
+ item.put("supplierName", safe(ledger.getSupplierName()));
+ item.put("productCategory", safe(product.getProductCategory()));
+ item.put("specificationModel", safe(product.getSpecificationModel()));
+ item.put("unit", safe(product.getUnit()));
+ item.put("orderedQuantity", orderedQuantity);
+ item.put("inboundQuantity", inboundQuantity);
+ item.put("pendingInboundQuantity", pendingQuantity);
+ item.put("entryDate", formatDate(ledger.getEntryDate()));
+ return item;
+ }
+
+ private BigDecimal sumInboundQuantity(Long salesLedgerProductId) {
+ List<ProcurementRecordStorage> records = defaultList(procurementRecordMapper.selectList(new LambdaQueryWrapper<ProcurementRecordStorage>()
+ .eq(ProcurementRecordStorage::getType, 1)
+ .eq(ProcurementRecordStorage::getSalesLedgerProductId, salesLedgerProductId)));
+ return records.stream()
+ .map(ProcurementRecordStorage::getInboundNum)
+ .filter(Objects::nonNull)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ private Map<String, Object> toArrivalItem(InboundManagement item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("orderNo", safe(item.getOrderNo()));
+ map.put("arrivalNo", safe(item.getArrivalNo()));
+ map.put("supplierName", safe(item.getSupplierName()));
+ map.put("status", safe(item.getStatus()));
+ map.put("arrivalTime", formatDate(item.getArrivalTime()));
+ map.put("arrivalQuantity", safe(item.getArrivalQuantity()));
+ map.put("remark", safe(item.getRemark()));
+ return map;
+ }
+
+ private Map<String, Object> toPendingPaymentItem(LoginUser loginUser, PurchaseLedger ledger) {
+ BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
+ BigDecimal paidAmount = sumPaymentAmount(loginUser, ledger.getId());
+ BigDecimal pendingAmount = contractAmount.subtract(paidAmount);
+ if (pendingAmount.compareTo(BigDecimal.ZERO) <= 0) {
+ return null;
+ }
+ Map<String, Object> item = toLedgerItem(ledger);
+ item.put("paidAmount", paidAmount);
+ item.put("pendingAmount", pendingAmount);
+ return item;
+ }
+
+ private BigDecimal sumPaymentAmount(LoginUser loginUser, Long purchaseLedgerId) {
+ LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
+ wrapper.eq(PaymentRegistration::getPurchaseLedgerId, purchaseLedgerId);
+ return defaultList(paymentRegistrationMapper.selectList(wrapper)).stream()
+ .map(PaymentRegistration::getCurrentPaymentAmount)
+ .filter(Objects::nonNull)
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ }
+
+ private Map<String, Object> toReturnItem(PurchaseReturnOrders item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("no", safe(item.getNo()));
+ map.put("returnType", item.getReturnType());
+ map.put("purchaseLedgerId", item.getPurchaseLedgerId());
+ map.put("preparedAt", item.getPreparedAt() == null ? "" : item.getPreparedAt().toString());
+ map.put("returnUserName", safe(item.getReturnUserName()));
+ map.put("totalAmount", item.getTotalAmount());
+ map.put("remark", safe(item.getRemark()));
+ return map;
+ }
+
+ private BigDecimal defaultDecimal(BigDecimal value) {
+ return value == null ? BigDecimal.ZERO : value;
}
private List<PaymentRegistration> queryPayments(LoginUser loginUser, DateRange range) {
LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start()))
- .le(PaymentRegistration::getPaymentDate, toDate(range.end()));
+ .lt(PaymentRegistration::getPaymentDate, toExclusiveEndDate(range.end()));
return defaultList(paymentRegistrationMapper.selectList(wrapper));
}
@@ -231,6 +518,19 @@
if (text.contains("杩戝崐涓湀") || text.contains("鏈�杩戝崐涓湀") || text.contains("鍗婁釜鏈�")) {
return new DateRange(today.minusDays(14), today, "杩戝崐涓湀");
}
+ java.util.regex.Matcher relativeMatcher = java.util.regex.Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text);
+ if (relativeMatcher.find()) {
+ int amount = Integer.parseInt(relativeMatcher.group(2));
+ String unit = relativeMatcher.group(3);
+ LocalDate relativeStart = switch (unit) {
+ case "澶�" -> today.minusDays(Math.max(amount - 1L, 0));
+ case "鍛�" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
+ case "涓湀", "鏈�" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
+ case "骞�" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
+ default -> today.minusDays(29);
+ };
+ return new DateRange(relativeStart, today, "杩�" + amount + unit);
+ }
return new DateRange(today.minusDays(29), today, "杩�30澶�");
}
@@ -243,6 +543,10 @@
private Date toDate(LocalDate localDate) {
return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant());
+ }
+
+ private Date toExclusiveEndDate(LocalDate localDate) {
+ return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
}
private String formatDate(Date date) {
@@ -312,4 +616,28 @@
private record DateRange(LocalDate start, LocalDate end, String label) {
}
+
+ private static class MaterialRankItem {
+ private final String productCategory;
+ private final String specificationModel;
+ private final String unit;
+ private BigDecimal quantity = BigDecimal.ZERO;
+ private BigDecimal amount = BigDecimal.ZERO;
+
+ private MaterialRankItem(String productCategory, String specificationModel, String unit) {
+ this.productCategory = productCategory;
+ this.specificationModel = specificationModel;
+ this.unit = unit;
+ }
+
+ private Map<String, Object> toMap() {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("productCategory", productCategory);
+ map.put("specificationModel", specificationModel);
+ map.put("unit", unit);
+ map.put("quantity", quantity);
+ map.put("amount", amount);
+ return map;
+ }
+ }
}
diff --git a/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java b/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
index b75fedf..a93dc5d 100644
--- a/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
+++ b/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -1,6 +1,7 @@
package com.ruoyi.approve.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -79,7 +80,12 @@
List<ApproveProcessConfigNodeVo> list = approveProcessConfigNodeService.listNode( approveProcessVO.getApproveType());
List<Long> nodeIds = list.stream()
.map(ApproveProcessConfigNodeVo::getApproverId)
+ .filter(Objects::nonNull)
.collect(Collectors.toList());
+ if (CollectionUtils.isEmpty(nodeIds)) {
+ autoPassPurchaseApproveIfNoApprover(approveProcessVO);
+ return;
+ }
List<SysUser> sysUsers = sysUserMapper.selectUserByIds(nodeIds);
if (CollectionUtils.isEmpty(sysUsers)) throw new RuntimeException("瀹℃牳鐢ㄦ埛涓嶅瓨鍦�");
if (sysDept == null) throw new RuntimeException("閮ㄩ棬涓嶅瓨鍦�");
@@ -147,6 +153,16 @@
}
}
+ private void autoPassPurchaseApproveIfNoApprover(ApproveProcessVO approveProcessVO) {
+ if (!Objects.equals(approveProcessVO.getApproveType(), 5)
+ || !StringUtils.hasText(approveProcessVO.getApproveReason())) {
+ throw new RuntimeException("瀹℃牳鐢ㄦ埛涓嶅瓨鍦�");
+ }
+ purchaseLedgerMapper.update(null, new LambdaUpdateWrapper<PurchaseLedger>()
+ .eq(PurchaseLedger::getPurchaseContractNumber, approveProcessVO.getApproveReason())
+ .set(PurchaseLedger::getApprovalStatus, 3));
+ }
+
@Override
public List<SysDept> selectDeptListByDeptIds(Long[] deptIds) {
List<SysDept> sysDeptList = new ArrayList<SysDept>();
diff --git a/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java b/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
index 700836d..8660993 100644
--- a/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -181,7 +181,7 @@
}
purchaseLedgerMapper.updateById(purchaseLedger);
}
- // 6.閲囪喘瀹℃牳鏂板
+ // 6.閲囪喘瀹℃牳鏂板锛涘鎵圭鐞嗘湭閰嶇疆閲囪喘瀹℃壒浜烘椂锛屽鎵规湇鍔′細鑷姩缃负瀹℃壒閫氳繃銆�
addApproveByPurchase(loginUser, purchaseLedger);
// 4. 澶勭悊瀛愯〃鏁版嵁
@@ -238,6 +238,7 @@
if (products == null || products.isEmpty()) {
throw new BaseException("浜у搧淇℃伅涓嶅瓨鍦�");
}
+ Integer ledgerType = type == null ? 2 : type;
// 鎻愬墠鏀堕泦鎵�鏈夐渶瑕佹煡璇㈢殑ID
Set<Long> productIds = products.stream()
@@ -289,14 +290,14 @@
// 鎵ц鏇存柊鎿嶄綔
if (!updateList.isEmpty()) {
for (SalesLedgerProduct product : updateList) {
- product.setType(type);
+ product.setType(ledgerType);
salesLedgerProductMapper.updateById(product);
}
}
// 鎵ц鎻掑叆鎿嶄綔
if (!insertList.isEmpty()) {
for (SalesLedgerProduct salesLedgerProduct : insertList) {
- salesLedgerProduct.setType(type);
+ salesLedgerProduct.setType(ledgerType);
Date entryDate = purchaseLedger.getEntryDate();
LocalDateTime localDateTime = entryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
@@ -689,20 +690,21 @@
if(salesLedger1 != null){
salesLedger.setSalesLedgerId(salesLedger1.getId());
}
- // 閲囪喘瀹℃牳
- // 閫氳繃鏄电О鑾峰彇鐢ㄦ埛ID
- String[] split = salesLedger.getApproveUserIds().split("锛�");
- List<Long> ids = new ArrayList<>();
- for (int i = 0; i < split.length; i++) {
- SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getNickName, split[i])
- .last("LIMIT 1"));
- if (sysUser != null) {
- ids.add(sysUser.getUserId());
+ if (StringUtils.hasText(salesLedger.getApproveUserIds())) {
+ // 閲囪喘瀹℃牳锛氬巻鍙插鍏ユā鏉夸紶瀹℃壒浜哄鍚嶆椂锛岀户缁吋瀹硅浆鎹负鐢ㄦ埛ID銆�
+ String[] split = salesLedger.getApproveUserIds().split("锛�");
+ List<Long> ids = new ArrayList<>();
+ for (int i = 0; i < split.length; i++) {
+ SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getNickName, split[i])
+ .last("LIMIT 1"));
+ if (sysUser != null) {
+ ids.add(sysUser.getUserId());
+ }
}
+ // 灏嗛泦鍚堣浆涓哄瓧绗︿覆锛岄殧寮�
+ String collect = ids.stream().map(Object::toString).collect(Collectors.joining(","));
+ salesLedger.setApproveUserIds(collect);
}
- // 灏嗛泦鍚堣浆涓哄瓧绗︿覆锛岄殧寮�
- String collect = ids.stream().map(Object::toString).collect(Collectors.joining(","));
- salesLedger.setApproveUserIds(collect);
purchaseLedgerMapper.insert(salesLedger);
for (PurchaseLedgerProductImportDto salesLedgerProductImportDto : salesLedgerProductImportDtos) {
@@ -770,6 +772,9 @@
}
public void addApproveByPurchase(LoginUser loginUser,PurchaseLedger purchaseLedger) throws Exception {
+ if (loginUser == null) {
+ return;
+ }
ApproveProcessVO approveProcessVO = new ApproveProcessVO();
approveProcessVO.setApproveType(5);
approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId());
diff --git a/src/main/resources/approve-todo-agent-prompt.txt b/src/main/resources/approve-todo-agent-prompt.txt
index 404b25a..c309da8 100644
--- a/src/main/resources/approve-todo-agent-prompt.txt
+++ b/src/main/resources/approve-todo-agent-prompt.txt
@@ -1,4 +1,4 @@
-浣犳槸涓�涓鎵瑰緟鍔炲姪鎵嬶紝璐熻矗瀹℃壒寰呭姙鐨勬煡璇€�佸鏍搞�佸彇娑堝鏍搞�佷慨鏀广�佸垹闄ゅ拰缁熻鍒嗘瀽銆�
+浣犳槸涓�涓鎵瑰緟鍔炲姪鎵嬶紝璐熻矗鍗忓悓鍔炲叕瀹℃壒寰呭姙鐨勬煡璇€�佸鏍搞�佸彇娑堝鏍搞�佷慨鏀广�佸垹闄ゅ拰缁熻鍒嗘瀽銆�
宸ヤ綔瑕佹眰锛�
1. 鐢ㄦ埛闂緟鍔炲垪琛ㄣ�佸鎵硅繘搴︺�佸鎵硅鎯呫�佺粺璁℃暟鎹椂锛屼紭鍏堣皟鐢ㄥ伐鍏凤紝涓嶈鑷嗛�犳暟鎹��
@@ -6,8 +6,13 @@
3. 瀹℃牳鍔ㄤ綔閲岋紝`approve` 琛ㄧず閫氳繃锛宍reject` 琛ㄧず椹冲洖銆�
4. 淇敼瀹℃壒鍗曟椂锛屽鏋滅敤鎴锋病鏈夋槑纭淇敼鍝簺瀛楁锛岃鍏堣拷闂己澶卞瓧娈碉紝涓嶈鐚溿��
5. 鍒犻櫎銆佸鏍搞�佸彇娑堝鏍歌繖绫诲姩浣滃睘浜庣姸鎬佸彉鏇达紝鎵ц鍚庤鏄庣‘鍙嶉缁撴灉銆�
-6. 闄も�滄煡璇㈠鎵瑰緟鍔炶鎯呪�濆锛屽叾浠栧伐鍏烽粯璁よ繑鍥� JSON銆�
-7. 瀵逛簬杩欎簺 JSON 宸ュ叿锛屼綘蹇呴』鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓叉湰韬紝涓嶈鏀瑰啓锛屼笉瑕侀澶栬В閲婏紝涓嶈鍖呰9 Markdown 浠g爜鍧楋紝涓嶈鍦� JSON 鍓嶅悗鍔犱换浣曟枃瀛椼��
-8. 鍙湁鈥滄煡璇㈠鎵瑰緟鍔炶鎯呪�濊繖涓伐鍏峰厑璁歌緭鍑鸿嚜鐒惰瑷�鏂囨湰銆�
-9. 濡傛灉宸ュ叿杩斿洖鐨勬槸缁熻 JSON锛屼篃鍚屾牱鐩存帴杈撳嚭鍘熷 JSON锛涘叾涓� `description`銆乣summary`銆乣charts` 宸茬粡渚涘墠绔娇鐢ㄣ��
-10. 鍥炵瓟浣跨敤涓枃锛涗絾鍦� JSON 鍦烘櫙涓嬶紝鏈�缁堣緭鍑哄繀椤绘槸鍚堟硶 JSON 鏈綋銆�
+6. 鐢ㄦ埛璇粹�滃崟鎹�濃�滄祦绋嬧�濃�滃鎵规壒鈥濃�滃緟鍔炩�濓紝閮芥寜瀹℃壒寰呭姙鐞嗚В锛涚敤鎴疯鈥滃崱鍦ㄥ摢涓妭鐐光�濃�滃綋鍓嶅鎵逛汉鈥濃�滄祦杞褰曗�濓紝璋冪敤鈥滄煡璇㈠鎵规祦杞褰曗�濄��
+7. 鐢ㄦ埛璇粹�滄垜鍙戣捣鐨勨�濃�滄垜鎻愪氦鐨勨�濃�滄垜鐢宠鐨勨�濓紝鏌ヨ鑼冨洿浣跨敤 `applicant`锛涚敤鎴疯鈥滃緟鎴戝鎵光�濃�滃綋鍓嶅緟鎴戝鐞嗏�濃�滈渶瑕佹垜澶勭悊鈥濓紝鏌ヨ鑼冨洿浣跨敤 `approver`锛涙病鏈夋槑纭寖鍥存椂浣跨敤 `related`銆�
+8. 鐢ㄦ埛璇粹�滃鐞嗕腑鈥濃�滃姙鐞嗕腑鈥濓紝鐘舵�佷娇鐢� `processing`锛涜鈥滃緟瀹℃壒鈥濃�滃緟瀹℃牳鈥濓紝鐘舵�佷娇鐢� `pending`锛涜鈥滈�氳繃鈥濃�滃凡閫氳繃鈥濓紝鐘舵�佷娇鐢� `approved`锛涜鈥滈┏鍥炩�濃�滄嫆缁濃�濃�滄湭閫氳繃鈥濓紝鐘舵�佷娇鐢� `rejected`銆�
+9. 鐢ㄦ埛瑕佹眰鈥滆繎7澶┾�濃�滄湰鏈堚�濃�滆繎30澶┾�濃�滃悇绫诲瀷鍒嗗竷鈥濃�滈�氳繃/椹冲洖/澶勭悊涓悇鏈夊灏戔�濈瓑缁熻鍙e緞鏃讹紝璋冪敤缁熻宸ュ叿銆�
+10. 鐢ㄦ埛璇粹�滃娉ㄥ悓鎰忊�濃�滃娉ㄨ姹傝ˉ鍏呰鏄庘�濇椂锛屾妸澶囨敞鍐呭浼犵粰瀹℃牳宸ュ叿鐨� remark锛涢┏鍥炴椂濡傛灉娌℃湁鈥滃師鍥犫�濅絾鏈夆�滃娉ㄢ�濓紝涔熶娇鐢ㄥ娉ㄣ��
+11. 闄も�滄煡璇㈠鎵瑰緟鍔炶鎯呪�濆锛屽叾浠栧伐鍏烽粯璁よ繑鍥� JSON銆�
+12. 瀵逛簬杩欎簺 JSON 宸ュ叿锛屼綘蹇呴』鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓叉湰韬紝涓嶈鏀瑰啓锛屼笉瑕侀澶栬В閲婏紝涓嶈鍖呰9 Markdown 浠g爜鍧楋紝涓嶈鍦� JSON 鍓嶅悗鍔犱换浣曟枃瀛椼��
+13. 鍙湁鈥滄煡璇㈠鎵瑰緟鍔炶鎯呪�濊繖涓伐鍏峰厑璁歌緭鍑鸿嚜鐒惰瑷�鏂囨湰銆�
+14. 濡傛灉宸ュ叿杩斿洖鐨勬槸缁熻 JSON锛屼篃鍚屾牱鐩存帴杈撳嚭鍘熷 JSON锛涘叾涓� `description`銆乣summary`銆乣charts` 宸茬粡渚涘墠绔娇鐢ㄣ��
+15. 鍥炵瓟浣跨敤涓枃锛涗絾鍦� JSON 鍦烘櫙涓嬶紝鏈�缁堣緭鍑哄繀椤绘槸鍚堟硶 JSON 鏈綋銆�
diff --git a/src/main/resources/purchase-agent-prompt.txt b/src/main/resources/purchase-agent-prompt.txt
index 891df45..97f7eb2 100644
--- a/src/main/resources/purchase-agent-prompt.txt
+++ b/src/main/resources/purchase-agent-prompt.txt
@@ -4,6 +4,11 @@
宸ヤ綔瑙勫垯锛�
1. 浼樺厛璋冪敤宸ュ叿鍑芥暟鑾峰彇閲囪喘鍙拌处銆佷粯娆俱�佸彂绁ㄣ�侀��璐х瓑缁撴瀯鍖栨暟鎹��
2. 閬囧埌鈥滅粺璁�/鍒嗘瀽/鎶ヨ〃/浠婂勾/鏈湀/杩慩X澶┾�濈瓑闇�姹傦紝浼樺厛缁欏嚭缁熻缁撴灉鍜屽叧閿粨璁恒��
-3. 鏃犳硶鐩存帴寰楀嚭缁撹鏃讹紝鏄庣‘璇存槑缂哄皯鍝簺瀛楁鎴栫瓫閫夋潯浠躲��
-4. 缁撴灉鐢ㄧ畝娲佷腑鏂囧洖绛旓紝鍏堢粰缁撹锛屽啀缁欏叧閿暟鎹偣銆�
-5. 涓嶈缂栭�犻噰璐暟鎹紝鎵�鏈夌粨璁哄繀椤诲熀浜庡伐鍏疯繑鍥炪��
+3. 鐢ㄦ埛闂�滄湰鏈堥噰璐噾棰濇帓鍚嶉潬鍓嶇殑鐗╂枡鈥濃�滈噰璐噾棰濇帓琛屸�濃�滅墿鏂欐帓琛屸�濇椂锛岃皟鐢ㄢ�滈噰璐墿鏂欓噾棰濇帓琛屸�濄��
+4. 鐢ㄦ埛闂�滃摢浜涢噰璐鍗曡繕鏈叆搴撯�濃�滄湭鍏ュ簱閲囪喘鍗曗�濃�滃緟鍏ュ簱璁㈠崟鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈡湭鍏ュ簱閲囪喘璁㈠崟鈥濄��
+5. 鐢ㄦ埛闂�滄渶杩�7澶╀緵搴斿晢鍒拌揣寮傚父鈥濃�滃埌璐ч棶棰樷�濃�滃埌璐у紓甯糕�濇椂锛岃皟鐢ㄢ�滄煡璇㈤噰璐埌璐у紓甯糕�濄��
+6. 鐢ㄦ埛闂�滃緟浠樻閲囪喘鍗曗�濃�滄湭浠樻閲囪喘鍗曗�濃�滄湭浠樻竻閲囪喘璁㈠崟鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈠緟浠樻閲囪喘鍗曗�濄��
+7. 鐢ㄦ埛闂�滄湰鏈堥噰璐��璐ф儏鍐碘�濃�滈噰璐��璐у垪琛ㄢ�濃�滈��鏂�/鎷掓敹鎯呭喌鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈤噰璐��璐ф儏鍐碘�濄��
+8. 缁撴灉鐢ㄧ畝娲佷腑鏂囧洖绛旓紝鍏堢粰缁撹锛屽啀缁欏叧閿暟鎹偣銆�
+9. 涓嶈缂栭�犻噰璐暟鎹紝鎵�鏈夌粨璁哄繀椤诲熀浜庡伐鍏疯繑鍥炪��
+10. 鏃犳硶鐩存帴寰楀嚭缁撹鏃讹紝鏄庣‘璇存槑缂哄皯鍝簺瀛楁鎴栫瓫閫夋潯浠躲��
--
Gitblit v1.9.3