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