config(zxsq): 更新配置文件以支持个推推送、MongoDB存储和文件上传功能

- 添加了个推Unipush推送服务配置(appId、appKey、密钥等)
- 修改服务器端口从9005调整为9003
- 更新数据库连接配置,修改数据库名称和密码
- 将Redis配置移至data.redis下并调整数据库索引为0
- 添加MongoDB配置用于聊天记忆存储
- 更换安全令牌密钥为更复杂的密钥串
- 新增文件上传相关配置(临时目录、正式目录、域名等)
- 添加文件压缩、过期时间及访问限制配置
已添加7个文件
已修改2个文件
1621 ■■■■■ 文件已修改
doc/20260516_制造智能助手前端联调文档.md 258 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java 136 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java 102 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java 1035 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/manufacturing-agent-prompt.txt 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml 1 ●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/20260516_ÖÆÔìÖÇÄÜÖúÊÖǰ¶ËÁªµ÷Îĵµ.md
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,258 @@
# åˆ¶é€ æ™ºèƒ½åŠ©æ‰‹å‰ç«¯è”è°ƒæ–‡æ¡£ï¼ˆ`manufacturing-ai`)
> æ›´æ–°æ—¥æœŸï¼š2026-05-16
> é€‚用模块:生产现场、计划、工单、设备、质量、物料、异常处理
> èƒ½åŠ›èŒƒå›´ï¼šæŸ¥ã€é—®ã€åŠžã€é¢„è­¦ã€åˆ†æž
## 1. æŽ¥å£æ€»è§ˆ
1. æµå¼å¯¹è¯ï¼š`POST /manufacturing-ai/chat`
2. ä¼šè¯åˆ—表:`GET /manufacturing-ai/history/sessions`
3. ä¼šè¯æ¶ˆæ¯ï¼š`GET /manufacturing-ai/history/messages/{memoryId}`
4. åˆ é™¤ä¼šè¯ï¼š`DELETE /manufacturing-ai/history/{memoryId}`
说明:
- `/chat` ä¸º **SSE/流式文本** è¿”回(`text/stream;charset=utf-8`)。
- å‘½ä¸­â€œæŸ¥/预警/分析/办”工具时,流式最终内容是 **JSON å­—符串**(不是 `AjaxResult`)。
- æœªå‘½ä¸­å·¥å…·æ—¶ï¼Œè¿”回普通自然语言文本。
## 2. é‰´æƒä¸Žè¯·æ±‚头
- ç»Ÿä¸€ä½¿ç”¨ç³»ç»Ÿç™»å½•态(`Authorization` ä¸ŽçŽ°æœ‰æŽ¥å£ä¸€è‡´ï¼‰ã€‚
- `POST /manufacturing-ai/chat` è¯·æ±‚头:`Content-Type: application/json`。
## 3. å¯¹è¯æŽ¥å£
### 3.1 è¯·æ±‚
```http
POST /manufacturing-ai/chat
Content-Type: application/json
```
```json
{
  "memoryId": "mfg-ai-001",
  "message": "查设备西门子变频器的维修情况"
}
```
字段说明:
| å­—段 | ç±»åž‹ | å¿…å¡« | è¯´æ˜Ž |
| --- | --- | --- | --- |
| memoryId | string | æ˜¯ | ä¼šè¯ ID,前端生成并复用 |
| message | string | æ˜¯ | ç”¨æˆ·è¾“å…¥ |
### 3.2 è¿”回(流式)
```http
Content-Type: text/stream;charset=utf-8
```
前端处理建议:
1. æŒ‰æµæ‹¼æŽ¥å®Œæ•´æ–‡æœ¬ã€‚
2. å°è¯• `JSON.parse(fullText)`:
   - æˆåŠŸï¼šæŒ‰ç»“æž„åŒ–ç»“æžœæ¸²æŸ“ã€‚
   - å¤±è´¥ï¼šæŒ‰æ™®é€šèŠå¤©æ–‡æœ¬æ¸²æŸ“。
## 4. ç»“构化响应协议
### 4.1 é€šç”¨ç»“æž„
```json
{
  "success": true,
  "type": "manufacturing_device_repair_list",
  "description": "已返回设备维修记录。",
  "summary": {},
  "data": {},
  "charts": {}
}
```
### 4.2 `type` æžšä¸¾
| type | åœºæ™¯ |
| --- | --- |
| manufacturing_site_snapshot | ç”Ÿäº§çŽ°åœºæ¦‚è§ˆ |
| manufacturing_plan_list | ç”Ÿäº§è®¡åˆ’查询 |
| manufacturing_workorder_list | å·¥å•查询 |
| manufacturing_device_list | è®¾å¤‡å°è´¦æŸ¥è¯¢ |
| manufacturing_device_repair_list | è®¾å¤‡ç»´ä¿®è®°å½•查询 |
| manufacturing_quality_list | è´¨é‡æŸ¥è¯¢ |
| manufacturing_material_list | ç‰©æ–™åº“存查询 |
| manufacturing_exception_list | å¼‚常处理查询 |
| manufacturing_warning | é¢„警看板 |
| manufacturing_analysis | ç»è¥åˆ†æž |
| manufacturing_action_plan | åŠžç†å»ºè®®ï¼ˆåŠ¨ä½œå¡ï¼‰ |
## 5. â€œæŸ¥â€èƒ½åŠ›è”è°ƒè¦ç‚¹
### 5.1 è®¾å¤‡ç›¸å…³è·¯ç”±è§„则(关键)
- å½“用户输入包含 `ç»´ä¿®/报修/检修/维护`,设备域会返回 `manufacturing_device_repair_list`(查 `device_repair`)。
- æœªåŒ…含以上词时,返回 `manufacturing_device_list`(查设备台账)。
示例:
- `查设备A-01` -> `manufacturing_device_list`
- `查设备A-01维修情况` -> `manufacturing_device_repair_list`
### 5.2 ç»´ä¿®è®°å½•时间过滤规则(关键)
- ç”¨æˆ·æ˜Žç¡®å¸¦æ—¶é—´æ¡ä»¶ï¼ˆå¦‚“本月/上周/近7天/2026-05-01 åˆ° 2026-05-16”)才按时间过滤维修记录。
- æœªå¸¦æ—¶é—´æ¡ä»¶æ—¶ï¼Œä¸é»˜è®¤æŒ‰è¿‘ 30 å¤©æˆªæ–­ï¼Œé¿å…åŽ†å²ç»´ä¿®è®°å½•è¢«è¯¯è¿‡æ»¤ã€‚
### 5.3 å…³é”®è¯å¤„理规则(设备/维修)
- ç³»ç»Ÿä¼šæ¸…洗噪音词:`查询/查看/请/设备/维修情况/记录/信息` ç­‰ã€‚
- åŒæ—¶ä¼šé€šè¿‡è®¾å¤‡å°è´¦åŒ¹é… `deviceLedgerId` å…œåº•,再回查维修记录,降低“有数据但查不到”的概率。
### 5.4 åˆ—表结果约定
- åˆ—表数据统一在 `data.items`
- ç»Ÿè®¡æ‘˜è¦åœ¨ `summary`
常用字段:
| type | å¸¸ç”¨å­—段 |
| --- | --- |
| manufacturing_plan_list | `mpsNo`, `requiredDate`, `status` |
| manufacturing_workorder_list | `workOrderNo`, `planStartTime`, `planEndTime`, `status` |
| manufacturing_device_list | `deviceName`, `deviceModel`, `pendingRepairCount` |
| manufacturing_device_repair_list | `deviceName`, `deviceModel`, `repairTime`, `repairName`, `maintenanceName`, `status`, `createTime` |
## 6. â€œé¢„警”联调要点
- `type = manufacturing_warning`
- é¢„警明细在 `data.items`,每项包含:
  - `level`:`high` / `medium`
  - `title`
  - `count`
  - `detail`
