zss
6 天以前 f8b236340b16d9dfe2ca88407343ac01f34f3cbf
Merge branch 'dev_New_pro' into dev_New_pro_财务

# Conflicts:
# src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
# src/main/java/com/ruoyi/home/controller/HomeController.java
# src/main/java/com/ruoyi/quality/controller/QualityReportController.java
已添加3个文件
已修改30个文件
1202 ■■■■ 文件已修改
doc/20260521_采购智能体优化前端变更文档.md 85 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260521_首页HomeController接口升级前端变更文档.md 113 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java 249 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/SalesAgent.java 4 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/SalesAiController.java 12 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java 11 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/service/PurchaseAiService.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java 62 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/home/controller/HomeController.java 140 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/bean/dto/ProductionOperationTaskDto.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/controller/ProductionOrderRoutingController.java 2 ●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java 18 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/ProductionOrderRoutingOperationService.java 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java 3 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOrderRoutingOperationServiceImpl.java 149 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/util/TaskPlanQuantityUtil.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityInspectParamController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityReportController.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardBindingController.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardController.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/approve-todo-agent-prompt.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/manufacturing-agent-prompt.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOrderMapper.xml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/purchase-agent-prompt.txt 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/sales-agent-prompt.txt 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260521_²É¹ºÖÇÄÜÌåÓÅ»¯Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,85 @@
# é‡‡è´­æ™ºèƒ½ä½“优化前端变更文档
## 1. å˜æ›´èƒŒæ™¯
本次针对采购智能体做了对齐优化(参考销售/审批/制造智能体):
1. æå‡ `quickPrompts` å‘½ä¸­ç¨³å®šæ€§ã€‚
2. å¢žå¼ºç›¸å¯¹æ—¶é—´è¯†åˆ«ï¼ˆä»Šå¤©/昨天/本周/上周/本月/上月/今年/去年/近N天等)。
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. æ—¶é—´å£å¾„优化
采购智能体现在统一按中国时区动态日期换算相对时间,支持:
- ä»Šå¤©ã€æ˜¨å¤©
- æœ¬å‘¨ã€ä¸Šå‘¨
- æœ¬æœˆã€ä¸Šæœˆ
- ä»Šå¹´ã€å޻年
- è¿‘N天/周/月/年、近半年、近半个月
前端无需改传参,但展示时间范围时请以后端返回 `summary.startDate/endDate/timeRange` ä¸ºå‡†ã€‚
## 6. å‰ç«¯è”调检查清单
1. `chat` æµå¼ç»“果拼接后,优先按 JSON è§£æžã€‚
2. è¦†ç›–新类型 `purchase_intent_not_recognized` çš„ UI å¤„理。
3. å¾…付款页面读取并展示 `summary.totalPendingAmount` ç­‰æ–°å¢žå­—段。
4. éªŒè¯ä»¥ä¸‹å¿«æ·é—®é¢˜å¯ç¨³å®šè¿”回结构化结果:
   - æœ¬æœˆé‡‡è´­é‡‘额排名前十的物料有哪些?
   - å“ªäº›é‡‡è´­è®¢å•还未入库?
   - æœ€è¿‘7天供应商到货异常有哪些?
   - å¸®æˆ‘统计待付款采购单!
   - åˆ—出本月采购退货情况
doc/20260521_Ê×Ò³HomeController½Ó¿ÚÉý¼¶Ç°¶Ë±ä¸üÎĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,113 @@
# é¦–页 HomeController æŽ¥å£å‡çº§å‰ç«¯å˜æ›´æ–‡æ¡£
更新时间:2026-05-21
适用模块:首页(`/home`)
## 1. å˜æ›´æ¦‚览
本次为 **兼容式升级**,旧调用方式仍可用。
重点是给生产看板接口增加更明确的筛选参数,便于前端按日期和状态查询。
涉及接口:
1. `GET /home/productionOrderProgress`
2. `GET /home/todayProductionPlan`
## 2. å‚数变更
### 2.1 ç”Ÿäº§è®¢å•进度 `GET /home/productionOrderProgress`
旧参数(仍兼容):
- `tab`:`all` / `inProgress` / `completed` / `paused`
- `pageNum`:默认 `1`
- `pageSize`:默认 `10`,最大 `50`
新增参数:
- `status`(可选):状态筛选,优先级高于 `tab`
  å¯é€‰å€¼ï¼š`all` / `waiting` / `inProgress` / `completed` / `paused` / `1` / `2` / `3` / `4`
