From 88dc4c9004c0a19ee7797e6fcd74b96efaffca0a Mon Sep 17 00:00:00 2001
From: jenkins <jenkins@example.com>
Date: 星期四, 21 五月 2026 15:29:56 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_New_pro' into dev_pro_河南鹤壁
---
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml | 3
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java | 339 +++++++++++++-----------
src/main/java/com/ruoyi/ai/service/PurchaseAiService.java | 52 +++
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java | 2
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java | 3
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java | 3
doc/20260521_采购智能体优化前端变更文档.md | 85 ++++++
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java | 149 +++++++++-
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java | 56 +++
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java | 2
src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java | 137 +++++++++
src/main/resources/purchase-agent-prompt.txt | 9
12 files changed, 661 insertions(+), 179 deletions(-)
diff --git "a/doc/20260521_\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\344\274\230\345\214\226\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md" "b/doc/20260521_\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\344\274\230\345\214\226\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..58089e8
--- /dev/null
+++ "b/doc/20260521_\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\344\274\230\345\214\226\345\211\215\347\253\257\345\217\230\346\233\264\346\226\207\346\241\243.md"
@@ -0,0 +1,85 @@
+# 閲囪喘鏅鸿兘浣撲紭鍖栧墠绔彉鏇存枃妗�
+
+## 1. 鍙樻洿鑳屾櫙
+
+鏈閽堝閲囪喘鏅鸿兘浣撳仛浜嗗榻愪紭鍖栵紙鍙傝�冮攢鍞�/瀹℃壒/鍒堕�犳櫤鑳戒綋锛夛細
+
+1. 鎻愬崌 `quickPrompts` 鍛戒腑绋冲畾鎬с��
+2. 澧炲己鐩稿鏃堕棿璇嗗埆锛堜粖澶�/鏄ㄥぉ/鏈懆/涓婂懆/鏈湀/涓婃湀/浠婂勾/鍘诲勾/杩慛澶╃瓑锛夈��
+3. 澧炲姞涓氬姟鎰忓浘鏈瘑鍒椂鐨勭粨鏋勫寲鍏滃簳鍝嶅簲锛岄伩鍏嶇紪閫犳暟鎹��
+4. 琛ュ厖寰呬粯娆炬煡璇㈢殑姹囨�诲瓧娈碉紝渚夸簬鍓嶇鐩存帴娓叉煋缁熻鍗$墖銆�
+
+## 2. 鎺ュ彛褰卞搷姒傝
+
+| 鎺ュ彛 | 鏂规硶 | 鏄惁鏀硅矾寰� | 鏄惁鏀瑰叆鍙� | 鏄惁鏀硅繑鍥炵粨鏋� |
+| --- | --- | --- | --- | --- |
+| `/purchase-ai/chat` | POST(SSE) | 鍚� | 鍚� | 鏄紙鏂板鍏滃簳 JSON 绫诲瀷锛� |
+| `/purchase-ai/analyze-files` | POST(SSE) | 鍚� | 鍚� | 鍚︼紙浠呭唴閮ㄦ彁绀鸿瘝澧炲己锛� |
+
+## 3. 鏂板鍏滃簳鍝嶅簲锛堥噸鐐癸級
+
+褰撶敤鎴锋槑鏄惧湪闂噰璐笟鍔★紝浣嗘潯浠朵笉鍏呭垎涓旀湭鍛戒腑鍙墽琛屾剰鍥炬椂锛宍/purchase-ai/chat` 浼氱洿鎺ヨ繑鍥炵粨鏋勫寲 JSON锛堣�屼笉鏄嚜鐢辨枃鏈級锛�
+
+```json
+{
+ "success": false,
+ "type": "purchase_intent_not_recognized",
+ "description": "鏈瘑鍒埌鍙墽琛岀殑閲囪喘鏌ヨ鏉′欢銆備负淇濊瘉缁撴灉鍑嗙‘锛屽綋鍓嶄笉浼氭帹娴嬫垨缂栭�犳暟鎹紝璇疯ˉ鍏呮槑纭椂闂磋寖鍥淬�佷緵搴斿晢銆侀噰璐悎鍚屽彿鎴栫墿鏂欏悗鍐嶆煡璇€��",
+ "summary": {},
+ "data": {
+ "quickPrompts": [
+ "鏈湀閲囪喘閲戦鎺掑悕鍓嶅崄鐨勭墿鏂欐湁鍝簺锛�",
+ "鍝簺閲囪喘璁㈠崟杩樻湭鍏ュ簱锛�",
+ "鏈�杩�7澶╀緵搴斿晢鍒拌揣寮傚父鏈夊摢浜涳紵",
+ "甯垜缁熻寰呬粯娆鹃噰璐崟锛�",
+ "鍒楀嚭鏈湀閲囪喘閫�璐ф儏鍐�"
+ ]
+ },
+ "charts": {}
+}
+```
+
+鍓嶇澶勭悊寤鸿锛�
+
+1. 褰� `type === "purchase_intent_not_recognized"` 鏃讹紝灞曠ず `description`銆�
+2. 璇诲彇 `data.quickPrompts` 浣滀负蹇嵎鎻愰棶鎸夐挳锛堝彲鐩存帴鍥炲~杈撳叆妗嗭級銆�
+
+## 4. 寰呬粯娆捐繑鍥炴柊澧炴眹鎬诲瓧娈�
+
+鎺ュ彛绫诲瀷锛歚type = "purchase_pending_payment_list"`
+浣嶇疆锛歚summary`
+
+鏂板瀛楁锛�
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+| --- | --- | --- |
+| pendingOrderCount | number | 寰呬粯娆捐鍗曟暟 |
+| totalContractAmount | number | 寰呬粯娆捐鍗曞悎鍚屾�婚 |
+| totalPaidAmount | number | 宸蹭粯娆炬�婚 |
+| totalPendingAmount | number | 寰呬粯娆炬�婚 |
+
+璇存槑锛氬師鏈夊瓧娈典粛淇濈暀锛堝吋瀹癸級锛屾湰娆′负澧為噺瀛楁锛屼笉鐮村潖鐜版湁娓叉煋銆�
+
+## 5. 鏃堕棿鍙e緞浼樺寲
+
+閲囪喘鏅鸿兘浣撶幇鍦ㄧ粺涓�鎸変腑鍥芥椂鍖哄姩鎬佹棩鏈熸崲绠楃浉瀵规椂闂达紝鏀寔锛�
+
+- 浠婂ぉ銆佹槰澶�
+- 鏈懆銆佷笂鍛�
+- 鏈湀銆佷笂鏈�
+- 浠婂勾銆佸幓骞�
+- 杩慛澶�/鍛�/鏈�/骞淬�佽繎鍗婂勾銆佽繎鍗婁釜鏈�
+
+鍓嶇鏃犻渶鏀逛紶鍙傦紝浣嗗睍绀烘椂闂磋寖鍥存椂璇蜂互鍚庣杩斿洖 `summary.startDate/endDate/timeRange` 涓哄噯銆�
+
+## 6. 鍓嶇鑱旇皟妫�鏌ユ竻鍗�
+
+1. `chat` 娴佸紡缁撴灉鎷兼帴鍚庯紝浼樺厛鎸� JSON 瑙f瀽銆�
+2. 瑕嗙洊鏂扮被鍨� `purchase_intent_not_recognized` 鐨� UI 澶勭悊銆�
+3. 寰呬粯娆鹃〉闈㈣鍙栧苟灞曠ず `summary.totalPendingAmount` 绛夋柊澧炲瓧娈点��
+4. 楠岃瘉浠ヤ笅蹇嵎闂鍙ǔ瀹氳繑鍥炵粨鏋勫寲缁撴灉锛�
+ - 鏈湀閲囪喘閲戦鎺掑悕鍓嶅崄鐨勭墿鏂欐湁鍝簺锛�
+ - 鍝簺閲囪喘璁㈠崟杩樻湭鍏ュ簱锛�
+ - 鏈�杩�7澶╀緵搴斿晢鍒拌揣寮傚父鏈夊摢浜涳紵
+ - 甯垜缁熻寰呬粯娆鹃噰璐崟锛�
+ - 鍒楀嚭鏈湀閲囪喘閫�璐ф儏鍐�
diff --git a/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
index 802cc13..c04915e 100644
--- a/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
@@ -4,10 +4,9 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
-import java.time.DayOfWeek;
import java.time.LocalDate;
+import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
-import java.util.Locale;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -15,10 +14,12 @@
public class PurchaseIntentExecutor {
private static final Pattern ID_PATTERN = Pattern.compile("\\b\\d{1,12}\\b");
- private static final Pattern LIMIT_PATTERN = Pattern.compile("(鍓峾鏈�杩�)?(\\d{1,2})鏉�");
- private static final Pattern DATE_PATTERN = Pattern.compile("\\d{4}-\\d{2}-\\d{2}");
- private static final Pattern RELATIVE_RANGE_PATTERN = Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)");
+ private static final Pattern LIMIT_PATTERN = Pattern.compile("(鍓峾鏈�杩�)?\\s*(\\d{1,2})\\s*鏉�");
+ private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
+ private static final Pattern RELATIVE_RANGE_PATTERN = Pattern.compile("(杩憒鏈�杩�)\\s*(\\d{1,3})\\s*(澶﹟鍛▅涓湀|鏈坾骞�)");
+ private static final Pattern HALF_RANGE_PATTERN = Pattern.compile("(鏈�杩憒杩�)?鍗�(涓�)?(鏈坾骞�)");
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
private final PurchaseAgentTools purchaseAgentTools;
@@ -31,74 +32,63 @@
return null;
}
String text = message.trim();
- String startDate = extractStartDate(text);
- String endDate = extractEndDate(text);
- Integer limit = extractLimit(text);
+ String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
+ if (StringUtils.hasText(quickPromptResponse)) {
+ return quickPromptResponse;
+ }
- if (containsAny(text, "鎺掕", "鎺掑悕", "鍓嶅嚑", "鍓嶄簲", "鍓嶅崄") && containsAny(text, "鐗╂枡", "浜у搧", "鍘熸潗鏂�", "閲囪喘閲戦", "閲戦")) {
- return purchaseAgentTools.rankPurchaseMaterials(
- memoryId,
- startDate,
- endDate,
- text,
- limit
- );
+ String keyword = extractKeyword(text);
+ Integer limit = extractLimit(text);
+ DateRange dateRange = extractDateRange(text);
+ String startDate = dateRange.startDate();
+ String endDate = dateRange.endDate();
+
+ if (containsAny(text, "鎺掕", "鎺掑悕", "鍓嶅嚑", "鍓嶄簲", "鍓嶅崄")
+ && containsAny(text, "鐗╂枡", "浜у搧", "鍘熸潗鏂�", "閲囪喘閲戦", "閲戦")) {
+ return purchaseAgentTools.rankPurchaseMaterials(memoryId, startDate, endDate, text, limit);
}
if (containsAny(text, "鏈叆搴�", "寰呭叆搴�", "娌℃湁鍏ュ簱", "杩樻湭鍏ュ簱")) {
- return purchaseAgentTools.listUnstockedPurchaseOrders(
- memoryId,
- startDate,
- endDate,
- extractKeyword(text),
- limit
- );
+ return purchaseAgentTools.listUnstockedPurchaseOrders(memoryId, startDate, endDate, keyword, limit);
}
if (containsAny(text, "鍒拌揣寮傚父", "鍒拌揣鏈夊紓甯�", "寮傚父鍒拌揣", "鍒拌揣闂", "渚涘簲鍟嗗埌璐у紓甯�")) {
- return purchaseAgentTools.listArrivalExceptions(
- memoryId,
- startDate,
- endDate,
- text,
- limit
- );
+ return purchaseAgentTools.listArrivalExceptions(memoryId, startDate, endDate, text, limit);
}
if (containsAny(text, "寰呬粯娆�", "鏈粯娆�", "鏈粯娓�", "寰呮敮浠�", "搴斾粯")) {
- return purchaseAgentTools.listPendingPaymentOrders(
- memoryId,
- startDate,
- endDate,
- extractKeyword(text),
- limit
- );
+ return purchaseAgentTools.listPendingPaymentOrders(memoryId, startDate, endDate, keyword, limit);
}
if (containsAny(text, "閫�璐�", "閫�鏂�", "鎷掓敹")) {
- return purchaseAgentTools.listPurchaseReturns(
- memoryId,
- startDate,
- endDate,
- extractKeyword(text),
- limit
- );
+ return purchaseAgentTools.listPurchaseReturns(memoryId, startDate, endDate, keyword, limit);
}
if (isStatsIntent(text)) {
- return purchaseAgentTools.getPurchaseStats(
- memoryId,
- startDate,
- endDate,
- text
- );
+ return purchaseAgentTools.getPurchaseStats(memoryId, startDate, endDate, text);
}
- if (containsAny(text, "璇︽儏", "鏄庣粏") && extractId(text) != null) {
- return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, extractId(text));
+
+ Long ledgerId = extractId(text);
+ if (containsAny(text, "璇︽儏", "鏄庣粏") && ledgerId != null) {
+ return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, ledgerId);
}
if (containsAny(text, "鍙拌处", "閲囪喘鍗�", "閲囪喘璁㈠崟", "璁㈠崟", "鍚堝悓", "鍒楄〃", "鏌ヨ")) {
- return purchaseAgentTools.listPurchaseLedgers(
- memoryId,
- extractKeyword(text),
- startDate,
- endDate,
- limit
- );
+ return purchaseAgentTools.listPurchaseLedgers(memoryId, keyword, startDate, endDate, limit);
+ }
+ return null;
+ }
+
+ private String tryExecuteQuickPrompt(String memoryId, String text) {
+ String normalized = normalizeForMatch(text);
+ if ("鏈湀閲囪喘閲戦鎺掑悕鍓嶅崄鐨勭墿鏂欐湁鍝簺".equals(normalized)) {
+ return purchaseAgentTools.rankPurchaseMaterials(memoryId, null, null, "鏈湀", 10);
+ }
+ if ("鍝簺閲囪喘璁㈠崟杩樻湭鍏ュ簱".equals(normalized)) {
+ return purchaseAgentTools.listUnstockedPurchaseOrders(memoryId, null, null, null, 10);
+ }
+ if ("鏈�杩�7澶╀緵搴斿晢鍒拌揣寮傚父鏈夊摢浜�".equals(normalized)) {
+ return purchaseAgentTools.listArrivalExceptions(memoryId, null, null, "鏈�杩�7澶�", 10);
+ }
+ if ("甯垜缁熻寰呬粯娆鹃噰璐崟".equals(normalized)) {
+ return purchaseAgentTools.listPendingPaymentOrders(memoryId, null, null, null, 10);
+ }
+ if ("鍒楀嚭鏈湀閲囪喘閫�璐ф儏鍐�".equals(normalized)) {
+ return purchaseAgentTools.listPurchaseReturns(memoryId, null, null, null, 10);
}
return null;
}
@@ -109,8 +99,10 @@
}
boolean queryWord = containsAny(text, "鏌ヨ", "鏌ョ湅", "鐪嬩笅", "鐪嬬湅", "鑾峰彇");
boolean dataWord = containsAny(text, "鏁版嵁", "閲戦", "鏁伴噺", "鍚堝悓棰�", "浠樻棰�", "鍙戠エ棰�");
- boolean timeWord = containsAny(text, "浠婂ぉ", "鏈懆", "鏈湀", "涓婃湀", "浠婂勾", "鍘诲勾", "杩戝崐骞�", "鏈�杩戝崐涓湀", "鍗婁釜鏈�")
- || DATE_PATTERN.matcher(text).find();
+ boolean timeWord = containsAny(text, "浠婂ぉ", "鏄ㄥぉ", "鏈懆", "涓婂懆", "鏈湀", "涓婃湀", "浠婂勾", "鍘诲勾", "杩戝崐骞�", "鏈�杩戝崐涓湀", "鍗婁釜鏈�")
+ || DATE_PATTERN.matcher(text).find()
+ || RELATIVE_RANGE_PATTERN.matcher(text).find()
+ || HALF_RANGE_PATTERN.matcher(text).find();
return queryWord && dataWord && timeWord;
}
@@ -136,114 +128,58 @@
return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
}
- private String extractStartDate(String text) {
+ private DateRange extractDateRange(String text) {
Matcher matcher = DATE_PATTERN.matcher(text);
if (matcher.find()) {
- return matcher.group();
+ String first = matcher.group(1);
+ String second = matcher.find() ? matcher.group(1) : first;
+ return buildDateRange(first, second);
}
- DateRange range = extractRelativeDateRange(text);
- return range == null ? null : range.start().format(DATE_FMT);
- }
- private String extractEndDate(String text) {
- Matcher matcher = DATE_PATTERN.matcher(text);
- if (!matcher.find()) {
- DateRange range = extractRelativeDateRange(text);
- return range == null ? null : range.end().format(DATE_FMT);
+ LocalDate today = LocalDate.now(CHINA_ZONE_ID);
+ if (text.contains("浠婂ぉ")) {
+ return new DateRange(formatDate(today), formatDate(today));
}
- return matcher.find() ? matcher.group() : null;
- }
-
- private String extractKeyword(String text) {
- String cleaned = text
- .replace("鏌ヨ", "")
- .replace("鏌ョ湅", "")
- .replace("閲囪喘", "")
- .replace("閲囪喘鍗�", "")
- .replace("閲囪喘璁㈠崟", "")
- .replace("璁㈠崟", "")
- .replace("浠婂勾", "")
- .replace("鏈勾", "")
- .replace("鍘诲勾", "")
- .replace("鏈湀", "")
- .replace("涓婃湀", "")
- .replace("鏈懆", "")
- .replace("涓婂懆", "")
- .replace("浠婂ぉ", "")
- .replace("鏄ㄥぉ", "")
- .replace("杩戝崐骞�", "")
- .replace("鏈�杩戝崐骞�", "")
- .replace("鏈�杩戝崐涓湀", "")
- .replace("鍗婁釜鏈�", "")
- .replace("鍙拌处", "")
- .replace("鍒楄〃", "")
- .replace("鍝簺", "")
- .replace("浠�涔�", "")
- .replace("鎯呭喌", "")
- .replace("鏈夋病鏈�", "")
- .replace("鏈夊暐", "")
- .replace("鏈夋棤", "")
- .replace("鍒楀嚭", "")
- .replace("甯垜", "")
- .replace("缁欐垜", "")
- .replace("鎴�", "")
- .replace("鏈�杩�10鏉�", "")
- .replace("鍓�10鏉�", "")
- .trim();
- cleaned = DATE_PATTERN.matcher(cleaned).replaceAll("");
- cleaned = RELATIVE_RANGE_PATTERN.matcher(cleaned).replaceAll("");
- return cleaned.length() >= 2 ? cleaned : null;
- }
-
- private DateRange extractRelativeDateRange(String text) {
- if (!StringUtils.hasText(text)) {
- return null;
- }
- String normalized = text.toLowerCase(Locale.ROOT);
- LocalDate today = LocalDate.now();
-
- if (normalized.contains("浠婂ぉ")) {
- return new DateRange(today, today);
- }
- if (normalized.contains("鏄ㄥぉ")) {
+ if (text.contains("鏄ㄥぉ")) {
LocalDate yesterday = today.minusDays(1);
- return new DateRange(yesterday, yesterday);
+ return new DateRange(formatDate(yesterday), formatDate(yesterday));
}
- if (normalized.contains("浠婂勾") || normalized.contains("鏈勾")) {
- return new DateRange(today.withDayOfYear(1), today);
+ if (text.contains("鏈懆")) {
+ LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+ return new DateRange(formatDate(start), formatDate(today));
}
- if (normalized.contains("鍘诲勾")) {
- LocalDate first = today.minusYears(1).withDayOfYear(1);
- LocalDate last = first.withDayOfYear(first.lengthOfYear());
- return new DateRange(first, last);
+ if (text.contains("涓婂懆")) {
+ LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+ LocalDate start = thisWeekStart.minusWeeks(1);
+ LocalDate end = start.plusDays(6);
+ return new DateRange(formatDate(start), formatDate(end));
}
- if (normalized.contains("鏈湀")) {
- return new DateRange(today.withDayOfMonth(1), today);
+ if (text.contains("鏈湀")) {
+ return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today));
}
- if (normalized.contains("涓婃湀")) {
- LocalDate first = today.minusMonths(1).withDayOfMonth(1);
- LocalDate last = first.withDayOfMonth(first.lengthOfMonth());
- return new DateRange(first, last);
+ if (text.contains("涓婃湀")) {
+ LocalDate start = today.minusMonths(1).withDayOfMonth(1);
+ return new DateRange(formatDate(start), formatDate(start.withDayOfMonth(start.lengthOfMonth())));
}
- if (normalized.contains("鏈懆")) {
- LocalDate weekStart = today.with(DayOfWeek.MONDAY);
- return new DateRange(weekStart, today);
+ if (text.contains("浠婂勾") || text.contains("鏈勾")) {
+ return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today));
}
- if (normalized.contains("涓婂懆")) {
- LocalDate weekStart = today.with(DayOfWeek.MONDAY).minusWeeks(1);
- return new DateRange(weekStart, weekStart.plusDays(6));
+ if (text.contains("鍘诲勾")) {
+ LocalDate start = today.minusYears(1).withDayOfYear(1);
+ LocalDate end = start.withDayOfYear(start.lengthOfYear());
+ return new DateRange(formatDate(start), formatDate(end));
}
- if (normalized.contains("杩戝崐骞�") || normalized.contains("鏈�杩戝崐骞�")) {
- return new DateRange(today.minusMonths(6).plusDays(1), today);
+ if (containsAny(text, "杩戝崐骞�", "鏈�杩戝崐骞�")) {
+ return new DateRange(formatDate(today.minusMonths(6).plusDays(1)), formatDate(today));
}
- if (normalized.contains("杩戝崐涓湀") || normalized.contains("鏈�杩戝崐涓湀") || normalized.contains("鍗婁釜鏈�")) {
- return new DateRange(today.minusDays(14), today);
+ if (containsAny(text, "杩戝崐涓湀", "鏈�杩戝崐涓湀", "鍗婁釜鏈�")) {
+ return new DateRange(formatDate(today.minusDays(14)), formatDate(today));
}
- Matcher matcher = RELATIVE_RANGE_PATTERN.matcher(normalized);
- if (matcher.find()) {
- int amount = Integer.parseInt(matcher.group(2));
- String unit = matcher.group(3);
+ Matcher relativeMatcher = RELATIVE_RANGE_PATTERN.matcher(text);
+ if (relativeMatcher.find()) {
+ int amount = Integer.parseInt(relativeMatcher.group(2));
+ String unit = relativeMatcher.group(3);
LocalDate start = switch (unit) {
case "澶�" -> today.minusDays(Math.max(amount - 1L, 0));
case "鍛�" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
@@ -251,11 +187,102 @@
case "骞�" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
default -> today.minusDays(29);
};
- return new DateRange(start, today);
+ return new DateRange(formatDate(start), formatDate(today));
}
- return null;
+
+ return new DateRange(null, null);
}
- private record DateRange(LocalDate start, LocalDate end) {
+ private DateRange buildDateRange(String start, String end) {
+ LocalDate startDate = parseDate(start);
+ LocalDate endDate = parseDate(end);
+ if (startDate == null || endDate == null) {
+ return new DateRange(null, null);
+ }
+ if (startDate.isAfter(endDate)) {
+ LocalDate temp = startDate;
+ startDate = endDate;
+ endDate = temp;
+ }
+ return new DateRange(formatDate(startDate), formatDate(endDate));
+ }
+
+ private LocalDate parseDate(String text) {
+ try {
+ return LocalDate.parse(text, DATE_FMT);
+ } catch (Exception ignored) {
+ return null;
+ }
+ }
+
+ private String formatDate(LocalDate date) {
+ return date == null ? null : date.format(DATE_FMT);
+ }
+
+ private String normalizeForMatch(String text) {
+ if (!StringUtils.hasText(text)) {
+ return "";
+ }
+ return text.replace("锛�", "")
+ .replace(",", "")
+ .replace("銆�", "")
+ .replace(".", "")
+ .replace("锛�", "")
+ .replace("!", "")
+ .replace("锛�", "")
+ .replace("?", "")
+ .replace("锛�", "")
+ .replace(":", "")
+ .replace("锛�", "")
+ .replace(";", "")
+ .replace(" ", "")
+ .trim();
+ }
+
+ private String extractKeyword(String text) {
+ String cleaned = text
+ .replace("鏌ヨ", "")
+ .replace("鏌ョ湅", "")
+ .replace("鐪嬩笅", "")
+ .replace("鐪嬬湅", "")
+ .replace("璇�", "")
+ .replace("涓�涓�", "")
+ .replace("閲囪喘", "")
+ .replace("閲囪喘鍗�", "")
+ .replace("閲囪喘璁㈠崟", "")
+ .replace("璁㈠崟", "")
+ .replace("鍙拌处", "")
+ .replace("鍒楄〃", "")
+ .replace("鍝簺", "")
+ .replace("鍒楀嚭", "")
+ .replace("甯垜", "")
+ .replace("缁熻", "")
+ .replace("鍒嗘瀽", "")
+ .replace("鏈湀", "")
+ .replace("涓婃湀", "")
+ .replace("鏈勾", "")
+ .replace("浠婂勾", "")
+ .replace("鍘诲勾", "")
+ .replace("鏈懆", "")
+ .replace("涓婂懆", "")
+ .replace("浠婂ぉ", "")
+ .replace("鏄ㄥぉ", "")
+ .replace("杩�30澶�", "")
+ .replace("杩�7澶�", "")
+ .replace("杩�15澶�", "")
+ .replace("杩�60澶�", "")
+ .replace("鏈�杩�30澶�", "")
+ .replace("鏈�杩�7澶�", "")
+ .replace("鏈�杩�15澶�", "")
+ .replace("鏈�杩�60澶�", "")
+ .replace("鏈�杩�10鏉�", "")
+ .replace("鍓�10鏉�", "")
+ .replace("鍓�20鏉�", "")
+ .replace("鏈�杩�20鏉�", "")
+ .trim();
+ return cleaned.length() >= 2 ? cleaned : null;
+ }
+
+ private record DateRange(String startDate, String endDate) {
}
}
diff --git a/src/main/java/com/ruoyi/ai/service/PurchaseAiService.java b/src/main/java/com/ruoyi/ai/service/PurchaseAiService.java
index 85e174f..a4a0dc7 100644
--- a/src/main/java/com/ruoyi/ai/service/PurchaseAiService.java
+++ b/src/main/java/com/ruoyi/ai/service/PurchaseAiService.java
@@ -1,5 +1,6 @@
package com.ruoyi.ai.service;
+import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.fasterxml.jackson.core.type.TypeReference;
import com.fasterxml.jackson.databind.ObjectMapper;
@@ -122,6 +123,16 @@
);
aiChatSessionService.refreshSessionStats(memoryId, loginUser);
return Flux.just(directResponse);
+ }
+
+ if (isPurchaseBusinessIntent(userMessage)) {
+ String noGuessResponse = buildNoGuessResponse();
+ mongoChatMemoryStore.appendMessages(
+ memoryId,
+ List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse))
+ );
+ aiChatSessionService.refreshSessionStats(memoryId, loginUser);
+ return Flux.just(noGuessResponse);
}
return purchaseAgent.chat(memoryId, userMessage, currentDateForPrompt())
@@ -461,6 +472,47 @@
return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
}
+ private boolean isPurchaseBusinessIntent(String message) {
+ if (!StringUtils.hasText(message)) {
+ return false;
+ }
+ String text = message.trim();
+ boolean hasDomainWord = containsAny(text,
+ "閲囪喘", "閲囪喘鍙拌处", "閲囪喘鍗�", "閲囪喘璁㈠崟", "渚涘簲鍟�", "鐗╂枡", "鍏ュ簱", "鍒拌揣", "寰呬粯娆�",
+ "浠樻", "閫�璐�", "閫�鏂�", "鍙戠エ", "鍚堝悓");
+ boolean hasIntentWord = containsAny(text,
+ "鏌ヨ", "鏌ョ湅", "缁熻", "鍒嗘瀽", "鎺掕", "鎺掑悕", "鍒楀嚭", "鏈夊摢浜�", "鎯呭喌", "鏄庣粏", "璇︽儏", "鎶ヨ〃");
+ return hasDomainWord && hasIntentWord;
+ }
+
+ private boolean containsAny(String text, String... keywords) {
+ for (String keyword : keywords) {
+ if (text.contains(keyword)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private String buildNoGuessResponse() {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("success", false);
+ result.put("type", "purchase_intent_not_recognized");
+ result.put("description", "鏈瘑鍒埌鍙墽琛岀殑閲囪喘鏌ヨ鏉′欢銆備负淇濊瘉缁撴灉鍑嗙‘锛屽綋鍓嶄笉浼氭帹娴嬫垨缂栭�犳暟鎹紝璇疯ˉ鍏呮槑纭椂闂磋寖鍥淬�佷緵搴斿晢銆侀噰璐悎鍚屽彿鎴栫墿鏂欏悗鍐嶆煡璇€��");
+ result.put("summary", Map.of());
+ result.put("data", Map.of(
+ "quickPrompts", List.of(
+ "鏈湀閲囪喘閲戦鎺掑悕鍓嶅崄鐨勭墿鏂欐湁鍝簺锛�",
+ "鍝簺閲囪喘璁㈠崟杩樻湭鍏ュ簱锛�",
+ "鏈�杩�7澶╀緵搴斿晢鍒拌揣寮傚父鏈夊摢浜涳紵",
+ "甯垜缁熻寰呬粯娆鹃噰璐崟锛�",
+ "鍒楀嚭鏈湀閲囪喘閫�璐ф儏鍐�"
+ )
+ ));
+ result.put("charts", Map.of());
+ return JSON.toJSONString(result);
+ }
+
private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) {
return """
浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝涓ユ牸鏍规嵁鐢ㄦ埛涓婁紶鐨勫涓枃浠跺拰鐢ㄦ埛瑕佹眰鎻愬彇閲囪喘涓氬姟鏁版嵁銆�
diff --git a/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
index 17b6868..2ec6003 100644
--- a/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -42,6 +42,7 @@
public class PurchaseAgentTools {
private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
private static final int DEFAULT_LIMIT = 10;
private static final int MAX_LIMIT = 30;
@@ -286,8 +287,24 @@
.sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder()))
.limit(normalizeLimit(limit))
.collect(Collectors.toList());
+
+ BigDecimal totalContractAmount = items.stream()
+ .map(item -> asBigDecimal(item.get("contractAmount")))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ BigDecimal totalPaidAmount = items.stream()
+ .map(item -> asBigDecimal(item.get("paidAmount")))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ BigDecimal totalPendingAmount = items.stream()
+ .map(item -> asBigDecimal(item.get("pendingAmount")))
+ .reduce(BigDecimal.ZERO, BigDecimal::add);
+ Map<String, Object> summary = rangeSummary(range, items.size());
+ summary.put("pendingOrderCount", items.size());
+ summary.put("totalContractAmount", totalContractAmount);
+ summary.put("totalPaidAmount", totalPaidAmount);
+ summary.put("totalPendingAmount", totalPendingAmount);
+
return jsonResponse(true, "purchase_pending_payment_list", "宸茶繑鍥炲緟浠樻閲囪喘鍗曘��",
- rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+ summary, Map.of("items", items), Map.of());
}
@Tool(name = "鏌ヨ閲囪喘閫�璐ф儏鍐�", value = "鎸夋椂闂磋寖鍥存煡璇㈤噰璐��璐у崟鍒楄〃鍜岄��璐ч噾棰濄��")
@@ -446,6 +463,19 @@
return value == null ? BigDecimal.ZERO : value;
}
+ private BigDecimal asBigDecimal(Object value) {
+ if (value == null) {
+ return BigDecimal.ZERO;
+ }
+ if (value instanceof BigDecimal decimal) {
+ return decimal;
+ }
+ if (value instanceof Number number) {
+ return new BigDecimal(String.valueOf(number));
+ }
+ return BigDecimal.ZERO;
+ }
+
private List<PaymentRegistration> queryPayments(LoginUser loginUser, DateRange range) {
LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
@@ -484,7 +514,7 @@
}
private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
- LocalDate today = LocalDate.now();
+ LocalDate today = LocalDate.now(CHINA_ZONE_ID);
LocalDate start = parseLocalDate(startDate);
LocalDate end = parseLocalDate(endDate);
if (start != null || end != null) {
@@ -501,6 +531,22 @@
return new DateRange(today.minusDays(29), today, "杩�30澶�");
}
String text = timeRange.trim();
+ if (text.contains("浠婂ぉ")) {
+ return new DateRange(today, today, "浠婂ぉ");
+ }
+ if (text.contains("鏄ㄥぉ")) {
+ LocalDate yesterday = today.minusDays(1);
+ return new DateRange(yesterday, yesterday, "鏄ㄥぉ");
+ }
+ if (text.contains("鏈懆")) {
+ LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+ return new DateRange(startOfWeek, today, "鏈懆");
+ }
+ if (text.contains("涓婂懆")) {
+ LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+ LocalDate startOfLastWeek = thisWeekStart.minusWeeks(1);
+ return new DateRange(startOfLastWeek, startOfLastWeek.plusDays(6), "涓婂懆");
+ }
if (text.contains("浠婂勾") || text.contains("鏈勾")) {
return new DateRange(today.withDayOfYear(1), today, "浠婂勾");
}
@@ -538,7 +584,11 @@
if (!StringUtils.hasText(text)) {
return null;
}
- return LocalDate.parse(text.trim(), DATE_FMT);
+ try {
+ return LocalDate.parse(text.trim(), DATE_FMT);
+ } catch (Exception ignored) {
+ return null;
+ }
}
private Date toDate(LocalDate localDate) {
diff --git a/src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java b/src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
index 965cbf3..8a75dcd 100644
--- a/src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
+++ b/src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java
@@ -44,4 +44,7 @@
@Schema(description = "缁撴潫鏃ユ湡")
@DateTimeFormat(pattern = "yyyy-MM-dd")
private LocalDate endDate;
+
+ @Schema(description = "鏄惁鐢熶骇")
+ private Integer isProduction;
}
diff --git a/src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java b/src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java
index dda1aa0..2765c10 100644
--- a/src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java
+++ b/src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java
@@ -39,7 +39,7 @@
@PostMapping("/updateRouteItem")
@Operation(summary = "淇敼鐢熶骇璁㈠崟鐨勫伐鑹鸿矾绾胯鎯�")
public R updateRouteItem(@RequestBody ProductionOrderRoutingOperation productionOrderRoutingOperation) {
- return R.ok(productionOrderRoutingOperationService.updateById(productionOrderRoutingOperation));
+ return R.ok(productionOrderRoutingOperationService.updateRouteItem(productionOrderRoutingOperation));
}
@DeleteMapping("/deleteRouteItem/{id}")
diff --git a/src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java b/src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java
index 2377dc3..8878006 100644
--- a/src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java
+++ b/src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java
@@ -8,6 +8,8 @@
R addRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation);
+ R updateRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation);
+
R deleteRouteItem(Long id);
int sortRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation);
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
index 200e95f..40dd74c 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -23,6 +23,7 @@
import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.production.service.ProductionBomStructureService;
+import com.ruoyi.production.util.TaskPlanQuantityUtil;
import com.ruoyi.technology.mapper.TechnologyOperationMapper;
import com.ruoyi.technology.mapper.TechnologyOperationParamMapper;
import com.ruoyi.technology.mapper.TechnologyParamMapper;
@@ -262,7 +263,7 @@
.filter(item -> item != null && item.getId() != null)
.collect(Collectors.toMap(ProductionOrderRoutingOperation::getId, item -> item, (left, right) -> left));
// Keep task plan quantities aligned with the same order BOM snapshot demand used during snapshot creation.
- Map<String, BigDecimal> demandedQuantityMap = buildOperationDemandedQuantityMap(structureList, rootProductModelId);
+ Map<String, BigDecimal> demandedQuantityMap = TaskPlanQuantityUtil.buildOperationDemandedQuantityMap(structureList, rootProductModelId);
for (ProductionOperationTask task : taskList) {
if (task == null || task.getId() == null || task.getProductionOrderRoutingOperationId() == null) {
continue;
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java
index 3d16a07..75d0696 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java
@@ -4,21 +4,15 @@
import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.framework.web.domain.R;
-import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
-import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
-import com.ruoyi.production.mapper.ProductionOrderRoutingOperationParamMapper;
-import com.ruoyi.production.mapper.ProductionProductMainMapper;
-import com.ruoyi.production.pojo.ProductionOperationTask;
-import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
-import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
-import com.ruoyi.production.pojo.ProductionProductMain;
+import com.ruoyi.production.mapper.*;
+import com.ruoyi.production.util.TaskPlanQuantityUtil;
+import com.ruoyi.technology.mapper.*;
+import com.ruoyi.production.pojo.*;
import com.ruoyi.production.service.ProductionOrderRoutingOperationService;
import com.ruoyi.production.service.ProductionProductMainService;
-import com.ruoyi.technology.mapper.TechnologyOperationParamMapper;
-import com.ruoyi.technology.mapper.TechnologyParamMapper;
-import com.ruoyi.technology.pojo.TechnologyOperationParam;
-import com.ruoyi.technology.pojo.TechnologyParam;
+import com.ruoyi.technology.pojo.*;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -26,8 +20,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.format.DateTimeFormatter;
-import java.util.ArrayList;
-import java.util.List;
+import java.util.*;
@Service
@Transactional(rollbackFor = Exception.class)
@@ -42,6 +35,11 @@
private final TechnologyOperationParamMapper technologyOperationParamMapper;
private final TechnologyParamMapper technologyParamMapper;
private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper;
+ private final ProductionOrderMapper productionOrderMapper;
+ private final ProductionOrderRoutingMapper productionOrderRoutingMapper;
+ private final ProductionOrderBomMapper productionOrderBomMapper;
+ private final ProductionBomStructureMapper productionBomStructureMapper;
+ private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
@Override
public R addRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation) {
@@ -102,6 +100,109 @@
productionOperationTaskMapper.insert(productionOperationTask);
}
return R.ok();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public R updateRouteItem(ProductionOrderRoutingOperation productionOrderRoutingOperation) {
+ Long operationId = productionOrderRoutingOperation.getId();
+
+ // 鏇存柊宸ヨ壓璺嚎宸ュ簭
+ productionOrderRoutingOperationMapper.updateById(productionOrderRoutingOperation);
+
+ // 閲嶆柊鏌ヨ瀹屾暣璁板綍锛堝墠绔彲鑳芥病鏈変紶閫掓墍鏈夊瓧娈碉紝濡� productionOrderId锛�
+ ProductionOrderRoutingOperation updatedOperation = productionOrderRoutingOperationMapper.selectById(operationId);
+ if (updatedOperation == null) {
+ throw new ServiceException("宸ヨ壓璺嚎宸ュ簭涓嶅瓨鍦�");
+ }
+
+ // 鏌ヨ鏄惁瀛樺湪宸ュ崟
+ ProductionOperationTask productionOperationTask = productionOperationTaskMapper.selectOne(
+ new LambdaQueryWrapper<ProductionOperationTask>()
+ .eq(ProductionOperationTask::getProductionOrderRoutingOperationId, operationId)
+ .last("limit 1"));
+
+ // 鏍规嵁鏄惁闇�瑕佺敓浜ц繘琛屽鐞�
+ Boolean isProduction = updatedOperation.getIsProduction();
+
+ if (Boolean.TRUE.equals(isProduction)) {
+ // 闇�瑕佺敓浜э細妫�鏌ュ伐鍗曟槸鍚﹀瓨鍦紝涓嶅瓨鍦ㄥ垯鐢熸垚
+ if (productionOperationTask == null) {
+ ProductionOperationTask task = new ProductionOperationTask();
+ task.setProductionOrderRoutingOperationId(updatedOperation.getId());
+ task.setProductionOrderId(updatedOperation.getProductionOrderId());
+ // 鑾峰彇鐢熶骇璁㈠崟
+ ProductionOrder productionOrder = productionOrderMapper.selectById(updatedOperation.getProductionOrderId());
+ if (productionOrder == null) {
+ throw new ServiceException("鐢熶骇璁㈠崟涓嶅瓨鍦�");
+ }
+
+ // 鑾峰彇璁㈠崟BOM
+ ProductionOrderBom orderBom = productionOrderBomMapper.selectOne(
+ Wrappers.<ProductionOrderBom>lambdaQuery()
+ .eq(ProductionOrderBom::getProductionOrderId, productionOrder.getId()));
+
+ // 纭畾鏍逛骇鍝佽鏍糏D
+ Long rootProductModelId = orderBom != null && orderBom.getProductModelId() != null
+ ? orderBom.getProductModelId()
+ : productionOrder.getProductModelId();
+
+ // 鑾峰彇BOM缁撴瀯鍒楄〃
+ List<ProductionBomStructure> orderBomStructureList = orderBom == null || orderBom.getId() == null
+ ? Collections.emptyList()
+ : productionBomStructureMapper.selectList(
+ Wrappers.<ProductionBomStructure>lambdaQuery()
+ .eq(ProductionBomStructure::getProductionOrderBomId, orderBom.getId())
+ .orderByAsc(ProductionBomStructure::getId));
+
+ // 鏋勫缓宸ュ簭闇�姹傞噺鏄犲皠
+ Map<String, BigDecimal> operationDemandedQuantityMap =
+ TaskPlanQuantityUtil.buildOperationDemandedQuantityMap(orderBomStructureList, rootProductModelId);
+
+ // 鑾峰彇宸ヨ壓璺嚎宸ュ簭锛堢敤浜庤绠楄鍒掓暟閲忥級
+ TechnologyRoutingOperation sourceOperation = technologyRoutingOperationMapper.selectById(
+ updatedOperation.getTechnologyRoutingOperationId());
+ // 灏嗗師鏉ョ殑绉佹湁鏂规硶鏇挎崲涓鸿皟鐢ㄥ伐鍏风被
+ BigDecimal planQuantity = TaskPlanQuantityUtil.resolveTaskPlanQuantity(
+ sourceOperation,
+ operationDemandedQuantityMap,
+ productionOrder,
+ rootProductModelId);
+ task.setPlanQuantity(planQuantity);
+ task.setCompleteQuantity(BigDecimal.ZERO);
+ task.setWorkOrderNo(generateNextTaskNo());
+ task.setStatus(2);
+ productionOperationTaskMapper.insert(task);
+ }
+ } else {
+ // 涓嶉渶瑕佺敓浜э細妫�鏌ュ伐鍗曟槸鍚﹀瓨鍦�
+ if (productionOperationTask != null) {
+ validateTaskCanRemove(productionOperationTask);
+ // 娌℃湁鎶ュ伐锛屽垯鍒犻櫎宸ュ崟
+ productionOperationTaskMapper.deleteById(productionOperationTask.getId());
+ }
+ }
+
+ return R.ok();
+ }
+
+ private void validateTaskCanRemove(ProductionOperationTask task) {
+ if (task == null || task.getId() == null) {
+ return;
+ }
+ if (defaultDecimal(task.getCompleteQuantity()).compareTo(BigDecimal.ZERO) > 0) {
+ throw new ServiceException("宸ュ簭宸蹭骇鐢熸姤宸ヨ褰曪紝鏃犳硶鏍规嵁 BOM 鍙樻洿鍒犻櫎瀵瑰簲宸ュ簭蹇収");
+ }
+ long reportCount = productionProductMainMapper.selectCount(
+ Wrappers.<ProductionProductMain>lambdaQuery()
+ .eq(ProductionProductMain::getProductionOperationTaskId, task.getId()));
+ if (reportCount > 0) {
+ throw new ServiceException("宸ュ簭宸蹭骇鐢熸姤宸ヨ褰曪紝鏃犳硶鏍规嵁 BOM 鍙樻洿鍒犻櫎瀵瑰簲宸ュ崟");
+ }
+ }
+
+ private BigDecimal defaultDecimal(BigDecimal value) {
+ return value == null ? BigDecimal.ZERO : value;
}
@Override
@@ -182,4 +283,24 @@
}
return 0;
}
+
+ private String generateNextTaskNo() {
+ // 鐢熸垚涓嬩竴涓敓浜у伐鍗曞彿
+ String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
+ String prefix = "GD" + datePrefix;
+ ProductionOperationTask lastTask = productionOperationTaskMapper.selectOne(
+ Wrappers.<ProductionOperationTask>lambdaQuery()
+ .likeRight(ProductionOperationTask::getWorkOrderNo, prefix)
+ .orderByDesc(ProductionOperationTask::getWorkOrderNo)
+ .last("limit 1"));
+ int sequence = 1;
+ if (lastTask != null && lastTask.getWorkOrderNo() != null && lastTask.getWorkOrderNo().startsWith(prefix)) {
+ try {
+ sequence = Integer.parseInt(lastTask.getWorkOrderNo().substring(prefix.length())) + 1;
+ } catch (NumberFormatException ignored) {
+ sequence = 1;
+ }
+ }
+ return prefix + String.format("%03d", sequence);
+ }
}
diff --git a/src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java b/src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java
new file mode 100644
index 0000000..e82d7e4
--- /dev/null
+++ b/src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java
@@ -0,0 +1,137 @@
+package com.ruoyi.production.util;
+
+import com.ruoyi.production.pojo.ProductionBomStructure;
+import com.ruoyi.production.pojo.ProductionOrder;
+import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
+import com.ruoyi.technology.pojo.TechnologyRoutingOperation;
+import lombok.experimental.UtilityClass;
+
+import java.math.BigDecimal;
+import java.util.*;
+import java.util.stream.Collectors;
+
+/**
+ * 宸ュ崟璁″垝鏁伴噺璁$畻宸ュ叿绫�
+ */
+@UtilityClass
+public class TaskPlanQuantityUtil {
+
+ /**
+ * 璁$畻宸ュ崟璁″垝鏁伴噺锛堜娇鐢� TechnologyRoutingOperation锛�
+ */
+ public BigDecimal resolveTaskPlanQuantity(TechnologyRoutingOperation sourceOperation,
+ Map<String, BigDecimal> operationDemandedQuantityMap,
+ ProductionOrder productionOrder,
+ Long rootProductModelId) {
+ if (sourceOperation == null || operationDemandedQuantityMap == null || operationDemandedQuantityMap.isEmpty()) {
+ return defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
+ }
+ Long outputProductModelId = sourceOperation.getProductModelId() != null
+ ? sourceOperation.getProductModelId()
+ : rootProductModelId;
+ String key = buildOperationDemandedQuantityKey(sourceOperation.getTechnologyOperationId(), outputProductModelId);
+ BigDecimal planQuantity = operationDemandedQuantityMap.get(key);
+ return planQuantity != null ? planQuantity : defaultDecimal(productionOrder == null ? null : productionOrder.getQuantity());
+ }
+
+ /**
+ * 璁$畻宸ュ崟璁″垝鏁伴噺锛堜娇鐢� ProductionOrderRoutingOperation锛�
+ */
+ public BigDecimal resolveTaskPlanQuantity(ProductionOrderRoutingOperation routingOperation,
+ Map<String, BigDecimal> demandedQuantityMap,
+ BigDecimal orderQuantity,
+ Long rootProductModelId) {
+ if (routingOperation == null || demandedQuantityMap == null || demandedQuantityMap.isEmpty()) {
+ return orderQuantity;
+ }
+ Long outputProductModelId = routingOperation.getProductModelId() != null
+ ? routingOperation.getProductModelId()
+ : rootProductModelId;
+ String key = buildOperationDemandedQuantityKey(routingOperation.getTechnologyOperationId(), outputProductModelId);
+ BigDecimal planQuantity = demandedQuantityMap.get(key);
+ return planQuantity != null ? planQuantity : orderQuantity;
+ }
+
+ /**
+ * 鏋勫缓宸ュ簭闇�姹傞噺鏄犲皠琛�
+ */
+ public Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> bomStructures, Long rootProductModelId) {
+ if (bomStructures == null || bomStructures.isEmpty()) {
+ return Collections.emptyMap();
+ }
+ Map<Long, ProductionBomStructure> structureById = new HashMap<>();
+ for (ProductionBomStructure item : bomStructures) {
+ if (item != null && item.getId() != null) {
+ structureById.put(item.getId(), item);
+ }
+ }
+ Map<String, BigDecimal> demandedQuantityMap = new HashMap<>();
+ Set<String> mergedOutputNodeKeySet = new HashSet<>();
+ for (ProductionBomStructure bomStructure : bomStructures) {
+ if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) {
+ continue;
+ }
+ ProductionBomStructure outputNode = resolveOperationOutputNode(bomStructure, structureById);
+ Long outputProductModelId = resolveOutputProductModelId(outputNode, rootProductModelId);
+ if (outputProductModelId == null) {
+ continue;
+ }
+ String mergedOutputNodeKey = buildOperationOutputNodeKey(bomStructure.getTechnologyOperationId(),
+ outputNode == null ? null : outputNode.getId(), outputProductModelId);
+ if (!mergedOutputNodeKeySet.add(mergedOutputNodeKey)) {
+ continue;
+ }
+ BigDecimal demandedQuantity = defaultDecimal(outputNode == null ? null : outputNode.getDemandedQuantity());
+ String key = buildOperationDemandedQuantityKey(bomStructure.getTechnologyOperationId(), outputProductModelId);
+ demandedQuantityMap.merge(key, demandedQuantity, BigDecimal::add);
+ }
+ return demandedQuantityMap;
+ }
+
+ /**
+ * 鏋勫缓宸ュ簭闇�姹傞噺key
+ */
+ public String buildOperationDemandedQuantityKey(Long operationId, Long outputProductModelId) {
+ return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId);
+ }
+
+ /**
+ * 鏋勫缓杈撳嚭鑺傜偣key
+ */
+ public String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) {
+ return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId);
+ }
+
+ /**
+ * 瑙f瀽宸ュ簭杈撳嚭鑺傜偣
+ */
+ public ProductionBomStructure resolveOperationOutputNode(ProductionBomStructure bomStructure,
+ Map<Long, ProductionBomStructure> structureById) {
+ if (bomStructure == null) {
+ return null;
+ }
+ if (bomStructure.getParentId() == null) {
+ return bomStructure;
+ }
+ ProductionBomStructure parent = structureById.get(bomStructure.getParentId());
+ return parent != null ? parent : bomStructure;
+ }
+
+ /**
+ * 瑙f瀽杈撳嚭浜у搧瑙勬牸ID
+ */
+ public Long resolveOutputProductModelId(ProductionBomStructure outputNode, Long rootProductModelId) {
+ if (outputNode == null) {
+ return rootProductModelId;
+ }
+ return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId;
+ }
+
+ /**
+ * 榛樿BigDecimal鍊�
+ */
+ public BigDecimal defaultDecimal(BigDecimal value) {
+ return value == null ? BigDecimal.ZERO : value;
+ }
+
+}
diff --git a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
index f029ef7..199eb21 100644
--- a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -64,6 +64,9 @@
<if test="c != null and c.status != null">
and pot.status = #{c.status}
</if>
+ <if test="c != null and c.isProduction != null">
+ and poro.is_production = #{c.isProduction}
+ </if>
<if test="c != null and c.workOrderNo != null and c.workOrderNo != ''">
and pot.work_order_no like concat('%', #{c.workOrderNo}, '%')
</if>
diff --git a/src/main/resources/purchase-agent-prompt.txt b/src/main/resources/purchase-agent-prompt.txt
index b4267b8..2fd5021 100644
--- a/src/main/resources/purchase-agent-prompt.txt
+++ b/src/main/resources/purchase-agent-prompt.txt
@@ -10,7 +10,8 @@
5. 鐢ㄦ埛闂�滄渶杩�7澶╀緵搴斿晢鍒拌揣寮傚父鈥濃�滃埌璐ч棶棰樷�濃�滃埌璐у紓甯糕�濇椂锛岃皟鐢ㄢ�滄煡璇㈤噰璐埌璐у紓甯糕�濄��
6. 鐢ㄦ埛闂�滃緟浠樻閲囪喘鍗曗�濃�滄湭浠樻閲囪喘鍗曗�濃�滄湭浠樻竻閲囪喘璁㈠崟鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈠緟浠樻閲囪喘鍗曗�濄��
7. 鐢ㄦ埛闂�滄湰鏈堥噰璐��璐ф儏鍐碘�濃�滈噰璐��璐у垪琛ㄢ�濃�滈��鏂�/鎷掓敹鎯呭喌鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈤噰璐��璐ф儏鍐碘�濄��
-8. 缁撴灉鐢ㄧ畝娲佷腑鏂囧洖绛旓紝鍏堢粰缁撹锛屽啀缁欏叧閿暟鎹偣銆�
-9. 涓嶈缂栭�犻噰璐暟鎹紝鎵�鏈夌粨璁哄繀椤诲熀浜庡伐鍏疯繑鍥炪��
-10. 鏃犳硶鐩存帴寰楀嚭缁撹鏃讹紝鏄庣‘璇存槑缂哄皯鍝簺瀛楁鎴栫瓫閫夋潯浠躲��
-11. 鐢ㄦ埛鎻愬埌鈥滀粖骞�/鏈湀/浠婂ぉ/鏈�杩�/涓婃湀/鍘诲勾鈥濈瓑鐩稿鏃堕棿鏃讹紝蹇呴』涓ユ牸鍩轰簬鈥滃綋鍓嶆棩鏈熲�濇崲绠楋紝绂佹鑷鍋囪骞翠唤銆�
+8. 宸ュ叿杩斿洖 JSON 鏃讹紝鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓诧紝涓嶈棰濆鍖呰9 Markdown锛屼篃涓嶈鍦ㄥ墠鍚庤拷鍔犺В閲婃枃鏈��
+9. 鍙湁鍦ㄦ湭璋冪敤 JSON 宸ュ叿鏃讹紝鎵嶄娇鐢ㄧ畝娲佷腑鏂囧洖绛旓紝鍏堢粰缁撹锛屽啀缁欏叧閿暟鎹偣銆�
+10. 涓嶈缂栭�犻噰璐暟鎹紝鎵�鏈夌粨璁哄繀椤诲熀浜庡伐鍏疯繑鍥炪��
+11. 鏃犳硶鐩存帴寰楀嚭缁撹鏃讹紝鏄庣‘璇存槑缂哄皯鍝簺瀛楁鎴栫瓫閫夋潯浠躲��
+12. 鐢ㄦ埛鎻愬埌鈥滀粖骞�/鏈湀/浠婂ぉ/鏈�杩�/涓婃湀/鍘诲勾鈥濈瓑鐩稿鏃堕棿鏃讹紝蹇呴』涓ユ牸鍩轰簬鈥滃綋鍓嶆棩鏈熲�濇崲绠楋紝绂佹鑷鍋囪骞翠唤銆�
--
Gitblit v1.9.3