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/basic/service/impl/ProductModelServiceImpl.java
@@ -18,8 +18,11 @@ import com.ruoyi.common.utils.bean.BeanUtils; import com.ruoyi.common.utils.poi.ExcelUtil; import com.ruoyi.framework.web.domain.AjaxResult; import com.ruoyi.sales.dto.LossProductModelDto; import com.ruoyi.sales.mapper.SalesLedgerProductMapper; import com.ruoyi.sales.pojo.SalesLedgerProduct; import com.ruoyi.technology.mapper.TechnologyBomMapper; import com.ruoyi.technology.pojo.TechnologyBom; import lombok.AllArgsConstructor; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; @@ -43,6 +46,7 @@ private final ProductMapper productMapper; private final SalesLedgerProductMapper salesLedgerProductMapper; private final TechnologyBomMapper technologyBomMapper; private ProductModelMapper productModelMapper; @Override @@ -86,6 +90,14 @@ throw new RuntimeException("å·²ç»åå¨è¯¥äº§åçéå®å°è´¦åéè´å°è´¦"); } // æ¯å¦åå¨BOM List<TechnologyBom> technologyBoms = technologyBomMapper.selectList(new QueryWrapper<TechnologyBom>() .lambda().in(TechnologyBom::getProductModelId, ids)); if (CollectionUtils.isNotEmpty(technologyBoms)) { throw new RuntimeException("å·²ç»åå¨è¯¥äº§åçBOMæ°æ®"); } return productModelMapper.deleteBatchIds(Arrays.asList(ids)); } src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -3,25 +3,40 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.production.bean.dto.ProductionBomStructureDto; import com.ruoyi.production.bean.vo.ProductionBomStructureVo; import com.ruoyi.production.mapper.ProductionBomStructureMapper; import com.ruoyi.production.mapper.ProductionOperationTaskMapper; import com.ruoyi.production.mapper.ProductionOrderBomMapper; import com.ruoyi.production.mapper.ProductionOrderMapper; import com.ruoyi.production.mapper.ProductionOrderRoutingMapper; import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper; import com.ruoyi.production.mapper.ProductionOrderRoutingOperationParamMapper; import com.ruoyi.production.mapper.ProductionProductMainMapper; import com.ruoyi.production.pojo.ProductionBomStructure; import com.ruoyi.production.pojo.ProductionOperationTask; import com.ruoyi.production.pojo.ProductionOrder; import com.ruoyi.production.pojo.ProductionOrderBom; import com.ruoyi.production.pojo.ProductionOrderRouting; import com.ruoyi.production.pojo.ProductionOrderRoutingOperation; import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam; import com.ruoyi.production.pojo.ProductionProductMain; import com.ruoyi.production.service.ProductionBomStructureService; import com.ruoyi.technology.mapper.TechnologyOperationMapper; import com.ruoyi.technology.mapper.TechnologyOperationParamMapper; import com.ruoyi.technology.mapper.TechnologyParamMapper; import com.ruoyi.technology.pojo.TechnologyOperation; import com.ruoyi.technology.pojo.TechnologyOperationParam; import com.ruoyi.technology.pojo.TechnologyParam; import lombok.RequiredArgsConstructor; import org.springframework.beans.BeanUtils; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import java.math.BigDecimal; import java.time.LocalDate; import java.time.format.DateTimeFormatter; import java.util.*; import java.util.stream.Collectors; @@ -40,8 +55,14 @@ private final ProductionBomStructureMapper productionBomStructureMapper; private final ProductionOrderBomMapper productionOrderBomMapper; private final ProductionOrderMapper productionOrderMapper; private final ProductionOrderRoutingMapper productionOrderRoutingMapper; private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper; private final ProductionOrderRoutingOperationParamMapper productionOrderRoutingOperationParamMapper; private final ProductionOperationTaskMapper productionOperationTaskMapper; private final ProductionProductMainMapper productionProductMainMapper; private final TechnologyOperationMapper technologyOperationMapper; private final TechnologyOperationParamMapper technologyOperationParamMapper; private final TechnologyParamMapper technologyParamMapper; /** * æ ¹æ®BOMæ¥è¯¢å¹¶ç»è£ ç»ææ ã @@ -177,12 +198,17 @@ Wrappers.<ProductionBomStructure>lambdaQuery() .eq(ProductionBomStructure::getProductionOrderBomId, orderBomId) .orderByAsc(ProductionBomStructure::getId)); //忥鿱æ°é syncStructureDemandedQuantity(structureList, orderQuantity); Long rootProductModelId = orderBom.getProductModelId() != null ? orderBom.getProductModelId() : productionOrder.getProductModelId(); //忥ç产工èºè·¯çº¿ syncRoutingOperationsByBom(currentProductionOrderId, productionOrder, orderBom, structureList, rootProductModelId); //忥工å syncTaskPlanQuantity( currentProductionOrderId, structureList, orderQuantity, orderBom.getProductModelId() != null ? orderBom.getProductModelId() : productionOrder.getProductModelId()); rootProductModelId); } private void syncStructureDemandedQuantity(List<ProductionBomStructure> structureList, BigDecimal orderQuantity) { @@ -190,19 +216,22 @@ return; } List<ProductionBomStructure> updateList = new ArrayList<>(); BigDecimal lastProcessDemandedQuantity = orderQuantity; for (ProductionBomStructure structure : structureList) { if (structure == null || structure.getId() == null) { continue; } BigDecimal demandedQuantity = defaultDecimal(structure.getUnitQuantity()).multiply(orderQuantity); if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) { continue; } BigDecimal demandedQuantity = lastProcessDemandedQuantity.multiply(defaultDecimal(structure.getUnitQuantity())); // if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) { // continue; // } ProductionBomStructure update = new ProductionBomStructure(); update.setId(structure.getId()); update.setDemandedQuantity(demandedQuantity); updateList.add(update); structure.setDemandedQuantity(demandedQuantity); lastProcessDemandedQuantity = demandedQuantity; } if (!updateList.isEmpty()) { this.updateBatchById(updateList); @@ -239,7 +268,7 @@ continue; } ProductionOrderRoutingOperation routingOperation = routingOperationMap.get(task.getProductionOrderRoutingOperationId()); if (routingOperation == null || routingOperation.getTechnologyRoutingOperationId() == null) { if (routingOperation == null) { continue; } BigDecimal planQuantity = resolveTaskPlanQuantity( @@ -256,6 +285,333 @@ productionOperationTaskMapper.updateById(update); } } private void syncRoutingOperationsByBom(Long productionOrderId, ProductionOrder productionOrder, ProductionOrderBom orderBom, List<ProductionBomStructure> structureList, Long rootProductModelId) { ProductionOrderRouting orderRouting = getOrCreateOrderRoutingSnapshot(productionOrderId, productionOrder, orderBom, rootProductModelId); List<ProductionOrderRoutingOperation> desiredOperationList = buildDesiredRoutingOperationList(structureList, rootProductModelId); List<ProductionOrderRoutingOperation> existingOperationList = productionOrderRoutingOperationMapper.selectList( Wrappers.<ProductionOrderRoutingOperation>lambdaQuery() .eq(ProductionOrderRoutingOperation::getOrderRoutingId, orderRouting.getId()) .eq(ProductionOrderRoutingOperation::getProductionOrderId, productionOrderId) .orderByAsc(ProductionOrderRoutingOperation::getDragSort) .orderByAsc(ProductionOrderRoutingOperation::getId)); Map<String, Deque<ProductionOrderRoutingOperation>> existingBucketMap = buildExistingRoutingOperationBucketMap(existingOperationList); List<ProductionOrderRoutingOperation> finalOperationList = new ArrayList<>(); for (ProductionOrderRoutingOperation desiredOperation : desiredOperationList) { String bucketKey = buildRoutingOperationBucketKey( desiredOperation.getTechnologyOperationId(), desiredOperation.getProductModelId()); Deque<ProductionOrderRoutingOperation> matchedQueue = existingBucketMap.get(bucketKey); ProductionOrderRoutingOperation matchedOperation = matchedQueue == null ? null : matchedQueue.pollFirst(); if (matchedOperation == null) { matchedOperation = insertRoutingOperationSnapshot(orderRouting.getId(), productionOrderId, desiredOperation); } else { updateRoutingOperationSnapshotIfNecessary(desiredOperation, orderRouting.getId(), productionOrderId, matchedOperation); } finalOperationList.add(matchedOperation); } for (Deque<ProductionOrderRoutingOperation> queue : existingBucketMap.values()) { while (queue != null && !queue.isEmpty()) { removeRoutingOperationSnapshot(queue.pollFirst()); } } syncRoutingOperationTasks(productionOrderId, finalOperationList); } private ProductionOrderRouting getOrCreateOrderRoutingSnapshot(Long productionOrderId, ProductionOrder productionOrder, ProductionOrderBom orderBom, Long rootProductModelId) { ProductionOrderRouting orderRouting = productionOrderRoutingMapper.selectOne( Wrappers.<ProductionOrderRouting>lambdaQuery() .eq(ProductionOrderRouting::getProductionOrderId, productionOrderId) .orderByDesc(ProductionOrderRouting::getId) .last("limit 1")); if (orderRouting == null) { orderRouting = new ProductionOrderRouting(); orderRouting.setProductionOrderId(productionOrderId); orderRouting.setProductModelId(rootProductModelId); orderRouting.setTechnologyRoutingId(productionOrder == null ? null : productionOrder.getTechnologyRoutingId()); orderRouting.setBomId(orderBom == null ? null : orderBom.getBomId()); orderRouting.setOrderBomId(orderBom == null ? null : orderBom.getId()); productionOrderRoutingMapper.insert(orderRouting); return orderRouting; } ProductionOrderRouting update = new ProductionOrderRouting(); update.setId(orderRouting.getId()); boolean changed = false; if (!Objects.equals(orderRouting.getProductModelId(), rootProductModelId)) { update.setProductModelId(rootProductModelId); orderRouting.setProductModelId(rootProductModelId); changed = true; } Long technologyRoutingId = productionOrder == null ? null : productionOrder.getTechnologyRoutingId(); if (!Objects.equals(orderRouting.getTechnologyRoutingId(), technologyRoutingId)) { update.setTechnologyRoutingId(technologyRoutingId); orderRouting.setTechnologyRoutingId(technologyRoutingId); changed = true; } Long bomId = orderBom == null ? null : orderBom.getBomId(); if (!Objects.equals(orderRouting.getBomId(), bomId)) { update.setBomId(bomId); orderRouting.setBomId(bomId); changed = true; } Long orderBomId = orderBom == null ? null : orderBom.getId(); if (!Objects.equals(orderRouting.getOrderBomId(), orderBomId)) { update.setOrderBomId(orderBomId); orderRouting.setOrderBomId(orderBomId); changed = true; } if (changed) { productionOrderRoutingMapper.updateById(update); } return orderRouting; } private List<ProductionOrderRoutingOperation> buildDesiredRoutingOperationList(List<ProductionBomStructure> structureList, Long rootProductModelId) { if (structureList == null || structureList.isEmpty()) { return Collections.emptyList(); } Map<Long, ProductionBomStructure> structureById = structureList.stream() .filter(item -> item != null && item.getId() != null) .collect(Collectors.toMap(ProductionBomStructure::getId, item -> item, (left, right) -> left)); Map<String, ProductionBomStructure> uniqueOperationMap = new LinkedHashMap<>(); for (ProductionBomStructure bomStructure : structureList) { if (bomStructure == null || bomStructure.getTechnologyOperationId() == null) { continue; } Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(bomStructure, structureById), rootProductModelId); uniqueOperationMap.putIfAbsent(buildBomOperationDedupKey(bomStructure, outputProductModelId), bomStructure); } List<ProductionOrderRoutingOperation> desiredOperationList = new ArrayList<>(); int dragSort = 1; for (ProductionBomStructure bomStructure : uniqueOperationMap.values()) { Long outputProductModelId = resolveOutputProductModelId(resolveOperationOutputNode(bomStructure, structureById), rootProductModelId); TechnologyOperation technologyOperation = getTechnologyOperation(bomStructure.getTechnologyOperationId()); ProductionOrderRoutingOperation routingOperation = new ProductionOrderRoutingOperation(); routingOperation.setProductModelId(outputProductModelId); routingOperation.setTechnologyOperationId(bomStructure.getTechnologyOperationId()); routingOperation.setOperationName(technologyOperation == null ? null : technologyOperation.getName()); routingOperation.setIsQuality(technologyOperation == null ? null : technologyOperation.getIsQuality()); routingOperation.setIsProduction(technologyOperation == null ? null : technologyOperation.getIsProduction()); routingOperation.setType(technologyOperation == null ? null : technologyOperation.getType()); routingOperation.setDragSort(dragSort++); desiredOperationList.add(routingOperation); } return desiredOperationList; } private Map<String, Deque<ProductionOrderRoutingOperation>> buildExistingRoutingOperationBucketMap(List<ProductionOrderRoutingOperation> existingOperationList) { Map<String, Deque<ProductionOrderRoutingOperation>> existingBucketMap = new LinkedHashMap<>(); if (existingOperationList == null || existingOperationList.isEmpty()) { return existingBucketMap; } for (ProductionOrderRoutingOperation routingOperation : existingOperationList) { String bucketKey = buildRoutingOperationBucketKey( routingOperation.getTechnologyOperationId(), routingOperation.getProductModelId()); existingBucketMap.computeIfAbsent(bucketKey, key -> new ArrayDeque<>()).addLast(routingOperation); } return existingBucketMap; } private ProductionOrderRoutingOperation insertRoutingOperationSnapshot(Long orderRoutingId, Long productionOrderId, ProductionOrderRoutingOperation desiredOperation) { ProductionOrderRoutingOperation insert = new ProductionOrderRoutingOperation(); insert.setOrderRoutingId(orderRoutingId); insert.setProductionOrderId(productionOrderId); insert.setProductModelId(desiredOperation.getProductModelId()); insert.setTechnologyOperationId(desiredOperation.getTechnologyOperationId()); insert.setOperationName(desiredOperation.getOperationName()); insert.setIsQuality(desiredOperation.getIsQuality()); insert.setIsProduction(desiredOperation.getIsProduction()); insert.setType(desiredOperation.getType()); insert.setDragSort(desiredOperation.getDragSort()); productionOrderRoutingOperationMapper.insert(insert); syncRoutingOperationParams(insert.getId(), productionOrderId, insert.getTechnologyOperationId()); return insert; } private void updateRoutingOperationSnapshotIfNecessary(ProductionOrderRoutingOperation currentOperation, Long orderRoutingId, Long productionOrderId, ProductionOrderRoutingOperation desiredOperation) { if (currentOperation == null || currentOperation.getId() == null) { return; } ProductionOrderRoutingOperation update = new ProductionOrderRoutingOperation(); update.setId(currentOperation.getId()); boolean changed = false; if (!Objects.equals(currentOperation.getOrderRoutingId(), orderRoutingId)) { update.setOrderRoutingId(orderRoutingId); currentOperation.setOrderRoutingId(orderRoutingId); changed = true; } if (!Objects.equals(currentOperation.getProductionOrderId(), productionOrderId)) { update.setProductionOrderId(productionOrderId); currentOperation.setProductionOrderId(productionOrderId); changed = true; } if (!Objects.equals(currentOperation.getProductModelId(), desiredOperation.getProductModelId())) { update.setProductModelId(desiredOperation.getProductModelId()); currentOperation.setProductModelId(desiredOperation.getProductModelId()); changed = true; } if (!Objects.equals(currentOperation.getTechnologyOperationId(), desiredOperation.getTechnologyOperationId())) { update.setTechnologyOperationId(desiredOperation.getTechnologyOperationId()); currentOperation.setTechnologyOperationId(desiredOperation.getTechnologyOperationId()); changed = true; } if (!Objects.equals(currentOperation.getOperationName(), desiredOperation.getOperationName())) { update.setOperationName(desiredOperation.getOperationName()); currentOperation.setOperationName(desiredOperation.getOperationName()); changed = true; } if (!Objects.equals(currentOperation.getIsQuality(), desiredOperation.getIsQuality())) { update.setIsQuality(desiredOperation.getIsQuality()); currentOperation.setIsQuality(desiredOperation.getIsQuality()); changed = true; } if (!Objects.equals(currentOperation.getIsProduction(), desiredOperation.getIsProduction())) { update.setIsProduction(desiredOperation.getIsProduction()); currentOperation.setIsProduction(desiredOperation.getIsProduction()); changed = true; } if (!Objects.equals(currentOperation.getType(), desiredOperation.getType())) { update.setType(desiredOperation.getType()); currentOperation.setType(desiredOperation.getType()); changed = true; } if (!Objects.equals(currentOperation.getDragSort(), desiredOperation.getDragSort())) { update.setDragSort(desiredOperation.getDragSort()); currentOperation.setDragSort(desiredOperation.getDragSort()); changed = true; } if (changed) { productionOrderRoutingOperationMapper.updateById(update); } } private void removeRoutingOperationSnapshot(ProductionOrderRoutingOperation routingOperation) { if (routingOperation == null || routingOperation.getId() == null) { return; } ProductionOperationTask task = productionOperationTaskMapper.selectOne( Wrappers.<ProductionOperationTask>lambdaQuery() .eq(ProductionOperationTask::getProductionOrderRoutingOperationId, routingOperation.getId()) .last("limit 1")); if (task != null) { validateTaskCanRemove(task); productionOperationTaskMapper.deleteById(task.getId()); } productionOrderRoutingOperationParamMapper.delete( Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery() .eq(ProductionOrderRoutingOperationParam::getProductionOrderRoutingOperationId, routingOperation.getId())); productionOrderRoutingOperationMapper.deleteById(routingOperation.getId()); } private void syncRoutingOperationTasks(Long productionOrderId, List<ProductionOrderRoutingOperation> routingOperationList) { if (routingOperationList == null || routingOperationList.isEmpty()) { return; } List<Long> routingOperationIdList = routingOperationList.stream() .map(ProductionOrderRoutingOperation::getId) .filter(Objects::nonNull) .collect(Collectors.toList()); if (routingOperationIdList.isEmpty()) { return; } Map<Long, ProductionOperationTask> taskByRoutingOperationId = productionOperationTaskMapper.selectList( Wrappers.<ProductionOperationTask>lambdaQuery() .in(ProductionOperationTask::getProductionOrderRoutingOperationId, routingOperationIdList) .orderByAsc(ProductionOperationTask::getId)) .stream() .filter(item -> item != null && item.getProductionOrderRoutingOperationId() != null) .collect(Collectors.toMap( ProductionOperationTask::getProductionOrderRoutingOperationId, item -> item, (left, right) -> left, LinkedHashMap::new)); for (int i = 0; i < routingOperationList.size(); i++) { ProductionOrderRoutingOperation routingOperation = routingOperationList.get(i); if (routingOperation == null || routingOperation.getId() == null) { continue; } boolean shouldHaveTask = i == routingOperationList.size() - 1 || Boolean.TRUE.equals(routingOperation.getIsProduction()); ProductionOperationTask existingTask = taskByRoutingOperationId.get(routingOperation.getId()); if (shouldHaveTask) { if (existingTask == null) { ProductionOperationTask task = new ProductionOperationTask(); task.setProductionOrderId(productionOrderId); task.setProductionOrderRoutingOperationId(routingOperation.getId()); task.setPlanQuantity(BigDecimal.ZERO); task.setCompleteQuantity(BigDecimal.ZERO); task.setWorkOrderNo(generateNextTaskNo()); task.setStatus(2); productionOperationTaskMapper.insert(task); } continue; } if (existingTask != null) { validateTaskCanRemove(existingTask); productionOperationTaskMapper.deleteById(existingTask.getId()); } } } private void validateTaskCanRemove(ProductionOperationTask task) { if (task == null || task.getId() == null) { return; } if (defaultDecimal(task.getCompleteQuantity()).compareTo(BigDecimal.ZERO) > 0) { throw new ServiceException("å·¥åºå·²äº§çæ¥å·¥è®°å½ï¼æ æ³æ ¹æ® BOM åæ´å é¤å¯¹åºå·¥åºå¿«ç §"); } long reportCount = productionProductMainMapper.selectCount( Wrappers.<ProductionProductMain>lambdaQuery() .eq(ProductionProductMain::getProductionOperationTaskId, task.getId())); if (reportCount > 0) { throw new ServiceException("å·¥åºå·²äº§çæ¥å·¥è®°å½ï¼æ æ³æ ¹æ® BOM åæ´å é¤å¯¹åºå·¥å"); } } private void syncRoutingOperationParams(Long routingOperationId, Long productionOrderId, Long technologyOperationId) { if (routingOperationId == null || technologyOperationId == null) { return; } List<TechnologyOperationParam> operationParamList = technologyOperationParamMapper.selectList( Wrappers.<TechnologyOperationParam>lambdaQuery() .eq(TechnologyOperationParam::getTechnologyOperationId, technologyOperationId) .orderByAsc(TechnologyOperationParam::getId)); for (TechnologyOperationParam operationParam : operationParamList) { TechnologyParam technologyParam = technologyParamMapper.selectById(operationParam.getTechnologyParamId()); if (technologyParam == null) { continue; } ProductionOrderRoutingOperationParam snapshot = new ProductionOrderRoutingOperationParam(); snapshot.setProductionOrderId(productionOrderId); snapshot.setProductionOrderRoutingOperationId(routingOperationId); snapshot.setTechnologyOperationId(operationParam.getTechnologyOperationId()); snapshot.setTechnologyOperationParamId(operationParam.getId()); snapshot.setParamId(technologyParam.getId()); snapshot.setParamCode(technologyParam.getParamCode()); snapshot.setParamName(technologyParam.getParamName()); snapshot.setParamType(technologyParam.getParamType()); snapshot.setParamFormat(technologyParam.getParamFormat()); snapshot.setUnit(technologyParam.getUnit()); snapshot.setIsRequired(technologyParam.getIsRequired()); snapshot.setRemark(technologyParam.getRemark()); snapshot.setStandardValue(operationParam.getStandardValue()); productionOrderRoutingOperationParamMapper.insert(snapshot); } } private Map<String, BigDecimal> buildOperationDemandedQuantityMap(List<ProductionBomStructure> structureList, Long rootProductModelId) { if (structureList == null || structureList.isEmpty()) { @@ -289,6 +645,7 @@ } return demandedQuantityMap; } private BigDecimal resolveTaskPlanQuantity(ProductionOrderRoutingOperation routingOperation, Map<String, BigDecimal> demandedQuantityMap, BigDecimal orderQuantity, @@ -310,6 +667,16 @@ return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId); } private String buildRoutingOperationBucketKey(Long operationId, Long outputProductModelId) { return String.valueOf(operationId) + "#" + String.valueOf(outputProductModelId); } private String buildBomOperationDedupKey(ProductionBomStructure bomStructure, Long outputProductModelId) { Long operationId = bomStructure == null ? null : bomStructure.getTechnologyOperationId(); Long parentId = bomStructure == null ? null : bomStructure.getParentId(); return operationId + "#" + outputProductModelId + "#" + parentId; } private String buildOperationOutputNodeKey(Long operationId, Long outputNodeId, Long outputProductModelId) { return String.valueOf(operationId) + "#" + String.valueOf(outputNodeId) + "#" + String.valueOf(outputProductModelId); } @@ -326,6 +693,7 @@ ProductionBomStructure parent = structureById.get(bomStructure.getParentId()); return parent != null ? parent : bomStructure; } private Long resolveOutputProductModelId(ProductionBomStructure outputNode, Long rootProductModelId) { if (outputNode == null) { @@ -334,6 +702,31 @@ return outputNode.getProductModelId() != null ? outputNode.getProductModelId() : rootProductModelId; } private TechnologyOperation getTechnologyOperation(Long technologyOperationId) { if (technologyOperationId == null) { return null; } return technologyOperationMapper.selectById(technologyOperationId); } private String generateNextTaskNo() { String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd")); ProductionOperationTask latestTask = productionOperationTaskMapper.selectOne( Wrappers.<ProductionOperationTask>lambdaQuery() .likeRight(ProductionOperationTask::getWorkOrderNo, "GD" + datePrefix) .orderByDesc(ProductionOperationTask::getWorkOrderNo) .last("limit 1")); int sequenceNumber = 1; if (latestTask != null && latestTask.getWorkOrderNo() != null && latestTask.getWorkOrderNo().startsWith("GD" + datePrefix)) { try { sequenceNumber = Integer.parseInt(latestTask.getWorkOrderNo().substring(("GD" + datePrefix).length())) + 1; } catch (NumberFormatException ignored) { sequenceNumber = 1; } } return "GD" + String.format("%s%03d", datePrefix, sequenceNumber); } private BigDecimal defaultDecimal(BigDecimal value) { return value == null ? BigDecimal.ZERO : value; } 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/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -437,6 +437,7 @@ productionOrderBomMapper.insert(orderBom); Map<Long, Long> idMap = new HashMap<>(); BigDecimal lastProcessDemandedQuantity = orderQuantity; for (TechnologyBomStructure source : structureList) { // åèç¹ parentId éè¦æ å°ææ°å¿«ç §èç¹ idï¼æè½ä¿çåå§ BOM å±çº§ã ProductionBomStructure target = new ProductionBomStructure(); @@ -446,10 +447,11 @@ target.setProductModelId(source.getProductModelId()); target.setTechnologyOperationId(source.getOperationId()); target.setUnitQuantity(source.getUnitQuantity()); target.setDemandedQuantity(source.getUnitQuantity().multiply(orderQuantity)); target.setDemandedQuantity(lastProcessDemandedQuantity.multiply(source.getUnitQuantity())); target.setUnit(source.getUnit()); productionBomStructureMapper.insert(target); idMap.put(source.getId(), target.getId()); lastProcessDemandedQuantity = target.getDemandedQuantity(); } return orderBom; } src/main/java/com/ruoyi/purchase/pojo/SalesLedgerProductTemplate.java
@@ -69,10 +69,10 @@ private Integer type; @Schema(description = "产åid") private Integer productId; private Long productId; @Schema(description = "åå·id") private Integer productModelId; private Long productModelId; private String register; src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -96,9 +96,17 @@ /** * æ°é */ @Excel(name = "æ°é") @Excel(name = "æ»æ°é") private BigDecimal quantity; @Excel(name = "åæ ¼æ°é") @TableField("qualified_quantity") private BigDecimal qualifiedQuantity; @Excel(name = "ä¸åæ ¼æ°é") @TableField("unqualified_quantity") private BigDecimal unqualifiedQuantity; /** * æ£æµåä½ */ src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -1,6 +1,7 @@ package com.ruoyi.quality.service.impl; import cn.hutool.core.lang.Assert; import com.baomidou.mybatisplus.core.metadata.IPage; import com.baomidou.mybatisplus.core.toolkit.ObjectUtils; import com.baomidou.mybatisplus.core.toolkit.Wrappers; @@ -35,6 +36,7 @@ import java.io.InputStream; import java.io.OutputStream; import java.math.BigDecimal; import java.net.URLEncoder; import java.util.Arrays; import java.util.HashMap; @@ -92,17 +94,10 @@ if (ObjectUtils.isNull(qualityInspect.getCheckResult())) { throw new RuntimeException("请å 夿æ¯å¦åæ ¼"); } /*夿ä¸åæ ¼*/ if (qualityInspect.getCheckResult().equals("ä¸åæ ¼")) { QualityUnqualified qualityUnqualified = new QualityUnqualified(); BeanUtils.copyProperties(qualityInspect, qualityUnqualified); qualityUnqualified.setInspectState(0);//å¾ å¤ç List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId())); String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(",")); qualityUnqualified.setDefectivePhenomena(text + "è¿äºææ ä¸åå¨ä¸åæ ¼");//ä¸åæ ¼ç°è±¡ qualityUnqualified.setInspectId(qualityInspect.getId()); qualityUnqualifiedMapper.insert(qualityUnqualified); } else { // åºååæ ¼æ°é以åä¸åæ ¼å¤çè¿è¡å¯¹åºçå¤ç Assert.isTrue(qualityInspect.getQuantity().compareTo(qualityInspect.getQualifiedQuantity().add(qualityInspect.getUnqualifiedQuantity())) == 0,"è¯·æ£æ¥åæ ¼æ°éåä¸åæ ¼æ°éï¼éè¦åæ ¼æ°é+ä¸åæ ¼æ°é䏿»æ°ä¿æä¸è´"); if(qualityInspect.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0){ //åæ ¼ç´æ¥å ¥åº // stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId()); //ä» æ·»å å ¥åºè®°å½ @@ -114,13 +109,25 @@ } stockInventoryDto.setRecordId(qualityInspect.getId()); stockInventoryDto.setProductModelId(qualityInspect.getProductModelId()); stockInventoryDto.setQualitity(qualityInspect.getQuantity()); stockInventoryDto.setQualitity(qualityInspect.getQualifiedQuantity()); stockInventoryDto.setBatchNo(resolveProductionBatchNo( qualityInspect.getProductMainId(), qualityInspect.getId(), qualityInspect.getProductModelId())); stockInventoryService.addStockInRecordOnly(stockInventoryDto); } if(qualityInspect.getUnqualifiedQuantity().compareTo(BigDecimal.ZERO) > 0){ QualityUnqualified qualityUnqualified = new QualityUnqualified(); BeanUtils.copyProperties(qualityInspect, qualityUnqualified); qualityUnqualified.setInspectState(0);//å¾ å¤ç qualityUnqualified.setQuantity(qualityInspect.getUnqualifiedQuantity()); List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId())); String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(",")); qualityUnqualified.setDefectivePhenomena(text + "è¿äºææ ä¸åå¨ä¸åæ ¼");//ä¸åæ ¼ç°è±¡ qualityUnqualified.setInspectId(qualityInspect.getId()); qualityUnqualifiedMapper.insert(qualityUnqualified); } qualityInspect.setInspectState(1);//å·²æäº¤ return qualityInspectMapper.updateById(qualityInspect); } 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,