- `bizDate`(可选):业务日期筛选,格式 `yyyy-MM-dd`(按订单创建时间过滤)
参数优先级:
1. å¦‚果传了 `status`,后端优先按 `status` è§£æžï¼›
2. æœªä¼  `status` æ—¶ï¼Œæ²¿ç”¨åŽŸæœ‰ `tab` è¡Œä¸ºï¼›
3. `status` æˆ– `bizDate` æ ¼å¼é”™è¯¯æ—¶è¿”回失败信息。
请求示例:
```http
GET /home/productionOrderProgress?status=completed&bizDate=2026-05-20&pageNum=1&pageSize=10
```
### 2.2 ä»Šæ—¥ç”Ÿäº§è®¡åˆ’ `GET /home/todayProductionPlan`
旧参数(仍兼容):
- `limit`:默认 `4`,最大 `20`
新增参数:
- `planDate`(可选):计划日期筛选,格式 `yyyy-MM-dd`(按 `plan_complete_time` è¿‡æ»¤ï¼‰
请求示例:
```http
GET /home/todayProductionPlan?limit=6&planDate=2026-05-21
```
## 3. è¿”回结构变更
### 3.1 `productionOrderProgress` è¿”回新增字段
新增:
- `status`:标准化状态回显(`all` / `waiting` / `inProgress` / `completed` / `paused`)
- `bizDate`:日期筛选回显(未传时为 `null`)
- `waitingCount`:待开始订单数量
兼容保留:
- `tab` å­—段继续返回(老页面无需改动可继续使用)
返回示例:
```json
{
  "tab": "completed",
  "status": "completed",
  "bizDate": "2026-05-20",
  "total": 24,
  "pageNum": 1,
  "pageSize": 10,
  "waitingCount": 3,
  "inProgressCount": 6,
  "completedCount": 12,
  "pausedCount": 2,
  "records": []
}
```
### 3.2 `todayProductionPlan` è¿”回新增字段
新增:
- `planDate`:日期筛选回显(未传时为 `null`)
返回示例:
```json
{
  "planDate": "2026-05-21",
  "total": 9,
  "records": []
}
```
## 4. å‰ç«¯æ”¹é€ å»ºè®®
1. æ–°é¡µé¢å»ºè®®ä¼˜å…ˆä¼  `status`,逐步替代 `tab`。
2. éœ€è¦æŒ‰æ—¥æœŸå¤ç›˜çœ‹æ¿æ—¶ï¼Œä½¿ç”¨ `bizDate` / `planDate`。
3. è€é¡µé¢å¯ä¸æ”¹ï¼Œç»§ç»­æ²¿ç”¨åŽŸå‚æ•°ä¹Ÿèƒ½æ­£å¸¸è”è°ƒã€‚
src/main/java/com/ruoyi/ai/assistant/ApproveTodoAgent.java
@@ -3,6 +3,7 @@
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
@@ -16,5 +17,5 @@
public interface ApproveTodoAgent {
    @SystemMessage(fromResource = "approve-todo-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
@@ -3,6 +3,7 @@
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
@@ -17,5 +18,5 @@
public interface ManufacturingAgent {
    @SystemMessage(fromResource = "manufacturing-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/PurchaseAgent.java
@@ -3,6 +3,7 @@
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
@@ -17,5 +18,5 @@
public interface PurchaseAgent {
    @SystemMessage(fromResource = "purchase-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
@@ -4,6 +4,9 @@
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@@ -11,8 +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 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;
@@ -25,71 +32,63 @@
            return null;
        }
        String text = message.trim();
        String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text);
        if (StringUtils.hasText(quickPromptResponse)) {
            return quickPromptResponse;
        }
        if (containsAny(text, "排行", "排名", "前几", "前五", "前十") && containsAny(text, "物料", "产品", "原材料", "采购金额", "金额")) {
            return purchaseAgentTools.rankPurchaseMaterials(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    text,
                    extractLimit(text)
            );
        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,
                    extractStartDate(text),
                    extractEndDate(text),
                    extractKeyword(text),
                    extractLimit(text)
            );
            return purchaseAgentTools.listUnstockedPurchaseOrders(memoryId, startDate, endDate, keyword, limit);
        }
        if (containsAny(text, "到货异常", "到货有异常", "异常到货", "到货问题", "供应商到货异常")) {
            return purchaseAgentTools.listArrivalExceptions(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    text,
                    extractLimit(text)
            );
            return purchaseAgentTools.listArrivalExceptions(memoryId, startDate, endDate, text, limit);
        }
        if (containsAny(text, "待付款", "未付款", "未付清", "待支付", "应付")) {
            return purchaseAgentTools.listPendingPaymentOrders(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    extractKeyword(text),
                    extractLimit(text)
            );
            return purchaseAgentTools.listPendingPaymentOrders(memoryId, startDate, endDate, keyword, limit);
        }
        if (containsAny(text, "退货", "退料", "拒收")) {
            return purchaseAgentTools.listPurchaseReturns(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    extractKeyword(text),
                    extractLimit(text)
            );
            return purchaseAgentTools.listPurchaseReturns(memoryId, startDate, endDate, keyword, limit);
        }
        if (isStatsIntent(text)) {
            return purchaseAgentTools.getPurchaseStats(
                    memoryId,
                    extractStartDate(text),
                    extractEndDate(text),
                    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),
                    extractStartDate(text),
                    extractEndDate(text),
                    extractLimit(text)
            );
            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;
    }
@@ -100,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;
    }
@@ -127,23 +128,125 @@
        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);
        return matcher.find() ? matcher.group() : null;
        if (matcher.find()) {
            String first = matcher.group(1);
            String second = matcher.find() ? matcher.group(1) : first;
            return buildDateRange(first, second);
    }
    private String extractEndDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (!matcher.find()) {
        LocalDate today = LocalDate.now(CHINA_ZONE_ID);
        if (text.contains("今天")) {
            return new DateRange(formatDate(today), formatDate(today));
        }
        if (text.contains("昨天")) {
            LocalDate yesterday = today.minusDays(1);
            return new DateRange(formatDate(yesterday), formatDate(yesterday));
        }
        if (text.contains("本周")) {
            LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(formatDate(start), formatDate(today));
        }
        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 (text.contains("本月")) {
            return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today));
        }
        if (text.contains("上月")) {
            LocalDate start = today.minusMonths(1).withDayOfMonth(1);
            return new DateRange(formatDate(start), formatDate(start.withDayOfMonth(start.lengthOfMonth())));
        }
        if (text.contains("今年") || text.contains("本年")) {
            return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today));
        }
        if (text.contains("去年")) {
            LocalDate start = today.minusYears(1).withDayOfYear(1);
            LocalDate end = start.withDayOfYear(start.lengthOfYear());
            return new DateRange(formatDate(start), formatDate(end));
        }
        if (containsAny(text, "近半年", "最近半年")) {
            return new DateRange(formatDate(today.minusMonths(6).plusDays(1)), formatDate(today));
        }
        if (containsAny(text, "近半个月", "最近半个月", "半个月")) {
            return new DateRange(formatDate(today.minusDays(14)), formatDate(today));
        }
        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);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(29);
            };
            return new DateRange(formatDate(start), formatDate(today));
        }
        return new DateRange(null, null);
    }
    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;
        }
        return matcher.find() ? matcher.group() : 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("采购订单", "")
@@ -153,9 +256,33 @@
                .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) {
    }
}
src/main/java/com/ruoyi/ai/assistant/SalesAgent.java
@@ -3,6 +3,7 @@
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.V;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
@@ -17,6 +18,5 @@
public interface SalesAgent {
    @SystemMessage(fromResource = "sales-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage, @V("currentDate") String currentDate);
}
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
@@ -24,12 +24,18 @@
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "制造智能助手")
@RestController
@RequestMapping("/manufacturing-ai")
public class ManufacturingAiController extends BaseController {
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final ManufacturingAgent manufacturingAgent;
    private final ManufacturingIntentExecutor manufacturingIntentExecutor;
@@ -76,7 +82,7 @@
            return Flux.just(directResponse);
        }
        return manufacturingAgent.chat(memoryId, userMessage)
        return manufacturingAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
@@ -99,4 +105,8 @@
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
}
src/main/java/com/ruoyi/ai/controller/SalesAiController.java
@@ -24,12 +24,18 @@
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Tag(name = "销售助手智能体")
@RestController
@RequestMapping("/sales-ai")
public class SalesAiController extends BaseController {
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final SalesAgent salesAgent;
    private final SalesIntentExecutor salesIntentExecutor;
@@ -86,7 +92,7 @@
            return Flux.just(noGuessResponse);
        }
        return salesAgent.chat(memoryId, userMessage)
        return salesAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