状态口径:
- è®¾å¤‡â€œå¾…维修”统计按 `status = 0` è®¡ç®—(不再把其他状态计入待维修)。
## 7. â€œåˆ†æžâ€è”调要点
- `type = manufacturing_analysis`
- å…³é”®æŒ‡æ ‡åœ¨ `summary`
- æŒ‡æ ‡å¡åœ¨ `data.coreMetrics`
- å›¾è¡¨é…ç½®åœ¨ `charts`:
  - `charts.domainBarOption`
  - `charts.qualityPieOption`
图表配置可直接给 ECharts ä½¿ç”¨ã€‚
## 8. â€œåŠžâ€èƒ½åŠ›è”è°ƒè¦ç‚¹
当前“办”为 **办理建议模式**(AI è¾“出动作卡,前端确认后调用目标业务接口)。
- `type = manufacturing_action_plan`
- åŠ¨ä½œå¡æ•°ç»„ï¼š`data.actionCards`
动作卡字段:
| å­—段 | è¯´æ˜Ž |
| --- | --- |
| code | åŠ¨ä½œç¼–ç  |
| name | åŠ¨ä½œåç§° |
| method | è¯·æ±‚方法 |
| targetApi | ç›®æ ‡ä¸šåŠ¡æŽ¥å£ |
| requiredFields | å¿…填字段 |
| examplePayload | ç¤ºä¾‹å‚æ•° |
| description | è¯´æ˜Ž |
内置动作示例:
1. `POST /productionOperationTask/assign`
2. `POST /device/repair`
3. `POST /quality/qualityUnqualified/deal`
4. `POST /stockInventory/addstockInventory`
5. `POST /procurementExceptionRecord/add`
## 9. ä¼šè¯ç®¡ç†æŽ¥å£
### 9.1 ä¼šè¯åˆ—表
```http
GET /manufacturing-ai/history/sessions
```
`AjaxResult.data` å­—段:
- `memoryId`
- `title`
- `lastMessage`
- `messageCount`
- `lastChatTime`
### 9.2 ä¼šè¯æ¶ˆæ¯
```http
GET /manufacturing-ai/history/messages/{memoryId}
```
`AjaxResult.data` å­—段:
- `role`:`user` / `assistant` / `system` / `tool`
- `content`
- `filePaths`
### 9.3 åˆ é™¤ä¼šè¯
```http
DELETE /manufacturing-ai/history/{memoryId}
```
返回标准 `AjaxResult`。
## 10. é”™è¯¯ä¸Žè¾¹ç•Œ
`/chat` å¸¸è§è¿”回文本:
- `memoryId不能为空`
- `message不能为空`
建议前端发送前先做必填校验。
## 11. å‰ç«¯è”调流程建议
1. ç™»å½•后创建并复用 `memoryId`。
2. è°ƒç”¨ `/manufacturing-ai/chat`,按 SSE æ‹¼æŽ¥å®Œæ•´æ–‡æœ¬ã€‚
3. å…ˆå°è¯• JSON è§£æžï¼š
   - æˆåŠŸï¼šæŒ‰ `type` è·¯ç”±åˆ°å¯¹åº” UI(列表/预警/分析/动作卡)。
   - å¤±è´¥ï¼šæŒ‰æ™®é€šèŠå¤©æ¶ˆæ¯å±•示。
4. â€œåŠžâ€åœºæ™¯ç”±ç”¨æˆ·ç¡®è®¤åŠ¨ä½œå¡åŽï¼Œå‰ç«¯è°ƒç”¨ `targetApi` å®Œæˆä¸šåŠ¡æäº¤ã€‚
5. é€šè¿‡åŽ†å²æŽ¥å£åšä¼šè¯å›žæ˜¾ä¸Žåˆ é™¤ã€‚
## 12. å‰ç«¯é›†æˆçº¦æŸï¼ˆæœ¬æ¬¡è¡¥å……)
### 12.1 æ™ºèƒ½ä½“新增与弹窗同步规则(强制)
1. å½“ `src/views/aiIndustrialBrain/index.vue` æ–°å¢žæ™ºèƒ½ä½“(`agents`)逻辑时,必须同步确认弹窗助手可用性。
2. å¼¹çª—助手统一由 `src/components/AIChatSidebar/assistants/index.js` çš„ `assistantRegistry` æ³¨å†Œã€‚
3. æ–°å¢žæ™ºèƒ½ä½“çš„ `key` è‹¥è¦åœ¨å¼¹çª—中可用,必须在 `assistantRegistry` ä¸­æä¾›åŒåé…ç½®ã€‚
4. æœªåœ¨ `assistantRegistry` æ³¨å†Œçš„æ™ºèƒ½ä½“,弹窗显示为 `pending`(开发中)态。
### 12.2 ç”Ÿäº§åŠ©æ‰‹æŽ¥å…¥çº¦å®š
1. ç”Ÿäº§åŠ©æ‰‹é…ç½®ä½äºŽ `src/components/AIChatSidebar/assistants/productionAssistant.js`,`apiBase = /manufacturing-ai`。
2. AI å·¥ä¸šå¤§è„‘中生产智能体进入弹窗后,默认使用 `production` åŠ©æ‰‹ã€‚
3. å…¨å±€å³ä¾§å¯¹è¯æ¡†åŠ©æ‰‹åˆ‡æ¢åˆ—è¡¨å·²åŒ…å«ï¼š
   - `general`(待办助理)
   - `purchase`(采购助理)
   - `production`(生产助理)
