From 3ff2a2c1789d692a64a43e9f7e70ca5b3578b0a8 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 21 五月 2026 15:01:22 +0800
Subject: [PATCH] feat(home): 添加生产看板功能并优化订单查询性能

---
 src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java |  339 ++++++++++++++++++---------------
 src/main/java/com/ruoyi/ai/service/PurchaseAiService.java        |   52 +++++
 doc/20260521_采购智能体优化前端变更文档.md                                    |   85 ++++++++
 src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java         |   56 +++++
 src/main/resources/purchase-agent-prompt.txt                     |    9 
 5 files changed, 378 insertions(+), 163 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/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