@@ -128,4 +134,8 @@
        }
        return false;
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
}
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
@@ -29,6 +29,9 @@
import reactor.core.publisher.Flux;
import java.io.IOException;
import java.time.LocalDate;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.List;
import java.util.NoSuchElementException;
import java.util.UUID;
@@ -39,6 +42,8 @@
public class XiaozhiController extends BaseController {
    private static final String FILE_ANALYZE_MEMORY_PREFIX = "file-analyze::";
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final ApproveTodoAgent approveTodoAgent;
    private final ApproveTodoIntentExecutor approveTodoIntentExecutor;
@@ -100,7 +105,7 @@
            return Flux.just(noGuessResponse);
        }
        return approveTodoAgent.chat(memoryId, userMessage)
        return approveTodoAgent.chat(memoryId, userMessage, currentDateForPrompt())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
@@ -190,4 +195,8 @@
        }
        return false;
    }
    private String currentDateForPrompt() {
        return LocalDate.now(CHINA_ZONE_ID).format(CURRENT_DATE_FMT);
    }
}
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;
@@ -68,6 +69,8 @@
    private static final int MAX_FILE_COUNT = 10;
    private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000;
    private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000;
    private static final DateTimeFormatter CURRENT_DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final ZoneId CHINA_ZONE_ID = ZoneId.of("Asia/Shanghai");
    private final PurchaseAgent purchaseAgent;
    private final PurchaseIntentExecutor purchaseIntentExecutor;