### 12.3 å­—段中文化展示规则
1. é¢å‘业务用户的字段名、标签、必填提示不直接展示英文 key。
2. `requiredFields`、`missingFields` æç¤ºéœ€è½¬æ¢ä¸ºä¸­æ–‡è·¯å¾„标签(示例:`缺少必填字段:工单号、计划结束时间`)。
3. ç»“构化列表列名、摘要指标、动作卡字段优先显示中文;英文 key ä»…用于接口通信与调试。
## 13. æœ¬æ¬¡æ›´æ–°è®°å½•(2026-05-16)
1. æ–°å¢žè®¾å¤‡ç»´ä¿®è®°å½•返回类型:`manufacturing_device_repair_list`。
2. ä¿®æ­£è®¾å¤‡åŸŸæ„å›¾åˆ†æµï¼š`ç»´ä¿®/报修/检修/维护` èµ°ç»´ä¿®è®°å½•,不再误走设备列表。
3. ä¿®æ­£ç»´ä¿®è®°å½•时间过滤:仅在用户明确时间条件时生效。
4. ä¿®æ­£å¾…维修统计口径:按 `status = 0` ç»Ÿè®¡ã€‚
5. æ–°å¢ž AI å·¥ä¸šå¤§è„‘智能体与弹窗同步维护规则:新增智能体必须同步注册弹窗助手。
6. ç”Ÿäº§åŠ©æ‰‹å·²æŽ¥å…¥å·¥ä¸šå¤§è„‘å¼¹çª—ä¸Žå…¨å±€å³ä¾§å¯¹è¯æ¡†åŠ©æ‰‹åˆ‡æ¢ã€‚
7. å¢žåŠ å­—æ®µä¸­æ–‡åŒ–å±•ç¤ºçº¦æŸï¼šé¿å…è‹±æ–‡å­—æ®µå¯¹ä¸šåŠ¡ç”¨æˆ·ç›´å‡ºã€‚
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.ai.assistant;
import dev.langchain4j.service.MemoryId;
import dev.langchain4j.service.SystemMessage;
import dev.langchain4j.service.UserMessage;
import dev.langchain4j.service.spring.AiService;
import reactor.core.publisher.Flux;
import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
@AiService(
        wiringMode = EXPLICIT,
        streamingChatModel = "qwenStreamingChatModel",
        chatMemoryProvider = "chatMemoryProviderManufacturing",
        tools = "manufacturingAgentTools"
)
public interface ManufacturingAgent {
    @SystemMessage(fromResource = "manufacturing-agent-prompt.txt")
    Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
}
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,136 @@
package com.ruoyi.ai.assistant;
import com.ruoyi.ai.tools.ManufacturingAgentTools;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
@Component
public class ManufacturingIntentExecutor {
    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 final ManufacturingAgentTools manufacturingAgentTools;
    public ManufacturingIntentExecutor(ManufacturingAgentTools manufacturingAgentTools) {
        this.manufacturingAgentTools = manufacturingAgentTools;
    }
    public String tryExecute(String memoryId, String message) {
        if (!StringUtils.hasText(message)) {
            return null;
        }
        String text = message.trim();
        String keyword = extractKeyword(text);
        Integer limit = extractLimit(text);
        String startDate = extractStartDate(text);
        String endDate = extractEndDate(text);
        if (containsAny(text, "预警", "告警", "风险", "提醒")) {
            return manufacturingAgentTools.getWarningBoard(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "分析", "统计", "趋势", "看板", "报表", "总览")) {
            return manufacturingAgentTools.analyzeFactory(memoryId, startDate, endDate, text);
        }
        if (containsAny(text, "办", "处理", "派工", "安排", "闭环", "跟进", "处置")) {
            return manufacturingAgentTools.planActions(memoryId, text);
        }
        if (containsAny(text, "生产现场", "现场", "车间")) {
            return manufacturingAgentTools.queryDomain(memoryId, "site", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "计划", "排产", "mps")) {
            return manufacturingAgentTools.queryDomain(memoryId, "plan", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "工单", "作业单", "任务单", "任务")) {
            return manufacturingAgentTools.queryDomain(memoryId, "workorder", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "设备", "ç»´ä¿®", "保养", "故障")) {
            return manufacturingAgentTools.queryDomain(memoryId, "device", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "质量", "质检", "不合格", "检验")) {
            return manufacturingAgentTools.queryDomain(memoryId, "quality", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "物料", "库存", "库位", "入库", "出库")) {
            return manufacturingAgentTools.queryDomain(memoryId, "material", keyword, limit, startDate, endDate, text);
        }
        if (containsAny(text, "异常", "例外", "偏差")) {
            return manufacturingAgentTools.queryDomain(memoryId, "exception", keyword, limit, startDate, endDate, text);
        }
        return null;
    }
    private boolean containsAny(String text, String... keywords) {
        for (String keyword : keywords) {
            if (text.toLowerCase().contains(keyword.toLowerCase())) {
                return true;
            }
        }
        return false;
    }
    private Integer extractLimit(String text) {
        Matcher matcher = LIMIT_PATTERN.matcher(text);
        return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
    }
    private String extractStartDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        return matcher.find() ? matcher.group(1) : null;
    }
    private String extractEndDate(String text) {
        Matcher matcher = DATE_PATTERN.matcher(text);
        if (!matcher.find()) {
            return null;
        }
        return matcher.find() ? matcher.group(1) : 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("近30天", "")
                .replace("近7天", "")
                .replace("近15天", "")
                .replace("近60天", "")
                .replace("最近30天", "")
                .replace("最近7天", "")
                .replace("最近15天", "")
                .replace("最近60天", "")
                .replace("生产现场", "")
                .replace("现场", "")
                .replace("生产工单", "")
                .replace("生产", "")
                .replace("计划", "")
                .replace("排产", "")
                .replace("工单", "")
                .replace("设备", "")
                .replace("质量", "")
                .replace("物料", "")
                .replace("库存", "")
                .replace("异常", "")
                .replace("前10条", "")
                .replace("最近10条", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
}
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.ai.config;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import dev.langchain4j.memory.chat.ChatMemoryProvider;
import dev.langchain4j.memory.chat.MessageWindowChatMemory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class ManufacturingAgentConfig {
    @Bean
    ChatMemoryProvider chatMemoryProviderManufacturing(MongoChatMemoryStore mongoChatMemoryStore) {
        return memoryId -> MessageWindowChatMemory.builder()
                .id(memoryId)
                .maxMessages(30)
                .chatMemoryStore(mongoChatMemoryStore)
                .build();
    }
}
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,102 @@
package com.ruoyi.ai.controller;
import com.ruoyi.ai.assistant.ManufacturingAgent;
import com.ruoyi.ai.assistant.ManufacturingIntentExecutor;
import com.ruoyi.ai.bean.ChatForm;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.ai.service.AiChatSessionService;
import com.ruoyi.ai.store.MongoChatMemoryStore;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import dev.langchain4j.data.message.AiMessage;
import dev.langchain4j.data.message.UserMessage;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import reactor.core.publisher.Flux;
import java.util.List;
@Tag(name = "制造智能助手")
@RestController
@RequestMapping("/manufacturing-ai")
public class ManufacturingAiController extends BaseController {
    private final ManufacturingAgent manufacturingAgent;
    private final ManufacturingIntentExecutor manufacturingIntentExecutor;
    private final AiSessionUserContext aiSessionUserContext;
    private final MongoChatMemoryStore mongoChatMemoryStore;
    private final AiChatSessionService aiChatSessionService;
    public ManufacturingAiController(ManufacturingAgent manufacturingAgent,
                                     ManufacturingIntentExecutor manufacturingIntentExecutor,
                                     AiSessionUserContext aiSessionUserContext,
                                     MongoChatMemoryStore mongoChatMemoryStore,
                                     AiChatSessionService aiChatSessionService) {
        this.manufacturingAgent = manufacturingAgent;
        this.manufacturingIntentExecutor = manufacturingIntentExecutor;
        this.aiSessionUserContext = aiSessionUserContext;
        this.mongoChatMemoryStore = mongoChatMemoryStore;
        this.aiChatSessionService = aiChatSessionService;
    }
    @Operation(summary = "制造对话")
    @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
    public Flux<String> chat(@RequestBody ChatForm chatForm) {
        if (!StringUtils.hasText(chatForm.getMemoryId())) {
            return Flux.just("memoryId不能为空");
        }
        if (!StringUtils.hasText(chatForm.getMessage())) {
            return Flux.just("message不能为空");
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        String memoryId = chatForm.getMemoryId();
        String userMessage = chatForm.getMessage();
        aiSessionUserContext.bind(memoryId, loginUser);
        aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
        String directResponse = manufacturingIntentExecutor.tryExecute(memoryId, userMessage);
        if (StringUtils.isNotEmpty(directResponse)) {
            mongoChatMemoryStore.appendMessages(
                    memoryId,
                    List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
            );
            aiChatSessionService.refreshSessionStats(memoryId, loginUser);
            return Flux.just(directResponse);
        }
        return manufacturingAgent.chat(memoryId, userMessage)
                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
                .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
    }
    @Operation(summary = "制造会话列表")
    @GetMapping("/history/sessions")
    public AjaxResult listSessions() {
        return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "制造会话消息")
    @GetMapping("/history/messages/{memoryId}")
    public AjaxResult listMessages(@PathVariable String memoryId) {
        return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
    }
    @Operation(summary = "删除制造会话")
    @DeleteMapping("/history/{memoryId}")
    public AjaxResult deleteSession(@PathVariable String memoryId) {
        aiSessionUserContext.remove(memoryId);
        return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
    }
}
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,1035 @@
package com.ruoyi.ai.tools;
import com.alibaba.fastjson2.JSON;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
import com.ruoyi.ai.context.AiSessionUserContext;
import com.ruoyi.common.utils.SecurityUtils;
import com.ruoyi.device.mapper.DeviceDefectRecordMapper;
import com.ruoyi.device.mapper.DeviceLedgerMapper;
import com.ruoyi.device.mapper.DeviceRepairMapper;
import com.ruoyi.device.pojo.DeviceDefectRecord;
import com.ruoyi.device.pojo.DeviceLedger;
import com.ruoyi.device.pojo.DeviceRepair;
import com.ruoyi.framework.security.LoginUser;
import com.ruoyi.procurementrecord.mapper.ProcurementExceptionRecordMapper;
import com.ruoyi.procurementrecord.pojo.ProcurementExceptionRecord;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionPlanMapper;
import com.ruoyi.production.mapper.ProductionProductMainMapper;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionPlan;
import com.ruoyi.production.pojo.ProductionProductMain;
import com.ruoyi.quality.mapper.QualityInspectMapper;
import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
import com.ruoyi.quality.pojo.QualityInspect;
import com.ruoyi.quality.pojo.QualityUnqualified;
import com.ruoyi.stock.mapper.StockInventoryMapper;
import com.ruoyi.stock.pojo.StockInventory;
import dev.langchain4j.agent.tool.P;
import dev.langchain4j.agent.tool.Tool;
import dev.langchain4j.agent.tool.ToolMemoryId;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
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.stream.Collectors;
@Component
public class ManufacturingAgentTools {
    private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final int DEFAULT_LIMIT = 10;
    private static final int MAX_LIMIT = 30;
    private static final int DEVICE_REPAIR_STATUS_PENDING = 0;
    private final ProductionPlanMapper productionPlanMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOperationTaskMapper productionOperationTaskMapper;
    private final ProductionProductMainMapper productionProductMainMapper;
    private final DeviceLedgerMapper deviceLedgerMapper;
    private final DeviceRepairMapper deviceRepairMapper;
    private final DeviceDefectRecordMapper deviceDefectRecordMapper;
    private final QualityInspectMapper qualityInspectMapper;
    private final QualityUnqualifiedMapper qualityUnqualifiedMapper;
    private final StockInventoryMapper stockInventoryMapper;
    private final ProcurementExceptionRecordMapper procurementExceptionRecordMapper;
    private final AiSessionUserContext aiSessionUserContext;
    public ManufacturingAgentTools(ProductionPlanMapper productionPlanMapper,
                                   ProductionOrderMapper productionOrderMapper,
                                   ProductionOperationTaskMapper productionOperationTaskMapper,
                                   ProductionProductMainMapper productionProductMainMapper,
                                   DeviceLedgerMapper deviceLedgerMapper,
                                   DeviceRepairMapper deviceRepairMapper,
                                   DeviceDefectRecordMapper deviceDefectRecordMapper,
                                   QualityInspectMapper qualityInspectMapper,
                                   QualityUnqualifiedMapper qualityUnqualifiedMapper,
                                   StockInventoryMapper stockInventoryMapper,
                                   ProcurementExceptionRecordMapper procurementExceptionRecordMapper,
                                   AiSessionUserContext aiSessionUserContext) {
        this.productionPlanMapper = productionPlanMapper;
        this.productionOrderMapper = productionOrderMapper;
        this.productionOperationTaskMapper = productionOperationTaskMapper;
        this.productionProductMainMapper = productionProductMainMapper;
        this.deviceLedgerMapper = deviceLedgerMapper;
        this.deviceRepairMapper = deviceRepairMapper;
        this.deviceDefectRecordMapper = deviceDefectRecordMapper;
        this.qualityInspectMapper = qualityInspectMapper;
        this.qualityUnqualifiedMapper = qualityUnqualifiedMapper;
        this.stockInventoryMapper = stockInventoryMapper;
        this.procurementExceptionRecordMapper = procurementExceptionRecordMapper;
        this.aiSessionUserContext = aiSessionUserContext;
    }
    @Tool(name = "查询制造业务域数据", value = "按业务域查询生产现场、计划、工单、设备、质量、物料、异常处理相关数据。")
    public String queryDomain(@ToolMemoryId String memoryId,
                              @P(value = "业务域,site/plan/workorder/device/quality/material/exception") String domain,
                              @P(value = "关键字,可不传", required = false) String keyword,
                              @P(value = "返回条数,默认10,最大30", required = false) Integer limit,
                              @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                              @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                              @P(value = "时间范围描述,例如今年、本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        int finalLimit = normalizeLimit(limit);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        boolean hasTimeConstraint = hasTimeConstraint(startDate, endDate, timeRange);
        String normalizedDomain = normalizeDomain(domain);
        return switch (normalizedDomain) {
            case "site" -> siteSnapshot(loginUser, range);
            case "plan" -> listProductionPlans(loginUser, keyword, finalLimit, range);
            case "workorder" -> listWorkOrders(loginUser, keyword, finalLimit, range);
            case "device" -> isRepairIntent(keyword, timeRange)
                    ? listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint)
                    : listDevices(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit);
            case "repair" -> listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint);
            case "quality" -> listQualityIssues(loginUser, keyword, finalLimit, range);
            case "material" -> listMaterialInventory(loginUser, keyword, finalLimit);
            case "exception" -> listExceptions(loginUser, keyword, finalLimit, range);
            default -> jsonResponse(false, "manufacturing_query", "不支持的业务域: " + safe(domain), Map.of(), Map.of(), Map.of());
        };
    }
    @Tool(name = "制造预警看板", value = "计算计划、工单、设备、质量、物料、异常处理的预警信息。")
    public String getWarningBoard(@ToolMemoryId String memoryId,
                                  @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                                  @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                                  @P(value = "时间范围描述,例如今天、本周、本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        LocalDate today = LocalDate.now();
        long overduePlanCount = countOverduePlans(loginUser, today);
        long overdueWorkOrderCount = countOverdueWorkOrders(loginUser, today);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        List<Map<String, Object>> warningItems = new ArrayList<>();
        if (overduePlanCount > 0) {
            warningItems.add(warningItem("high", "计划逾期", overduePlanCount, "有生产计划超过需求日期仍未完成"));
        }
        if (overdueWorkOrderCount > 0) {
            warningItems.add(warningItem("high", "工单逾期", overdueWorkOrderCount, "有工单计划结束日期已过仍未完工"));
        }
        if (pendingRepairCount > 0) {
            warningItems.add(warningItem("medium", "设备待维修", pendingRepairCount, "存在待维修/维修中的设备"));
        }
        if (qualityOpenCount > 0) {
            warningItems.add(warningItem("high", "质量未闭环", qualityOpenCount, "存在未处理完成的不合格记录"));
        }
        if (lowStockCount > 0) {
            warningItems.add(warningItem("medium", "物料低库存", lowStockCount, "库存数量低于或等于预警阈值"));
        }
        if (exceptionCount > 0) {
            warningItems.add(warningItem("medium", "异常记录", exceptionCount, "时间范围内存在异常处理记录"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("warningCount", warningItems.size());
        summary.put("overduePlanCount", overduePlanCount);
        summary.put("overdueWorkOrderCount", overdueWorkOrderCount);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityOpenCount", qualityOpenCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        return jsonResponse(true, "manufacturing_warning", "已返回制造预警看板。", summary,
                Map.of("items", warningItems), Map.of());
    }
    @Tool(name = "制造经营分析", value = "按时间范围输出制造关键指标,支持查、问、分析场景。")
    public String analyzeFactory(@ToolMemoryId String memoryId,
                                 @P(value = "开始日期 yyyy-MM-dd,可不传", required = false) String startDate,
                                 @P(value = "结束日期 yyyy-MM-dd,可不传", required = false) String endDate,
                                 @P(value = "时间范围描述,例如本月、近30天", required = false) String timeRange) {
        LoginUser loginUser = currentLoginUser(memoryId);
        DateRange range = resolveDateRange(startDate, endDate, timeRange);
        long planTotal = countPlans(loginUser, range);
        long planCompleted = countPlansByStatus(loginUser, range, 2);
        long workOrderTotal = countWorkOrders(loginUser, range);
        long workOrderCompleted = countWorkOrdersByStatus(loginUser, range, 2);
        long workOrderInProgress = countWorkOrdersByStatus(loginUser, range, 1);
        long outputCount = countOutputs(loginUser, range);
        long deviceTotal = countDevices(loginUser);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityInspectTotal = countQualityInspect(loginUser, range);
        long qualityNgCount = countOpenQualityIssues(loginUser, range);
        long materialSkuCount = countInventorySku(loginUser);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("planTotal", planTotal);
        summary.put("planCompleted", planCompleted);
        summary.put("planCompletionRate", toRate(planCompleted, planTotal));
        summary.put("workOrderTotal", workOrderTotal);
        summary.put("workOrderCompleted", workOrderCompleted);
        summary.put("workOrderInProgress", workOrderInProgress);
        summary.put("workOrderCompletionRate", toRate(workOrderCompleted, workOrderTotal));
        summary.put("outputCount", outputCount);
        summary.put("deviceTotal", deviceTotal);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityInspectTotal", qualityInspectTotal);
        summary.put("qualityNgCount", qualityNgCount);
        summary.put("qualityIssueRate", toRate(qualityNgCount, qualityInspectTotal));
        summary.put("materialSkuCount", materialSkuCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        List<Map<String, Object>> coreMetrics = List.of(
                metric("计划完成率", toRate(planCompleted, planTotal)),
                metric("工单完成率", toRate(workOrderCompleted, workOrderTotal)),
                metric("质量异常率", toRate(qualityNgCount, qualityInspectTotal)),
                metric("低库存占比", toRate(lowStockCount, materialSkuCount))
        );
        Map<String, Object> charts = new LinkedHashMap<>();
        charts.put("domainBarOption", buildDomainBarOption(summary));
        charts.put("qualityPieOption", buildQualityPieOption(qualityInspectTotal, qualityNgCount));
        return jsonResponse(true, "manufacturing_analysis", "已返回制造分析结果。", summary,
                Map.of("coreMetrics", coreMetrics), charts);
    }
    @Tool(name = "生成制造办理建议", value = "根据用户问题输出可执行的办理动作建议,包括目标业务接口、必填字段和示例。")
    public String planActions(@ToolMemoryId String memoryId,
                              @P("用户诉求原文") String userQuery) {
        LoginUser loginUser = currentLoginUser(memoryId);
        List<Map<String, Object>> actionCards = new ArrayList<>();
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "工单", "派工", "作业")) {
            actionCards.add(actionCard(
                    "workorder_assign",
                    "工单派工",
                    "POST",
                    "/productionOperationTask/assign",
                    List.of("id", "userIds"),
                    Map.of("id", 10001, "userIds", "12,13"),
                    "将工单分配给指定人员,适用于现场调度。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "设备", "ç»´ä¿®", "故障")) {
            actionCards.add(actionCard(
                    "device_repair_create",
                    "创建设备维修单",
                    "POST",
                    "/device/repair",
                    List.of("deviceLedgerId", "deviceName", "repairName", "remark"),
                    Map.of("deviceLedgerId", 1001, "deviceName", "空压机A-01", "repairName", "张三", "remark", "异响并伴随温升"),
                    "新建维修单,进入设备异常处理闭环。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "质量", "不合格", "闭环")) {
            actionCards.add(actionCard(
                    "quality_unqualified_deal",
                    "处理不合格单",
                    "POST",
                    "/quality/qualityUnqualified/deal",
                    List.of("id", "dealResult", "dealName"),
                    Map.of("id", 3001, "dealResult", "返工后复检", "dealName", "李四"),
                    "对不合格记录执行处置并闭环。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "物料", "库存", "补料")) {
            actionCards.add(actionCard(
                    "material_inbound",
                    "补充库存",
                    "POST",
                    "/stockInventory/addstockInventory",
                    List.of("productModelId", "batchNo", "qualitity"),
                    Map.of("productModelId", 5001, "batchNo", "B2026051601", "qualitity", 120),
                    "当低库存预警触发时,增加库存数量。"));
        }
        if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "异常", "采购异常", "来料异常")) {
            actionCards.add(actionCard(
                    "procurement_exception_add",
                    "登记异常记录",
                    "POST",
                    "/procurementExceptionRecord/add",
                    List.of("purchaseLedgerId", "exceptionReason", "exceptionNum"),
                    Map.of("purchaseLedgerId", 888, "exceptionReason", "到料短缺", "exceptionNum", 24),
                    "登记采购/来料异常,便于后续追踪和分析。"));
        }
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("actionCount", actionCards.size());
        summary.put("userId", loginUser.getUserId());
        summary.put("tenantId", loginUser.getTenantId());
        return jsonResponse(true, "manufacturing_action_plan", "已生成办理建议,请前端引导用户确认后调用目标业务接口。",
                summary, Map.of("actionCards", actionCards), Map.of());
    }
    private String siteSnapshot(LoginUser loginUser, DateRange range) {
        long planTotal = countPlans(loginUser, range);
        long workOrderTotal = countWorkOrders(loginUser, range);
        long outputCount = countOutputs(loginUser, range);
        long deviceTotal = countDevices(loginUser);
        long pendingRepairCount = countPendingRepairs(loginUser);
        long qualityOpenCount = countOpenQualityIssues(loginUser, range);
        long lowStockCount = countLowStock(loginUser);
        long exceptionCount = countExceptionRecords(loginUser, range);
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("planTotal", planTotal);
        summary.put("workOrderTotal", workOrderTotal);
        summary.put("outputCount", outputCount);
        summary.put("deviceTotal", deviceTotal);
        summary.put("pendingRepairCount", pendingRepairCount);
        summary.put("qualityOpenCount", qualityOpenCount);
        summary.put("lowStockCount", lowStockCount);
        summary.put("exceptionCount", exceptionCount);
        return jsonResponse(true, "manufacturing_site_snapshot", "已返回生产现场概览。", summary, Map.of(), Map.of());
    }
    private String listProductionPlans(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ProductionPlan::getMpsNo, keyword)
                    .or().like(ProductionPlan::getRemark, keyword)
                    .or().like(ProductionPlan::getSource, keyword));
        }
        wrapper.orderByDesc(ProductionPlan::getRequiredDate, ProductionPlan::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(productionPlanMapper.selectList(wrapper)).stream()
                .map(this::toPlanItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_plan_list", "已返回生产计划列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listWorkOrders(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end());
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(ProductionOperationTask::getWorkOrderNo, keyword)
                    .or().like(ProductionOperationTask::getUserIds, keyword));
        }
        wrapper.orderByDesc(ProductionOperationTask::getPlanEndTime, ProductionOperationTask::getId)
                .last("limit " + limit);
        List<Map<String, Object>> items = defaultList(productionOperationTaskMapper.selectList(wrapper)).stream()
                .map(this::toWorkOrderItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_workorder_list", "已返回工单列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listDevices(LoginUser loginUser, String keyword, int limit) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
                    .or().like(DeviceLedger::getDeviceModel, keyword)
                    .or().like(DeviceLedger::getDeviceBrand, keyword));
        }
        wrapper.orderByDesc(DeviceLedger::getId).last("limit " + limit);
        Map<Long, Long> pendingRepairMap = pendingRepairCountByDevice(loginUser);
        List<Map<String, Object>> items = defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
                .map(item -> toDeviceItem(item, pendingRepairMap.getOrDefault(item.getId(), 0L)))
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_device_list", "已返回设备列表。",
                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
    }
    private String listDeviceRepairs(LoginUser loginUser, String keyword, int limit, DateRange range, boolean hasTimeConstraint) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        Long currentDeptId = loginUser.getCurrentDeptId();
        if (currentDeptId != null) {
            wrapper.and(w -> w.eq(DeviceRepair::getDeptId, currentDeptId).or().isNull(DeviceRepair::getDeptId));
        }
        if (hasTimeConstraint) {
            wrapper.ge(DeviceRepair::getCreateTime, range.start().atStartOfDay())
                    .lt(DeviceRepair::getCreateTime, range.end().plusDays(1).atStartOfDay());
        }
        if (StringUtils.hasText(keyword)) {
            List<Long> matchedDeviceIds = findDeviceLedgerIdsByKeyword(loginUser, keyword);
            wrapper.and(w -> {
                w.like(DeviceRepair::getDeviceName, keyword)
                        .or().like(DeviceRepair::getDeviceModel, keyword)
                        .or().like(DeviceRepair::getRemark, keyword)
                        .or().like(DeviceRepair::getRepairName, keyword)
                        .or().like(DeviceRepair::getMaintenanceName, keyword);
                if (!matchedDeviceIds.isEmpty()) {
                    w.or().in(DeviceRepair::getDeviceLedgerId, matchedDeviceIds);
                }
            });
        }
        wrapper.orderByDesc(DeviceRepair::getCreateTime, DeviceRepair::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(deviceRepairMapper.selectList(wrapper)).stream()
                .map(this::toDeviceRepairItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_device_repair_list", "已返回设备维修记录。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listQualityIssues(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()));
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(QualityUnqualified::getProductName, keyword)
                    .or().like(QualityUnqualified::getDefectivePhenomena, keyword)
                    .or().like(QualityUnqualified::getDealResult, keyword));
        }
        wrapper.orderByDesc(QualityUnqualified::getCheckTime, QualityUnqualified::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(qualityUnqualifiedMapper.selectList(wrapper)).stream()
                .map(this::toQualityItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_quality_list", "已返回质量异常列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private String listMaterialInventory(LoginUser loginUser, String keyword, int limit) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        if (StringUtils.hasText(keyword)) {
            wrapper.and(w -> w.like(StockInventory::getBatchNo, keyword)
                    .or().like(StockInventory::getProductModelId, keyword));
        }
        wrapper.orderByDesc(StockInventory::getId).last("limit " + limit);
        List<Map<String, Object>> items = defaultList(stockInventoryMapper.selectList(wrapper)).stream()
                .map(this::toMaterialItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_material_list", "已返回物料库存列表。",
                Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
    }
    private String listExceptions(LoginUser loginUser, String keyword, int limit, DateRange range) {
        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
        if (StringUtils.hasText(keyword)) {
            wrapper.like(ProcurementExceptionRecord::getExceptionReason, keyword);
        }
        wrapper.orderByDesc(ProcurementExceptionRecord::getCreateTime, ProcurementExceptionRecord::getId)
                .last("limit " + limit);
        List<Map<String, Object>> items = defaultList(procurementExceptionRecordMapper.selectList(wrapper)).stream()
                .map(this::toExceptionItem)
                .collect(Collectors.toList());
        return jsonResponse(true, "manufacturing_exception_list", "已返回异常处理列表。",
                rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
    }
    private long countPlans(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countPlansByStatus(LoginUser loginUser, DateRange range, int status) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.ge(ProductionPlan::getRequiredDate, range.start())
                .le(ProductionPlan::getRequiredDate, range.end())
                .eq(ProductionPlan::getStatus, status);
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countWorkOrders(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end());
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private long countWorkOrdersByStatus(LoginUser loginUser, DateRange range, int status) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
                .le(ProductionOperationTask::getPlanEndTime, range.end())
                .eq(ProductionOperationTask::getStatus, status);
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private long countOutputs(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProductionProductMain> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
        wrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay())
                .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay());
        return productionProductMainMapper.selectCount(wrapper);
    }
    private long countDevices(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
        return deviceLedgerMapper.selectCount(wrapper);
    }
    private long countPendingRepairs(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
        return deviceRepairMapper.selectCount(wrapper);
    }
    private long countQualityInspect(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<QualityInspect> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityInspect::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityInspect::getDeptId);
        wrapper.ge(QualityInspect::getCheckTime, toDate(range.start()))
                .lt(QualityInspect::getCheckTime, toExclusiveEndDate(range.end()));
        return qualityInspectMapper.selectCount(wrapper);
    }
    private long countOpenQualityIssues(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
        wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
                .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()))
                .ne(QualityUnqualified::getInspectState, 2);
        return qualityUnqualifiedMapper.selectCount(wrapper);
    }
    private long countInventorySku(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        return stockInventoryMapper.selectCount(wrapper);
    }
    private long countLowStock(LoginUser loginUser) {
        LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
        wrapper.isNotNull(StockInventory::getWarnNum);
        List<StockInventory> stocks = defaultList(stockInventoryMapper.selectList(wrapper));
        return stocks.stream()
                .filter(this::isLowStock)
                .count();
    }
    private long countExceptionRecords(LoginUser loginUser, DateRange range) {
        LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
        wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
                .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
        return procurementExceptionRecordMapper.selectCount(wrapper);
    }
    private long countOverduePlans(LoginUser loginUser, LocalDate today) {
        LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
        wrapper.lt(ProductionPlan::getRequiredDate, today).ne(ProductionPlan::getStatus, 2);
        return productionPlanMapper.selectCount(wrapper);
    }
    private long countOverdueWorkOrders(LoginUser loginUser, LocalDate today) {
        LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
        wrapper.lt(ProductionOperationTask::getPlanEndTime, today).ne(ProductionOperationTask::getStatus, 2);
        return productionOperationTaskMapper.selectCount(wrapper);
    }
    private Map<Long, Long> pendingRepairCountByDevice(LoginUser loginUser) {
        LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
        wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
        return defaultList(deviceRepairMapper.selectList(wrapper)).stream()
                .filter(item -> item.getDeviceLedgerId() != null)
                .collect(Collectors.groupingBy(DeviceRepair::getDeviceLedgerId, Collectors.counting()));
    }
    private Map<String, Object> toPlanItem(ProductionPlan item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("mpsNo", safe(item.getMpsNo()));
        map.put("requiredDate", formatDate(item.getRequiredDate()));
        map.put("promisedDeliveryDate", formatDate(item.getPromisedDeliveryDate()));
        map.put("qtyRequired", item.getQtyRequired());
        map.put("quantityIssued", item.getQuantityIssued());
        map.put("status", item.getStatus());
        map.put("source", safe(item.getSource()));
        map.put("remark", safe(item.getRemark()));
        return map;
    }
    private Map<String, Object> toWorkOrderItem(ProductionOperationTask item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("workOrderNo", safe(item.getWorkOrderNo()));
        map.put("productionOrderId", item.getProductionOrderId());
        map.put("planStartTime", formatDate(item.getPlanStartTime()));
        map.put("planEndTime", formatDate(item.getPlanEndTime()));
        map.put("actualStartTime", formatDate(item.getActualStartTime()));
        map.put("actualEndTime", formatDate(item.getActualEndTime()));
        map.put("planQuantity", item.getPlanQuantity());
        map.put("completeQuantity", item.getCompleteQuantity());
        map.put("status", item.getStatus());
        map.put("userIds", safe(item.getUserIds()));
        return map;
    }
    private Map<String, Object> toDeviceItem(DeviceLedger item, long pendingRepairCount) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("deviceName", safe(item.getDeviceName()));
        map.put("deviceModel", safe(item.getDeviceModel()));
        map.put("deviceBrand", safe(item.getDeviceBrand()));
        map.put("status", safe(item.getStatus()));
        map.put("storageLocation", safe(item.getStorageLocation()));
        map.put("supplierName", safe(item.getSupplierName()));
        map.put("pendingRepairCount", pendingRepairCount);
        return map;
    }
    private Map<String, Object> toDeviceRepairItem(DeviceRepair item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("deviceLedgerId", item.getDeviceLedgerId());
        map.put("deviceName", safe(item.getDeviceName()));
        map.put("deviceModel", safe(item.getDeviceModel()));
        map.put("repairTime", formatDate(item.getRepairTime()));
        map.put("repairName", safe(item.getRepairName()));
        map.put("maintenanceName", safe(item.getMaintenanceName()));
        map.put("maintenanceTime", formatDateTime(item.getMaintenanceTime()));
        map.put("maintenanceResult", safe(item.getMaintenanceResult()));
        map.put("acceptanceName", safe(item.getAcceptanceName()));
        map.put("acceptanceTime", formatDateTime(item.getAcceptanceTime()));
        map.put("status", item.getStatus());
        map.put("remark", safe(item.getRemark()));
        map.put("createTime", formatDateTime(item.getCreateTime()));
        return map;
    }
    private Map<String, Object> toQualityItem(QualityUnqualified item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("checkTime", formatDate(item.getCheckTime()));
        map.put("inspectState", item.getInspectState());
        map.put("productId", item.getProductId());
        map.put("productName", safe(item.getProductName()));
        map.put("model", safe(item.getModel()));
        map.put("quantity", item.getQuantity());
        map.put("defectivePhenomena", safe(item.getDefectivePhenomena()));
        map.put("dealResult", safe(item.getDealResult()));
        map.put("dealName", safe(item.getDealName()));
        map.put("dealTime", formatDate(item.getDealTime()));
        return map;
    }
    private Map<String, Object> toMaterialItem(StockInventory item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("productModelId", item.getProductModelId());
        map.put("batchNo", safe(item.getBatchNo()));
        map.put("qualitity", item.getQualitity());
        map.put("lockedQuantity", item.getLockedQuantity());
        map.put("warnNum", item.getWarnNum());
        map.put("lowStock", isLowStock(item));
        map.put("remark", safe(item.getRemark()));
        return map;
    }
    private Map<String, Object> toExceptionItem(ProcurementExceptionRecord item) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("id", item.getId());
        map.put("purchaseLedgerId", item.getPurchaseLedgerId());
        map.put("exceptionReason", safe(item.getExceptionReason()));
        map.put("exceptionNum", item.getExceptionNum());
        map.put("createTime", formatDateTime(item.getCreateTime()));
        return map;
    }
    private boolean isLowStock(StockInventory item) {
        BigDecimal quantity = item.getQualitity();
        BigDecimal warnNum = item.getWarnNum();
        if (quantity == null || warnNum == null) {
            return false;
        }
        return quantity.compareTo(warnNum) <= 0;
    }
    private Map<String, Object> warningItem(String level, String title, long count, String detail) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("level", level);
        map.put("title", title);
        map.put("count", count);
        map.put("detail", detail);
        return map;
    }
    private Map<String, Object> actionCard(String code,
                                           String name,
                                           String method,
                                           String targetApi,
                                           List<String> requiredFields,
                                           Map<String, Object> examplePayload,
                                           String description) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("code", code);
        map.put("name", name);
        map.put("method", method);
        map.put("targetApi", targetApi);
        map.put("requiredFields", requiredFields);
        map.put("examplePayload", examplePayload);
        map.put("description", description);
        return map;
    }
    private Map<String, Object> metric(String label, String value) {
        Map<String, Object> map = new LinkedHashMap<>();
        map.put("label", label);
        map.put("value", value);
        return map;
    }
    private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
        Map<String, Object> summary = new LinkedHashMap<>();
        summary.put("timeRange", range.label());
        summary.put("startDate", range.start().toString());
        summary.put("endDate", range.end().toString());
        summary.put("count", count);
        summary.put("keyword", safe(keyword));
        return summary;
    }
    private Map<String, Object> buildDomainBarOption(Map<String, Object> summary) {
        List<String> xData = List.of("计划", "工单", "设备", "质量", "物料", "异常");
        List<Number> yData = List.of(
                numberValue(summary.get("planTotal")),
                numberValue(summary.get("workOrderTotal")),
                numberValue(summary.get("deviceTotal")),
                numberValue(summary.get("qualityNgCount")),
                numberValue(summary.get("lowStockCount")),
                numberValue(summary.get("exceptionCount"))
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "制造域关键数量", "left", "center"));
        option.put("tooltip", Map.of("trigger", "axis"));
        option.put("xAxis", Map.of("type", "category", "data", xData));
        option.put("yAxis", Map.of("type", "value"));
        option.put("series", List.of(Map.of("name", "数量", "type", "bar", "data", yData)));
        return option;
    }
    private Map<String, Object> buildQualityPieOption(long inspectTotal, long ngCount) {
        long passCount = Math.max(inspectTotal - ngCount, 0);
        List<Map<String, Object>> data = List.of(
                Map.of("name", "不合格", "value", ngCount),
                Map.of("name", "非不合格", "value", passCount)
        );
        Map<String, Object> option = new LinkedHashMap<>();
        option.put("title", Map.of("text", "质量结果分布", "left", "center"));
        option.put("tooltip", Map.of("trigger", "item"));
        option.put("series", List.of(Map.of("name", "质量", "type", "pie", "radius", "60%", "data", data)));
        return option;
    }
    private int numberValue(Object value) {
        if (value instanceof Number number) {
            return number.intValue();
        }
        return 0;
    }
    private String toRate(long numerator, long denominator) {
        if (denominator <= 0) {
            return "0.00%";
        }
        BigDecimal rate = new BigDecimal(numerator)
                .multiply(new BigDecimal("100"))
                .divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP);
        return rate.toPlainString() + "%";
    }
    private String normalizeDomain(String domain) {
        if (!StringUtils.hasText(domain)) {
            return "";
        }
        String value = domain.trim().toLowerCase();
        return switch (value) {
            case "生产现场", "site", "factory", "workshop" -> "site";
            case "计划", "plan", "schedule" -> "plan";
            case "工单", "workorder", "work_order", "task" -> "workorder";
            case "设备", "device", "equipment" -> "device";
            case "ç»´ä¿®", "repair", "maintenance" -> "repair";
            case "质量", "quality", "qc" -> "quality";
            case "物料", "material", "inventory", "stock" -> "material";
            case "异常", "exception", "abnormal" -> "exception";
            default -> value;
        };
    }
    private boolean isRepairIntent(String keyword, String userQuery) {
        String query = safe(userQuery);
        return containsAny(safe(keyword), "ç»´ä¿®", "报修", "检修", "维护")
                || containsAny(query, "ç»´ä¿®", "报修", "检修", "维护");
    }
    private String normalizeDeviceQueryKeyword(String keyword, String userQuery) {
        String source = StringUtils.hasText(keyword) ? keyword : userQuery;
        if (!StringUtils.hasText(source)) {
            return null;
        }
        String cleaned = source
                .replace("查询", "")
                .replace("查看", "")
                .replace("帮我", "")
                .replace("请", "")
                .replace("查", "")
                .replace("设备", "")
                .replace("维修记录", "")
                .replace("维修情况", "")
                .replace("报修记录", "")
                .replace("报修情况", "")
                .replace("ç»´ä¿®", "")
                .replace("报修", "")
                .replace("情况", "")
                .replace("记录", "")
                .replace("信息", "")
                .replace("的", "")
                .replace("一下", "")
                .trim();
        return cleaned.length() >= 2 ? cleaned : null;
    }
    private List<Long> findDeviceLedgerIdsByKeyword(LoginUser loginUser, String keyword) {
        if (!StringUtils.hasText(keyword)) {
            return List.of();
        }
        LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
        applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
        Long currentDeptId = loginUser.getCurrentDeptId();
        if (currentDeptId != null) {
            wrapper.and(w -> w.eq(DeviceLedger::getDeptId, currentDeptId).or().isNull(DeviceLedger::getDeptId));
        }
        wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
                .or().like(DeviceLedger::getDeviceModel, keyword)
                .or().like(DeviceLedger::getDeviceBrand, keyword));
        wrapper.orderByDesc(DeviceLedger::getId).last("limit 200");
        return defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
                .map(DeviceLedger::getId)
                .filter(Objects::nonNull)
                .collect(Collectors.toList());
    }
    private boolean hasTimeConstraint(String startDate, String endDate, String userQuery) {
        if (StringUtils.hasText(startDate) || StringUtils.hasText(endDate)) {
            return true;
        }
        if (!StringUtils.hasText(userQuery)) {
            return false;
        }
        String text = userQuery.trim();
        return containsAny(text, "今天", "昨天", "本周", "上周", "本月", "上月", "今年", "去年", "近", "最近");
    }
    private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
        LocalDate today = LocalDate.now();
        LocalDate start = parseLocalDate(startDate);
        LocalDate end = parseLocalDate(endDate);
        if (start != null || end != null) {
            LocalDate s = start != null ? start : end;
            LocalDate e = end != null ? end : start;
            if (s.isAfter(e)) {
                LocalDate temp = s;
                s = e;
                e = temp;
            }
            return new DateRange(s, e, s + "至" + e);
        }
        if (!StringUtils.hasText(timeRange)) {
            return new DateRange(today.minusDays(29), today, "近30天");
        }
        String text = timeRange.trim();
        if (text.contains("今天")) {
            return new DateRange(today, today, "今天");
        }
        if (text.contains("本周")) {
            LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
            return new DateRange(startOfWeek, today, "本周");
        }
        if (text.contains("本月")) {
            return new DateRange(today.withDayOfMonth(1), today, "本月");
        }
        if (text.contains("本年") || text.contains("今年")) {
            return new DateRange(today.withDayOfYear(1), today, "今年");
        }
        if (text.contains("去年")) {
            LocalDate firstDay = today.minusYears(1).withDayOfYear(1);
            LocalDate lastDay = today.minusYears(1).withMonth(12).withDayOfMonth(31);
            return new DateRange(firstDay, lastDay, "去年");
        }
        if (text.contains("上月")) {
            LocalDate startOfLastMonth = today.minusMonths(1).withDayOfMonth(1);
            return new DateRange(startOfLastMonth, startOfLastMonth.withDayOfMonth(startOfLastMonth.lengthOfMonth()), "上月");
        }
        java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(近|最近)(\\d+)(天|周|个月|月|å¹´)").matcher(text);
        if (matcher.find()) {
            int amount = Integer.parseInt(matcher.group(2));
            String unit = matcher.group(3);
            LocalDate relativeStart = switch (unit) {
                case "天" -> today.minusDays(Math.max(amount - 1L, 0));
                case "周" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
                case "个月", "月" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
                case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
                default -> today.minusDays(29);
            };
            return new DateRange(relativeStart, today, "近" + amount + unit);
        }
        return new DateRange(today.minusDays(29), today, "近30天");
    }
    private LocalDate parseLocalDate(String text) {
        if (!StringUtils.hasText(text)) {
            return null;
        }
        return LocalDate.parse(text.trim(), DATE_FMT);
    }
    private Date toDate(LocalDate date) {
        return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private Date toExclusiveEndDate(LocalDate date) {
        return Date.from(date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
    }
    private String formatDate(LocalDate date) {
        return date == null ? "" : DATE_FMT.format(date);
    }
    private String formatDate(Date date) {
        if (date == null) {
            return "";
        }
        return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
    }
    private String formatDateTime(LocalDateTime time) {
        if (time == null) {
            return "";
        }
        return time.truncatedTo(ChronoUnit.SECONDS).toString().replace('T', ' ');
    }
    private int normalizeLimit(Integer limit) {
        if (limit == null || limit <= 0) {
            return DEFAULT_LIMIT;
        }
        return Math.min(limit, MAX_LIMIT);
    }
    private boolean containsAny(String text, String... values) {
        for (String value : values) {
            if (text.contains(value)) {
                return true;
            }
        }
        return false;
    }
    private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
        if (tenantId != null) {
            wrapper.eq(field, tenantId);
        }
    }
    private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> field) {
        if (deptId != null) {
            wrapper.eq(field, deptId);
        }
    }
    private LoginUser currentLoginUser(String memoryId) {
        LoginUser loginUser = aiSessionUserContext.get(memoryId);
        if (loginUser != null) {
            return loginUser;
        }
        return SecurityUtils.getLoginUser();
    }
    private String safe(Object value) {
        return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
    }
    private <T> List<T> defaultList(List<T> list) {
        return list == null ? List.of() : list;
    }
    private String jsonResponse(boolean success,
                                String type,
                                String description,
                                Map<String, Object> summary,
                                Map<String, Object> data,
                                Map<String, Object> charts) {
        Map<String, Object> result = new LinkedHashMap<>();
        result.put("success", success);
        result.put("type", type);
        result.put("description", description);
        result.put("summary", summary == null ? Map.of() : summary);
        result.put("data", data == null ? Map.of() : data);
        result.put("charts", charts == null ? Map.of() : charts);
        return JSON.toJSONString(result);
    }
    private record DateRange(LocalDate start, LocalDate end, String label) {
    }
}
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -23,9 +23,11 @@
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.mapper.ProductionOrderMapper;
import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOperationTask;
import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.service.ProductionOperationTaskService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
@@ -48,6 +50,7 @@
    private final SysUserMapper sysUserMapper;
    private final ProductionOrderMapper productionOrderMapper;
    private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
    private final FileUtil fileUtil;
