From 6f469ecf16ee1b7b13fa4d4b30fca3457fddddd0 Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期六, 16 五月 2026 16:12:05 +0800
Subject: [PATCH] config(zxsq): 更新配置文件以支持个推推送、MongoDB存储和文件上传功能
---
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml | 1
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java | 20
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java | 102 +++
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java | 136 ++++
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java | 21
doc/20260516_制造智能助手前端联调文档.md | 258 +++++++++
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java | 1035 ++++++++++++++++++++++++++++++++++++
src/main/resources/manufacturing-agent-prompt.txt | 8
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java | 40 +
9 files changed, 1,620 insertions(+), 1 deletions(-)
diff --git "a/doc/20260516_\345\210\266\351\200\240\346\231\272\350\203\275\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260516_\345\210\266\351\200\240\346\231\272\350\203\275\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..d4b9a1f
--- /dev/null
+++ "b/doc/20260516_\345\210\266\351\200\240\346\231\272\350\203\275\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,258 @@
+# 鍒堕�犳櫤鑳藉姪鎵嬪墠绔仈璋冩枃妗o紙`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` 涓庣幇鏈夋帴鍙d竴鑷达級銆�
+- `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`锛堟煡璁惧鍙拌处锛夈��
+
+绀轰緥锛�
+- `鏌ヨ澶嘇-01` -> `manufacturing_device_list`
+- `鏌ヨ澶嘇-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. 鈥滃姙鈥濊兘鍔涜仈璋冭鐐�
+
+褰撳墠鈥滃姙鈥濅负 **鍔炵悊寤鸿妯″紡**锛圓I 杈撳嚭鍔ㄤ綔鍗★紝鍓嶇纭鍚庤皟鐢ㄧ洰鏍囦笟鍔℃帴鍙o級銆�
+
+- `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 瑙f瀽锛�
+ - 鎴愬姛锛氭寜 `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 浠呯敤浜庢帴鍙i�氫俊涓庤皟璇曘��
+
+## 13. 鏈鏇存柊璁板綍锛�2026-05-16锛�
+
+1. 鏂板璁惧缁翠慨璁板綍杩斿洖绫诲瀷锛歚manufacturing_device_repair_list`銆�
+2. 淇璁惧鍩熸剰鍥惧垎娴侊細`缁翠慨/鎶ヤ慨/妫�淇�/缁存姢` 璧扮淮淇褰曪紝涓嶅啀璇蛋璁惧鍒楄〃銆�
+3. 淇缁翠慨璁板綍鏃堕棿杩囨护锛氫粎鍦ㄧ敤鎴锋槑纭椂闂存潯浠舵椂鐢熸晥銆�
+4. 淇寰呯淮淇粺璁″彛寰勶細鎸� `status = 0` 缁熻銆�
+5. 鏂板 AI 宸ヤ笟澶ц剳鏅鸿兘浣撲笌寮圭獥鍚屾缁存姢瑙勫垯锛氭柊澧炴櫤鑳戒綋蹇呴』鍚屾娉ㄥ唽寮圭獥鍔╂墜銆�
+6. 鐢熶骇鍔╂墜宸叉帴鍏ュ伐涓氬ぇ鑴戝脊绐椾笌鍏ㄥ眬鍙充晶瀵硅瘽妗嗗姪鎵嬪垏鎹€��
+7. 澧炲姞瀛楁涓枃鍖栧睍绀虹害鏉燂細閬垮厤鑻辨枃瀛楁瀵逛笟鍔$敤鎴风洿鍑恒��
diff --git a/src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java b/src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
new file mode 100644
index 0000000..f0e8cf7
--- /dev/null
+++ b/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);
+}
diff --git a/src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java
new file mode 100644
index 0000000..e9d9396
--- /dev/null
+++ b/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;
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java b/src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java
new file mode 100644
index 0000000..79aa222
--- /dev/null
+++ b/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();
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java b/src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
new file mode 100644
index 0000000..cb7c0ba
--- /dev/null
+++ b/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()));
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java b/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
new file mode 100644
index 0000000..1ff96c1
--- /dev/null
+++ b/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 = "鏍规嵁鐢ㄦ埛闂杈撳嚭鍙墽琛岀殑鍔炵悊鍔ㄤ綔寤鸿锛屽寘鎷洰鏍囦笟鍔℃帴鍙c�佸繀濉瓧娈靛拰绀轰緥銆�")
+ 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", "绌哄帇鏈篈-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) {
+ }
+}
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
index 6e9457d..bd6f2df 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
+++ b/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()));
+ }
}
}
diff --git a/src/main/resources/manufacturing-agent-prompt.txt b/src/main/resources/manufacturing-agent-prompt.txt
new file mode 100644
index 0000000..c1a30c8
--- /dev/null
+++ b/src/main/resources/manufacturing-agent-prompt.txt
@@ -0,0 +1,8 @@
+浣犳槸浼佷笟鍒堕�犳櫤鑳藉姪鎵嬶紝瑕嗙洊鐢熶骇鐜板満銆佽鍒掋�佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞嗕竷涓煙銆�
+
+宸ヤ綔瑙勫垯锛�
+1. 鐢ㄦ埛鎻愬嚭鈥滄煡銆侀棶銆侀璀︺�佸垎鏋愨�濋渶姹傛椂锛屼紭鍏堣皟鐢ㄥ伐鍏锋嬁缁撴瀯鍖栫粨鏋滐紝涓嶈鑷嗛�犱笟鍔℃暟鎹��
+2. 鐢ㄦ埛鎻愬嚭鈥滃姙鈥濋渶姹傛椂锛屼紭鍏堣緭鍑哄姙鐞嗗缓璁姩浣滃崱锛堟帴鍙c�佸繀濉瓧娈点�佺ず渚嬶級锛屾槑纭渶瑕佸墠绔簩娆$‘璁ゃ��
+3. 宸ュ叿杩斿洖 JSON 鏃讹紝鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓诧紝涓嶈棰濆鍖呰9 Markdown锛屼笉瑕佸湪鍓嶅悗鍔犺В閲婃枃瀛椼��
+4. 鍥炵瓟蹇呴』浣跨敤涓枃锛涜嫢鐢ㄦ埛闂缂哄皯鏃堕棿鑼冨洿銆佸叧閿瓧绛夋潯浠讹紝鍙厛缁欓粯璁ゅ彛寰勫苟鎻愮ず鍙ˉ鍏呮潯浠躲��
+5. 鑻ユ棤娉曚粠宸ュ叿缁撴灉寰楀埌缁撹锛屾槑纭鏄庣己灏戠殑绛涢�夋潯浠舵垨涓氬姟瀛楁銆�
diff --git a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
index 6db5455..f029ef7 100644
--- a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
+++ b/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,
--
Gitblit v1.9.3