@@ -132,7 +135,17 @@
            return Flux.just(directResponse);
        }
        return purchaseAgent.chat(memoryId, userMessage)
        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())
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
@@ -194,10 +207,10 @@
                    .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
        }
        return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt))
        return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt()))
                .onErrorResume(NoSuchElementException.class, ex -> {
                    mongoChatMemoryStore.deleteMessages(finalMemoryId);
                    return purchaseAgent.chat(finalMemoryId, userPrompt);
                    return purchaseAgent.chat(finalMemoryId, userPrompt, currentDateForPrompt());
                })
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
@@ -464,6 +477,51 @@
        };
    }
    private String currentDateForPrompt() {
        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 """
                ä½ æ˜¯é‡‡è´­ä¸šåŠ¡æ–‡ä»¶åˆ†æžåŠ©æ‰‹ã€‚è¯·ä¸¥æ ¼æ ¹æ®ç”¨æˆ·ä¸Šä¼ çš„å¤šä¸ªæ–‡ä»¶å’Œç”¨æˆ·è¦æ±‚æå–é‡‡è´­ä¸šåŠ¡æ•°æ®ã€‚
src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -23,6 +23,7 @@
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.util.Date;
@@ -37,6 +38,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;
@@ -279,8 +281,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 = "按时间范围查询采购退货单列表和退货金额。")
@@ -440,6 +458,26 @@
        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);
        wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start()))
                .lt(PaymentRegistration::getPaymentDate, toExclusiveEndDate(range.end()));
        return defaultList(paymentRegistrationMapper.selectList(wrapper));
    }
    private List<PurchaseReturnOrders> queryReturns(LoginUser loginUser, DateRange range) {
@@ -464,7 +502,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) {
@@ -481,6 +519,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, "今年");
        }
@@ -518,7 +572,11 @@
        if (!StringUtils.hasText(text)) {
            return null;
        }
        try {
        return LocalDate.parse(text.trim(), DATE_FMT);
        } catch (Exception ignored) {
            return null;
        }
    }
    private Date toDate(LocalDate localDate) {
src/main/java/com/ruoyi/home/controller/HomeController.java
@@ -16,8 +16,8 @@
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionProductOutputMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.Operation;
import lombok.AllArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
@@ -28,8 +28,16 @@
import java.math.RoundingMode;
import java.text.ParseException;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.Date;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.*;
/**
@@ -49,6 +57,7 @@
    private final DeviceRepairMapper deviceRepairMapper;
    private static final DateTimeFormatter DATE_TIME_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final DateTimeFormatter DATE_FORMATTER = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final Integer ORDER_STATUS_WAIT = 1;
    private static final Integer ORDER_STATUS_RUNNING = 2;
    private static final Integer ORDER_STATUS_COMPLETED = 3;
@@ -58,7 +67,7 @@
    @GetMapping("/todos")
    @Log(title = "待办事项", businessType = BusinessType.OTHER)
    @Operation(summary = "待办事项")
    public R todos(ApproveProcess req) throws ParseException {
    public R todos() throws ParseException {
        List<ApproveProcess> approveProcessList = homeService.todos();
        return R.ok(approveProcessList);
    }
@@ -130,7 +139,7 @@
    @GetMapping("/business")
    @Log(title = "销售-采购-库存数据", businessType = BusinessType.OTHER)
    @Operation(summary = "销售-采购-库存数据")
    public R business(HomeBusinessDto req) {
    public R business() {
        HomeBusinessDto homeBusinessDto = homeService.business();
        return R.ok(homeBusinessDto);
    }
@@ -138,7 +147,7 @@
    @GetMapping("/analysisCustomerContractAmounts")
    @Log(title = "客户合同金额分析", businessType = BusinessType.OTHER)
    @Operation(summary = "客户合同金额分析")
    public R analysisCustomerContractAmounts(AnalysisCustomerContractAmountsDto req) {
    public R analysisCustomerContractAmounts() {
        AnalysisCustomerContractAmountsDto analysisCustomerContractAmounts = homeService.analysisCustomerContractAmounts();
        return R.ok(analysisCustomerContractAmounts);
    }
@@ -241,13 +250,27 @@
    @GetMapping("/productionOrderProgress")
    @Operation(summary = "Production Order Progress")
    public R productionOrderProgress(@RequestParam(defaultValue = "all") String tab,
                                     @RequestParam(required = false) String status,
                                     @RequestParam(required = false) String bizDate,
                                              @RequestParam(defaultValue = "1") Long pageNum,
                                              @RequestParam(defaultValue = "10") Long pageSize) {
        LocalDate queryDate = parseDateOrNull(bizDate);
        if (!isBlank(bizDate) && queryDate == null) {
            return R.fail("bizDate格式错误,请使用yyyy-MM-dd");
        }
        Integer statusFromParam = parseOrderStatus(status);
        if (!isBlank(status) && statusFromParam == null && !"all".equalsIgnoreCase(status.trim())) {
            return R.fail("status参数不合法,可选值:all/waiting/inProgress/completed/paused æˆ– 1/2/3/4");
        }
        Integer queryStatus = resolveOrderStatus(status, tab);
        long safePageNum = pageNum == null || pageNum < 1 ? 1 : pageNum;
        long safePageSize = pageSize == null || pageSize < 1 ? 10 : Math.min(pageSize, 50);
        Integer status = resolveOrderStatus(tab);
        long offset = (safePageNum - 1) * safePageSize;
        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeOrderProgressPage(status, offset, safePageSize);
        LocalDateTime startTime = queryDate == null ? null : queryDate.atStartOfDay();
        LocalDateTime endTime = queryDate == null ? null : queryDate.plusDays(1).atStartOfDay();
        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeOrderProgressPage(queryStatus, offset, safePageSize, startTime, endTime);
        List<Map<String, Object>> records = new ArrayList<>();
        if (rawRows != null) {
            for (Map<String, Object> rawRow : rawRows) {
@@ -255,15 +278,18 @@
            }
        }
        long waitingCount = 0L;
        long inProgressCount = 0L;
        long completedCount = 0L;
        long pausedCount = 0L;
        List<Map<String, Object>> statusCountRows = productionOrderMapper.countHomeOrderProgressByStatus();
        List<Map<String, Object>> statusCountRows = productionOrderMapper.countHomeOrderProgressByStatus(startTime, endTime);
        if (statusCountRows != null) {
            for (Map<String, Object> countRow : statusCountRows) {
                Integer statusKey = toInteger(countRow.get("status"));
                long cnt = toLong(countRow.get("cnt"));
                if (Objects.equals(statusKey, ORDER_STATUS_RUNNING)) {
                if (Objects.equals(statusKey, ORDER_STATUS_WAIT)) {
                    waitingCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_RUNNING)) {
                    inProgressCount = cnt;
                } else if (Objects.equals(statusKey, ORDER_STATUS_COMPLETED)) {
                    completedCount = cnt;
@@ -274,10 +300,13 @@
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("tab", tab);
        result.put("total", toLong(productionOrderMapper.countHomeOrderProgress(status)));
        result.put("tab", mapOrderTab(queryStatus));
        result.put("status", mapOrderStatus(queryStatus));
        result.put("bizDate", queryDate == null ? null : queryDate.format(DATE_FORMATTER));
        result.put("total", toLong(productionOrderMapper.countHomeOrderProgress(queryStatus, startTime, endTime)));
        result.put("pageNum", safePageNum);
        result.put("pageSize", safePageSize);
        result.put("waitingCount", waitingCount);
        result.put("inProgressCount", inProgressCount);
        result.put("completedCount", completedCount);
        result.put("pausedCount", pausedCount);
@@ -287,10 +316,18 @@
    @GetMapping("/todayProductionPlan")
    @Operation(summary = "Today Production Plan")
    public R todayProductionPlan(@RequestParam(defaultValue = "4") Long limit) {
    public R todayProductionPlan(@RequestParam(defaultValue = "4") Long limit,
                                 @RequestParam(required = false) String planDate) {
        LocalDate queryDate = parseDateOrNull(planDate);
        if (!isBlank(planDate) && queryDate == null) {
            return R.fail("planDate格式错误,请使用yyyy-MM-dd");
        }
        long safeLimit = limit == null || limit < 1 ? 4 : Math.min(limit, 20);
        LocalDateTime planStart = queryDate == null ? null : queryDate.atStartOfDay();
        LocalDateTime planEnd = queryDate == null ? null : queryDate.plusDays(1).atStartOfDay();
        List<Map<String, Object>> records = new ArrayList<>();
        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeTodayProductionPlan(safeLimit);
        List<Map<String, Object>> rawRows = productionOrderMapper.selectHomeTodayProductionPlan(safeLimit, planStart, planEnd);
        if (rawRows != null) {
            for (Map<String, Object> rawRow : rawRows) {
                Map<String, Object> row = new LinkedHashMap<>();
@@ -306,7 +343,8 @@
        }
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("total", toLong(productionOrderMapper.countHomeTodayProductionPlan()));
        result.put("planDate", queryDate == null ? null : queryDate.format(DATE_FORMATTER));
        result.put("total", toLong(productionOrderMapper.countHomeTodayProductionPlan(planStart, planEnd)));
        result.put("records", records);
        return R.ok(result);
    }
@@ -367,7 +405,7 @@
    @GetMapping("/qualityStatistics")
    @Log(title = "质量分析", businessType = BusinessType.OTHER)
    @Operation(summary = "质量分析")
    public R qualityStatistics(QualityStatisticsDto req) {
    public R qualityStatistics() {
        QualityStatisticsDto qualityStatisticsDto = homeService.qualityStatistics();
        return R.ok(qualityStatisticsDto);
    }
@@ -418,7 +456,7 @@
    @GetMapping("/statisticsReceivablePayable")
    @Log(title = "应收应付统计", businessType = BusinessType.OTHER)
    @Operation(summary = "应收应付统计")
    public R statisticsReceivablePayable(StatisticsReceivablePayableDto req, @DefaultType Integer type ) {
    public R statisticsReceivablePayable(@DefaultType Integer type ) {
        StatisticsReceivablePayableDto statisticsReceivablePayable = homeService.statisticsReceivablePayable(type);
        return R.ok(statisticsReceivablePayable);
    }
@@ -467,21 +505,66 @@
        return row;
    }
    private Integer resolveOrderStatus(String tab) {
        if (tab == null) {
    private Integer resolveOrderStatus(String status, String tab) {
        if (!isBlank(status)) {
            return parseOrderStatus(status);
        }
        return parseOrderStatus(tab);
    }
    private Integer parseOrderStatus(String rawStatus) {
        if (isBlank(rawStatus)) {
            return null;
        }
        String normalized = tab.trim().toLowerCase();
        if ("inprogress".equals(normalized)) {
        String normalized = rawStatus.trim().toLowerCase();
        if ("all".equals(normalized)) {
            return null;
        }
        if ("1".equals(normalized) || "waiting".equals(normalized) || "wait".equals(normalized)) {
            return ORDER_STATUS_WAIT;
        }
        if ("2".equals(normalized) || "inprogress".equals(normalized) || "running".equals(normalized)) {
            return ORDER_STATUS_RUNNING;
        }
        if ("completed".equals(normalized)) {
        if ("3".equals(normalized) || "completed".equals(normalized)) {
            return ORDER_STATUS_COMPLETED;
        }
        if ("paused".equals(normalized)) {
        if ("4".equals(normalized) || "paused".equals(normalized)) {
            return ORDER_STATUS_PAUSED;
        }
        return null;
    }
    private String mapOrderTab(Integer status) {
        if (Objects.equals(status, ORDER_STATUS_RUNNING)) {
            return "inProgress";
        }
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "completed";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "paused";
        }
        if (Objects.equals(status, ORDER_STATUS_WAIT)) {
            return "waiting";
        }
        return "all";
    }
    private String mapOrderStatus(Integer status) {
        if (Objects.equals(status, ORDER_STATUS_WAIT)) {
            return "waiting";
        }
        if (Objects.equals(status, ORDER_STATUS_RUNNING)) {
            return "inProgress";
        }
        if (Objects.equals(status, ORDER_STATUS_COMPLETED)) {
            return "completed";
        }
        if (Objects.equals(status, ORDER_STATUS_PAUSED)) {
            return "paused";
        }
        return "all";
    }
    private String mapOrderStatusLabel(Integer status) {
@@ -639,6 +722,21 @@
        }
    }
    private LocalDate parseDateOrNull(String rawDate) {
        if (isBlank(rawDate)) {
            return null;
        }
        try {
            return LocalDate.parse(rawDate.trim(), DATE_FORMATTER);
        } catch (DateTimeParseException ex) {
            return null;
        }
    }
    private boolean isBlank(String value) {
        return value == null || value.trim().isEmpty();
    }
    private BigDecimal zeroIfNull(BigDecimal value) {
        return value == null ? BigDecimal.ZERO : value;
    }
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;
}
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}")
src/main/java/com/ruoyi/production/mapper/ProductionOrderMapper.java
@@ -42,14 +42,22 @@
    List<Map<String, Object>> selectHomeOrderProgressPage(@Param("status") Integer status,
                                                          @Param("offset") Long offset,
                                                          @Param("size") Long size);
                                                          @Param("size") Long size,
                                                          @Param("startTime") LocalDateTime startTime,
                                                          @Param("endTime") LocalDateTime endTime);
    Long countHomeOrderProgress(@Param("status") Integer status);
    Long countHomeOrderProgress(@Param("status") Integer status,
                                @Param("startTime") LocalDateTime startTime,
                                @Param("endTime") LocalDateTime endTime);
    List<Map<String, Object>> countHomeOrderProgressByStatus();
    List<Map<String, Object>> countHomeOrderProgressByStatus(@Param("startTime") LocalDateTime startTime,
                                                              @Param("endTime") LocalDateTime endTime);
    List<Map<String, Object>> selectHomeTodayProductionPlan(@Param("size") Long size);
    List<Map<String, Object>> selectHomeTodayProductionPlan(@Param("size") Long size,
                                                             @Param("planStart") LocalDateTime planStart,
                                                             @Param("planEnd") LocalDateTime planEnd);
    Long countHomeTodayProductionPlan();
    Long countHomeTodayProductionPlan(@Param("planStart") LocalDateTime planStart,
                                      @Param("planEnd") LocalDateTime planEnd);
}
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);
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;
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()));
                // ç¡®å®šæ ¹äº§å“è§„æ ¼ID
                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);
    }
}
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);
    }
    /**
     * è§£æžå·¥åºè¾“出节点
     */
    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;
    }
    /**
     * è§£æžè¾“出产品规格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;
    }
}
src/main/java/com/ruoyi/quality/controller/QualityInspectController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.dto.QualityInspectDto;
import com.ruoyi.quality.pojo.QualityInspect;
@@ -39,6 +41,7 @@
     */
    @PostMapping("/add")
    @Operation(summary = "新增检验")
    @Log(title = "新增检验", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityInspectDto qualityInspectDto) {
        return R.ok(qualityInspectService.add(qualityInspectDto));
    }
@@ -51,6 +54,7 @@
     */
    @DeleteMapping("/del")
    @Operation(summary = "删除检验")
    @Log(title = "删除检验", businessType = BusinessType.DELETE)
    public R<?> delQualityInspect(@RequestBody List<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return R.fail("请选择至少一条数据");
@@ -80,6 +84,7 @@
     */
    @GetMapping("/{id}")
    @Operation(summary = "检验详情")
    @Log(title = "检验详情", businessType = BusinessType.OTHER)
    public R<?> QualityInspectDetail(@PathVariable("id") Integer id) {
        return R.ok(qualityInspectService.getDetailById(id));
    }
@@ -92,6 +97,7 @@
     */
    @PostMapping("/update")
    @Operation(summary = "修改检验")
    @Log(title = "修改检验", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityInspectDto qualityInspectDto) {
        return R.ok(qualityInspectService.updateQualityInspect(qualityInspectDto));
    }
@@ -105,6 +111,7 @@
     */
    @GetMapping("/listPage")
    @Operation(summary = "分页查询检验")
    @Log(title = "分页查询检验", businessType = BusinessType.OTHER)
    public R<?> qualityInspectListPage(Page page, QualityInspectDto qualityInspect) {
        return R.ok(qualityInspectService.qualityInspectListPage(page, qualityInspect));
    }
@@ -117,6 +124,7 @@
     */
    @PostMapping("/export")
    @Operation(summary = "导出检验")
    @Log(title = "导出检验", businessType = BusinessType.EXPORT)
    public void qualityInspectExport(HttpServletResponse response, QualityInspect qualityInspect) {
        qualityInspectService.qualityInspectExport(response, qualityInspect);
    }
@@ -129,6 +137,7 @@
     */
    @PostMapping("/submit")
    @Operation(summary = "提交检验")
    @Log(title = "提交检验", businessType = BusinessType.OTHER)
    public R<?> submit(@RequestBody QualityInspect qualityInspect) {
        return R.ok(qualityInspectService.submit(qualityInspect));
    }
@@ -141,6 +150,7 @@
     */
    @PostMapping("/down")
    @Operation(summary = "下载检验")
    @Log(title = "下载检验", businessType = BusinessType.OTHER)
    public void down(HttpServletResponse response, @RequestBody QualityInspect qualityInspect) {
        qualityInspectService.down(response, qualityInspect);
    }
src/main/java/com/ruoyi/quality/controller/QualityInspectParamController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityInspectFile;
@@ -34,6 +36,7 @@
     */
    @GetMapping("/{inspectId}")
    @Operation(summary = "检验参数项详情")
    @Log(title = "检验参数项详情", businessType = BusinessType.OTHER)
    public R<?> QualityInspectParamDetail(@PathVariable("inspectId") Integer inspectId) {
        return R.ok(qualityInspectParamService.qualityInspectParamDetail(inspectId));
    }
@@ -46,6 +49,7 @@
     */
    @PostMapping("/update")
    @Operation(summary = "修改检验参数项")
    @Log(title = "修改检验参数项", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody List<QualityInspectParam> qualityInspectParams) {
        return R.ok(qualityInspectParamService.updateBatchById(qualityInspectParams));
    }
@@ -57,6 +61,7 @@
     */
    @DeleteMapping("/del")
    @Operation(summary = "删除检验参数项")
    @Log(title = "删除检验参数项", businessType = BusinessType.DELETE)
    public R<?> delQualityUnqualified(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return R.fail("请选择至少一条数据");
src/main/java/com/ruoyi/quality/controller/QualityReportController.java
@@ -1,6 +1,8 @@
package com.ruoyi.quality.controller;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.service.QualityReportService;
import io.swagger.v3.oas.annotations.tags.Tag;
import io.swagger.v3.oas.annotations.media.Schema;
@@ -30,8 +32,9 @@
     */
    @Operation(summary = "获取检验统计数据")
    @GetMapping("/getInspectStatistics")
    public AjaxResult getInspectStatistics() {
        return AjaxResult.success(qualityReportService.getInspectStatistics());
    @Log(title = "获取检验统计数据", businessType = BusinessType.OTHER)
    public R<?> getInspectStatistics() {
        return R.ok(qualityReportService.getInspectStatistics());
    }
    /**
@@ -39,8 +42,9 @@
     */
    @Operation(summary = "获取合格率统计数据")
    @GetMapping("/getPassRateStatistics")
    public AjaxResult getPassRateStatistics() {
        return AjaxResult.success(qualityReportService.getPassRateStatistics());
    @Log(title = "获取合格率统计数据", businessType = BusinessType.OTHER)
    public R<?> getPassRateStatistics() {
        return R.ok(qualityReportService.getPassRateStatistics());
    }
    /**
@@ -48,8 +52,9 @@
     */
    @Operation(summary = "获取月度合格率统计数据")
    @GetMapping("/getMonthlyPassRateStatistics")
    public AjaxResult getMonthlyPassRateStatistics(@RequestParam("year") String year) {
        return AjaxResult.success(qualityReportService.getMonthlyPassRateStatistics(year));
    @Log(title = "获取月度合格率统计数据", businessType = BusinessType.OTHER)
    public R<?> getMonthlyPassRateStatistics(@RequestParam("year") String year) {
        return R.ok(qualityReportService.getMonthlyPassRateStatistics(year));
    }
    /**
@@ -57,8 +62,9 @@
     */
    @Operation(summary = "获取年度总合格率统计数据")
    @GetMapping("/getYearlyPassRateStatistics")
    public AjaxResult getYearlyPassRateStatistics(@RequestParam("year") String year) {
        return AjaxResult.success(qualityReportService.getYearlyPassRateStatistics(year));
    @Log(title = "获取年度总合格率统计数据", businessType = BusinessType.OTHER)
    public R<?> getYearlyPassRateStatistics(@RequestParam("year") String year) {
        return R.ok(qualityReportService.getYearlyPassRateStatistics(year));
    }
    /**
@@ -66,8 +72,9 @@
     */
    @Operation(summary = "获取月度完成明细数据")
    @GetMapping("/getMonthlyCompletionDetails")
    public AjaxResult getMonthlyCompletionDetails(@RequestParam("year") String year) {
        return AjaxResult.success(qualityReportService.getMonthlyCompletionDetails(year));
    @Log(title = "获取月度完成明细数据", businessType = BusinessType.OTHER)
    public R<?> getMonthlyCompletionDetails(@RequestParam("year") String year) {
        return R.ok(qualityReportService.getMonthlyCompletionDetails(year));
    }
    /**
@@ -75,8 +82,9 @@
     */
    @Operation(summary = "获取热点检测指标统计")
    @GetMapping("/getTopParameters")
    public AjaxResult getTopParameters(@RequestParam("modelType") Integer modelType) {
        return AjaxResult.success(qualityReportService.getTopParameters(modelType));
    @Log(title = "获取热点检测指标统计", businessType = BusinessType.OTHER)
    public R<?> getTopParameters(@RequestParam("modelType") Integer modelType) {
        return R.ok(qualityReportService.getTopParameters(modelType));
    }
}
src/main/java/com/ruoyi/quality/controller/QualityTestStandardBindingController.java
@@ -1,5 +1,7 @@
package com.ruoyi.quality.controller;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityTestStandardBinding;
import com.ruoyi.quality.service.QualityTestStandardBindingService;
@@ -35,6 +37,7 @@
     */
    @PostMapping("/add")
    @Operation(summary = "新增检测标准主表与产品关联表")
    @Log(title = "新增检测标准主表与产品关联表", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody List<QualityTestStandardBinding> qualityTestStandardBindings) {
        return R.ok(qualityTestStandardBindingService.add(qualityTestStandardBindings));
    }
@@ -47,6 +50,7 @@
     */
    @DeleteMapping("/del")
    @Operation(summary = "删除检测标准主表与产品关联表")
    @Log(title = "删除检测标准主表与产品关联表", businessType = BusinessType.DELETE)
    public R<?> delQualityTestStandard(@RequestBody List<Integer> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            return R.fail("请选择至少一条数据");
@@ -61,6 +65,7 @@
     */
    @GetMapping("/list")
    @Operation(summary = "检测指标维护查询")
    @Log(title = "检测指标维护查询", businessType = BusinessType.OTHER)
    public R<?> listBinding(Long testStandardId) {
        return R.ok(qualityTestStandardBindingService.listBinding(testStandardId));
    }
src/main/java/com/ruoyi/quality/controller/QualityTestStandardController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
@@ -43,6 +45,7 @@
     */
    @PostMapping("/add")
    @Operation(summary = "新增检测标准主表")
    @Log(title = "新增检测标准主表", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.save(qualityTestStandard));
    }
@@ -54,6 +57,7 @@
     */
    @DeleteMapping("/del")
    @Operation(summary = "删除检测标准主表")
    @Log(title = "删除检测标准主表", businessType = BusinessType.DELETE)
    public R<?> delQualityTestStandard(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return R.fail("请选择至少一条数据");
@@ -68,6 +72,7 @@
     */
    @PostMapping("/update")
    @Operation(summary = "检测标准主表修改")
    @Log(title = "检测标准主表修改", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.updateById(qualityTestStandard));
    }
@@ -80,6 +85,7 @@
     */
    @GetMapping("/listPage")
    @Operation(summary = "检测标准主表分页查询")
    @Log(title = "检测标准主表分页查询", businessType = BusinessType.OTHER)
    public R<?> qualityTestStandardListPage(Page page, QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.qualityTestStandardListPage(page, qualityTestStandard));
    }
@@ -91,6 +97,7 @@
     */
    @PostMapping("/copyParam")
    @Operation(summary = "检测标准复制参数")
    @Log(title = "检测标准复制参数", businessType = BusinessType.OTHER)
    public R<?> copyParam(@RequestBody QualityTestStandard qualityTestStandard) {
        return R.ok(qualityTestStandardService.copyParam(qualityTestStandard));
    }
@@ -102,6 +109,7 @@
     */
    @PostMapping("/qualityTestStandardAudit")
    @Operation(summary = "检测标准批量审核")
    @Log(title = "检测标准批量审核", businessType = BusinessType.OTHER)
    public R<?> qualityTestStandardAudit(@RequestBody List<QualityTestStandard> qualityTestStandards) {
        return R.ok(qualityTestStandardService.updateBatchById(qualityTestStandards));
    }
@@ -112,6 +120,7 @@
     */
    @GetMapping("/getQualityTestStandardByProductId")
    @Operation(summary = "根据产品id查询相关的检验标准")
    @Log(title = "根据产品id查询相关的检验标准", businessType = BusinessType.OTHER)
    public R<?> getQualityTestStandardByProductId(@Nonnull Long productId, @Nonnull Integer inspectType, String process) {
        return R.ok(qualityTestStandardService.getQualityTestStandardByProductId(productId,inspectType,process));
    }
@@ -122,6 +131,7 @@
     */
    @GetMapping("/getQualityTestStandardParamByTestStandardId")
    @Operation(summary = "根据检测标准id查询相关的检验标准参数")
    @Log(title = "根据检测标准id查询相关的检验标准参数", businessType = BusinessType.OTHER)
    public R<?> getQualityTestStandardParamByTestStandardId(Long testStandardId) {
        return R.ok(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery().eq(QualityTestStandardParam::getTestStandardId, testStandardId)));
    }
src/main/java/com/ruoyi/quality/controller/QualityTestStandardParamController.java
@@ -2,6 +2,8 @@
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityTestStandard;
import com.ruoyi.quality.pojo.QualityTestStandardParam;
@@ -38,6 +40,7 @@
     */
    @PostMapping("/add")
    @Operation(summary = "新增检测标准参数")
    @Log(title = "新增检测标准参数", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityTestStandardParam qualityTestStandardParam) {
        return R.ok(qualityTestStandardParamService.save(qualityTestStandardParam));
    }
@@ -49,6 +52,7 @@
     */
    @DeleteMapping("/del")
    @Operation(summary = "删除检测指标维护")
    @Log(title = "删除检测指标维护", businessType = BusinessType.DELETE)
    public R<?> delQualityTestStandard(@RequestBody List<Integer> ids) {
        if(CollectionUtils.isEmpty(ids)){
            return R.fail("请选择至少一条数据");
@@ -63,6 +67,7 @@
     */
    @PostMapping("/update")
    @Operation(summary = "检测指标维护修改")
    @Log(title = "检测指标维护修改", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityTestStandardParam qualityTestStandardParam) {
        return R.ok(qualityTestStandardParamService.updateById(qualityTestStandardParam));
    }
@@ -73,6 +78,7 @@
     */
    @GetMapping("/list")
    @Operation(summary = "检测指标维护查询")
    @Log(title = "检测指标维护查询", businessType = BusinessType.OTHER)
    public R<?> list(Long testStandardId) {
        return R.ok(qualityTestStandardParamService.list(Wrappers.<QualityTestStandardParam>lambdaQuery().eq(QualityTestStandardParam::getTestStandardId,testStandardId)));
    }
src/main/java/com/ruoyi/quality/controller/QualityUnqualifiedController.java
@@ -1,6 +1,8 @@
package com.ruoyi.quality.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.domain.R;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.quality.service.IQualityUnqualifiedService;
@@ -32,6 +34,7 @@
     */
    @PostMapping("/add")
    @Operation(summary = "新增不合格管理")
    @Log(title = "新增不合格管理", businessType = BusinessType.INSERT)
    public R<?> add(@RequestBody QualityUnqualified qualityUnqualified) {
        qualityUnqualified.setInspectState(0);
        return R.ok(qualityUnqualifiedService.save(qualityUnqualified));
@@ -44,6 +47,7 @@
     */
    @DeleteMapping("/del")
    @Operation(summary = "删除不合格管理")
    @Log(title = "删除不合格管理", businessType = BusinessType.DELETE)
    public R<?> delQualityUnqualified(@RequestBody List<Integer> ids) {
       qualityUnqualifiedService.listByIds(ids).stream().forEach(qualityUnqualified -> {
           if (qualityUnqualified.getInspectState()==1){
@@ -60,6 +64,7 @@
     */
    @GetMapping("/{id}")
    @Operation(summary = "不合格管理详情")
    @Log(title = "不合格管理详情", businessType = BusinessType.OTHER)
    public R<?> QualityUnqualifiedDetail(@PathVariable("id") Integer id) {
        return R.ok(qualityUnqualifiedService.getUnqualified(id));
    }
@@ -71,6 +76,7 @@
     */
    @PostMapping("/update")
    @Operation(summary = "不合格管理修改")
    @Log(title = "不合格管理修改", businessType = BusinessType.UPDATE)
    public R<?> update(@RequestBody QualityUnqualified qualityUnqualified) {
        return R.ok(qualityUnqualifiedService.updateById(qualityUnqualified));
    }
@@ -83,6 +89,7 @@
     */
    @GetMapping("/listPage")
    @Operation(summary = "不合格管理分页查询")
    @Log(title = "不合格管理分页查询", businessType = BusinessType.OTHER)
    public R<?> qualityUnqualifiedListPage(Page page, QualityUnqualified qualityUnqualified) {
        return R.ok(qualityUnqualifiedService.qualityUnqualifiedListPage(page, qualityUnqualified));
    }
@@ -94,6 +101,7 @@
     */
    @PostMapping("/export")
    @Operation(summary = "不合格管理导出")
    @Log(title = "不合格管理导出", businessType = BusinessType.EXPORT)
    public void qualityUnqualifiedExport(HttpServletResponse response,QualityUnqualified qualityUnqualified) {
        qualityUnqualifiedService.qualityUnqualifiedExport(response, qualityUnqualified);
    }
@@ -105,6 +113,7 @@
     */
    @PostMapping("/deal")
    @Operation(summary = "不合格管理处理")
    @Log(title = "不合格管理处理", businessType = BusinessType.OTHER)
    public R<?> deal(@RequestBody QualityUnqualified qualityUnqualified) {
        return R.ok(qualityUnqualifiedService.deal(qualityUnqualified));
    }
src/main/resources/approve-todo-agent-prompt.txt
@@ -1,4 +1,5 @@
你是一个审批待办助手,负责协同办公审批待办的查询、审核、取消审核、修改、删除和统计分析。
当前日期:{{currentDate}}(中国时区)。
工作要求:
1. ç”¨æˆ·é—®å¾…办列表、审批进度、审批详情、统计数据时,优先调用工具,不要臆造数据。
@@ -16,3 +17,4 @@
13. åªæœ‰â€œæŸ¥è¯¢å®¡æ‰¹å¾…办详情”这个工具允许输出自然语言文本。
14. å¦‚果工具返回的是统计 JSON,也同样直接输出原始 JSON;其中 `description`、`summary`、`charts` å·²ç»ä¾›å‰ç«¯ä½¿ç”¨ã€‚
15. å›žç­”使用中文;但在 JSON åœºæ™¯ä¸‹ï¼Œæœ€ç»ˆè¾“出必须是合法 JSON æœ¬ä½“。
16. ç”¨æˆ·æåˆ°â€œä»Šå¹´/本月/今天/最近/上月/去年”等相对时间时,必须严格基于“当前日期”换算,禁止自行假设年份。
src/main/resources/manufacturing-agent-prompt.txt
@@ -1,4 +1,5 @@
你是企业制造智能助手,覆盖生产现场、计划、工单、设备、质量、物料、异常处理七个域。
当前日期:{{currentDate}}(中国时区)。
工作规则:
1. ç”¨æˆ·æå‡ºâ€œæŸ¥ã€é—®ã€é¢„警、分析”需求时,优先调用工具拿结构化结果,不要臆造业务数据。
@@ -6,3 +7,4 @@
3. å·¥å…·è¿”回 JSON æ—¶ï¼Œç›´æŽ¥è¾“出原始 JSON å­—符串,不要额外包裹 Markdown,不要在前后加解释文字。
4. å›žç­”必须使用中文;若用户问题缺少时间范围、关键字等条件,可先给默认口径并提示可补充条件。
5. è‹¥æ— æ³•从工具结果得到结论,明确说明缺少的筛选条件或业务字段。
6. ç”¨æˆ·æåˆ°â€œä»Šå¹´/本月/今天/最近/上月/去年”等相对时间时,必须严格基于“当前日期”换算,禁止自行假设年份。
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>
src/main/resources/mapper/production/ProductionOrderMapper.xml
@@ -227,6 +227,10 @@
            <if test="status != null">
                and po.status = #{status}
            </if>
            <if test="startTime != null and endTime != null">
                and po.create_time &gt;= #{startTime}
                and po.create_time &lt; #{endTime}
            </if>
        </where>
        order by po.id desc
        limit #{offset}, #{size}
@@ -239,13 +243,21 @@
            <if test="status != null">
                and po.status = #{status}
            </if>
            <if test="startTime != null and endTime != null">
                and po.create_time &gt;= #{startTime}
                and po.create_time &lt; #{endTime}
            </if>
        </where>
    </select>
    <select id="countHomeOrderProgressByStatus" resultType="java.util.Map">
        select po.status as status, count(1) as cnt
        from production_order po
        where po.status in (2, 3, 4)
        where po.status in (1, 2, 3, 4)
        <if test="startTime != null and endTime != null">
            and po.create_time &gt;= #{startTime}
            and po.create_time &lt; #{endTime}
        </if>
        group by po.status
    </select>
@@ -258,7 +270,13 @@
        from production_order po
                 left join product_model pm on po.product_model_id = pm.id
                 left join product p on pm.product_id = p.id
        where po.status in (1, 2)
        <where>
            po.status in (1, 2)
            <if test="planStart != null and planEnd != null">
                and po.plan_complete_time &gt;= #{planStart}
                and po.plan_complete_time &lt; #{planEnd}
            </if>
        </where>
        order by case when po.status = 2 then 0 else 1 end, po.id desc
        limit #{size}
    </select>
@@ -266,7 +284,13 @@
    <select id="countHomeTodayProductionPlan" resultType="java.lang.Long">
        select count(1)
        from production_order po
        where po.status in (1, 2)
        <where>
            po.status in (1, 2)
            <if test="planStart != null and planEnd != null">
                and po.plan_complete_time &gt;= #{planStart}
                and po.plan_complete_time &lt; #{planEnd}
            </if>
        </where>
    </select>
</mapper>
src/main/resources/purchase-agent-prompt.txt
@@ -1,5 +1,6 @@
你是企业采购智能助理。
你的目标是帮助用户快速完成采购相关信息查询与解读。
当前日期:{{currentDate}}(中国时区)。
工作规则:
1. ä¼˜å…ˆè°ƒç”¨å·¥å…·å‡½æ•°èŽ·å–é‡‡è´­å°è´¦ã€ä»˜æ¬¾ã€å‘ç¥¨ã€é€€è´§ç­‰ç»“æž„åŒ–æ•°æ®ã€‚
@@ -9,6 +10,8 @@
5. ç”¨æˆ·é—®â€œæœ€è¿‘7天供应商到货异常”“到货问题”“到货异常”时,调用“查询采购到货异常”。
6. ç”¨æˆ·é—®â€œå¾…付款采购单”“未付款采购单”“未付清采购订单”时,调用“查询待付款采购单”。
7. ç”¨æˆ·é—®â€œæœ¬æœˆé‡‡è´­é€€è´§æƒ…况”“采购退货列表”“退料/拒收情况”时,调用“查询采购退货情况”。
8. ç»“果用简洁中文回答,先给结论,再给关键数据点。
9. ä¸è¦ç¼–造采购数据,所有结论必须基于工具返回。
10. æ— æ³•直接得出结论时,明确说明缺少哪些字段或筛选条件。
8. å·¥å…·è¿”回 JSON æ—¶ï¼Œç›´æŽ¥è¾“出原始 JSON å­—符串,不要额外包裹 Markdown,也不要在前后追加解释文本。
9. åªæœ‰åœ¨æœªè°ƒç”¨ JSON å·¥å…·æ—¶ï¼Œæ‰ä½¿ç”¨ç®€æ´ä¸­æ–‡å›žç­”,先给结论,再给关键数据点。
10. ä¸è¦ç¼–造采购数据,所有结论必须基于工具返回。
11. æ— æ³•直接得出结论时,明确说明缺少哪些字段或筛选条件。
12. ç”¨æˆ·æåˆ°â€œä»Šå¹´/本月/今天/最近/上月/去年”等相对时间时,必须严格基于“当前日期”换算,禁止自行假设年份。
src/main/resources/sales-agent-prompt.txt
@@ -1,7 +1,9 @@
你是企业销售助手,覆盖客户档案、销售报价、销售台账、销售退货、客户往来、发货台账、指标统计、客户流失风险分析、回款与报价策略建议等场景。
当前日期:{{currentDate}}(中国时区)。
工作规则:
1. ç”¨æˆ·æå‡ºâ€œæŸ¥ã€é—®ã€ç»Ÿè®¡ã€åˆ†æžã€å»ºè®®â€éœ€æ±‚时,优先调用工具返回结构化数据,不编造业务数据。
2. å‘½ä¸­â€œå®¢æˆ·æµå¤±é£Žé™©åˆ†æžâ€æˆ–“回款与报价策略建议”时,优先使用工具输出结构化 JSON。
3. å·¥å…·è¿”回 JSON æ—¶ï¼Œç›´æŽ¥è¾“出原始 JSON å­—符串,不要额外包裹 Markdown,也不要在前后追加解释文本。
4. å›žå¤å¿…须使用中文;若用户缺少时间范围、关键词等条件,可先使用默认口径并提示可补充条件。
5. è‹¥æ•°æ®ä¸è¶³ä»¥å¾—出结论,明确指出缺少的筛选条件或关键字段。
6. ç”¨æˆ·æåˆ°â€œä»Šå¹´/本月/今天/最近/上月/去年”等相对时间时,必须严格基于“当前日期”换算,禁止自行假设年份。