@@ -61,6 +64,7 @@
        // åˆ†é¡µæŸ¥è¯¢ç”Ÿäº§å·¥åºä»»åŠ¡
        Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
        IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
        fillOperationTypes(result.getRecords());
        fillUserNames(result.getRecords());
        return result;
    }
@@ -69,6 +73,7 @@
    public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
        // æŸ¥è¯¢å·¥åºä»»åŠ¡åˆ—è¡¨
        List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
        fillOperationTypes(result);
        fillUserNames(result);
        return result;
    }
@@ -81,6 +86,7 @@
            return null;
        }
        ProductionOperationTaskVo vo = BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
        fillOperationTypes(Collections.singletonList(vo));
        if (item.getProductionOrderId() != null) {
            ProductionOrder productionOrder = productionOrderMapper.selectById(item.getProductionOrderId());
            if (productionOrder != null) {
@@ -370,6 +376,38 @@
    @Override
    public List<ProductionOperationTaskVo> getOperation(ProductionOperationTaskDto dto) {
        // æŸ¥è¯¢å·¥åºä»»åŠ¡åˆ—è¡¨
        return baseMapper.getOperation(dto);
        List<ProductionOperationTaskVo> result = baseMapper.getOperation(dto);
        fillOperationTypes(result);
        return result;
    }
    private void fillOperationTypes(List<ProductionOperationTaskVo> voList) {
        // å›žå¡«å·¥åºç±»åž‹ï¼ˆ0 è®¡æ—¶ / 1 è®¡ä»¶ï¼‰
        if (voList == null || voList.isEmpty()) {
            return;
        }
        Set<Long> operationIds = voList.stream()
                .filter(Objects::nonNull)
                .map(ProductionOperationTaskVo::getProductionOrderRoutingOperationId)
                .filter(Objects::nonNull)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        if (operationIds.isEmpty()) {
            return;
        }
        Map<Long, Integer> typeByOperationId = productionOrderRoutingOperationMapper
                .selectBatchIds(new ArrayList<>(operationIds))
                .stream()
                .filter(Objects::nonNull)
                .collect(Collectors.toMap(
                        ProductionOrderRoutingOperation::getId,
                        ProductionOrderRoutingOperation::getType,
                        (left, right) -> left
                ));
        for (ProductionOperationTaskVo vo : voList) {
            if (vo == null || vo.getType() != null || vo.getProductionOrderRoutingOperationId() == null) {
                continue;
            }
            vo.setType(typeByOperationId.get(vo.getProductionOrderRoutingOperationId()));
        }
    }
}
src/main/resources/manufacturing-agent-prompt.txt
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
你是企业制造智能助手,覆盖生产现场、计划、工单、设备、质量、物料、异常处理七个域。
工作规则:
1. ç”¨æˆ·æå‡ºâ€œæŸ¥ã€é—®ã€é¢„警、分析”需求时,优先调用工具拿结构化结果,不要臆造业务数据。
2. ç”¨æˆ·æå‡ºâ€œåŠžâ€éœ€æ±‚æ—¶ï¼Œä¼˜å…ˆè¾“å‡ºåŠžç†å»ºè®®åŠ¨ä½œå¡ï¼ˆæŽ¥å£ã€å¿…å¡«å­—æ®µã€ç¤ºä¾‹ï¼‰ï¼Œæ˜Žç¡®éœ€è¦å‰ç«¯äºŒæ¬¡ç¡®è®¤ã€‚
3. å·¥å…·è¿”回 JSON æ—¶ï¼Œç›´æŽ¥è¾“出原始 JSON å­—符串,不要额外包裹 Markdown,不要在前后加解释文字。
4. å›žç­”必须使用中文;若用户问题缺少时间范围、关键字等条件,可先给默认口径并提示可补充条件。
5. è‹¥æ— æ³•从工具结果得到结论,明确说明缺少的筛选条件或业务字段。
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -159,6 +159,7 @@
    <select id="getOperation" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
        select poro.operation_name as operationName,
               max(poro.type) as type,
               count(pot.id) as productionTaskCount,
               sum(ifnull(pot.plan_quantity, 0)) as planQuantity,
               sum(ifnull(pot.complete_quantity, 0)) as completeQuantity,