Merge remote-tracking branch 'origin/dev_New_pro' into dev_山西_晋和园_pro
已添加14个文件
已重命名23个文件
已修改30个文件
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # å¶é æºè½å©æå端èè°ææ¡£ï¼`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. å¢å åæ®µä¸æåå±ç¤ºçº¦æï¼é¿å
è±æåæ®µå¯¹ä¸å¡ç¨æ·ç´åºã |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | # éå®å©æå端èè°ææ¡£ï¼`/sales-ai`ï¼ |
| | | > æ´æ°æ¶é´ï¼2026-05-18 |
| | | > éç¨æ¨¡åï¼å®¢æ·æ¡£æ¡ï¼ç§æµ·/å
¬æµ·ï¼ãé宿¥ä»·ãéå®å°è´¦ãéå®éè´§ã客æ·å¾æ¥ãåè´§å°è´¦ãææ ç»è®¡ |
| | | > éç¹è½åï¼å®¢æ·æµå¤±é£é©åæã忬¾ä¸æ¥ä»·çç¥å»ºè®® |
| | | |
| | | ## 1. æ¥å£æ»è§ |
| | | |
| | | 1. æµå¼å¯¹è¯ï¼`POST /sales-ai/chat` |
| | | 2. ä¼è¯å表ï¼`GET /sales-ai/history/sessions` |
| | | 3. ä¼è¯æ¶æ¯ï¼`GET /sales-ai/history/messages/{memoryId}` |
| | | 4. å é¤ä¼è¯ï¼`DELETE /sales-ai/history/{memoryId}` |
| | | |
| | | 说æï¼ |
| | | - `/chat` è¿å `text/stream;charset=utf-8`ï¼SSE ææ¬æµï¼ã |
| | | - å½ä¸å·¥å
·æ¶ï¼æç»å
容为 **JSON å符串**ï¼é `AjaxResult`ï¼ã |
| | | - æªå½ä¸å·¥å
·æ¶ï¼è¿åæ®éä¸æææ¬ã |
| | | |
| | | ## 2. å¯¹è¯æ¥å£ |
| | | |
| | | ### 2.1 è¯·æ± |
| | | |
| | | ```http |
| | | POST /sales-ai/chat |
| | | Content-Type: application/json |
| | | ``` |
| | | |
| | | ```json |
| | | { |
| | | "memoryId": "sales-ai-001", |
| | | "message": "帮æåå®¢æ·æµå¤±é£é©åæï¼è¿90天ï¼å10æ¡" |
| | | } |
| | | ``` |
| | | |
| | | åæ®µè¯´æï¼ |
| | | |
| | | | åæ®µ | ç±»å | å¿
å¡« | 说æ | |
| | | | --- | --- | --- | --- | |
| | | | `memoryId` | string | æ¯ | ä¼è¯ IDï¼å端çæå¹¶å¤ç¨ | |
| | | | `message` | string | æ¯ | ç¨æ·è¾å
¥ | |
| | | |
| | | ### 2.2 è¿åå¤ç |
| | | |
| | | å端建议æµç¨ï¼ |
| | | 1. å
ææµæ¼æ¥å®æ´ææ¬ `fullText`ã |
| | | 2. å°è¯ `JSON.parse(fullText)`ï¼ |
| | | - æåï¼æ `type` è·¯ç±å°ç»æåç»ä»¶ã |
| | | - å¤±è´¥ï¼ææ®éèå¤©ææ¬å±ç¤ºã |
| | | |
| | | ## 3. ç»æåååºåè®® |
| | | |
| | | ### 3.1 éç¨ç»æ |
| | | |
| | | ```json |
| | | { |
| | | "success": true, |
| | | "type": "sales_dashboard", |
| | | "description": "å·²è¿åé宿æ ç»è®¡", |
| | | "summary": {}, |
| | | "data": {}, |
| | | "charts": {} |
| | | } |
| | | ``` |
| | | |
| | | ### 3.2 `type` æä¸¾ |
| | | |
| | | | type | åºæ¯ | |
| | | | --- | --- | |
| | | | `sales_customer_profile_list` | å®¢æ·æ¡£æ¡ï¼ç§æµ·/å
¬æµ·ï¼ | |
| | | | `sales_quotation_list` | é宿¥ä»· | |
| | | | `sales_ledger_list` | éå®å°è´¦ | |
| | | | `sales_return_list` | éå®éè´§ | |
| | | | `sales_customer_interaction_list` | 客æ·å¾æ¥ï¼åæ¬¾ï¼ | |
| | | | `sales_shipping_list` | åè´§å°è´¦ | |
| | | | `sales_dashboard` | ææ ç»è®¡ | |
| | | | `sales_customer_churn_risk` | å®¢æ·æµå¤±é£é©åæ | |
| | | | `sales_collection_quote_strategy` | 忬¾ä¸æ¥ä»·çç¥å»ºè®® | |
| | | |
| | | ## 4. èåè½åæ å°ï¼å¯¹åºè¥é管çï¼ |
| | | |
| | | 1. å®¢æ·æ¡£æ¡ï¼ç§æµ·ï¼ï¼ç¤ºä¾æé® `æ¥è¯¢ç§æµ·å®¢æ·æ¡£æ¡å10æ¡` |
| | | 2. å®¢æ·æ¡£æ¡ï¼å
¬æµ·ï¼ï¼ç¤ºä¾æé® `æ¥è¯¢å
¬æµ·å®¢æ·æ¡£æ¡` |
| | | 3. é宿¥ä»·ï¼ç¤ºä¾æé® `æ¥è¯¢æ¬æé宿¥ä»·` |
| | | 4. éå®å°è´¦ï¼ç¤ºä¾æé® `æ¥è¯¢æ¬æéå®å°è´¦` |
| | | 5. éå®éè´§ï¼ç¤ºä¾æé® `æ¥è¯¢è¿30天éå®éè´§` |
| | | 6. 客æ·å¾æ¥ï¼ç¤ºä¾æé® `æ¥è¯¢è¿30天客æ·åæ¬¾å¾æ¥` |
| | | 7. åè´§å°è´¦ï¼ç¤ºä¾æé® `æ¥è¯¢æ¬æåè´§å°è´¦` |
| | | 8. ææ ç»è®¡ï¼ç¤ºä¾æé® `æ¥çé宿æ ç»è®¡` |
| | | |
| | | ## 5. éç¹è½åèè° |
| | | |
| | | ### 5.1 å®¢æ·æµå¤±é£é©åæï¼`sales_customer_churn_risk`ï¼ |
| | | |
| | | æ°æ®ä½ç½®ï¼ |
| | | - å表ï¼`data.items` |
| | | - æ±æ»ï¼`summary.highRiskCount / mediumRiskCount / lowRiskCount` |
| | | - å¾è¡¨ï¼`charts.riskLevelPieOption`ã`charts.riskScoreBarOption` |
| | | |
| | | å项常ç¨åæ®µï¼ |
| | | - `customerName` |
| | | - `riskLevel`ï¼`high`/`medium`/`low`ï¼ |
| | | - `riskScore`ï¼0-100ï¼ |
| | | - `pendingAmount` |
| | | - `pendingRate` |
| | | - `daysSinceLastOrder` |
| | | - `riskReasons`ï¼å符串æ°ç»ï¼ |
| | | |
| | | ### 5.2 忬¾ä¸æ¥ä»·çç¥å»ºè®®ï¼`sales_collection_quote_strategy`ï¼ |
| | | |
| | | æ°æ®ä½ç½®ï¼ |
| | | - çç¥å¡ï¼`data.items` |
| | | - æ±æ»ï¼`summary.highPriorityCount / mediumPriorityCount / lowPriorityCount` |
| | | - å¾è¡¨ï¼`charts.pendingAmountBarOption`ã`charts.priorityPieOption` |
| | | |
| | | å项常ç¨åæ®µï¼ |
| | | - `customerName` |
| | | - `priority`ï¼`high`/`medium`/`low`ï¼ |
| | | - `pendingAmount` |
| | | - `quoteConversionRate` |
| | | - `collectionStrategy` |
| | | - `quotationStrategy` |
| | | - `nextAction` |
| | | |
| | | ## 6. ææ ç»è®¡èè°ï¼`sales_dashboard`ï¼ |
| | | |
| | | å
³é®åæ®µï¼ |
| | | - `summary.contractAmountTotal` |
| | | - `summary.receivedAmountTotal` |
| | | - `summary.pendingAmountTotal` |
| | | - `summary.shipRate` |
| | | |
| | | å¾è¡¨å段ï¼å¯ç´æ¥ç» EChartsï¼ï¼ |
| | | - `charts.amountBarOption` |
| | | - `charts.shippingPieOption` |
| | | - `charts.customerTopBarOption` |
| | | - `charts.contractTrendLineOption` |
| | | |
| | | éå æ°æ®ï¼ |
| | | - `data.topCustomers` |
| | | - `data.contractTrend` |
| | | |
| | | ## 7. ä¼è¯å岿¥å£ |
| | | |
| | | ### 7.1 ä¼è¯å表 |
| | | |
| | | ```http |
| | | GET /sales-ai/history/sessions |
| | | ``` |
| | | |
| | | è¿å `AjaxResult.data` åæ®µï¼ |
| | | - `memoryId` |
| | | - `title` |
| | | - `lastMessage` |
| | | - `messageCount` |
| | | - `lastChatTime` |
| | | |
| | | ### 7.2 ä¼è¯æ¶æ¯ |
| | | |
| | | ```http |
| | | GET /sales-ai/history/messages/{memoryId} |
| | | ``` |
| | | |
| | | è¿å `AjaxResult.data` åæ®µï¼ |
| | | - `role`ï¼`user` / `assistant` / `system` / `tool` |
| | | - `content` |
| | | - `filePaths`ï¼å½åéå®å©ææªä½¿ç¨æä»¶åæï¼å¯å¿½ç¥ï¼ |
| | | |
| | | ### 7.3 å é¤ä¼è¯ |
| | | |
| | | ```http |
| | | DELETE /sales-ai/history/{memoryId} |
| | | ``` |
| | | |
| | | è¿åæ å `AjaxResult`ã |
| | | |
| | | ## 8. å端æ¥å
¥çº¦æ |
| | | |
| | | 1. æ°å¢å©æé
ç½®æ¶ï¼`assistantRegistry` å¿
须注å `sales`ï¼æä½ æ¹çº¦å® keyï¼ï¼å¹¶æå `apiBase = /sales-ai`ã |
| | | 2. ç»æåæ¸²æå¿
é¡»åºäº `type` ååï¼ä¸è¦ä»
é å
³é®è¯ã |
| | | 3. è天渲æéä¿çâææ¬å
åºâï¼é¿å
JSON è§£æå¤±è´¥æ¶é¡µé¢ç©ºç½ã |
| | | 4. ä¸å¡å±ç¤ºåæ®µå»ºè®®ä¸æåï¼ä¸ç´æ¥å±ç¤ºè±æå段 keyã |
| | | |
| | | ## 9. èè°éªæ¶æ¸
å |
| | | |
| | | 1. è½æ£å¸¸æµå¼æ¥æ¶ `/sales-ai/chat` ååºå¹¶æ¼æ¥ææ¬ã |
| | | 2. è½æ `type` æ£ç¡®æ¸²æ 9 ç±»ç»æåç»æã |
| | | 3. è½æ£ç¡®å±ç¤ºâå®¢æ·æµå¤±é£é©åæâåâ忬¾ä¸æ¥ä»·çç¥å»ºè®®â两个éç¹åºæ¯ã |
| | | 4. ä¼è¯å表ãä¼è¯æ¶æ¯ãå é¤ä¼è¯å
¨é¾è·¯å¯ç¨ã |
| | | 5. `memoryId` å¤ç¨åå¯åçåå²ï¼ä¸ä¼ä¸²ä¼è¯ã |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/AccountSubjectDto.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.dto; |
| | | package com.ruoyi.account.bean.dto.financial; |
| | | |
| | | import com.ruoyi.account.pojo.AccountSubject; |
| | | import com.ruoyi.account.pojo.financial.AccountSubject; |
| | | import lombok.Data; |
| | | import lombok.EqualsAndHashCode; |
| | | |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/AccountSubjectImportDto.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.dto; |
| | | package com.ruoyi.account.bean.dto.financial; |
| | | |
| | | import com.ruoyi.framework.aspectj.lang.annotation.Excel; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.dto; |
| | | package com.ruoyi.account.bean.dto.purchase; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.dto; |
| | | package com.ruoyi.account.bean.dto.purchase; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/SalesOutboundDto.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.dto; |
| | | package com.ruoyi.account.bean.dto.sales; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/dto/SalesReturnDto.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.dto; |
| | | package com.ruoyi.account.bean.dto.sales; |
| | | |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| | | import io.swagger.v3.oas.annotations.media.Schema; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.vo; |
| | | package com.ruoyi.account.bean.vo.financial; |
| | | |
| | | import com.ruoyi.account.pojo.AccountSubject; |
| | | import com.ruoyi.account.pojo.financial.AccountSubject; |
| | | import lombok.Data; |
| | | |
| | | import java.util.ArrayList; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/PurchaseInboundVo.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.vo; |
| | | package com.ruoyi.account.bean.vo.purchase; |
| | | |
| | | import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/PurchaseReturnVo.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.vo; |
| | | package com.ruoyi.account.bean.vo.purchase; |
| | | |
| | | import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/SalesOutboundVo.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.vo; |
| | | package com.ruoyi.account.bean.vo.sales; |
| | | |
| | | import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/bean/vo/SalesReturnVo.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.bean.vo; |
| | | package com.ruoyi.account.bean.vo.sales; |
| | | |
| | | import com.alibaba.excel.annotation.ExcelIgnoreUnannotated; |
| | | import com.fasterxml.jackson.annotation.JsonFormat; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/controller/AccountSubjectController.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.controller; |
| | | package com.ruoyi.account.controller.financial; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.AccountSubjectDto; |
| | | import com.ruoyi.account.bean.vo.AccountSubjectVo; |
| | | import com.ruoyi.account.service.AccountSubjectService; |
| | | import com.ruoyi.account.bean.dto.financial.AccountSubjectDto; |
| | | import com.ruoyi.account.bean.vo.financial.AccountSubjectVo; |
| | | import com.ruoyi.account.service.purchase.AccountSubjectService; |
| | | import com.ruoyi.framework.aspectj.lang.annotation.Log; |
| | | import com.ruoyi.framework.aspectj.lang.enums.BusinessType; |
| | | import com.ruoyi.framework.web.domain.R; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/controller/AccounPurchaseController.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.controller; |
| | | package com.ruoyi.account.controller.purchase; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.dto.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.PurchaseInboundVo; |
| | | import com.ruoyi.account.bean.vo.PurchaseReturnVo; |
| | | import com.ruoyi.account.service.AccountPurchaseService; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo; |
| | | import com.ruoyi.account.service.financial.AccountPurchaseService; |
| | | import com.ruoyi.framework.aspectj.lang.annotation.Log; |
| | | import com.ruoyi.framework.aspectj.lang.enums.BusinessType; |
| | | import com.ruoyi.framework.web.domain.R; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/controller/AccountSalesController.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.controller; |
| | | package com.ruoyi.account.controller.sales; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.dto.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.SalesOutboundVo; |
| | | import com.ruoyi.account.bean.vo.SalesReturnVo; |
| | | import com.ruoyi.account.service.AccountSalesService; |
| | | import com.ruoyi.account.bean.dto.sales.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.dto.sales.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.sales.SalesOutboundVo; |
| | | import com.ruoyi.account.bean.vo.sales.SalesReturnVo; |
| | | import com.ruoyi.account.service.sales.AccountSalesService; |
| | | import com.ruoyi.framework.aspectj.lang.annotation.Log; |
| | | import com.ruoyi.framework.aspectj.lang.enums.BusinessType; |
| | | import com.ruoyi.framework.web.domain.R; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.mapper; |
| | | package com.ruoyi.account.mapper.financial; |
| | | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.ruoyi.account.pojo.AccountSubject; |
| | | import com.ruoyi.account.pojo.financial.AccountSubject; |
| | | import org.apache.ibatis.annotations.Mapper; |
| | | import org.apache.ibatis.annotations.Param; |
| | | |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/pojo/AccountSubject.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.pojo; |
| | | package com.ruoyi.account.pojo.financial; |
| | | |
| | | import com.baomidou.mybatisplus.annotation.FieldFill; |
| | | import com.baomidou.mybatisplus.annotation.IdType; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/AccountPurchaseService.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.service; |
| | | package com.ruoyi.account.service.financial; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.dto.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.PurchaseInboundVo; |
| | | import com.ruoyi.account.bean.vo.PurchaseReturnVo; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | |
| | | /** |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.service.impl; |
| | | package com.ruoyi.account.service.impl.financial; |
| | | |
| | | import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.account.bean.dto.AccountSubjectDto; |
| | | import com.ruoyi.account.bean.dto.AccountSubjectImportDto; |
| | | import com.ruoyi.account.bean.vo.AccountSubjectVo; |
| | | import com.ruoyi.account.mapper.AccountSubjectMapper; |
| | | import com.ruoyi.account.pojo.AccountSubject; |
| | | import com.ruoyi.account.service.AccountSubjectService; |
| | | import com.ruoyi.account.bean.dto.financial.AccountSubjectDto; |
| | | import com.ruoyi.account.bean.dto.financial.AccountSubjectImportDto; |
| | | import com.ruoyi.account.bean.vo.financial.AccountSubjectVo; |
| | | import com.ruoyi.account.mapper.financial.AccountSubjectMapper; |
| | | import com.ruoyi.account.pojo.financial.AccountSubject; |
| | | import com.ruoyi.account.service.purchase.AccountSubjectService; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.common.utils.StringUtils; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | |
| | | import com.ruoyi.account.bean.dto.financial.FinVoucherEntryDto; |
| | | import com.ruoyi.account.bean.dto.financial.FinVoucherPageDto; |
| | | import com.ruoyi.account.bean.vo.financial.FinVoucherDetailVo; |
| | | import com.ruoyi.account.mapper.AccountSubjectMapper; |
| | | import com.ruoyi.account.mapper.financial.AccountSubjectMapper; |
| | | import com.ruoyi.account.mapper.financial.FinVoucherEntryMapper; |
| | | import com.ruoyi.account.mapper.financial.FinVoucherMapper; |
| | | import com.ruoyi.account.pojo.AccountSubject; |
| | | import com.ruoyi.account.pojo.financial.AccountSubject; |
| | | import com.ruoyi.account.pojo.financial.FinVoucher; |
| | | import com.ruoyi.account.pojo.financial.FinVoucherEntry; |
| | | import com.ruoyi.account.service.financial.FinVoucherService; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/impl/AccountPurchaseServiceImpl.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.service.impl; |
| | | package com.ruoyi.account.service.impl.purchase; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.dto.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.PurchaseInboundVo; |
| | | import com.ruoyi.account.bean.vo.PurchaseReturnVo; |
| | | import com.ruoyi.account.service.AccountPurchaseService; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo; |
| | | import com.ruoyi.account.service.financial.AccountPurchaseService; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | | import com.ruoyi.purchase.mapper.PurchaseReturnOrdersMapper; |
| | | import com.ruoyi.stock.mapper.StockInRecordMapper; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/impl/AccountSalesServiceImpl.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.service.impl; |
| | | package com.ruoyi.account.service.impl.sales; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.dto.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.SalesOutboundVo; |
| | | import com.ruoyi.account.bean.vo.SalesReturnVo; |
| | | import com.ruoyi.account.service.AccountSalesService; |
| | | import com.ruoyi.account.bean.dto.sales.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.dto.sales.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.sales.SalesOutboundVo; |
| | | import com.ruoyi.account.bean.vo.sales.SalesReturnVo; |
| | | import com.ruoyi.account.service.sales.AccountSalesService; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | | import com.ruoyi.procurementrecord.mapper.ReturnManagementMapper; |
| | | import com.ruoyi.stock.mapper.StockOutRecordMapper; |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/AccountSubjectService.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.service; |
| | | package com.ruoyi.account.service.purchase; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.AccountSubjectDto; |
| | | import com.ruoyi.account.bean.vo.AccountSubjectVo; |
| | | import com.ruoyi.account.pojo.AccountSubject; |
| | | import com.ruoyi.account.bean.dto.financial.AccountSubjectDto; |
| | | import com.ruoyi.account.bean.vo.financial.AccountSubjectVo; |
| | | import com.ruoyi.account.pojo.financial.AccountSubject; |
| | | import com.baomidou.mybatisplus.extension.service.IService; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | |
| ÎļþÃû´Ó src/main/java/com/ruoyi/account/service/AccountSalesService.java ÐÞ¸Ä |
| | |
| | | package com.ruoyi.account.service; |
| | | package com.ruoyi.account.service.sales; |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.dto.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.SalesOutboundVo; |
| | | import com.ruoyi.account.bean.vo.SalesReturnVo; |
| | | import com.ruoyi.account.bean.dto.sales.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.dto.sales.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.sales.SalesOutboundVo; |
| | | import com.ruoyi.account.bean.vo.sales.SalesReturnVo; |
| | | import jakarta.servlet.http.HttpServletResponse; |
| | | |
| | | /** |
| | |
| | | @Component |
| | | public class ApproveTodoIntentExecutor { |
| | | |
| | | private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{8,}\\b"); |
| | | private static final Pattern LIMIT_PATTERN = Pattern.compile("(å|æè¿)?(\\d{1,2})æ¡"); |
| | | private static final Pattern APPROVE_ID_BY_LABEL_PATTERN = Pattern.compile("(æµç¨ç¼å·|æµç¨å·|æµç¨ID|审æ¹ç¼å·|ç¼å·)\\s*[:ï¼]?\\s*([A-Za-z0-9_-]{2,64})"); |
| | | private static final Pattern APPROVE_ID_PATTERN = Pattern.compile("\\b[A-Za-z]*\\d{6,}[A-Za-z0-9_-]*\\b"); |
| | | private static final Pattern LIMIT_PATTERN = Pattern.compile("(å|æè¿)?\\s*(\\d{1,2})\\s*æ¡"); |
| | | private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})"); |
| | | private static final Pattern NUMBER_PATTERN = Pattern.compile("(\\d+(?:\\.\\d+)?)"); |
| | | private static final Pattern RECENT_RANGE_PATTERN = Pattern.compile("è¿\\d+(天|å¨|个æ|æ|å¹´)"); |
| | |
| | | } |
| | | |
| | | String text = message.trim(); |
| | | String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text); |
| | | if (StringUtils.hasText(quickPromptResponse)) { |
| | | return quickPromptResponse; |
| | | } |
| | | |
| | | String approveId = extractApproveId(text); |
| | | boolean hasApproveId = StringUtils.hasText(approveId) && !isPlaceholderApproveId(approveId); |
| | | String startDate = extractStartDate(text); |
| | | String endDate = extractEndDate(text); |
| | | String timeRange = extractTimeRange(text); |
| | | |
| | | if (isStatsIntent(text)) { |
| | | return approveTodoTools.getTodoStats( |
| | | memoryId, |
| | | extractStartDate(text), |
| | | extractEndDate(text), |
| | | extractTimeRange(text) |
| | | startDate, |
| | | endDate, |
| | | timeRange |
| | | ); |
| | | } |
| | | if (containsAny(text, "æµè½¬", "è¿åº¦", "èç¹", "æ¥å¿", "å¡å¨", "å¡å°", "å½å审æ¹äºº", "å¤çè®°å½")) { |
| | | return StringUtils.hasText(approveId) |
| | | return hasApproveId |
| | | ? approveTodoTools.getTodoProgress(memoryId, approveId) |
| | | : missingApproveId("todo_progress", "æ¥è¯¢å®¡æ¹è¿åº¦éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "详æ
", "æç»") && !containsAny(text, "å表")) { |
| | | return StringUtils.hasText(approveId) |
| | | return hasApproveId |
| | | ? approveTodoTools.getTodoDetail(memoryId, approveId) |
| | | : missingApproveId("todo_detail", "æ¥è¯¢å®¡æ¹è¯¦æ
éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "åæ¶å®¡æ ¸", "æ¤éå®¡æ ¸", "åéå®¡æ ¸", "æ¤é审æ¹", "æ¤å审æ¹") |
| | | || (containsAny(text, "æ¤é", "æ¤å") && containsAny(text, "å®¡æ¹æä½", "å®¡æ ¸æä½"))) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.cancelReviewTodo(memoryId, approveId, firstNonBlank(extractTail(text, "åå "), extractTail(text, "夿³¨"))) |
| | | return hasApproveId |
| | | ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractRemark(text)) |
| | | : missingApproveId("cancel_review_action", "åæ¶å®¡æ ¸éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "å é¤", "ç§»é¤")) { |
| | | return StringUtils.hasText(approveId) |
| | | return hasApproveId |
| | | ? approveTodoTools.deleteTodo(memoryId, approveId) |
| | | : missingApproveId("delete_action", "å é¤å®¡æ¹åéè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "驳å", "æç»")) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", firstNonBlank(extractTail(text, "åå "), extractTail(text, "夿³¨"))) |
| | | return hasApproveId |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractRemark(text)) |
| | | : missingApproveId("review_action", "驳å审æ¹éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (containsAny(text, "å®¡æ ¸éè¿", "审æ¹éè¿", "éè¿å®¡æ¹", "åæå®¡æ¹", "审æ¹åæ")) { |
| | | return StringUtils.hasText(approveId) |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "夿³¨")) |
| | | return hasApproveId |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text)) |
| | | : missingApproveId("review_action", "审æ¹éè¿éè¦æä¾æµç¨ç¼å·ã"); |
| | | } |
| | | if (StringUtils.hasText(approveId) |
| | | if (hasApproveId |
| | | && containsAny(text, "éè¿", "åæ") |
| | | && !containsAny(text, "æªéè¿", "éè¿ç", "审æ¹éè¿ç", "å®¡æ ¸éè¿ç")) { |
| | | return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "夿³¨")); |
| | | return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text)); |
| | | } |
| | | if (containsAny(text, "ä¿®æ¹", "æ´æ°", "åæ´")) { |
| | | return StringUtils.hasText(approveId) |
| | | return hasApproveId |
| | | ? approveTodoTools.updateTodo( |
| | | memoryId, |
| | | approveId, |
| | |
| | | extractApproveType(text), |
| | | extractKeyword(text), |
| | | extractLimit(text), |
| | | extractScope(text)); |
| | | extractScope(text), |
| | | startDate, |
| | | endDate, |
| | | timeRange); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String tryExecuteQuickPrompt(String memoryId, String text) { |
| | | String normalized = normalizeForMatch(text); |
| | | String approveId = extractApproveId(text); |
| | | boolean hasApproveId = StringUtils.hasText(approveId) && !isPlaceholderApproveId(approveId); |
| | | |
| | | if ("æå½åæåªäºå®¡æ¹å¾
åéè¦å¤ç".equals(normalized)) { |
| | | return approveTodoTools.listTodos(memoryId, "pending", null, null, 10, "approver", null, null, null); |
| | | } |
| | | if ("帮æååºä»å¤©æ°å¢ç审æ¹å¾
å".equals(normalized)) { |
| | | return approveTodoTools.listTodos(memoryId, "all", null, null, 10, "related", null, null, "ä»å¤©"); |
| | | } |
| | | if ("å½åå¾
æå®¡æ¹çåæ®ææ¶é´ååºååºæ¥".equals(normalized)) { |
| | | return approveTodoTools.listTodos(memoryId, "pending", null, null, 10, "approver", null, null, null); |
| | | } |
| | | if ("æåèµ·ç审æ¹éåªäºè¿å¨å¤çä¸".equals(normalized)) { |
| | | return approveTodoTools.listTodos(memoryId, "processing", null, null, 10, "applicant", null, null, null); |
| | | } |
| | | if ("è¿7天æç审æ¹å¾
åç»è®¡æ
嵿乿 ·".equals(normalized)) { |
| | | return approveTodoTools.getTodoStats(memoryId, null, null, "è¿7天"); |
| | | } |
| | | if ("æ¬ææç审æ¹ä¸éè¿é©³åå¤çä¸åæå¤å°".equals(normalized)) { |
| | | return approveTodoTools.getTodoStats(memoryId, null, null, "æ¬æ"); |
| | | } |
| | | if ("è¿30天åç±»åå®¡æ¹æ°éå叿¯ä»ä¹".equals(normalized)) { |
| | | return approveTodoTools.getTodoStats(memoryId, null, null, "è¿30天"); |
| | | } |
| | | |
| | | if (normalized.startsWith("æ¥è¯¢æµç¨ç¼å·") && normalized.contains("审æ¹è¯¦æ
")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.getTodoDetail(memoryId, approveId) |
| | | : missingApproveId("todo_detail", "æ¥è¯¢å®¡æ¹è¯¦æ
éè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | if (normalized.startsWith("æµç¨ç¼å·") |
| | | && normalized.contains("å¡å¨åªä¸ªå®¡æ¹èç¹") |
| | | && normalized.contains("å½å审æ¹äººæ¯è°")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.getTodoProgress(memoryId, approveId) |
| | | : missingApproveId("todo_progress", "æ¥è¯¢å®¡æ¹è¿åº¦éè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | if (normalized.startsWith("å¸®ææ¥çæµç¨ç¼å·") && normalized.contains("å®¡æ¹æµè½¬è®°å½")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.getTodoProgress(memoryId, approveId) |
| | | : missingApproveId("todo_progress", "æ¥è¯¢å®¡æ¹æµè½¬è®°å½éè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | if (normalized.startsWith("帮æå®¡æ¹éè¿æµç¨ç¼å·")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractRemark(text)) |
| | | : missingApproveId("review_action", "审æ¹éè¿éè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | if (normalized.startsWith("帮æé©³åæµç¨ç¼å·")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractRemark(text)) |
| | | : missingApproveId("review_action", "驳å审æ¹éè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | if (normalized.startsWith("æ¤éæåå对æµç¨ç¼å·") && normalized.contains("å®¡æ¹æä½")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractRemark(text)) |
| | | : missingApproveId("cancel_review_action", "æ¤éå®¡æ¹æä½éè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | if (normalized.startsWith("帮æä¿®æ¹æµç¨ç¼å·") && normalized.contains("夿³¨ä¸º")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.updateTodo(memoryId, approveId, null, null, null, null, null, null, extractRemark(text)) |
| | | : missingApproveId("update_action", "ä¿®æ¹å®¡æ¹åéè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | if (normalized.startsWith("å 餿åèµ·çæµç¨ç¼å·")) { |
| | | return hasApproveId |
| | | ? approveTodoTools.deleteTodo(memoryId, approveId) |
| | | : missingApproveId("delete_action", "å é¤å®¡æ¹åéè¦æä¾ç宿µç¨ç¼å·ã"); |
| | | } |
| | | return null; |
| | | } |
| | |
| | | } |
| | | |
| | | private String extractApproveId(String text) { |
| | | Matcher keywordMatcher = APPROVE_ID_BY_LABEL_PATTERN.matcher(text); |
| | | if (keywordMatcher.find()) { |
| | | return keywordMatcher.group(2); |
| | | } |
| | | Matcher matcher = APPROVE_ID_PATTERN.matcher(text); |
| | | return matcher.find() ? matcher.group() : null; |
| | | } |
| | |
| | | .replace("åæ®", "") |
| | | .replace("å¾
å", "") |
| | | .replace("å表", "") |
| | | .replace("æµç¨ç¼å·", "") |
| | | .replace("æµç¨å·", "") |
| | | .replace("å10æ¡", "") |
| | | .replace("æè¿10æ¡", "") |
| | | .trim(); |
| | |
| | | } |
| | | |
| | | private String extractValue(String text, String fieldName) { |
| | | Pattern pattern = Pattern.compile(fieldName + "(æ¹ä¸º|ä¿®æ¹ä¸º|æ¯)[:ï¼]?[\\s]*([^,ï¼ãï¼;\\s]+)"); |
| | | Pattern pattern = Pattern.compile(fieldName + "(æ¹ä¸º|ä¿®æ¹ä¸º|为|æ¯)[:ï¼]?[\\s]*([^,ï¼ãï¼;\\s]+)"); |
| | | Matcher matcher = pattern.matcher(text); |
| | | return matcher.find() ? matcher.group(2) : null; |
| | | } |
| | |
| | | if (!text.contains(fieldName)) { |
| | | return null; |
| | | } |
| | | Matcher matcher = Pattern.compile(fieldName + "(æ¹ä¸º|ä¿®æ¹ä¸º|æ¯)[:ï¼]?[\\s]*(\\d{1,2})").matcher(text); |
| | | Matcher matcher = Pattern.compile(fieldName + "(æ¹ä¸º|ä¿®æ¹ä¸º|为|æ¯)[:ï¼]?[\\s]*(\\d{1,2})").matcher(text); |
| | | return matcher.find() ? Integer.parseInt(matcher.group(2)) : null; |
| | | } |
| | | |
| | |
| | | } |
| | | |
| | | private String extractTail(String text, String key) { |
| | | Pattern quotedPattern = Pattern.compile(key + "(æ¯|为)?[:ï¼]?[\\s]*[â\"]([^â\"]+)[â\"]"); |
| | | Matcher quotedMatcher = quotedPattern.matcher(text); |
| | | if (quotedMatcher.find()) { |
| | | return cleanContent(quotedMatcher.group(2)); |
| | | } |
| | | Pattern pattern = Pattern.compile(key + "(æ¯|为)?[:ï¼]?[\\s]*(.+)"); |
| | | Matcher matcher = pattern.matcher(text); |
| | | return matcher.find() ? matcher.group(2).trim() : null; |
| | | return matcher.find() ? cleanContent(matcher.group(2)) : null; |
| | | } |
| | | |
| | | private String extractScope(String text) { |
| | | if (containsAny(text, "æåèµ·", "ææäº¤", "æç³è¯·", "ç³è¯·äººæ¯æ")) { |
| | | return "applicant"; |
| | | } |
| | | if (containsAny(text, "å¾
æå®¡æ¹", "å¾
æå®¡æ ¸", "æå¤ç", "æå®¡æ¹", "å½åå¾
æ", "éè¦æå¤ç")) { |
| | | if (containsAny(text, "å¾
æå®¡æ¹", "å¾
æå®¡æ ¸", "æå¤ç", "æå®¡æ¹", "å½åå¾
æ", "éè¦æå¤ç", "éè¦å¤ç")) { |
| | | return "approver"; |
| | | } |
| | | return "related"; |
| | | } |
| | | |
| | | private String extractRemark(String text) { |
| | | return firstNonBlank(firstNonBlank(extractTail(text, "夿³¨"), extractTail(text, "åå ")), extractQuotedContent(text)); |
| | | } |
| | | |
| | | private String extractQuotedContent(String text) { |
| | | Matcher matcher = Pattern.compile("[â\"]([^â\"]+)[â\"]").matcher(text); |
| | | return matcher.find() ? cleanContent(matcher.group(1)) : null; |
| | | } |
| | | |
| | | private String normalizeForMatch(String text) { |
| | | if (!StringUtils.hasText(text)) { |
| | | return ""; |
| | | } |
| | | return text.replace("ï¼", "") |
| | | .replace(",", "") |
| | | .replace("ã", "") |
| | | .replace(".", "") |
| | | .replace("ï¼", "") |
| | | .replace("!", "") |
| | | .replace("ï¼", "") |
| | | .replace("?", "") |
| | | .replace("ï¼", "") |
| | | .replace(":", "") |
| | | .replace("ï¼", "") |
| | | .replace(";", "") |
| | | .replace("â", "") |
| | | .replace("â", "") |
| | | .replace("\"", "") |
| | | .replace(" ", "") |
| | | .trim(); |
| | | } |
| | | |
| | | private boolean isPlaceholderApproveId(String approveId) { |
| | | if (!StringUtils.hasText(approveId)) { |
| | | return true; |
| | | } |
| | | String value = approveId.trim(); |
| | | return "xxx".equalsIgnoreCase(value) |
| | | || value.matches("[xX]{2,}") |
| | | || "æµç¨ç¼å·".equals(value) |
| | | || "ç¼å·".equals(value) |
| | | || value.contains("示ä¾") |
| | | || value.contains("请è¾å
¥"); |
| | | } |
| | | |
| | | private String cleanContent(String text) { |
| | | if (!StringUtils.hasText(text)) { |
| | | return null; |
| | | } |
| | | return text.trim() |
| | | .replace("â", "") |
| | | .replace("â", "") |
| | | .replace("\"", "") |
| | | .replace("ã", "") |
| | | .replace("ï¼", "") |
| | | .replace(";", "") |
| | | .trim(); |
| | | } |
| | | |
| | | private String firstNonBlank(String first, String second) { |
| | | return StringUtils.hasText(first) ? first : second; |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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); |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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; |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 = "chatMemoryProviderSales", |
| | | tools = "salesAgentTools" |
| | | ) |
| | | public interface SalesAgent { |
| | | |
| | | @SystemMessage(fromResource = "sales-agent-prompt.txt") |
| | | Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage); |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package com.ruoyi.ai.assistant; |
| | | |
| | | import com.ruoyi.ai.tools.SalesAgentTools; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.time.LocalDate; |
| | | import java.time.YearMonth; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | |
| | | @Component |
| | | public class SalesIntentExecutor { |
| | | |
| | | private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd"); |
| | | private static final Pattern LIMIT_PATTERN = Pattern.compile("(å|æè¿)?\\s*(\\d{1,2})\\s*æ¡"); |
| | | private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})"); |
| | | private static final Pattern RELATIVE_DAY_PATTERN = Pattern.compile("(è¿|æè¿)?\\s*(\\d{1,3})\\s*天"); |
| | | |
| | | private final SalesAgentTools salesAgentTools; |
| | | |
| | | public SalesIntentExecutor(SalesAgentTools salesAgentTools) { |
| | | this.salesAgentTools = salesAgentTools; |
| | | } |
| | | |
| | | public String tryExecute(String memoryId, String message) { |
| | | if (!StringUtils.hasText(message)) { |
| | | return null; |
| | | } |
| | | String text = message.trim(); |
| | | |
| | | String quickPromptResponse = tryExecuteQuickPrompt(memoryId, text); |
| | | if (StringUtils.hasText(quickPromptResponse)) { |
| | | return quickPromptResponse; |
| | | } |
| | | |
| | | String keyword = extractKeyword(text); |
| | | Integer limit = extractLimit(text); |
| | | DateRange dateRange = extractDateRange(text); |
| | | String startDate = dateRange.startDate(); |
| | | String endDate = dateRange.endDate(); |
| | | |
| | | if (containsAny(text, "æµå¤±", "æµå¤±é£é©", "å®¢æ·æµå¤±", "é£é©åæ")) { |
| | | return salesAgentTools.analyzeCustomerChurnRisk(memoryId, startDate, endDate, text, keyword, limit); |
| | | } |
| | | if (containsAny(text, "忬¾", "æ¶æ¬¾", "æ¥ä»·") |
| | | && containsAny(text, "建议", "çç¥", "ä¼å", "æ¹æ¡")) { |
| | | return salesAgentTools.suggestCollectionAndQuotationStrategy( |
| | | memoryId, startDate, endDate, text, keyword, limit, shouldPrioritizeHighRisk(text)); |
| | | } |
| | | if (containsAny(text, "ææ ", "ç»è®¡", "çæ¿", "æ»è§", "ç»è¥åæ")) { |
| | | return salesAgentTools.getSalesDashboard(memoryId, startDate, endDate, text); |
| | | } |
| | | if (containsAny(text, "å®¢æ·æ¡£æ¡", "ç§æµ·", "å
¬æµ·", "å®¢æ·æ± ")) { |
| | | return salesAgentTools.listCustomerProfiles(memoryId, extractSeaType(text), keyword, limit); |
| | | } |
| | | if (containsAny(text, "é宿¥ä»·", "æ¥ä»·å", "æ¥ä»·", "询价")) { |
| | | return salesAgentTools.listSalesQuotations(memoryId, keyword, startDate, endDate, limit); |
| | | } |
| | | if (containsAny(text, "éå®éè´§", "éè´§", "鿬¾")) { |
| | | return salesAgentTools.listSalesReturns(memoryId, startDate, endDate, keyword, limit); |
| | | } |
| | | if (containsAny(text, "客æ·å¾æ¥", "徿¥", "忬¾", "åºæ¶", "æ¥æ¬¾", "æ¶æ¬¾æç»")) { |
| | | return salesAgentTools.listCustomerInteractions(memoryId, keyword, startDate, endDate, limit); |
| | | } |
| | | if (containsAny(text, "åè´§å°è´¦", "åè´§", "ç©æµ", "å¿«é", "è¿è¾")) { |
| | | return salesAgentTools.listShippingLedgers(memoryId, keyword, startDate, endDate, limit); |
| | | } |
| | | if (containsAny(text, "éå®å°è´¦", "éå®åå", "éå®è®¢å", "ååå°è´¦", "订åå°è´¦")) { |
| | | return salesAgentTools.listSalesLedgers(memoryId, keyword, startDate, endDate, limit); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private String tryExecuteQuickPrompt(String memoryId, String text) { |
| | | String normalized = normalizeForMatch(text); |
| | | if ("æ¥è¯¢ç§æµ·å®¢æ·æ¡£æ¡å10æ¡".equals(normalized)) { |
| | | return salesAgentTools.listCustomerProfiles(memoryId, "private", null, 10); |
| | | } |
| | | if ("æ¥è¯¢å
¬æµ·å®¢æ·æ¡£æ¡".equals(normalized)) { |
| | | return salesAgentTools.listCustomerProfiles(memoryId, "public", null, 10); |
| | | } |
| | | if ("æ¥è¯¢æ¬æé宿¥ä»·".equals(normalized)) { |
| | | DateRange range = monthRange(); |
| | | return salesAgentTools.listSalesQuotations(memoryId, null, range.startDate(), range.endDate(), 10); |
| | | } |
| | | if ("æ¥è¯¢æ¬æéå®å°è´¦".equals(normalized)) { |
| | | DateRange range = monthRange(); |
| | | return salesAgentTools.listSalesLedgers(memoryId, null, range.startDate(), range.endDate(), 10); |
| | | } |
| | | if ("æ¥è¯¢è¿30天éå®éè´§".equals(normalized)) { |
| | | DateRange range = recentDaysRange(30); |
| | | return salesAgentTools.listSalesReturns(memoryId, range.startDate(), range.endDate(), null, 10); |
| | | } |
| | | if ("æ¥è¯¢è¿30天客æ·åæ¬¾å¾æ¥".equals(normalized)) { |
| | | DateRange range = recentDaysRange(30); |
| | | return salesAgentTools.listCustomerInteractions(memoryId, null, range.startDate(), range.endDate(), 10); |
| | | } |
| | | if ("æ¥è¯¢æ¬æåè´§å°è´¦".equals(normalized)) { |
| | | DateRange range = monthRange(); |
| | | return salesAgentTools.listShippingLedgers(memoryId, null, range.startDate(), range.endDate(), 10); |
| | | } |
| | | if ("æ¥çé宿æ ç»è®¡".equals(normalized)) { |
| | | return salesAgentTools.getSalesDashboard(memoryId, null, null, "æ¬æ"); |
| | | } |
| | | if ("帮æåå®¢æ·æµå¤±é£é©åæè¿30天å20æ¡".equals(normalized)) { |
| | | DateRange range = recentDaysRange(30); |
| | | return salesAgentTools.analyzeCustomerChurnRisk(memoryId, range.startDate(), range.endDate(), "è¿30天", null, 20); |
| | | } |
| | | if ("çæåæ¬¾ä¸æ¥ä»·çç¥å»ºè®®ä¼å
é«é£é©å®¢æ·".equals(normalized)) { |
| | | DateRange range = recentDaysRange(30); |
| | | return salesAgentTools.suggestCollectionAndQuotationStrategy(memoryId, range.startDate(), range.endDate(), "è¿30天", null, 10, true); |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private boolean containsAny(String text, String... keywords) { |
| | | for (String keyword : keywords) { |
| | | if (text.contains(keyword)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | |
| | | private String extractSeaType(String text) { |
| | | if (text.contains("å
¬æµ·")) { |
| | | return "public"; |
| | | } |
| | | if (text.contains("ç§æµ·")) { |
| | | return "private"; |
| | | } |
| | | return null; |
| | | } |
| | | |
| | | private Integer extractLimit(String text) { |
| | | Matcher matcher = LIMIT_PATTERN.matcher(text); |
| | | return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10; |
| | | } |
| | | |
| | | private DateRange extractDateRange(String text) { |
| | | Matcher matcher = DATE_PATTERN.matcher(text); |
| | | if (matcher.find()) { |
| | | String first = matcher.group(1); |
| | | String second = matcher.find() ? matcher.group(1) : first; |
| | | return buildDateRange(first, second); |
| | | } |
| | | if (text.contains("æ¬æ")) { |
| | | return monthRange(); |
| | | } |
| | | if (text.contains("䏿")) { |
| | | return lastMonthRange(); |
| | | } |
| | | if (text.contains("æ¬å¹´") || text.contains("ä»å¹´")) { |
| | | return yearRange(); |
| | | } |
| | | Matcher relativeDayMatcher = RELATIVE_DAY_PATTERN.matcher(text); |
| | | if (relativeDayMatcher.find()) { |
| | | int days = Integer.parseInt(relativeDayMatcher.group(2)); |
| | | return recentDaysRange(days); |
| | | } |
| | | return new DateRange(null, null); |
| | | } |
| | | |
| | | private DateRange buildDateRange(String start, String end) { |
| | | LocalDate startDate = parseDate(start); |
| | | LocalDate endDate = parseDate(end); |
| | | if (startDate == null || endDate == null) { |
| | | return new DateRange(null, null); |
| | | } |
| | | if (startDate.isAfter(endDate)) { |
| | | LocalDate temp = startDate; |
| | | startDate = endDate; |
| | | endDate = temp; |
| | | } |
| | | return new DateRange(formatDate(startDate), formatDate(endDate)); |
| | | } |
| | | |
| | | private DateRange recentDaysRange(int days) { |
| | | LocalDate end = LocalDate.now(); |
| | | int safeDays = Math.max(days, 1); |
| | | LocalDate start = end.minusDays(safeDays - 1L); |
| | | return new DateRange(formatDate(start), formatDate(end)); |
| | | } |
| | | |
| | | private DateRange monthRange() { |
| | | LocalDate today = LocalDate.now(); |
| | | return new DateRange(formatDate(today.withDayOfMonth(1)), formatDate(today)); |
| | | } |
| | | |
| | | private DateRange lastMonthRange() { |
| | | YearMonth lastMonth = YearMonth.now().minusMonths(1); |
| | | return new DateRange(formatDate(lastMonth.atDay(1)), formatDate(lastMonth.atEndOfMonth())); |
| | | } |
| | | |
| | | private DateRange yearRange() { |
| | | LocalDate today = LocalDate.now(); |
| | | return new DateRange(formatDate(today.withDayOfYear(1)), formatDate(today)); |
| | | } |
| | | |
| | | private LocalDate parseDate(String text) { |
| | | try { |
| | | return LocalDate.parse(text, DATE_FMT); |
| | | } catch (Exception ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private String formatDate(LocalDate date) { |
| | | return date == null ? null : date.format(DATE_FMT); |
| | | } |
| | | |
| | | private String normalizeForMatch(String text) { |
| | | if (!StringUtils.hasText(text)) { |
| | | return ""; |
| | | } |
| | | return text.replace("ï¼", "") |
| | | .replace(",", "") |
| | | .replace("ã", "") |
| | | .replace(".", "") |
| | | .replace("ï¼", "") |
| | | .replace("!", "") |
| | | .replace("ï¼", "") |
| | | .replace("?", "") |
| | | .replace("ï¼", "") |
| | | .replace(":", "") |
| | | .replace("ï¼", "") |
| | | .replace(";", "") |
| | | .replace(" ", "") |
| | | .trim(); |
| | | } |
| | | |
| | | private Boolean shouldPrioritizeHighRisk(String text) { |
| | | return containsAny(text, "ä¼å
é«é£é©", "é«é£é©å®¢æ·", "é«é£é©"); |
| | | } |
| | | |
| | | private String extractKeyword(String text) { |
| | | String cleaned = text |
| | | .replace("æ¥è¯¢", "") |
| | | .replace("æ¥ç", "") |
| | | .replace("çä¸", "") |
| | | .replace("çç", "") |
| | | .replace("帮æ", "") |
| | | .replace("请", "") |
| | | .replace("ä¸ä¸", "") |
| | | .replace("éå®", "") |
| | | .replace("å®¢æ·æ¡£æ¡", "") |
| | | .replace("æ¥ä»·å", "") |
| | | .replace("é宿¥ä»·", "") |
| | | .replace("éå®å°è´¦", "") |
| | | .replace("åè´§å°è´¦", "") |
| | | .replace("客æ·å¾æ¥", "") |
| | | .replace("éå®éè´§", "") |
| | | .replace("å10æ¡", "") |
| | | .replace("æè¿10æ¡", "") |
| | | .replace("å20æ¡", "") |
| | | .replace("æè¿20æ¡", "") |
| | | .replace("è¿30天", "") |
| | | .replace("æ¬æ", "") |
| | | .replace("æ¬å¹´", "") |
| | | .replace("ä»å¹´", "") |
| | | .replace("æ¡", "") |
| | | .trim(); |
| | | return cleaned.length() >= 2 ? cleaned : null; |
| | | } |
| | | |
| | | private record DateRange(String startDate, String endDate) { |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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(); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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 SalesAgentConfig { |
| | | |
| | | @Bean |
| | | ChatMemoryProvider chatMemoryProviderSales(MongoChatMemoryStore mongoChatMemoryStore) { |
| | | return memoryId -> MessageWindowChatMemory.builder() |
| | | .id(memoryId) |
| | | .maxMessages(30) |
| | | .chatMemoryStore(mongoChatMemoryStore) |
| | | .build(); |
| | | } |
| | | } |
| | | |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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())); |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package com.ruoyi.ai.controller; |
| | | |
| | | import com.ruoyi.ai.assistant.SalesAgent; |
| | | import com.ruoyi.ai.assistant.SalesIntentExecutor; |
| | | 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("/sales-ai") |
| | | public class SalesAiController extends BaseController { |
| | | |
| | | private final SalesAgent salesAgent; |
| | | private final SalesIntentExecutor salesIntentExecutor; |
| | | private final AiSessionUserContext aiSessionUserContext; |
| | | private final MongoChatMemoryStore mongoChatMemoryStore; |
| | | private final AiChatSessionService aiChatSessionService; |
| | | |
| | | public SalesAiController(SalesAgent salesAgent, |
| | | SalesIntentExecutor salesIntentExecutor, |
| | | AiSessionUserContext aiSessionUserContext, |
| | | MongoChatMemoryStore mongoChatMemoryStore, |
| | | AiChatSessionService aiChatSessionService) { |
| | | this.salesAgent = salesAgent; |
| | | this.salesIntentExecutor = salesIntentExecutor; |
| | | 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 = salesIntentExecutor.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); |
| | | } |
| | | |
| | | if (isBusinessDataIntent(userMessage)) { |
| | | String noGuessResponse = "æªè¯å«å°å¯æ§è¡çæ°æ®æ¥è¯¢æ¡ä»¶ã为ä¿è¯ç»æåç¡®ï¼å½åä¸ä¼æ¨æµæç¼é æ°æ®ï¼è¯·è¡¥å
æç¡®æ¶é´èå´ãå®¢æ·æåå·ååæ¥è¯¢ã"; |
| | | mongoChatMemoryStore.appendMessages( |
| | | memoryId, |
| | | List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse)) |
| | | ); |
| | | aiChatSessionService.refreshSessionStats(memoryId, loginUser); |
| | | return Flux.just(noGuessResponse); |
| | | } |
| | | |
| | | return salesAgent.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())); |
| | | } |
| | | |
| | | private boolean isBusinessDataIntent(String message) { |
| | | if (!StringUtils.hasText(message)) { |
| | | return false; |
| | | } |
| | | String text = message.trim(); |
| | | return containsAny(text, |
| | | "æ¥è¯¢", "æ¥ç", "ç»è®¡", "åæ", "建议", "å®¢æ·æ¡£æ¡", "ç§æµ·", "å
¬æµ·", |
| | | "é宿¥ä»·", "éå®å°è´¦", "éå®éè´§", "客æ·å¾æ¥", "åè´§å°è´¦", "忬¾", "æ¥ä»·", "é£é©"); |
| | | } |
| | | |
| | | private boolean containsAny(String text, String... keywords) { |
| | | for (String keyword : keywords) { |
| | | if (text.contains(keyword)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | |
| | | return Flux.just(directResponse); |
| | | } |
| | | |
| | | if (isApproveTodoBusinessIntent(userMessage)) { |
| | | String noGuessResponse = "æªè¯å«å°å¯æ§è¡ç审æ¹å¾
åæä½æ¡ä»¶ã为ä¿è¯ç»æåç¡®ï¼å½åä¸ä¼æ¨æµæç¼é å®¡æ¹æ°æ®ï¼è¯·è¡¥å
æµç¨ç¼å·ãæ¶é´èå´ææç¡®æä½æä»¤ååè¯ã"; |
| | | mongoChatMemoryStore.appendMessages( |
| | | memoryId, |
| | | List.of(UserMessage.from(userMessage), AiMessage.from(noGuessResponse)) |
| | | ); |
| | | aiChatSessionService.refreshSessionStats(memoryId, loginUser); |
| | | return Flux.just(noGuessResponse); |
| | | } |
| | | |
| | | return approveTodoAgent.chat(memoryId, userMessage) |
| | | .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)) |
| | | .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser)); |
| | |
| | | aiSessionUserContext.remove(memoryId); |
| | | return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser())); |
| | | } |
| | | |
| | | private boolean isApproveTodoBusinessIntent(String message) { |
| | | if (!StringUtils.hasText(message)) { |
| | | return false; |
| | | } |
| | | String text = message.trim(); |
| | | boolean hasDomainWord = containsAny(text, |
| | | "审æ¹", "å¾
å", "æµç¨ç¼å·", "æµç¨å·", "å®¡æ¹æµè½¬", "审æ¹èç¹", "å½å审æ¹äºº", "驳å", "éè¿", "æ¤é", "å é¤"); |
| | | boolean hasIntentWord = containsAny(text, |
| | | "æ¥è¯¢", "æ¥ç", "ååº", "ç»è®¡", "åæ", "åå¸", "éè¿", "驳å", "æ¤é", "å é¤", "ä¿®æ¹", "æåªäº", "å¡å¨"); |
| | | return hasDomainWord && hasIntentWord; |
| | | } |
| | | |
| | | private boolean containsAny(String text, String... keywords) { |
| | | for (String keyword : keywords) { |
| | | if (text.contains(keyword)) { |
| | | return true; |
| | | } |
| | | } |
| | | return false; |
| | | } |
| | | } |
| | |
| | | @P(value = "审æ¹ç±»åç¼å·ï¼å¯ä¸ä¼ ", required = false) Integer approveType, |
| | | @P(value = "å
³é®åï¼å¯å¹é
æµç¨ç¼å·ãæ é¢ãç³è¯·äººãå½å审æ¹äºº", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§20", required = false) Integer limit, |
| | | @P(value = "æ¥è¯¢èå´ï¼å¯éå¼ï¼relatedãapplicantãapproverï¼related 表示å½åç¨æ·ç¸å
³ï¼applicant 表示æåèµ·çï¼approver 表示å¾
æå¤çç", required = false) String scope) { |
| | | @P(value = "æ¥è¯¢èå´ï¼å¯éå¼ï¼relatedãapplicantãapproverï¼related 表示å½åç¨æ·ç¸å
³ï¼applicant 表示æåèµ·çï¼approver 表示å¾
æå¤çç", required = false) String scope, |
| | | @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); |
| | | Long userId = loginUser.getUserId(); |
| | | Integer statusCode = parseStatus(status); |
| | | String normalizedScope = normalizeScope(scope); |
| | | boolean hasDateFilter = StringUtils.hasText(startDate) || StringUtils.hasText(endDate) || StringUtils.hasText(timeRange); |
| | | DateRange dateRange = hasDateFilter ? resolveDateRange(startDate, endDate, timeRange) : null; |
| | | |
| | | LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>(); |
| | | wrapper.eq(ApproveProcess::getApproveDelete, 0); |
| | |
| | | } |
| | | } |
| | | |
| | | if (dateRange != null) { |
| | | wrapper.ge(ApproveProcess::getCreateTime, dateRange.start().atStartOfDay()) |
| | | .lt(ApproveProcess::getCreateTime, dateRange.end().plusDays(1).atStartOfDay()); |
| | | } |
| | | |
| | | wrapper.orderByDesc(ApproveProcess::getCreateTime) |
| | | .last("limit " + normalizeLimit(limit)); |
| | | |
| | |
| | | "statusFilter", StringUtils.hasText(status) ? status : "all", |
| | | "approveType", approveType == null ? "" : approveType, |
| | | "keyword", keyword == null ? "" : keyword, |
| | | "scope", normalizedScope |
| | | "scope", normalizedScope, |
| | | "timeRange", dateRange == null ? "all" : dateRange.label(), |
| | | "startDate", dateRange == null ? "" : dateRange.start().toString(), |
| | | "endDate", dateRange == null ? "" : dateRange.end().toString() |
| | | ), |
| | | Map.of("columns", todoColumns(), "items", items), |
| | | Map.of()); |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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) { |
| | | } |
| | | } |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | 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.account.mapper.SalesReceiptReturnMapper; |
| | | import com.ruoyi.account.pojo.SalesReceiptReturn; |
| | | import com.ruoyi.ai.context.AiSessionUserContext; |
| | | import com.ruoyi.basic.dto.CustomerDto; |
| | | import com.ruoyi.basic.mapper.CustomerMapper; |
| | | import com.ruoyi.basic.vo.CustomerVo; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | import com.ruoyi.framework.security.LoginUser; |
| | | import com.ruoyi.sales.dto.InvoiceLedgerDto; |
| | | import com.ruoyi.sales.mapper.InvoiceLedgerMapper; |
| | | import com.ruoyi.sales.mapper.ReceiptPaymentMapper; |
| | | import com.ruoyi.sales.mapper.SalesLedgerMapper; |
| | | import com.ruoyi.sales.mapper.SalesQuotationMapper; |
| | | import com.ruoyi.sales.mapper.ShippingInfoMapper; |
| | | import com.ruoyi.sales.pojo.ReceiptPayment; |
| | | import com.ruoyi.sales.pojo.SalesLedger; |
| | | import com.ruoyi.sales.pojo.SalesQuotation; |
| | | import com.ruoyi.sales.pojo.ShippingInfo; |
| | | 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.YearMonth; |
| | | import java.time.ZoneId; |
| | | import java.time.format.DateTimeFormatter; |
| | | import java.util.ArrayList; |
| | | import java.util.Comparator; |
| | | import java.util.Date; |
| | | import java.util.HashMap; |
| | | import java.util.LinkedHashMap; |
| | | import java.util.LinkedList; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | import java.util.Map; |
| | | import java.util.Objects; |
| | | import java.util.regex.Matcher; |
| | | import java.util.regex.Pattern; |
| | | import java.util.stream.Collectors; |
| | | |
| | | @Component |
| | | public class SalesAgentTools { |
| | | |
| | | 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 BigDecimal ONE_HUNDRED = new BigDecimal("100"); |
| | | private static final Pattern RELATIVE_PATTERN = Pattern.compile("(è¿|æè¿)?\\s*(\\d+)\\s*(天|å¨|个æ|æ|å¹´)"); |
| | | private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})"); |
| | | |
| | | private final CustomerMapper customerMapper; |
| | | private final SalesLedgerMapper salesLedgerMapper; |
| | | private final SalesQuotationMapper salesQuotationMapper; |
| | | private final ShippingInfoMapper shippingInfoMapper; |
| | | private final ReceiptPaymentMapper receiptPaymentMapper; |
| | | private final InvoiceLedgerMapper invoiceLedgerMapper; |
| | | private final SalesReceiptReturnMapper salesReceiptReturnMapper; |
| | | private final AiSessionUserContext aiSessionUserContext; |
| | | |
| | | public SalesAgentTools(CustomerMapper customerMapper, |
| | | SalesLedgerMapper salesLedgerMapper, |
| | | SalesQuotationMapper salesQuotationMapper, |
| | | ShippingInfoMapper shippingInfoMapper, |
| | | ReceiptPaymentMapper receiptPaymentMapper, |
| | | InvoiceLedgerMapper invoiceLedgerMapper, |
| | | SalesReceiptReturnMapper salesReceiptReturnMapper, |
| | | AiSessionUserContext aiSessionUserContext) { |
| | | this.customerMapper = customerMapper; |
| | | this.salesLedgerMapper = salesLedgerMapper; |
| | | this.salesQuotationMapper = salesQuotationMapper; |
| | | this.shippingInfoMapper = shippingInfoMapper; |
| | | this.receiptPaymentMapper = receiptPaymentMapper; |
| | | this.invoiceLedgerMapper = invoiceLedgerMapper; |
| | | this.salesReceiptReturnMapper = salesReceiptReturnMapper; |
| | | this.aiSessionUserContext = aiSessionUserContext; |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢å®¢æ·æ¡£æ¡", value = "æç§æµ·/å
¬æµ·ç±»ååå
³é®è¯æ¥è¯¢å®¢æ·æ¡£æ¡å表") |
| | | public String listCustomerProfiles(@ToolMemoryId String memoryId, |
| | | @P(value = "å®¢æ·æ± ç±»åï¼å¯é private/public", required = false) String seaType, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
客æ·åç§°/è系人/çµè¯", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | CustomerDto customerDto = new CustomerDto(); |
| | | customerDto.setType(normalizeSeaType(seaType)); |
| | | customerDto.setUsageStatus(1L); |
| | | |
| | | List<CustomerVo> rows = defaultList(customerMapper.list(customerDto, loginUser.getUserId())); |
| | | List<CustomerVo> filtered = rows.stream() |
| | | .filter(item -> matchCustomerKeyword(item, keyword)) |
| | | .sorted(Comparator.comparing(CustomerVo::getId, Comparator.nullsLast(Comparator.reverseOrder()))) |
| | | .limit(normalizeLimit(limit)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | List<Map<String, Object>> items = filtered.stream().map(item -> { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("id", item.getId()); |
| | | map.put("customerName", safe(item.getCustomerName())); |
| | | map.put("customerType", safe(item.getCustomerType())); |
| | | map.put("contactPerson", safe(item.getContactPerson())); |
| | | map.put("contactPhone", safe(item.getContactPhone())); |
| | | map.put("companyPhone", safe(item.getCompanyPhone())); |
| | | map.put("maintainer", safe(item.getMaintainer())); |
| | | map.put("maintenanceTime", formatDate(item.getMaintenanceTime())); |
| | | map.put("usageUserName", safe(item.getUsageUserName())); |
| | | map.put("seaType", customerSeaTypeName(item.getType())); |
| | | map.put("isAssigned", item.getIsAssigned()); |
| | | return map; |
| | | }).collect(Collectors.toList()); |
| | | |
| | | Map<String, Object> summary = new LinkedHashMap<>(); |
| | | summary.put("count", items.size()); |
| | | summary.put("seaType", seaType == null ? "all" : seaType); |
| | | summary.put("keyword", safe(keyword)); |
| | | summary.put("userId", loginUser.getUserId()); |
| | | |
| | | return jsonResponse(true, "sales_customer_profile_list", "å·²è¿åå®¢æ·æ¡£æ¡å表", summary, Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢é宿¥ä»·", value = "æå
³é®è¯åæ¶é´èå´æ¥è¯¢é宿¥ä»·å") |
| | | public String listSalesQuotations(@ToolMemoryId String memoryId, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
æ¥ä»·åå·/客æ·/ä¸å¡å/ç¶æ", required = false) String keyword, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | LambdaQueryWrapper<SalesQuotation> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), SalesQuotation::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesQuotation::getDeptId); |
| | | if (StringUtils.hasText(keyword)) { |
| | | wrapper.and(w -> w.like(SalesQuotation::getQuotationNo, keyword) |
| | | .or().like(SalesQuotation::getCustomer, keyword) |
| | | .or().like(SalesQuotation::getSalesperson, keyword) |
| | | .or().like(SalesQuotation::getStatus, keyword)); |
| | | } |
| | | wrapper.ge(SalesQuotation::getQuotationDate, range.start()) |
| | | .le(SalesQuotation::getQuotationDate, range.end()) |
| | | .orderByDesc(SalesQuotation::getQuotationDate, SalesQuotation::getId) |
| | | .last("limit " + normalizeLimit(limit)); |
| | | |
| | | List<SalesQuotation> rows = defaultList(salesQuotationMapper.selectList(wrapper)); |
| | | BigDecimal quotationAmountTotal = rows.stream() |
| | | .map(SalesQuotation::getTotalAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | |
| | | List<Map<String, Object>> items = rows.stream().map(item -> { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("id", item.getId()); |
| | | map.put("quotationNo", safe(item.getQuotationNo())); |
| | | map.put("customer", safe(item.getCustomer())); |
| | | map.put("salesperson", safe(item.getSalesperson())); |
| | | map.put("quotationDate", formatDate(item.getQuotationDate())); |
| | | map.put("validDate", formatDate(item.getValidDate())); |
| | | map.put("status", safe(item.getStatus())); |
| | | map.put("paymentMethod", safe(item.getPaymentMethod())); |
| | | map.put("deliveryPeriod", safe(item.getDeliveryPeriod())); |
| | | map.put("totalAmount", item.getTotalAmount()); |
| | | return map; |
| | | }).collect(Collectors.toList()); |
| | | |
| | | Map<String, Object> summary = rangeSummary(range, items.size(), keyword); |
| | | summary.put("quotationAmountTotal", quotationAmountTotal); |
| | | return jsonResponse(true, "sales_quotation_list", "å·²è¿åé宿¥ä»·å表", summary, Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢éå®å°è´¦", value = "æå
³é®è¯åæ¶é´èå´æ¥è¯¢éå®å°è´¦ï¼å¹¶è¿åå¼ç¥¨å款ä¸åè´§ç¶æ") |
| | | public String listSalesLedgers(@ToolMemoryId String memoryId, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
éå®ååå·/客æ·ååå·/客æ·/项ç®", required = false) String keyword, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId); |
| | | if (StringUtils.hasText(keyword)) { |
| | | wrapper.and(w -> w.like(SalesLedger::getSalesContractNo, keyword) |
| | | .or().like(SalesLedger::getCustomerContractNo, keyword) |
| | | .or().like(SalesLedger::getCustomerName, keyword) |
| | | .or().like(SalesLedger::getProjectName, keyword) |
| | | .or().like(SalesLedger::getSalesman, keyword)); |
| | | } |
| | | wrapper.ge(SalesLedger::getEntryDate, toDate(range.start())) |
| | | .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end())) |
| | | .orderByDesc(SalesLedger::getEntryDate, SalesLedger::getId) |
| | | .last("limit " + normalizeLimit(limit)); |
| | | List<SalesLedger> rows = defaultList(salesLedgerMapper.selectList(wrapper)); |
| | | if (rows.isEmpty()) { |
| | | return jsonResponse(true, "sales_ledger_list", "æªæ¥è¯¢å°ç¬¦åæ¡ä»¶çéå®å°è´¦", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of()); |
| | | } |
| | | |
| | | List<Long> ledgerIds = rows.stream().map(SalesLedger::getId).filter(Objects::nonNull).collect(Collectors.toList()); |
| | | Map<Long, BigDecimal> invoiceAmountByLedgerId = sumInvoiceAmounts(ledgerIds); |
| | | Map<Long, BigDecimal> receiptAmountByLedgerId = sumReceiptAmounts(loginUser, ledgerIds); |
| | | Map<Long, List<ShippingInfo>> shippingByLedgerId = queryShippingsByLedgerIds(loginUser, ledgerIds).stream() |
| | | .collect(Collectors.groupingBy(ShippingInfo::getSalesLedgerId)); |
| | | |
| | | BigDecimal contractAmountTotal = BigDecimal.ZERO; |
| | | BigDecimal invoicedAmountTotal = BigDecimal.ZERO; |
| | | BigDecimal receivedAmountTotal = BigDecimal.ZERO; |
| | | BigDecimal pendingAmountTotal = BigDecimal.ZERO; |
| | | |
| | | List<Map<String, Object>> items = new ArrayList<>(); |
| | | for (SalesLedger ledger : rows) { |
| | | BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount()); |
| | | BigDecimal invoicedAmount = invoiceAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO); |
| | | BigDecimal receivedAmount = receiptAmountByLedgerId.getOrDefault(ledger.getId(), BigDecimal.ZERO); |
| | | BigDecimal unbilledAmount = maxZero(contractAmount.subtract(invoicedAmount)); |
| | | BigDecimal pendingAmount = maxZero(invoicedAmount.subtract(receivedAmount)); |
| | | |
| | | contractAmountTotal = contractAmountTotal.add(contractAmount); |
| | | invoicedAmountTotal = invoicedAmountTotal.add(invoicedAmount); |
| | | receivedAmountTotal = receivedAmountTotal.add(receivedAmount); |
| | | pendingAmountTotal = pendingAmountTotal.add(pendingAmount); |
| | | |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("id", ledger.getId()); |
| | | item.put("salesContractNo", safe(ledger.getSalesContractNo())); |
| | | item.put("customerContractNo", safe(ledger.getCustomerContractNo())); |
| | | item.put("customerName", safe(ledger.getCustomerName())); |
| | | item.put("projectName", safe(ledger.getProjectName())); |
| | | item.put("salesman", safe(ledger.getSalesman())); |
| | | item.put("entryDate", formatDate(ledger.getEntryDate())); |
| | | item.put("executionDate", formatDate(ledger.getExecutionDate())); |
| | | item.put("deliveryDate", formatDate(ledger.getDeliveryDate())); |
| | | item.put("contractAmount", contractAmount); |
| | | item.put("invoicedAmount", invoicedAmount); |
| | | item.put("receivedAmount", receivedAmount); |
| | | item.put("unbilledAmount", unbilledAmount); |
| | | item.put("pendingAmount", pendingAmount); |
| | | item.put("shippingStatus", calcLedgerShippingStatus(shippingByLedgerId.get(ledger.getId()))); |
| | | items.add(item); |
| | | } |
| | | |
| | | Map<String, Object> summary = rangeSummary(range, items.size(), keyword); |
| | | summary.put("contractAmountTotal", contractAmountTotal); |
| | | summary.put("invoicedAmountTotal", invoicedAmountTotal); |
| | | summary.put("receivedAmountTotal", receivedAmountTotal); |
| | | summary.put("pendingAmountTotal", pendingAmountTotal); |
| | | return jsonResponse(true, "sales_ledger_list", "å·²è¿åéå®å°è´¦å表", summary, Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢éå®éè´§", value = "ææ¶é´èå´åå
³é®è¯æ¥è¯¢éå®éè´§è®°å½") |
| | | public String listSalesReturns(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
鿬¾åå·/交æå·/仿¬¾è´¦æ·", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | LambdaQueryWrapper<SalesReceiptReturn> wrapper = new LambdaQueryWrapper<>(); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesReceiptReturn::getDeptId); |
| | | if (StringUtils.hasText(keyword)) { |
| | | wrapper.and(w -> w.like(SalesReceiptReturn::getRefundId, keyword) |
| | | .or().like(SalesReceiptReturn::getTransactionNo, keyword) |
| | | .or().like(SalesReceiptReturn::getPaymentAccountName, keyword)); |
| | | } |
| | | wrapper.ge(SalesReceiptReturn::getCreateTime, range.start().atStartOfDay()) |
| | | .le(SalesReceiptReturn::getCreateTime, range.end().atTime(23, 59, 59)) |
| | | .orderByDesc(SalesReceiptReturn::getCreateTime, SalesReceiptReturn::getId) |
| | | .last("limit " + normalizeLimit(limit)); |
| | | List<SalesReceiptReturn> rows = defaultList(salesReceiptReturnMapper.selectList(wrapper)); |
| | | |
| | | BigDecimal returnAmount = rows.stream() |
| | | .map(SalesReceiptReturn::getActualAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | |
| | | List<Map<String, Object>> items = rows.stream().map(item -> { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("id", item.getId()); |
| | | map.put("refundId", safe(item.getRefundId())); |
| | | map.put("paymentAccount", safe(item.getPaymentAccount())); |
| | | map.put("paymentAccountName", safe(item.getPaymentAccountName())); |
| | | map.put("paymentMethod", item.getPaymentMethod()); |
| | | map.put("actualAmount", item.getActualAmount()); |
| | | map.put("fee", item.getFee()); |
| | | map.put("discountAmount", item.getDiscountAmount()); |
| | | map.put("transactionNo", safe(item.getTransactionNo())); |
| | | map.put("createTime", formatDateTime(item.getCreateTime())); |
| | | return map; |
| | | }).collect(Collectors.toList()); |
| | | |
| | | Map<String, Object> summary = rangeSummary(range, items.size(), keyword); |
| | | summary.put("returnAmount", returnAmount); |
| | | return jsonResponse(true, "sales_return_list", "å·²è¿åéå®éè´§è®°å½", summary, Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢å®¢æ·å¾æ¥", value = "ææ¶é´èå´åå
³é®è¯æ¥è¯¢å®¢æ·åæ¬¾å¾æ¥æç»") |
| | | public String listCustomerInteractions(@ToolMemoryId String memoryId, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
客æ·åç§°/éå®ååå·/项ç®å", required = false) String keyword, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | LambdaQueryWrapper<ReceiptPayment> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), ReceiptPayment::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ReceiptPayment::getDeptId); |
| | | wrapper.ge(ReceiptPayment::getReceiptPaymentDate, range.start()) |
| | | .le(ReceiptPayment::getReceiptPaymentDate, range.end()) |
| | | .orderByDesc(ReceiptPayment::getReceiptPaymentDate, ReceiptPayment::getId); |
| | | List<ReceiptPayment> payments = defaultList(receiptPaymentMapper.selectList(wrapper)); |
| | | if (payments.isEmpty()) { |
| | | return jsonResponse(true, "sales_customer_interaction_list", "æªæ¥è¯¢å°å®¢æ·å¾æ¥è®°å½", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of()); |
| | | } |
| | | |
| | | List<Long> ledgerIds = payments.stream() |
| | | .map(ReceiptPayment::getSalesLedgerId) |
| | | .filter(Objects::nonNull) |
| | | .distinct() |
| | | .collect(Collectors.toList()); |
| | | Map<Long, SalesLedger> ledgerMap = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream() |
| | | .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId())) |
| | | .collect(Collectors.toMap(SalesLedger::getId, item -> item, (a, b) -> a, LinkedHashMap::new)); |
| | | |
| | | List<ReceiptPayment> filtered = payments.stream() |
| | | .filter(item -> matchInteractionKeyword(item, ledgerMap.get(item.getSalesLedgerId()), keyword)) |
| | | .limit(normalizeLimit(limit)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | BigDecimal totalReceiptAmount = filtered.stream() |
| | | .map(ReceiptPayment::getReceiptPaymentAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | |
| | | List<Map<String, Object>> items = filtered.stream().map(item -> { |
| | | SalesLedger ledger = ledgerMap.get(item.getSalesLedgerId()); |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("id", item.getId()); |
| | | map.put("salesLedgerId", item.getSalesLedgerId()); |
| | | map.put("salesContractNo", ledger == null ? "" : safe(ledger.getSalesContractNo())); |
| | | map.put("customerName", ledger == null ? "" : safe(ledger.getCustomerName())); |
| | | map.put("projectName", ledger == null ? "" : safe(ledger.getProjectName())); |
| | | map.put("receiptPaymentDate", formatDate(item.getReceiptPaymentDate())); |
| | | map.put("receiptPaymentAmount", item.getReceiptPaymentAmount()); |
| | | map.put("receiptPaymentType", safe(item.getReceiptPaymentType())); |
| | | map.put("registrant", safe(item.getRegistrant())); |
| | | return map; |
| | | }).collect(Collectors.toList()); |
| | | |
| | | Map<String, Object> summary = rangeSummary(range, items.size(), keyword); |
| | | summary.put("totalReceiptAmount", totalReceiptAmount); |
| | | summary.put("customerCount", items.stream().map(item -> String.valueOf(item.get("customerName"))).filter(StringUtils::hasText).distinct().count()); |
| | | return jsonResponse(true, "sales_customer_interaction_list", "å·²è¿å客æ·å¾æ¥æç»", summary, Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢åè´§å°è´¦", value = "æå
³é®è¯åæ¶é´èå´æ¥è¯¢åè´§å°è´¦") |
| | | public String listShippingLedgers(@ToolMemoryId String memoryId, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
åè´§åå·/å¿«éåå·/ç©æµå
¬å¸/车çå·", required = false) String keyword, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, null); |
| | | LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId); |
| | | if (StringUtils.hasText(keyword)) { |
| | | wrapper.and(w -> w.like(ShippingInfo::getShippingNo, keyword) |
| | | .or().like(ShippingInfo::getExpressNumber, keyword) |
| | | .or().like(ShippingInfo::getExpressCompany, keyword) |
| | | .or().like(ShippingInfo::getShippingCarNumber, keyword) |
| | | .or().like(ShippingInfo::getStatus, keyword)); |
| | | } |
| | | wrapper.ge(ShippingInfo::getShippingDate, toDate(range.start())) |
| | | .lt(ShippingInfo::getShippingDate, toExclusiveEndDate(range.end())) |
| | | .orderByDesc(ShippingInfo::getShippingDate, ShippingInfo::getId) |
| | | .last("limit " + normalizeLimit(limit)); |
| | | List<ShippingInfo> rows = defaultList(shippingInfoMapper.selectList(wrapper)); |
| | | if (rows.isEmpty()) { |
| | | return jsonResponse(true, "sales_shipping_list", "æªæ¥è¯¢å°åè´§å°è´¦è®°å½", rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of()); |
| | | } |
| | | |
| | | List<Long> ledgerIds = rows.stream().map(ShippingInfo::getSalesLedgerId).filter(Objects::nonNull).distinct().collect(Collectors.toList()); |
| | | Map<Long, SalesLedger> ledgerMap = defaultList(salesLedgerMapper.selectBatchIds(ledgerIds)).stream() |
| | | .filter(ledger -> tenantMatched(ledger.getTenantId(), loginUser.getTenantId())) |
| | | .collect(Collectors.toMap(SalesLedger::getId, item -> item, (a, b) -> a, LinkedHashMap::new)); |
| | | |
| | | long shippedCount = rows.stream().filter(item -> isShippedStatus(item.getStatus())).count(); |
| | | List<Map<String, Object>> items = rows.stream().map(item -> { |
| | | SalesLedger ledger = ledgerMap.get(item.getSalesLedgerId()); |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("id", item.getId()); |
| | | map.put("salesLedgerId", item.getSalesLedgerId()); |
| | | map.put("salesContractNo", ledger == null ? "" : safe(ledger.getSalesContractNo())); |
| | | map.put("customerName", ledger == null ? "" : safe(ledger.getCustomerName())); |
| | | map.put("shippingNo", safe(item.getShippingNo())); |
| | | map.put("status", safe(item.getStatus())); |
| | | map.put("shippingDate", formatDate(item.getShippingDate())); |
| | | map.put("type", safe(item.getType())); |
| | | map.put("shippingCarNumber", safe(item.getShippingCarNumber())); |
| | | map.put("expressCompany", safe(item.getExpressCompany())); |
| | | map.put("expressNumber", safe(item.getExpressNumber())); |
| | | return map; |
| | | }).collect(Collectors.toList()); |
| | | |
| | | Map<String, Object> summary = rangeSummary(range, items.size(), keyword); |
| | | summary.put("shippingCount", rows.size()); |
| | | summary.put("shippedCount", shippedCount); |
| | | summary.put("pendingCount", Math.max(rows.size() - shippedCount, 0)); |
| | | return jsonResponse(true, "sales_shipping_list", "å·²è¿ååè´§å°è´¦è®°å½", summary, Map.of("items", items), Map.of()); |
| | | } |
| | | |
| | | @Tool(name = "æ¥è¯¢é宿æ ç»è®¡", value = "ææ¶é´èå´ç»è®¡éå®ååãæ¥ä»·ãåè´§ã忬¾çå
³é®ææ ") |
| | | public String getSalesDashboard(@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); |
| | | |
| | | List<SalesLedger> ledgers = querySalesLedgers(loginUser, range); |
| | | List<SalesQuotation> quotations = querySalesQuotations(loginUser, range); |
| | | List<ShippingInfo> shippings = queryShippings(loginUser, range); |
| | | List<ReceiptPayment> receipts = queryReceipts(loginUser, range); |
| | | |
| | | BigDecimal contractAmountTotal = ledgers.stream() |
| | | .map(SalesLedger::getContractAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | BigDecimal quotationAmountTotal = quotations.stream() |
| | | .map(SalesQuotation::getTotalAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | BigDecimal receivedAmountTotal = receipts.stream() |
| | | .map(ReceiptPayment::getReceiptPaymentAmount) |
| | | .filter(Objects::nonNull) |
| | | .reduce(BigDecimal.ZERO, BigDecimal::add); |
| | | BigDecimal pendingAmountTotal = maxZero(contractAmountTotal.subtract(receivedAmountTotal)); |
| | | |
| | | long shippingCount = shippings.size(); |
| | | long shippedCount = shippings.stream().filter(item -> isShippedStatus(item.getStatus())).count(); |
| | | String shipRate = toRate(shippedCount, shippingCount); |
| | | |
| | | List<Map<String, Object>> topCustomers = buildTopCustomers(ledgers); |
| | | TrendData trendData = buildContractTrendData(ledgers, 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("orderCount", ledgers.size()); |
| | | summary.put("quotationCount", quotations.size()); |
| | | summary.put("shippingCount", shippingCount); |
| | | summary.put("shippedCount", shippedCount); |
| | | summary.put("shipRate", shipRate); |
| | | summary.put("contractAmountTotal", contractAmountTotal); |
| | | summary.put("quotationAmountTotal", quotationAmountTotal); |
| | | summary.put("receivedAmountTotal", receivedAmountTotal); |
| | | summary.put("pendingAmountTotal", pendingAmountTotal); |
| | | |
| | | Map<String, Object> charts = new LinkedHashMap<>(); |
| | | charts.put("amountBarOption", buildAmountBarOption(contractAmountTotal, quotationAmountTotal, receivedAmountTotal, pendingAmountTotal)); |
| | | charts.put("shippingPieOption", buildShippingPieOption(shippedCount, Math.max(shippingCount - shippedCount, 0))); |
| | | charts.put("customerTopBarOption", buildCustomerTopBarOption(topCustomers)); |
| | | charts.put("contractTrendLineOption", buildContractTrendLineOption(trendData.labels(), trendData.values())); |
| | | |
| | | Map<String, Object> data = new LinkedHashMap<>(); |
| | | data.put("topCustomers", topCustomers); |
| | | data.put("contractTrend", trendData.toItemList()); |
| | | |
| | | return jsonResponse(true, "sales_dashboard", "å·²è¿åé宿æ ç»è®¡", summary, data, charts); |
| | | } |
| | | |
| | | @Tool(name = "å®¢æ·æµå¤±é£é©åæ", value = "æå®¢æ·ç»´åº¦è¯ä¼°æµå¤±é£é©ï¼è¾åºé£é©å级ãåå å建议ä¼å
级") |
| | | public String analyzeCustomerChurnRisk(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "æ¶é´èå´æè¿°ï¼å¦è¿90å¤©ãæ¬å¹´", required = false) String timeRange, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
客æ·åç§°", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, StringUtils.hasText(timeRange) ? timeRange : "è¿180天"); |
| | | List<CustomerRiskMetric> metrics = buildCustomerRiskMetrics(loginUser, range, keyword); |
| | | if (metrics.isEmpty()) { |
| | | return jsonResponse(true, "sales_customer_churn_risk", "å½åèå´å
æªæ¥è¯¢å°å¯åæçå®¢æ·æ°æ®", |
| | | rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of()); |
| | | } |
| | | |
| | | List<CustomerRiskMetric> sorted = metrics.stream() |
| | | .sorted(Comparator.comparing(CustomerRiskMetric::getRiskScore).reversed() |
| | | .thenComparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder())) |
| | | .limit(normalizeLimit(limit)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | long highCount = sorted.stream().filter(item -> "high".equals(item.getRiskLevel())).count(); |
| | | long mediumCount = sorted.stream().filter(item -> "medium".equals(item.getRiskLevel())).count(); |
| | | long lowCount = sorted.stream().filter(item -> "low".equals(item.getRiskLevel())).count(); |
| | | |
| | | List<Map<String, Object>> items = sorted.stream().map(this::toRiskItem).collect(Collectors.toList()); |
| | | Map<String, Object> summary = rangeSummary(range, items.size(), keyword); |
| | | summary.put("highRiskCount", highCount); |
| | | summary.put("mediumRiskCount", mediumCount); |
| | | summary.put("lowRiskCount", lowCount); |
| | | |
| | | Map<String, Object> charts = new LinkedHashMap<>(); |
| | | charts.put("riskLevelPieOption", buildRiskLevelPieOption(highCount, mediumCount, lowCount)); |
| | | charts.put("riskScoreBarOption", buildRiskScoreBarOption(sorted)); |
| | | |
| | | return jsonResponse(true, "sales_customer_churn_risk", "å·²å®æå®¢æ·æµå¤±é£é©åæ", summary, Map.of("items", items), charts); |
| | | } |
| | | |
| | | @Tool(name = "忬¾ä¸æ¥ä»·çç¥å»ºè®®", value = "åºäºå®¢æ·é£é©ã忬¾åæ¥ä»·æ
åµçæå¯æ§è¡çè·è¿çç¥") |
| | | public String suggestCollectionAndQuotationStrategy(@ToolMemoryId String memoryId, |
| | | @P(value = "å¼å§æ¥æ yyyy-MM-dd", required = false) String startDate, |
| | | @P(value = "ç»ææ¥æ yyyy-MM-dd", required = false) String endDate, |
| | | @P(value = "æ¶é´èå´æè¿°ï¼å¦è¿90å¤©ãæ¬æ", required = false) String timeRange, |
| | | @P(value = "å
³é®è¯ï¼å¯å¹é
客æ·åç§°", required = false) String keyword, |
| | | @P(value = "è¿åæ¡æ°ï¼é»è®¤10ï¼æå¤§30", required = false) Integer limit, |
| | | @P(value = "æ¯å¦ä¼å
é«é£é©å®¢æ·ï¼true 表示é«é£é©ä¼å
", required = false) Boolean prioritizeHighRisk) { |
| | | LoginUser loginUser = currentLoginUser(memoryId); |
| | | DateRange range = resolveDateRange(startDate, endDate, StringUtils.hasText(timeRange) ? timeRange : "è¿90天"); |
| | | List<CustomerRiskMetric> metrics = buildCustomerRiskMetrics(loginUser, range, keyword); |
| | | if (metrics.isEmpty()) { |
| | | return jsonResponse(true, "sales_collection_quote_strategy", "å½åèå´å
æªæ¥è¯¢å°å¯çæçç¥çå®¢æ·æ°æ®", |
| | | rangeSummary(range, 0, keyword), Map.of("items", List.of()), Map.of()); |
| | | } |
| | | |
| | | boolean highRiskFirst = Boolean.TRUE.equals(prioritizeHighRisk); |
| | | Comparator<CustomerRiskMetric> sortComparator; |
| | | if (highRiskFirst) { |
| | | sortComparator = Comparator |
| | | .comparingInt((CustomerRiskMetric metric) -> riskLevelRank(metric.getRiskLevel())).reversed() |
| | | .thenComparing(CustomerRiskMetric::getRiskScore, Comparator.reverseOrder()) |
| | | .thenComparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder()); |
| | | } else { |
| | | sortComparator = Comparator |
| | | .comparing(CustomerRiskMetric::getPendingAmount, Comparator.reverseOrder()) |
| | | .thenComparing(CustomerRiskMetric::getRiskScore, Comparator.reverseOrder()); |
| | | } |
| | | |
| | | List<CustomerRiskMetric> sorted = metrics.stream() |
| | | .sorted(sortComparator) |
| | | .limit(normalizeLimit(limit)) |
| | | .collect(Collectors.toList()); |
| | | |
| | | List<Map<String, Object>> items = sorted.stream().map(this::toStrategyItem).collect(Collectors.toList()); |
| | | long highPriorityCount = items.stream().filter(item -> "high".equals(item.get("priority"))).count(); |
| | | long mediumPriorityCount = items.stream().filter(item -> "medium".equals(item.get("priority"))).count(); |
| | | long lowPriorityCount = items.stream().filter(item -> "low".equals(item.get("priority"))).count(); |
| | | |
| | | Map<String, Object> summary = rangeSummary(range, items.size(), keyword); |
| | | summary.put("highPriorityCount", highPriorityCount); |
| | | summary.put("mediumPriorityCount", mediumPriorityCount); |
| | | summary.put("lowPriorityCount", lowPriorityCount); |
| | | summary.put("prioritizeHighRisk", highRiskFirst); |
| | | summary.put("priorityMode", highRiskFirst ? "high_risk_first" : "pending_amount_first"); |
| | | |
| | | Map<String, Object> charts = new LinkedHashMap<>(); |
| | | charts.put("pendingAmountBarOption", buildPendingAmountBarOption(sorted)); |
| | | charts.put("priorityPieOption", buildPriorityPieOption(highPriorityCount, mediumPriorityCount, lowPriorityCount)); |
| | | |
| | | return jsonResponse(true, "sales_collection_quote_strategy", "å·²çæåæ¬¾ä¸æ¥ä»·çç¥å»ºè®®", summary, Map.of("items", items), charts); |
| | | } |
| | | |
| | | private List<CustomerRiskMetric> buildCustomerRiskMetrics(LoginUser loginUser, DateRange range, String keyword) { |
| | | List<SalesLedger> ledgers = querySalesLedgers(loginUser, range).stream() |
| | | .filter(item -> matchLedgerCustomerKeyword(item, keyword)) |
| | | .collect(Collectors.toList()); |
| | | if (ledgers.isEmpty()) { |
| | | return List.of(); |
| | | } |
| | | |
| | | Map<String, CustomerRiskMetric> metricMap = new LinkedHashMap<>(); |
| | | for (SalesLedger ledger : ledgers) { |
| | | String customerName = StringUtils.hasText(ledger.getCustomerName()) ? ledger.getCustomerName().trim() : "æªç¥å®¢æ·"; |
| | | CustomerRiskMetric metric = metricMap.computeIfAbsent(customerName, CustomerRiskMetric::new); |
| | | metric.setOrderCount(metric.getOrderCount() + 1); |
| | | metric.setContractAmount(metric.getContractAmount().add(defaultDecimal(ledger.getContractAmount()))); |
| | | metric.setTopSingleOrderAmount(metric.getTopSingleOrderAmount().max(defaultDecimal(ledger.getContractAmount()))); |
| | | LocalDate entryDate = toLocalDate(ledger.getEntryDate()); |
| | | if (entryDate != null && (metric.getLastOrderDate() == null || entryDate.isAfter(metric.getLastOrderDate()))) { |
| | | metric.setLastOrderDate(entryDate); |
| | | } |
| | | if (ledger.getId() != null) { |
| | | metric.getLedgerIds().add(ledger.getId()); |
| | | if (ledger.getDeliveryDate() != null) { |
| | | metric.getDeliveryDateByLedgerId().put(ledger.getId(), ledger.getDeliveryDate()); |
| | | } |
| | | } |
| | | } |
| | | |
| | | List<Long> allLedgerIds = metricMap.values().stream() |
| | | .flatMap(metric -> metric.getLedgerIds().stream()) |
| | | .distinct() |
| | | .collect(Collectors.toList()); |
| | | Map<Long, BigDecimal> receiptAmountByLedgerId = sumReceiptAmounts(loginUser, allLedgerIds); |
| | | Map<Long, List<ShippingInfo>> shippingByLedgerId = queryShippingsByLedgerIds(loginUser, allLedgerIds).stream() |
| | | .collect(Collectors.groupingBy(ShippingInfo::getSalesLedgerId)); |
| | | |
| | | List<SalesQuotation> quotations = querySalesQuotations(loginUser, range); |
| | | for (SalesQuotation quotation : quotations) { |
| | | String customerName = safe(quotation.getCustomer()); |
| | | CustomerRiskMetric metric = metricMap.get(customerName); |
| | | if (metric == null) { |
| | | continue; |
| | | } |
| | | metric.setQuoteCount(metric.getQuoteCount() + 1); |
| | | metric.setQuoteAmount(metric.getQuoteAmount().add(defaultDecimal(quotation.getTotalAmount()))); |
| | | } |
| | | |
| | | LocalDate today = LocalDate.now(); |
| | | for (CustomerRiskMetric metric : metricMap.values()) { |
| | | BigDecimal receivedAmount = BigDecimal.ZERO; |
| | | long overdueDeliveryCount = 0; |
| | | for (Long ledgerId : metric.getLedgerIds()) { |
| | | receivedAmount = receivedAmount.add(receiptAmountByLedgerId.getOrDefault(ledgerId, BigDecimal.ZERO)); |
| | | LocalDate deliveryDate = metric.getDeliveryDateByLedgerId().get(ledgerId); |
| | | if (deliveryDate != null && deliveryDate.isBefore(today) && !isLedgerFullyShipped(ledgerId, shippingByLedgerId)) { |
| | | overdueDeliveryCount++; |
| | | } |
| | | } |
| | | metric.setReceivedAmount(receivedAmount); |
| | | metric.setPendingAmount(maxZero(metric.getContractAmount().subtract(receivedAmount))); |
| | | if (metric.getContractAmount().compareTo(BigDecimal.ZERO) > 0) { |
| | | metric.setPendingRate(metric.getPendingAmount() |
| | | .divide(metric.getContractAmount(), 4, RoundingMode.HALF_UP)); |
| | | } else { |
| | | metric.setPendingRate(BigDecimal.ZERO); |
| | | } |
| | | metric.setOverdueDeliveryCount(overdueDeliveryCount); |
| | | if (metric.getLastOrderDate() == null) { |
| | | metric.setDaysSinceLastOrder(999); |
| | | } else { |
| | | metric.setDaysSinceLastOrder(Math.max(today.toEpochDay() - metric.getLastOrderDate().toEpochDay(), 0)); |
| | | } |
| | | evaluateRiskMetric(metric); |
| | | } |
| | | return new ArrayList<>(metricMap.values()); |
| | | } |
| | | |
| | | private void evaluateRiskMetric(CustomerRiskMetric metric) { |
| | | int score = 0; |
| | | List<String> reasons = new ArrayList<>(); |
| | | if (metric.getDaysSinceLastOrder() >= 90) { |
| | | score += 35; |
| | | reasons.add("è¿90å¤©æ æ°å¢è®¢å"); |
| | | } else if (metric.getDaysSinceLastOrder() >= 60) { |
| | | score += 25; |
| | | reasons.add("è¿60å¤©è®¢åæ´»è·åº¦ä¸é"); |
| | | } else if (metric.getDaysSinceLastOrder() >= 30) { |
| | | score += 12; |
| | | reasons.add("è¿30å¤©è®¢åæ³¢å¨åå¼±"); |
| | | } |
| | | |
| | | if (metric.getPendingRate().compareTo(new BigDecimal("0.60")) >= 0) { |
| | | score += 30; |
| | | reasons.add("å¾
忬¾å æ¯é«äº60%"); |
| | | } else if (metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) { |
| | | score += 20; |
| | | reasons.add("å¾
忬¾å æ¯é«äº30%"); |
| | | } else if (metric.getPendingRate().compareTo(new BigDecimal("0.10")) >= 0) { |
| | | score += 10; |
| | | reasons.add("åå¨å¾
忬¾é£é©"); |
| | | } |
| | | |
| | | if (metric.getOverdueDeliveryCount() > 0) { |
| | | score += Math.min((int) metric.getOverdueDeliveryCount() * 6, 20); |
| | | reasons.add("åå¨äº¤æé¾æè®¢å"); |
| | | } |
| | | |
| | | if (metric.getOrderCount() <= 1) { |
| | | score += 8; |
| | | reasons.add("订ååºæ°åä½"); |
| | | } |
| | | |
| | | if (metric.getQuoteCount() > 0 && metric.getOrderCount() == 0) { |
| | | score += 10; |
| | | reasons.add("æ¥ä»·æªå½¢æè®¢å转å"); |
| | | } |
| | | |
| | | score = Math.min(score, 100); |
| | | metric.setRiskScore(score); |
| | | if (score >= 70) { |
| | | metric.setRiskLevel("high"); |
| | | } else if (score >= 40) { |
| | | metric.setRiskLevel("medium"); |
| | | } else { |
| | | metric.setRiskLevel("low"); |
| | | } |
| | | metric.setRiskReasons(reasons); |
| | | } |
| | | |
| | | private Map<String, Object> toRiskItem(CustomerRiskMetric metric) { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("customerName", metric.getCustomerName()); |
| | | map.put("riskLevel", metric.getRiskLevel()); |
| | | map.put("riskScore", metric.getRiskScore()); |
| | | map.put("contractAmount", metric.getContractAmount()); |
| | | map.put("receivedAmount", metric.getReceivedAmount()); |
| | | map.put("pendingAmount", metric.getPendingAmount()); |
| | | map.put("pendingRate", toPercent(metric.getPendingRate())); |
| | | map.put("orderCount", metric.getOrderCount()); |
| | | map.put("quoteCount", metric.getQuoteCount()); |
| | | map.put("overdueDeliveryCount", metric.getOverdueDeliveryCount()); |
| | | map.put("daysSinceLastOrder", metric.getDaysSinceLastOrder()); |
| | | map.put("lastOrderDate", formatDate(metric.getLastOrderDate())); |
| | | map.put("riskReasons", metric.getRiskReasons()); |
| | | return map; |
| | | } |
| | | |
| | | private Map<String, Object> toStrategyItem(CustomerRiskMetric metric) { |
| | | String priority = strategyPriority(metric); |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("customerName", metric.getCustomerName()); |
| | | map.put("riskLevel", metric.getRiskLevel()); |
| | | map.put("riskScore", metric.getRiskScore()); |
| | | map.put("priority", priority); |
| | | map.put("pendingAmount", metric.getPendingAmount()); |
| | | map.put("pendingRate", toPercent(metric.getPendingRate())); |
| | | map.put("quoteCount", metric.getQuoteCount()); |
| | | map.put("orderCount", metric.getOrderCount()); |
| | | map.put("quoteConversionRate", toRate(metric.getOrderCount(), Math.max(metric.getQuoteCount(), 1))); |
| | | map.put("collectionStrategy", buildCollectionStrategy(metric)); |
| | | map.put("quotationStrategy", buildQuotationStrategy(metric)); |
| | | map.put("nextAction", buildNextAction(priority)); |
| | | map.put("topSingleOrderAmount", metric.getTopSingleOrderAmount()); |
| | | return map; |
| | | } |
| | | |
| | | private String buildCollectionStrategy(CustomerRiskMetric metric) { |
| | | if (metric.getPendingAmount().compareTo(BigDecimal.ZERO) <= 0) { |
| | | return "ä¿ææ£å¸¸æåº¦å¯¹è´¦ä¸å款确认ï¼ç»´æå®¢æ·å款èå¥ã"; |
| | | } |
| | | if (metric.getPendingRate().compareTo(new BigDecimal("0.60")) >= 0) { |
| | | return "ä¼å
éå®å款计åï¼æå¨æå忬¾èç¹å¹¶ç»å®åè´§æ¡ä»¶ï¼é¿å
æ°å¢ä¿¡ç¨æå£ã"; |
| | | } |
| | | if (metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) { |
| | | return "建议æ§è¡åå¨å¬æ¶æºå¶ï¼åæ¥è´¢å¡ä¸ä¸å¡èåè·è¿éç¹ååã"; |
| | | } |
| | | return "ä¿ææ£å¸¸å¬æ¶èå¥ï¼æååèç¹æå3天æé客æ·ä»æ¬¾ã"; |
| | | } |
| | | |
| | | private String buildQuotationStrategy(CustomerRiskMetric metric) { |
| | | if ("high".equals(metric.getRiskLevel())) { |
| | | return "æ¥ä»·ä¼å
ä¿æ¯å©ä¸åæ¬¾æ¡æ¬¾ï¼åå°è¶
é¿è´¦æï¼å¿
è¦æ¶éç¨åé¶æ®µæ¥ä»·ã"; |
| | | } |
| | | if (metric.getQuoteCount() > 0 && metric.getOrderCount() < metric.getQuoteCount()) { |
| | | return "ä¼åæ¥ä»·ç»æï¼å»ºè®®æä¾åºç¡ç+å级çç»åæ¥ä»·ï¼æé«è½¬åçã"; |
| | | } |
| | | if (metric.getOrderCount() <= 1) { |
| | | return "å å¼ºéæ±ææï¼å´ç»å®¢æ·åºæ¯è¡¥å
å¢å¼é¡¹ä¸äº¤ä»ä¿éæ¡æ¬¾ã"; |
| | | } |
| | | return "ä¿æå½åæ¥ä»·çç¥ï¼éç¹å´ç»äº¤æåæå¡è½ååå·®å¼ååç°ã"; |
| | | } |
| | | |
| | | private String buildNextAction(String priority) { |
| | | return switch (priority) { |
| | | case "high" -> "48å°æ¶å
宿客æ·å访ï¼ç¡®è®¤å款计å并夿 ¸æ¥ä»·æææã"; |
| | | case "medium" -> "æ¬å¨å
宿客æ·éæ±å¤çï¼æ´æ°æ¥ä»·çæ¬å¹¶åæ¥å款èç¹ã"; |
| | | default -> "ä¿ææåº¦ä¾è¡è·è¿ï¼æç»è¿½è¸ªå®¢æ·éè´è®¡åååã"; |
| | | }; |
| | | } |
| | | |
| | | private String strategyPriority(CustomerRiskMetric metric) { |
| | | if ("high".equals(metric.getRiskLevel()) || metric.getPendingRate().compareTo(new BigDecimal("0.50")) >= 0) { |
| | | return "high"; |
| | | } |
| | | if ("medium".equals(metric.getRiskLevel()) || metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) { |
| | | return "medium"; |
| | | } |
| | | return "low"; |
| | | } |
| | | |
| | | private int riskLevelRank(String riskLevel) { |
| | | if ("high".equals(riskLevel)) { |
| | | return 3; |
| | | } |
| | | if ("medium".equals(riskLevel)) { |
| | | return 2; |
| | | } |
| | | return 1; |
| | | } |
| | | |
| | | private List<Map<String, Object>> buildTopCustomers(List<SalesLedger> ledgers) { |
| | | Map<String, BigDecimal> grouped = new LinkedHashMap<>(); |
| | | for (SalesLedger ledger : ledgers) { |
| | | String customerName = StringUtils.hasText(ledger.getCustomerName()) ? ledger.getCustomerName().trim() : "æªç¥å®¢æ·"; |
| | | grouped.merge(customerName, defaultDecimal(ledger.getContractAmount()), BigDecimal::add); |
| | | } |
| | | return grouped.entrySet().stream() |
| | | .sorted(Map.Entry.<String, BigDecimal>comparingByValue().reversed()) |
| | | .limit(5) |
| | | .map(entry -> { |
| | | Map<String, Object> map = new LinkedHashMap<>(); |
| | | map.put("customerName", entry.getKey()); |
| | | map.put("contractAmount", entry.getValue()); |
| | | return map; |
| | | }) |
| | | .collect(Collectors.toList()); |
| | | } |
| | | |
| | | private TrendData buildContractTrendData(List<SalesLedger> ledgers, DateRange range) { |
| | | Map<String, BigDecimal> amountByMonth = new LinkedHashMap<>(); |
| | | YearMonth startMonth = YearMonth.from(range.start()); |
| | | YearMonth endMonth = YearMonth.from(range.end()); |
| | | for (YearMonth month = startMonth; !month.isAfter(endMonth); month = month.plusMonths(1)) { |
| | | amountByMonth.put(month.toString(), BigDecimal.ZERO); |
| | | } |
| | | for (SalesLedger ledger : ledgers) { |
| | | LocalDate entryDate = toLocalDate(ledger.getEntryDate()); |
| | | if (entryDate == null) { |
| | | continue; |
| | | } |
| | | String monthKey = YearMonth.from(entryDate).toString(); |
| | | if (!amountByMonth.containsKey(monthKey)) { |
| | | continue; |
| | | } |
| | | amountByMonth.put(monthKey, amountByMonth.get(monthKey).add(defaultDecimal(ledger.getContractAmount()))); |
| | | } |
| | | return new TrendData(new ArrayList<>(amountByMonth.keySet()), new ArrayList<>(amountByMonth.values())); |
| | | } |
| | | |
| | | private List<SalesLedger> querySalesLedgers(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<SalesLedger> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), SalesLedger::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesLedger::getDeptId); |
| | | if (range != null) { |
| | | wrapper.ge(SalesLedger::getEntryDate, toDate(range.start())) |
| | | .lt(SalesLedger::getEntryDate, toExclusiveEndDate(range.end())); |
| | | } |
| | | return defaultList(salesLedgerMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private List<SalesQuotation> querySalesQuotations(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<SalesQuotation> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), SalesQuotation::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), SalesQuotation::getDeptId); |
| | | if (range != null) { |
| | | wrapper.ge(SalesQuotation::getQuotationDate, range.start()) |
| | | .le(SalesQuotation::getQuotationDate, range.end()); |
| | | } |
| | | return defaultList(salesQuotationMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private List<ShippingInfo> queryShippings(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId); |
| | | if (range != null) { |
| | | wrapper.ge(ShippingInfo::getShippingDate, toDate(range.start())) |
| | | .lt(ShippingInfo::getShippingDate, toExclusiveEndDate(range.end())); |
| | | } |
| | | return defaultList(shippingInfoMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private List<ReceiptPayment> queryReceipts(LoginUser loginUser, DateRange range) { |
| | | LambdaQueryWrapper<ReceiptPayment> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), ReceiptPayment::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ReceiptPayment::getDeptId); |
| | | if (range != null) { |
| | | wrapper.ge(ReceiptPayment::getReceiptPaymentDate, range.start()) |
| | | .le(ReceiptPayment::getReceiptPaymentDate, range.end()); |
| | | } |
| | | return defaultList(receiptPaymentMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private List<ReceiptPayment> queryReceiptsByLedgerIds(LoginUser loginUser, List<Long> ledgerIds) { |
| | | if (ledgerIds == null || ledgerIds.isEmpty()) { |
| | | return List.of(); |
| | | } |
| | | LambdaQueryWrapper<ReceiptPayment> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), ReceiptPayment::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ReceiptPayment::getDeptId); |
| | | wrapper.in(ReceiptPayment::getSalesLedgerId, ledgerIds); |
| | | return defaultList(receiptPaymentMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private List<ShippingInfo> queryShippingsByLedgerIds(LoginUser loginUser, List<Long> ledgerIds) { |
| | | if (ledgerIds == null || ledgerIds.isEmpty()) { |
| | | return List.of(); |
| | | } |
| | | LambdaQueryWrapper<ShippingInfo> wrapper = new LambdaQueryWrapper<>(); |
| | | applyTenantFilter(wrapper, loginUser.getTenantId(), ShippingInfo::getTenantId); |
| | | applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ShippingInfo::getDeptId); |
| | | wrapper.in(ShippingInfo::getSalesLedgerId, ledgerIds); |
| | | return defaultList(shippingInfoMapper.selectList(wrapper)); |
| | | } |
| | | |
| | | private Map<Long, BigDecimal> sumInvoiceAmounts(List<Long> ledgerIds) { |
| | | if (ledgerIds == null || ledgerIds.isEmpty()) { |
| | | return Map.of(); |
| | | } |
| | | Map<Long, BigDecimal> result = new HashMap<>(); |
| | | for (InvoiceLedgerDto item : defaultList(invoiceLedgerMapper.invoicedTotal(ledgerIds))) { |
| | | if (item.getSalesLedgerId() == null) { |
| | | continue; |
| | | } |
| | | result.merge(item.getSalesLedgerId().longValue(), defaultDecimal(item.getInvoiceTotal()), BigDecimal::add); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private Map<Long, BigDecimal> sumReceiptAmounts(LoginUser loginUser, List<Long> ledgerIds) { |
| | | Map<Long, BigDecimal> result = new HashMap<>(); |
| | | for (ReceiptPayment item : queryReceiptsByLedgerIds(loginUser, ledgerIds)) { |
| | | if (item.getSalesLedgerId() == null) { |
| | | continue; |
| | | } |
| | | result.merge(item.getSalesLedgerId(), defaultDecimal(item.getReceiptPaymentAmount()), BigDecimal::add); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private boolean isLedgerFullyShipped(Long ledgerId, Map<Long, List<ShippingInfo>> shippingByLedgerId) { |
| | | List<ShippingInfo> shippingInfos = shippingByLedgerId.get(ledgerId); |
| | | if (shippingInfos == null || shippingInfos.isEmpty()) { |
| | | return false; |
| | | } |
| | | return shippingInfos.stream().allMatch(item -> isShippedStatus(item.getStatus())); |
| | | } |
| | | |
| | | private String calcLedgerShippingStatus(List<ShippingInfo> shippingInfos) { |
| | | if (shippingInfos == null || shippingInfos.isEmpty()) { |
| | | return "æªåè´§"; |
| | | } |
| | | long shippedCount = shippingInfos.stream().filter(item -> isShippedStatus(item.getStatus())).count(); |
| | | if (shippedCount == 0) { |
| | | return "å¾
åè´§"; |
| | | } |
| | | if (shippedCount == shippingInfos.size()) { |
| | | return "å·²åè´§"; |
| | | } |
| | | return "é¨ååè´§"; |
| | | } |
| | | |
| | | private boolean isShippedStatus(String status) { |
| | | return StringUtils.hasText(status) && status.contains("å·²åè´§"); |
| | | } |
| | | |
| | | private boolean matchCustomerKeyword(CustomerVo customer, String keyword) { |
| | | if (!StringUtils.hasText(keyword)) { |
| | | return true; |
| | | } |
| | | String text = keyword.trim(); |
| | | return safe(customer.getCustomerName()).contains(text) |
| | | || safe(customer.getContactPerson()).contains(text) |
| | | || safe(customer.getContactPhone()).contains(text) |
| | | || safe(customer.getCompanyPhone()).contains(text) |
| | | || safe(customer.getUsageUserName()).contains(text); |
| | | } |
| | | |
| | | private boolean matchInteractionKeyword(ReceiptPayment payment, SalesLedger ledger, String keyword) { |
| | | if (!StringUtils.hasText(keyword)) { |
| | | return true; |
| | | } |
| | | String text = keyword.trim(); |
| | | return safe(payment.getRegistrant()).contains(text) |
| | | || (ledger != null && (safe(ledger.getCustomerName()).contains(text) |
| | | || safe(ledger.getSalesContractNo()).contains(text) |
| | | || safe(ledger.getProjectName()).contains(text))); |
| | | } |
| | | |
| | | private boolean matchLedgerCustomerKeyword(SalesLedger ledger, String keyword) { |
| | | if (!StringUtils.hasText(keyword)) { |
| | | return true; |
| | | } |
| | | String text = keyword.trim(); |
| | | return safe(ledger.getCustomerName()).contains(text) |
| | | || safe(ledger.getSalesContractNo()).contains(text) |
| | | || safe(ledger.getProjectName()).contains(text); |
| | | } |
| | | |
| | | private Integer normalizeSeaType(String seaType) { |
| | | if (!StringUtils.hasText(seaType)) { |
| | | return null; |
| | | } |
| | | String value = seaType.trim().toLowerCase(Locale.ROOT); |
| | | return switch (value) { |
| | | case "private", "ç§æµ·", "0" -> 0; |
| | | case "public", "å
¬æµ·", "1" -> 1; |
| | | default -> null; |
| | | }; |
| | | } |
| | | |
| | | private String customerSeaTypeName(Integer type) { |
| | | if (type == null) { |
| | | return "æªç¥"; |
| | | } |
| | | return type == 1 ? "å
¬æµ·" : "ç§æµ·"; |
| | | } |
| | | |
| | | private int normalizeLimit(Integer limit) { |
| | | if (limit == null || limit <= 0) { |
| | | return DEFAULT_LIMIT; |
| | | } |
| | | return Math.min(limit, MAX_LIMIT); |
| | | } |
| | | |
| | | private boolean tenantMatched(Long dataTenantId, Long userTenantId) { |
| | | if (userTenantId == null) { |
| | | return true; |
| | | } |
| | | return Objects.equals(dataTenantId, userTenantId); |
| | | } |
| | | |
| | | 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 DateRange resolveDateRange(String startDate, String endDate, String timeRange) { |
| | | LocalDate today = LocalDate.now(); |
| | | LocalDate explicitStart = parseLocalDate(startDate); |
| | | LocalDate explicitEnd = parseLocalDate(endDate); |
| | | if (explicitStart != null || explicitEnd != null) { |
| | | LocalDate start = explicitStart != null ? explicitStart : explicitEnd; |
| | | LocalDate end = explicitEnd != null ? explicitEnd : explicitStart; |
| | | if (start.isAfter(end)) { |
| | | LocalDate temp = start; |
| | | start = end; |
| | | end = temp; |
| | | } |
| | | return new DateRange(start, end, start + "è³" + end); |
| | | } |
| | | 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("æ¨å¤©") || text.contains("æ¨æ¥")) { |
| | | LocalDate day = today.minusDays(1); |
| | | return new DateRange(day, day, "æ¨å¤©"); |
| | | } |
| | | if (text.contains("æ¬å¨")) { |
| | | LocalDate start = today.minusDays(today.getDayOfWeek().getValue() - 1L); |
| | | return new DateRange(start, today, "æ¬å¨"); |
| | | } |
| | | if (text.contains("ä¸å¨")) { |
| | | LocalDate thisWeekStart = today.minusDays(today.getDayOfWeek().getValue() - 1L); |
| | | LocalDate start = thisWeekStart.minusWeeks(1); |
| | | LocalDate end = thisWeekStart.minusDays(1); |
| | | return new DateRange(start, end, "ä¸å¨"); |
| | | } |
| | | if (text.contains("æ¬æ")) { |
| | | return new DateRange(today.withDayOfMonth(1), today, "æ¬æ"); |
| | | } |
| | | if (text.contains("䏿")) { |
| | | YearMonth lastMonth = YearMonth.from(today).minusMonths(1); |
| | | return new DateRange(lastMonth.atDay(1), lastMonth.atEndOfMonth(), "䏿"); |
| | | } |
| | | if (text.contains("ä»å¹´") || text.contains("æ¬å¹´")) { |
| | | return new DateRange(today.withDayOfYear(1), today, "ä»å¹´"); |
| | | } |
| | | if (text.contains("å»å¹´")) { |
| | | LocalDate start = today.minusYears(1).withDayOfYear(1); |
| | | LocalDate end = today.minusYears(1).withMonth(12).withDayOfMonth(31); |
| | | return new DateRange(start, end, "å»å¹´"); |
| | | } |
| | | Matcher relativeMatcher = RELATIVE_PATTERN.matcher(text); |
| | | if (relativeMatcher.find()) { |
| | | int amount = Integer.parseInt(relativeMatcher.group(2)); |
| | | String unit = relativeMatcher.group(3); |
| | | LocalDate start = switch (unit) { |
| | | case "天" -> today.minusDays(Math.max(amount - 1L, 0)); |
| | | case "å¨" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1); |
| | | case "个æ", "æ" -> today.minusMonths(Math.max(amount, 1)).plusDays(1); |
| | | case "å¹´" -> today.minusYears(Math.max(amount, 1)).plusDays(1); |
| | | default -> today.minusDays(29); |
| | | }; |
| | | return new DateRange(start, today, "è¿" + amount + unit); |
| | | } |
| | | Matcher dateMatcher = DATE_PATTERN.matcher(text); |
| | | if (dateMatcher.find()) { |
| | | LocalDate start = parseLocalDate(dateMatcher.group(1)); |
| | | LocalDate end = dateMatcher.find() ? parseLocalDate(dateMatcher.group(1)) : start; |
| | | if (start != null && end != null) { |
| | | if (start.isAfter(end)) { |
| | | LocalDate temp = start; |
| | | start = end; |
| | | end = temp; |
| | | } |
| | | return new DateRange(start, end, start + "è³" + end); |
| | | } |
| | | } |
| | | return new DateRange(today.minusDays(29), today, "è¿30天"); |
| | | } |
| | | |
| | | private LocalDate parseLocalDate(String text) { |
| | | if (!StringUtils.hasText(text)) { |
| | | return null; |
| | | } |
| | | try { |
| | | return LocalDate.parse(text.trim(), DATE_FMT); |
| | | } catch (Exception ignored) { |
| | | return null; |
| | | } |
| | | } |
| | | |
| | | private Date toDate(LocalDate localDate) { |
| | | return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); |
| | | } |
| | | |
| | | private Date toExclusiveEndDate(LocalDate localDate) { |
| | | return Date.from(localDate.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant()); |
| | | } |
| | | |
| | | private LocalDate toLocalDate(Date date) { |
| | | if (date == null) { |
| | | return null; |
| | | } |
| | | return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); |
| | | } |
| | | |
| | | private String formatDate(Date date) { |
| | | LocalDate localDate = toLocalDate(date); |
| | | return formatDate(localDate); |
| | | } |
| | | |
| | | private String formatDate(LocalDate date) { |
| | | return date == null ? "" : date.format(DATE_FMT); |
| | | } |
| | | |
| | | private String formatDateTime(LocalDateTime time) { |
| | | return time == null ? "" : time.toString().replace('T', ' '); |
| | | } |
| | | |
| | | private BigDecimal defaultDecimal(BigDecimal value) { |
| | | return value == null ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private BigDecimal maxZero(BigDecimal value) { |
| | | return value == null || value.compareTo(BigDecimal.ZERO) < 0 ? BigDecimal.ZERO : value; |
| | | } |
| | | |
| | | private String toRate(long numerator, long denominator) { |
| | | if (denominator <= 0) { |
| | | return "0.00%"; |
| | | } |
| | | BigDecimal rate = new BigDecimal(numerator) |
| | | .multiply(ONE_HUNDRED) |
| | | .divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP); |
| | | return rate.toPlainString() + "%"; |
| | | } |
| | | |
| | | private String toPercent(BigDecimal decimal) { |
| | | if (decimal == null) { |
| | | return "0.00%"; |
| | | } |
| | | BigDecimal rate = decimal.multiply(ONE_HUNDRED).setScale(2, RoundingMode.HALF_UP); |
| | | return rate.toPlainString() + "%"; |
| | | } |
| | | |
| | | private String safe(Object value) { |
| | | return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ').trim(); |
| | | } |
| | | |
| | | private <T> List<T> defaultList(List<T> list) { |
| | | return list == null ? List.of() : list; |
| | | } |
| | | |
| | | 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> buildAmountBarOption(BigDecimal contractAmount, |
| | | BigDecimal quotationAmount, |
| | | BigDecimal receivedAmount, |
| | | BigDecimal pendingAmount) { |
| | | List<String> xData = List.of("ååé¢", "æ¥ä»·é¢", "忬¾é¢", "å¾
忬¾"); |
| | | List<BigDecimal> yData = List.of(contractAmount, quotationAmount, receivedAmount, pendingAmount); |
| | | 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> buildShippingPieOption(long shippedCount, long pendingCount) { |
| | | List<Map<String, Object>> data = List.of( |
| | | Map.of("name", "å·²åè´§", "value", shippedCount), |
| | | Map.of("name", "æªåè´§", "value", pendingCount) |
| | | ); |
| | | 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("type", "pie", "radius", "60%", "data", data))); |
| | | return option; |
| | | } |
| | | |
| | | private Map<String, Object> buildCustomerTopBarOption(List<Map<String, Object>> topCustomers) { |
| | | List<String> xData = new ArrayList<>(); |
| | | List<BigDecimal> yData = new ArrayList<>(); |
| | | for (Map<String, Object> item : topCustomers) { |
| | | xData.add(String.valueOf(item.get("customerName"))); |
| | | yData.add((BigDecimal) item.get("contractAmount")); |
| | | } |
| | | Map<String, Object> option = new LinkedHashMap<>(); |
| | | option.put("title", Map.of("text", "客æ·ååé¢TOP5", "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> buildContractTrendLineOption(List<String> labels, List<BigDecimal> values) { |
| | | 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", labels)); |
| | | option.put("yAxis", Map.of("type", "value")); |
| | | option.put("series", List.of(Map.of("name", "ååé¢", "type", "line", "smooth", true, "data", values))); |
| | | return option; |
| | | } |
| | | |
| | | private Map<String, Object> buildRiskLevelPieOption(long highCount, long mediumCount, long lowCount) { |
| | | List<Map<String, Object>> data = List.of( |
| | | Map.of("name", "é«é£é©", "value", highCount), |
| | | Map.of("name", "ä¸é£é©", "value", mediumCount), |
| | | Map.of("name", "ä½é£é©", "value", lowCount) |
| | | ); |
| | | 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 Map<String, Object> buildRiskScoreBarOption(List<CustomerRiskMetric> metrics) { |
| | | List<String> xData = metrics.stream().map(CustomerRiskMetric::getCustomerName).collect(Collectors.toList()); |
| | | List<Integer> yData = metrics.stream().map(CustomerRiskMetric::getRiskScore).collect(Collectors.toList()); |
| | | 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", "max", 100)); |
| | | option.put("series", List.of(Map.of("name", "é£é©åå¼", "type", "bar", "data", yData))); |
| | | return option; |
| | | } |
| | | |
| | | private Map<String, Object> buildPendingAmountBarOption(List<CustomerRiskMetric> metrics) { |
| | | List<String> xData = metrics.stream().map(CustomerRiskMetric::getCustomerName).collect(Collectors.toList()); |
| | | List<BigDecimal> yData = metrics.stream().map(CustomerRiskMetric::getPendingAmount).collect(Collectors.toList()); |
| | | 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> buildPriorityPieOption(long high, long medium, long low) { |
| | | List<Map<String, Object>> data = List.of( |
| | | Map.of("name", "é«ä¼å
级", "value", high), |
| | | Map.of("name", "ä¸ä¼å
级", "value", medium), |
| | | Map.of("name", "ä½ä¼å
级", "value", low) |
| | | ); |
| | | 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 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) { |
| | | } |
| | | |
| | | private record TrendData(List<String> labels, List<BigDecimal> values) { |
| | | private List<Map<String, Object>> toItemList() { |
| | | List<Map<String, Object>> items = new LinkedList<>(); |
| | | for (int i = 0; i < labels.size(); i++) { |
| | | Map<String, Object> item = new LinkedHashMap<>(); |
| | | item.put("month", labels.get(i)); |
| | | item.put("amount", values.get(i)); |
| | | items.add(item); |
| | | } |
| | | return items; |
| | | } |
| | | } |
| | | |
| | | private static class CustomerRiskMetric { |
| | | private final String customerName; |
| | | private final List<Long> ledgerIds = new ArrayList<>(); |
| | | private final Map<Long, LocalDate> deliveryDateByLedgerId = new HashMap<>(); |
| | | private BigDecimal contractAmount = BigDecimal.ZERO; |
| | | private BigDecimal receivedAmount = BigDecimal.ZERO; |
| | | private BigDecimal pendingAmount = BigDecimal.ZERO; |
| | | private BigDecimal pendingRate = BigDecimal.ZERO; |
| | | private BigDecimal quoteAmount = BigDecimal.ZERO; |
| | | private BigDecimal topSingleOrderAmount = BigDecimal.ZERO; |
| | | private int orderCount; |
| | | private int quoteCount; |
| | | private LocalDate lastOrderDate; |
| | | private long daysSinceLastOrder; |
| | | private long overdueDeliveryCount; |
| | | private int riskScore; |
| | | private String riskLevel = "low"; |
| | | private List<String> riskReasons = new ArrayList<>(); |
| | | |
| | | private CustomerRiskMetric(String customerName) { |
| | | this.customerName = customerName; |
| | | } |
| | | |
| | | private String getCustomerName() { |
| | | return customerName; |
| | | } |
| | | |
| | | private List<Long> getLedgerIds() { |
| | | return ledgerIds; |
| | | } |
| | | |
| | | private Map<Long, LocalDate> getDeliveryDateByLedgerId() { |
| | | return deliveryDateByLedgerId; |
| | | } |
| | | |
| | | private BigDecimal getContractAmount() { |
| | | return contractAmount; |
| | | } |
| | | |
| | | private void setContractAmount(BigDecimal contractAmount) { |
| | | this.contractAmount = contractAmount; |
| | | } |
| | | |
| | | private BigDecimal getReceivedAmount() { |
| | | return receivedAmount; |
| | | } |
| | | |
| | | private void setReceivedAmount(BigDecimal receivedAmount) { |
| | | this.receivedAmount = receivedAmount; |
| | | } |
| | | |
| | | private BigDecimal getPendingAmount() { |
| | | return pendingAmount; |
| | | } |
| | | |
| | | private void setPendingAmount(BigDecimal pendingAmount) { |
| | | this.pendingAmount = pendingAmount; |
| | | } |
| | | |
| | | private BigDecimal getPendingRate() { |
| | | return pendingRate; |
| | | } |
| | | |
| | | private void setPendingRate(BigDecimal pendingRate) { |
| | | this.pendingRate = pendingRate; |
| | | } |
| | | |
| | | private BigDecimal getQuoteAmount() { |
| | | return quoteAmount; |
| | | } |
| | | |
| | | private void setQuoteAmount(BigDecimal quoteAmount) { |
| | | this.quoteAmount = quoteAmount; |
| | | } |
| | | |
| | | private BigDecimal getTopSingleOrderAmount() { |
| | | return topSingleOrderAmount; |
| | | } |
| | | |
| | | private void setTopSingleOrderAmount(BigDecimal topSingleOrderAmount) { |
| | | this.topSingleOrderAmount = topSingleOrderAmount; |
| | | } |
| | | |
| | | private int getOrderCount() { |
| | | return orderCount; |
| | | } |
| | | |
| | | private void setOrderCount(int orderCount) { |
| | | this.orderCount = orderCount; |
| | | } |
| | | |
| | | private int getQuoteCount() { |
| | | return quoteCount; |
| | | } |
| | | |
| | | private void setQuoteCount(int quoteCount) { |
| | | this.quoteCount = quoteCount; |
| | | } |
| | | |
| | | private LocalDate getLastOrderDate() { |
| | | return lastOrderDate; |
| | | } |
| | | |
| | | private void setLastOrderDate(LocalDate lastOrderDate) { |
| | | this.lastOrderDate = lastOrderDate; |
| | | } |
| | | |
| | | private long getDaysSinceLastOrder() { |
| | | return daysSinceLastOrder; |
| | | } |
| | | |
| | | private void setDaysSinceLastOrder(long daysSinceLastOrder) { |
| | | this.daysSinceLastOrder = daysSinceLastOrder; |
| | | } |
| | | |
| | | private long getOverdueDeliveryCount() { |
| | | return overdueDeliveryCount; |
| | | } |
| | | |
| | | private void setOverdueDeliveryCount(long overdueDeliveryCount) { |
| | | this.overdueDeliveryCount = overdueDeliveryCount; |
| | | } |
| | | |
| | | private int getRiskScore() { |
| | | return riskScore; |
| | | } |
| | | |
| | | private void setRiskScore(int riskScore) { |
| | | this.riskScore = riskScore; |
| | | } |
| | | |
| | | private String getRiskLevel() { |
| | | return riskLevel; |
| | | } |
| | | |
| | | private void setRiskLevel(String riskLevel) { |
| | | this.riskLevel = riskLevel; |
| | | } |
| | | |
| | | private List<String> getRiskReasons() { |
| | | return riskReasons; |
| | | } |
| | | |
| | | private void setRiskReasons(List<String> riskReasons) { |
| | | this.riskReasons = riskReasons; |
| | | } |
| | | } |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.SalesReturnVo; |
| | | import com.ruoyi.account.bean.dto.sales.SalesReturnDto; |
| | | import com.ruoyi.account.bean.vo.sales.SalesReturnVo; |
| | | import com.ruoyi.procurementrecord.bean.dto.ReturnManagementDto; |
| | | import com.ruoyi.procurementrecord.pojo.ReturnManagement; |
| | | import org.apache.ibatis.annotations.Param; |
| | |
| | | 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); |
| | |
| | | if (matchedOperation == null) { |
| | | matchedOperation = insertRoutingOperationSnapshot(orderRouting.getId(), productionOrderId, desiredOperation); |
| | | } else { |
| | | updateRoutingOperationSnapshotIfNecessary(matchedOperation, orderRouting.getId(), productionOrderId, desiredOperation); |
| | | updateRoutingOperationSnapshotIfNecessary(desiredOperation, orderRouting.getId(), productionOrderId, matchedOperation); |
| | | } |
| | | finalOperationList.add(matchedOperation); |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | private final SysUserMapper sysUserMapper; |
| | | private final ProductionOrderMapper productionOrderMapper; |
| | | private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper; |
| | | |
| | | private final FileUtil fileUtil; |
| | | |
| | |
| | | // å页æ¥è¯¢ç产工åºä»»å¡ |
| | | 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; |
| | | } |
| | |
| | | public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) { |
| | | // æ¥è¯¢å·¥åºä»»å¡å表 |
| | | List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class); |
| | | fillOperationTypes(result); |
| | | fillUserNames(result); |
| | | return result; |
| | | } |
| | |
| | | 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) { |
| | |
| | | @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())); |
| | | } |
| | | } |
| | | } |
| | |
| | | productionOrderBomMapper.insert(orderBom); |
| | | |
| | | Map<Long, Long> idMap = new HashMap<>(); |
| | | BigDecimal lastProcessDemandedQuantity = orderQuantity; |
| | | for (TechnologyBomStructure source : structureList) { |
| | | // åèç¹ parentId éè¦æ å°ææ°å¿«ç
§èç¹ idï¼æè½ä¿çåå§ BOM å±çº§ã |
| | | ProductionBomStructure target = new ProductionBomStructure(); |
| | |
| | | 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; |
| | | } |
| | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.PurchaseReturnVo; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseReturnDto; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo; |
| | | import com.ruoyi.purchase.dto.PurchaseReturnOrderDto; |
| | | import com.ruoyi.purchase.dto.PurchaseReturnOrderHasAllInfoDto; |
| | | import com.ruoyi.purchase.pojo.PurchaseReturnOrders; |
| | |
| | | import com.ruoyi.sales.pojo.SalesLedgerProduct; |
| | | import com.ruoyi.sales.service.ISalesLedgerService; |
| | | import com.ruoyi.stock.mapper.StockOutRecordMapper; |
| | | import com.ruoyi.stock.pojo.StockOutRecord; |
| | | import lombok.RequiredArgsConstructor; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | import java.util.List; |
| | | import java.util.stream.Collectors; |
| | | |
| | | /** |
| | | * <p> |
| | |
| | | updateWrapper.eq(PurchaseReturnOrderProducts::getPurchaseReturnOrderId, id); |
| | | purchaseReturnOrderProductsMapper.delete(updateWrapper); |
| | | //(éè´éè´§çæ°æ®éè¦å æ) |
| | | stockOutRecordMapper.delete(Wrappers.<StockOutRecord>lambdaQuery() |
| | | .eq(StockOutRecord::getRecordType,StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode()) |
| | | .in(StockOutRecord::getRecordId, purchaseReturnOrderProducts.stream().map(PurchaseReturnOrderProducts::getId).collect(Collectors.toList()))); |
| | | purchaseReturnOrderProducts.stream().forEach(purchaseReturnOrderProducts1 -> { |
| | | stockUtils.deleteStockOutRecord(purchaseReturnOrderProducts1.getId(),StockOutQualifiedRecordTypeEnum.PURCHASE_RETURN_STOCK_OUT.getCode()); |
| | | }); |
| | | // è´¢å¡ |
| | | LambdaUpdateWrapper<AccountIncome> updateWrapperAccountIncome = new LambdaUpdateWrapper<>(); |
| | | updateWrapperAccountIncome.eq(AccountIncome::getBusinessId, id); |
| | |
| | | /** |
| | | * æ°é |
| | | */ |
| | | @Excel(name = "æ°é") |
| | | @Excel(name = "æ»æ°é") |
| | | private BigDecimal quantity; |
| | | |
| | | @Excel(name = "åæ ¼æ°é") |
| | | @TableField("qualified_quantity") |
| | | private BigDecimal qualifiedQuantity; |
| | | |
| | | @Excel(name = "ä¸åæ ¼æ°é") |
| | | @TableField("unqualified_quantity") |
| | | private BigDecimal unqualifiedQuantity; |
| | | |
| | | /** |
| | | * æ£æµåä½ |
| | | */ |
| | |
| | | |
| | | @TableField(fill = FieldFill.INSERT) |
| | | private Long deptId; |
| | | |
| | | @Schema(description = "å
³è产ååå·id") |
| | | private Long productModelId; |
| | | } |
| | |
| | | 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; |
| | |
| | | |
| | | import java.io.InputStream; |
| | | import java.io.OutputStream; |
| | | import java.math.BigDecimal; |
| | | import java.net.URLEncoder; |
| | | import java.util.Arrays; |
| | | import java.util.HashMap; |
| | |
| | | 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()); |
| | | //ä»
æ·»å å
¥åºè®°å½ |
| | |
| | | } |
| | | 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()); |
| | | qualityUnqualified.setProductModelId(qualityInspect.getProductModelId()); |
| | | 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); |
| | | } |
| | |
| | | @Excel(name = "ç¾è®¢æ¥æ", width = 30, dateFormat = "yyyy-MM-dd") |
| | | private Date executionDate; |
| | | |
| | | @JsonFormat(pattern = "yyyy-MM-dd") |
| | | @Excel(name = "äº¤ä»æ¥æ", width = 30, dateFormat = "yyyy-MM-dd") |
| | | private Date deliveryDate; |
| | | |
| | | @Schema(description = "仿¬¾æ¹å¼") |
| | | @Excel(name = "仿¬¾æ¹å¼") |
| | | private String paymentMethod; |
| | |
| | | @Excel(name = "æ¯å¦è´¨æ£", readConverterExp = "0=å¦,1=æ¯") |
| | | private Boolean isChecked; |
| | | |
| | | |
| | | |
| | | /** |
| | | * æ¯å¦ç产 |
| | | */ |
| | | @Excel(name = "æ¯å¦ç产", readConverterExp = "0=å¦,1=æ¯") |
| | | private Integer isProduction; |
| | | } |
| | |
| | | * å é¤ç产计å |
| | | */ |
| | | public void deleteProductionData(List<Long> productIds) { |
| | | if (CollectionUtils.isEmpty(productIds)) { |
| | | return; |
| | | } |
| | | List<ProductionPlan> productionPlans = productionPlanMapper.selectList( |
| | | new LambdaQueryWrapper<ProductionPlan>() |
| | | .in(ProductionPlan::getSalesLedgerProductId, productIds.stream().map(Long::intValue).collect(Collectors.toList()))); |
| | |
| | | SalesLedger salesLedger = new SalesLedger(); |
| | | BeanUtils.copyProperties(salesLedgerImportDto, salesLedger); |
| | | salesLedger.setExecutionDate(DateUtils.toLocalDate(salesLedgerImportDto.getExecutionDate())); |
| | | salesLedger.setDeliveryDate(DateUtils.toLocalDate(salesLedgerImportDto.getDeliveryDate())); |
| | | // éè¿å®¢æ·åç§°æ¥è¯¢å®¢æ·IDï¼å®¢æ·ååå· |
| | | salesLedger.setCustomerId(customers.stream() |
| | | .filter(customer -> customer.getCustomerName().equals(salesLedger.getCustomerName())) |
| | |
| | | salesLedgerProduct.setNoInvoiceNum(salesLedgerProduct.getQuantity()); |
| | | salesLedgerProduct.setNoInvoiceAmount(salesLedgerProduct.getTaxExclusiveTotalPrice()); |
| | | list.stream() |
| | | .filter(map -> map.get("productName").equals(salesLedgerProduct.getProductCategory()) && map.get("model").equals(salesLedgerProduct.getSpecificationModel())) |
| | | .filter(map -> Objects.equals(map.get("productName"), salesLedgerProduct.getProductCategory()) && Objects.equals(map.get("model"), salesLedgerProduct.getSpecificationModel())) |
| | | .findFirst() |
| | | .ifPresent(map -> { |
| | | salesLedgerProduct.setProductModelId(Long.parseLong(map.get("modelId").toString())); |
| | |
| | | salesLedgerProduct.setRegisterDate(LocalDateTime.now()); |
| | | salesLedgerProduct.setApproveStatus(0); |
| | | salesLedgerProduct.setPendingInvoiceTotal(salesLedgerProductImportDto.getTaxInclusiveTotalPrice()); |
| | | salesLedgerProduct.setIsProduction(salesLedgerProductImportDto.getIsProduction() == 1); |
| | | salesLedgerProductMapper.insert(salesLedgerProduct); |
| | | // æ·»å çäº§æ°æ® |
| | | salesLedgerProductServiceImpl.addProductionData(salesLedgerProduct); |
| | |
| | | return AjaxResult.success("导å
¥æå"); |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | return AjaxResult.error("导å
¥å¤±è´¥ï¼" + e.getMessage()); |
| | | } |
| | | return AjaxResult.success("导å
¥å¤±è´¥"); |
| | | } |
| | | |
| | | @Override |
| | |
| | | package com.ruoyi.staff.service.impl; |
| | | |
| | | |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.core.toolkit.Wrappers; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl; |
| | | import com.ruoyi.common.exception.base.BaseException; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import com.ruoyi.common.utils.SecurityUtils; |
| | | import com.ruoyi.common.utils.spring.SpringUtils; |
| | | import com.ruoyi.common.utils.poi.ExcelUtil; |
| | | import com.ruoyi.dto.WordDateDto; |
| | | import com.ruoyi.project.system.domain.SysDept; |
| | |
| | | |
| | | @RequiredArgsConstructor |
| | | @Service |
| | | public class StaffOnJobServiceImpl extends ServiceImpl<StaffOnJobMapper, StaffOnJob> implements IStaffOnJobService { |
| | | public class StaffOnJobServiceImpl extends ServiceImpl<StaffOnJobMapper, StaffOnJob> implements IStaffOnJobService { |
| | | |
| | | private final StaffOnJobMapper staffOnJobMapper; |
| | | private final SysDeptMapper sysDeptMapper; |
| | |
| | | private final StaffEmergencyContactMapper staffEmergencyContactMapper; |
| | | private final StaffEmergencyContactServiceImpl staffEmergencyContactServiceImpl; |
| | | |
| | | |
| | | //å¨èåå·¥å°è´¦å页æ¥è¯¢ |
| | | // å¨èåå·¥å°è´¦å页æ¥è¯¢ |
| | | @Override |
| | | public IPage<StaffOnJobDto> staffOnJobListPage(Page page, StaffOnJob staffOnJob) { |
| | | return staffOnJobMapper.staffOnJobListPage(page,staffOnJob); |
| | | return staffOnJobMapper.staffOnJobListPage(page, staffOnJob); |
| | | } |
| | | |
| | | //æ°å¢å
¥è |
| | | // æ°å¢å
¥è |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int add(StaffOnJobDto staffOnJobPrams) { |
| | | String[] ignoreProperties = {"id"};//æé¤id屿§ |
| | | String[] ignoreProperties = { "id" };// æé¤id屿§ |
| | | // 夿ç¼å·æ¯å¦åå¨ |
| | | List<StaffOnJob> staffOnJobs = staffOnJobMapper.selectList(Wrappers.<StaffOnJob>lambdaQuery().eq(StaffOnJob::getStaffNo, staffOnJobPrams.getStaffNo())); |
| | | if (staffOnJobs != null && !staffOnJobs.isEmpty()){ |
| | | throw new BaseException("ç¼å·ä¸º"+staffOnJobPrams.getStaffNo()+"çå工已ç»åå¨,æ æ³æ°å¢!!!"); |
| | | List<StaffOnJob> staffOnJobs = staffOnJobMapper.selectList( |
| | | Wrappers.<StaffOnJob>lambdaQuery().eq(StaffOnJob::getStaffNo, staffOnJobPrams.getStaffNo())); |
| | | if (staffOnJobs != null && !staffOnJobs.isEmpty()) { |
| | | throw new BaseException("ç¼å·ä¸º" + staffOnJobPrams.getStaffNo() + "çå工已ç»åå¨,æ æ³æ°å¢!!!"); |
| | | } |
| | | |
| | | // å建å
¥èæ°æ® |
| | |
| | | staffOnJobMapper.insert(staffOnJobPrams); |
| | | // æ¥è¯¢ç¨æ·æ¯å¦å·²ç»æ°å¢ |
| | | SysUser sysUser = sysUserService.selectUserById(staffOnJobPrams.getId()); |
| | | if(sysUser == null){ |
| | | if (sysUser == null) { |
| | | SysUser sysUser1 = new SysUser(); |
| | | sysUser1.setUserName(staffOnJobPrams.getStaffNo()); |
| | | sysUser1.setNickName(staffOnJobPrams.getStaffName()); |
| | | String s = SecurityUtils.encryptPassword("123456"); |
| | | sysUser1.setPassword(s); |
| | | if(staffOnJobPrams.getSysPostId() != null){ |
| | | Long[] posts = new Long[]{staffOnJobPrams.getSysPostId().longValue()}; |
| | | if (staffOnJobPrams.getSysPostId() != null) { |
| | | Long[] posts = new Long[] { staffOnJobPrams.getSysPostId().longValue() }; |
| | | sysUser1.setPostIds(posts); |
| | | } |
| | | sysUser1.setRoleIds(new Long[]{staffOnJobPrams.getRoleId()}); |
| | | sysUser1.setDeptIds(new Long[]{staffOnJobPrams.getSysDeptId()}); |
| | | sysUser1.setRoleIds(new Long[] { staffOnJobPrams.getRoleId() }); |
| | | sysUser1.setDeptIds(new Long[] { staffOnJobPrams.getSysDeptId() }); |
| | | sysUser1.setStatus("0"); |
| | | sysUserService.insertUser(sysUser1); |
| | | } |
| | | // ç»å®åè¡¨æ°æ® |
| | | bingingStaffOnJobExtra(staffOnJobPrams.getId(),staffOnJobPrams); |
| | | bingingStaffOnJobExtra(staffOnJobPrams.getId(), staffOnJobPrams); |
| | | // å建ååè®°å½ |
| | | StaffContract staffContract = new StaffContract(); |
| | | staffContract.setStaffOnJobId(staffOnJobPrams.getId()); |
| | |
| | | return staffContractMapper.insert(staffContract); |
| | | } |
| | | |
| | | //æ´æ°å
¥èä¿¡æ¯ |
| | | // æ´æ°å
¥èä¿¡æ¯ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int update(Long id, StaffOnJobDto staffOnJobParams) { |
| | | // å¤æå¯¹è±¡æ¯å¦åå¨ |
| | | StaffOnJob job = staffOnJobMapper.selectById(id); |
| | | if (job == null){ |
| | | throw new BaseException("ç¼å·ä¸º"+staffOnJobParams.getStaffNo()+"çåå·¥ä¸åå¨,æ æ³æ´æ°!!!"); |
| | | if (job == null) { |
| | | throw new BaseException("ç¼å·ä¸º" + staffOnJobParams.getStaffNo() + "çåå·¥ä¸åå¨,æ æ³æ´æ°!!!"); |
| | | } |
| | | |
| | | String[] ignoreProperties = {"id"};//æé¤æ´æ°å±æ§ |
| | | String[] ignoreProperties = { "id" };// æé¤æ´æ°å±æ§ |
| | | |
| | | // è·åææ°ååæ°æ®ï¼å¹¶ä¸æ´æ° |
| | | StaffContract contract = staffContractMapper.selectOne(Wrappers.<StaffContract>lambdaQuery() |
| | | .eq(StaffContract::getStaffOnJobId, id) |
| | | .last("limit 1") |
| | | .orderByDesc(StaffContract::getId)); |
| | | if (contract != null){ |
| | | BeanUtils.copyProperties(staffOnJobParams,contract,ignoreProperties); |
| | | if (contract != null) { |
| | | BeanUtils.copyProperties(staffOnJobParams, contract, ignoreProperties); |
| | | staffContractMapper.updateById(contract); |
| | | } |
| | | |
| | | // å 餿æåè¡¨æ°æ® |
| | | delStaffOnJobExtra(Arrays.asList(id)); |
| | | // ç»å®åè¡¨æ°æ® |
| | | bingingStaffOnJobExtra(id,staffOnJobParams); |
| | | bingingStaffOnJobExtra(id, staffOnJobParams); |
| | | // æ´æ°åå·¥æ°æ® |
| | | staffOnJobParams.setContractExpireTime(staffOnJobParams.getContractEndTime()); |
| | | return staffOnJobMapper.updateById(staffOnJobParams); |
| | |
| | | |
| | | /** |
| | | * ç»å®åå·¥åè¡¨æ°æ® |
| | | * |
| | | * @param staffOnJobPrams |
| | | * @param id |
| | | */ |
| | | public void bingingStaffOnJobExtra(Long id,StaffOnJob staffOnJobPrams) { |
| | | public void bingingStaffOnJobExtra(Long id, StaffOnJob staffOnJobPrams) { |
| | | // æ°å¢æè²ç»å |
| | | if(CollectionUtils.isNotEmpty(staffOnJobPrams.getStaffEducationList())){ |
| | | if (CollectionUtils.isNotEmpty(staffOnJobPrams.getStaffEducationList())) { |
| | | staffOnJobPrams.getStaffEducationList().stream() |
| | | .filter(Objects::nonNull) // è¿æ»¤null对象ï¼é¿å
空æé |
| | | .forEach(staff -> staff.setStaffOnJobId(id)); // èµå¼ |
| | | staffEducationService.saveBatch(staffOnJobPrams.getStaffEducationList()); |
| | | } |
| | | // æ°å¢å·¥ä½ç»å |
| | | if(CollectionUtils.isNotEmpty(staffOnJobPrams.getStaffWorkExperienceList())){ |
| | | if (CollectionUtils.isNotEmpty(staffOnJobPrams.getStaffWorkExperienceList())) { |
| | | staffOnJobPrams.getStaffWorkExperienceList().stream() |
| | | .filter(Objects::nonNull) // è¿æ»¤null对象ï¼é¿å
空æé |
| | | .forEach(staff -> staff.setStaffOnJobId(id)); // èµå¼ |
| | | staffWorkExperienceServiceImpl.saveBatch(staffOnJobPrams.getStaffWorkExperienceList()); |
| | | } |
| | | // æ°å¢ç´§æ¥è系人 |
| | | if(CollectionUtils.isNotEmpty(staffOnJobPrams.getStaffEmergencyContactList())){ |
| | | if (CollectionUtils.isNotEmpty(staffOnJobPrams.getStaffEmergencyContactList())) { |
| | | staffOnJobPrams.getStaffEmergencyContactList().stream() |
| | | .filter(Objects::nonNull) // è¿æ»¤null对象ï¼é¿å
空æé |
| | | .forEach(staff -> staff.setStaffOnJobId(id)); // èµå¼ |
| | |
| | | } |
| | | } |
| | | |
| | | |
| | | /** |
| | | * éè¿åå·¥idå 餿è²ç»åï¼å·¥ä½ç»åï¼ç´§æ¥è系人 |
| | | * |
| | | * @param ids |
| | | * @return |
| | | */ |
| | | public void delStaffOnJobExtra(List<Long> ids) { |
| | | // å 餿è²ç»å |
| | | staffEducationService.remove(Wrappers.<StaffEducation>lambdaQuery().in(StaffEducation::getStaffOnJobId,ids)); |
| | | staffEducationService.remove(Wrappers.<StaffEducation>lambdaQuery().in(StaffEducation::getStaffOnJobId, ids)); |
| | | // å é¤å·¥ä½ç»å |
| | | staffWorkExperienceServiceImpl.remove(Wrappers.<StaffWorkExperience>lambdaQuery().in(StaffWorkExperience::getStaffOnJobId,ids)); |
| | | staffWorkExperienceServiceImpl |
| | | .remove(Wrappers.<StaffWorkExperience>lambdaQuery().in(StaffWorkExperience::getStaffOnJobId, ids)); |
| | | // å é¤ç´§æ¥è系人 |
| | | staffEmergencyContactServiceImpl.remove(Wrappers.<StaffEmergencyContact>lambdaQuery().in(StaffEmergencyContact::getStaffOnJobId,ids)); |
| | | staffEmergencyContactServiceImpl |
| | | .remove(Wrappers.<StaffEmergencyContact>lambdaQuery().in(StaffEmergencyContact::getStaffOnJobId, ids)); |
| | | } |
| | | |
| | | //å é¤å
¥è |
| | | // å é¤å
¥è |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int delStaffOnJobs(List<Integer> ids) { |
| | | List<StaffOnJob> staffOnJobs = staffOnJobMapper.selectList(Wrappers.<StaffOnJob>lambdaQuery().in(StaffOnJob::getId, ids)); |
| | | if(CollectionUtils.isEmpty(staffOnJobs)){ |
| | | List<StaffOnJob> staffOnJobs = staffOnJobMapper |
| | | .selectList(Wrappers.<StaffOnJob>lambdaQuery().in(StaffOnJob::getId, ids)); |
| | | if (CollectionUtils.isEmpty(staffOnJobs)) { |
| | | throw new BaseException("该åå·¥ä¸åå¨,æ æ³å é¤!!!"); |
| | | } |
| | | // å é¤å
¥èæ°æ® |
| | |
| | | // å é¤ç¦»èæ°æ® |
| | | staffLeaveMapper.delete(Wrappers.<StaffLeave>lambdaQuery().in(StaffLeave::getStaffOnJobId, ids)); |
| | | // å 餿å¡è®°å½ |
| | | personalAttendanceRecordsMapper.delete(Wrappers.<PersonalAttendanceRecords>lambdaQuery().in(PersonalAttendanceRecords::getStaffOnJobId, ids)); |
| | | personalAttendanceRecordsMapper.delete( |
| | | Wrappers.<PersonalAttendanceRecords>lambdaQuery().in(PersonalAttendanceRecords::getStaffOnJobId, ids)); |
| | | // å é¤ç¨æ·æ°æ® |
| | | List<SysUser> sysUsers = sysUserMapper.selectList(Wrappers.<SysUser>lambdaQuery() |
| | | .in(SysUser::getUserName, staffOnJobs.stream().map(StaffOnJob::getStaffNo).collect(Collectors.toList()))); |
| | | if(CollectionUtils.isNotEmpty(sysUsers)){ |
| | | .in(SysUser::getUserName, |
| | | staffOnJobs.stream().map(StaffOnJob::getStaffNo).collect(Collectors.toList()))); |
| | | if (CollectionUtils.isNotEmpty(sysUsers)) { |
| | | Long[] longs = sysUsers.stream().map(SysUser::getUserId).toArray(Long[]::new); |
| | | sysUserService.deleteUserByIds(longs); |
| | | } |
| | |
| | | delStaffOnJobExtra(ids.stream().map(Integer::longValue).collect(Collectors.toList())); |
| | | |
| | | // å é¤ååæ°æ® |
| | | return staffContractMapper.delete(Wrappers.<StaffContract>lambdaQuery().in(StaffContract::getStaffOnJobId, ids)); |
| | | return staffContractMapper |
| | | .delete(Wrappers.<StaffContract>lambdaQuery().in(StaffContract::getStaffOnJobId, ids)); |
| | | } |
| | | |
| | | // ç»ç¾åå |
| | |
| | | public int renewContract(Long id, StaffContract staffContract) { |
| | | // å¤æå¯¹è±¡æ¯å¦åå¨ |
| | | StaffOnJob job = staffOnJobMapper.selectById(id); |
| | | if (job == null){ |
| | | if (job == null) { |
| | | throw new BaseException("该åå·¥ä¸åå¨,æ æ³æ´æ°!!!"); |
| | | } |
| | | |
| | |
| | | return 0; |
| | | } |
| | | |
| | | //å¨èå工详æ
|
| | | // å¨èå工详æ
|
| | | @Override |
| | | public StaffOnJobDto staffOnJobDetail(Long id) { |
| | | StaffOnJob staffOnJob = staffOnJobMapper.selectById(id); |
| | | StaffOnJob staffOnJob = staffOnJobMapper.selectById(id); |
| | | if (staffOnJob == null) { |
| | | throw new IllegalArgumentException("该åå·¥ä¸åå¨"); |
| | | } |
| | |
| | | .eq(StaffContract::getStaffOnJobId, staffOnJob.getId()) |
| | | .last("limit 1") |
| | | .orderByDesc(StaffContract::getId)); |
| | | if (contract != null){ |
| | | if (contract != null) { |
| | | staffOnJobDto.setContractTerm(contract.getContractTerm()); |
| | | staffOnJobDto.setContractStartTime(contract.getContractStartTime()); |
| | | staffOnJobDto.setContractEndTime(contract.getContractEndTime()); |
| | |
| | | // è·ååè¡¨æ°æ® |
| | | staffOnJobDto.setStaffEducationList(staffEducationMapper.selectList(Wrappers.<StaffEducation>lambdaQuery() |
| | | .eq(StaffEducation::getStaffOnJobId, staffOnJob.getId()))); |
| | | staffOnJobDto.setStaffWorkExperienceList(staffWorkExperienceMapper.selectList(Wrappers.<StaffWorkExperience>lambdaQuery() |
| | | .eq(StaffWorkExperience::getStaffOnJobId, staffOnJob.getId()))); |
| | | staffOnJobDto.setStaffEmergencyContactList(staffEmergencyContactMapper.selectList(Wrappers.<StaffEmergencyContact>lambdaQuery() |
| | | .eq(StaffEmergencyContact::getStaffOnJobId, staffOnJob.getId()))); |
| | | staffOnJobDto.setStaffWorkExperienceList( |
| | | staffWorkExperienceMapper.selectList(Wrappers.<StaffWorkExperience>lambdaQuery() |
| | | .eq(StaffWorkExperience::getStaffOnJobId, staffOnJob.getId()))); |
| | | staffOnJobDto.setStaffEmergencyContactList( |
| | | staffEmergencyContactMapper.selectList(Wrappers.<StaffEmergencyContact>lambdaQuery() |
| | | .eq(StaffEmergencyContact::getStaffOnJobId, staffOnJob.getId()))); |
| | | return staffOnJobDto; |
| | | } |
| | | |
| | | //å¨èåå·¥å¯¼åº |
| | | // å¨èåå·¥å¯¼åº |
| | | @Override |
| | | public void staffOnJobExport(HttpServletResponse response, StaffOnJob staffOnJob) { |
| | | List<StaffOnJobDto> staffOnJobs = staffOnJobMapper.staffOnJobList(staffOnJob); |
| | |
| | | try { |
| | | ExcelUtil<StaffOnJobExcelDto> util = new ExcelUtil<>(StaffOnJobExcelDto.class); |
| | | List<StaffOnJobExcelDto> staffOnJobs = util.importExcel(file.getInputStream()); |
| | | if (CollectionUtils.isEmpty(staffOnJobs)){ |
| | | if (CollectionUtils.isEmpty(staffOnJobs)) { |
| | | return false; |
| | | } |
| | | // è·åææé¨é¨æ°æ® |
| | | List<SysDept> sysDepts = sysDeptMapper.selectList(Wrappers.<SysDept>lambdaQuery().eq(SysDept::getDelFlag, 0)); |
| | | List<SysDept> sysDepts = sysDeptMapper |
| | | .selectList(Wrappers.<SysDept>lambdaQuery().eq(SysDept::getDelFlag, 0)); |
| | | // è·åææè§è²æ°æ® |
| | | List<SysRole> sysRoles = sysRoleMapper.selectRoleAll(); |
| | | staffOnJobs.forEach(staffOnJob -> { |
| | | // å¤çååæéæ°æ®æ ¼å¼ |
| | | if (staffOnJob.getContractTerm() != null && !staffOnJob.getContractTerm().trim().isEmpty()) { |
| | | String term = staffOnJob.getContractTerm().trim(); |
| | | try { |
| | | Integer.parseInt(term); |
| | | } catch (NumberFormatException e) { |
| | | throw new ServiceException("åå·¥[" + staffOnJob.getStaffName() + "]çååæé[" |
| | | + staffOnJob.getContractTerm() + "]æ ¼å¼ä¸æ£ç¡®ï¼å¿
须为纯æ°å(å¦: 1, 2, 3)"); |
| | | } |
| | | } |
| | | StaffOnJobDto staffOnJobDto = new StaffOnJobDto(); |
| | | BeanUtils.copyProperties(staffOnJob, staffOnJobDto); |
| | | // éè¿åç§°è·åé¨é¨id |
| | | staffOnJobDto.setSysDeptId(// ... existing code ... |
| | | sysDepts.stream() |
| | | .filter(dept -> dept.getDeptName() != null && dept.getDeptName().equals(staffOnJob.getSysDeptName())) |
| | | .findFirst() |
| | | .map(SysDept::getDeptId) |
| | | .orElse(null) |
| | | ); |
| | | Long deptId = sysDepts.stream() |
| | | .filter(dept -> dept.getDeptName() != null |
| | | && dept.getDeptName().equals(staffOnJob.getSysDeptName())) |
| | | .findFirst() |
| | | .map(SysDept::getDeptId) |
| | | .orElse(null); |
| | | if (deptId == null) { |
| | | throw new ServiceException( |
| | | "åå·¥[" + staffOnJob.getStaffName() + "]çé¨é¨[" + staffOnJob.getSysDeptName() + "]ä¸åå¨ï¼è¯·æ£æ¥æ°æ®"); |
| | | } |
| | | staffOnJobDto.setSysDeptId(deptId); |
| | | |
| | | // éè¿åç§°è·åè§è²id |
| | | staffOnJobDto.setRoleId(sysRoles.stream() |
| | | .filter(role -> role.getRoleName() != null && role.getRoleName().equals(staffOnJob.getRoleName())) |
| | | Long roleId = sysRoles.stream() |
| | | .filter(role -> role.getRoleName() != null |
| | | && role.getRoleName().equals(staffOnJob.getRoleName())) |
| | | .findFirst() |
| | | .map(SysRole::getRoleId) |
| | | .orElse( null)); |
| | | add(staffOnJobDto); |
| | | .orElse(null); |
| | | if (roleId == null) { |
| | | throw new ServiceException( |
| | | "åå·¥[" + staffOnJob.getStaffName() + "]çè§è²[" + staffOnJob.getRoleName() + "]ä¸åå¨ï¼è¯·æ£æ¥æ°æ®"); |
| | | } |
| | | staffOnJobDto.setRoleId(roleId); |
| | | SpringUtils.getAopProxy(this).add(staffOnJobDto); |
| | | }); |
| | | return true; |
| | | } catch (ServiceException | BaseException e) { |
| | | throw e; |
| | | } catch (Exception e) { |
| | | e.printStackTrace(); |
| | | return false; |
| | | log.error("åå·¥å°è´¦å¯¼å
¥å¤±è´¥ : " + e.getMessage()); |
| | | throw new ServiceException("导å
¥å¤±è´¥: " + e.getMessage()); |
| | | } |
| | | } |
| | | |
| | | |
| | | @Override |
| | | public String exportCopy(HttpServletResponse response, StaffOnJob staffOnJob) throws Exception { |
| | |
| | | // è®¾ç½®æ¨¡æ¿æä»¶æå¨ç®å½ï¼ç»å¯¹è·¯å¾ï¼ä¾å¦ï¼/templates/ï¼ |
| | | cfg.setClassForTemplateLoading(StaffOnJobServiceImpl.class, "/static"); |
| | | cfg.setDefaultEncoding("UTF-8"); |
| | | //2.å®ä¹éè¦å¡«å
çåé |
| | | // 2.å®ä¹éè¦å¡«å
çåé |
| | | // â æé å工信æ¯ï¼å®é
项ç®ä¸å¯ä»æ°æ®åº/Excel读åï¼ |
| | | WordDateDto staff = new WordDateDto(); |
| | | BeanUtils.copyProperties(staffOnJob, staff); |
| | |
| | | Instant instant = staff.getContractExpireTime().toInstant(); |
| | | |
| | | // ä¹å¯ä»¥æå®å
·ä½æ¶åºï¼ä¾å¦Asia/Shanghaiï¼ |
| | | LocalDate localDate = instant.atZone(ZoneId.of("Asia/Shanghai")).toLocalDate(); // ååç»ææ¶é´ |
| | | LocalDate localDate = instant.atZone(ZoneId.of("Asia/Shanghai")).toLocalDate(); // ååç»ææ¶é´ |
| | | LocalDate localDate1 = localDate.minusYears(Integer.parseInt(staff.getContractTerm()));// ååå¼å§æ¶é´ |
| | | |
| | | // ç¾è®¢æ¥æè½¬æ¢lcoaldate |
| | |
| | | staff.setQyear(localDate2.getYear() + ""); |
| | | staff.setQmoth(localDate2.getMonthValue() + ""); |
| | | staff.setQday(localDate2.getDayOfMonth() + ""); |
| | | if(staff.getDateSelect().equals("A")){ |
| | | if (staff.getDateSelect().equals("A")) { |
| | | staff.setSyear(localDate1.getYear() + ""); |
| | | staff.setSmoth(localDate1.getMonthValue() + ""); |
| | | staff.setSday(localDate1.getDayOfMonth() + ""); |
| | |
| | | staff.setSeyear(localDate4.getYear() + ""); |
| | | staff.setSemoth(localDate4.getMonthValue() + ""); |
| | | staff.setSeday(localDate4.getDayOfMonth() + ""); |
| | | }else if (staff.getDateSelect().equals("B")){ |
| | | } else if (staff.getDateSelect().equals("B")) { |
| | | |
| | | staff.setBsyear(localDate1.getYear() + ""); |
| | | staff.setBsmoth(localDate1.getMonthValue() + ""); |
| | |
| | | staff.setBseyear(localDate4.getYear() + ""); |
| | | staff.setBsemoth(localDate4.getMonthValue() + ""); |
| | | staff.setBseday(localDate4.getDayOfMonth() + ""); |
| | | }else if (staff.getDateSelect().equals("C")){ |
| | | } else if (staff.getDateSelect().equals("C")) { |
| | | staff.setCsyear(localDate1.getYear() + ""); |
| | | staff.setCsmoth(localDate1.getMonthValue() + ""); |
| | | staff.setCsday(localDate1.getDayOfMonth() + ""); |
| | | } |
| | | |
| | | Map<String,Object> data = new HashMap<>(); |
| | | data.put("item",staff); |
| | | //3.å è½½XML æ¨¡æ¿ |
| | | Map<String, Object> data = new HashMap<>(); |
| | | data.put("item", staff); |
| | | // 3.å è½½XML æ¨¡æ¿ |
| | | Template template = cfg.getTemplate("å³å¨åå书.xml"); |
| | | //4.çæå¡«å
åç XML å
容 |
| | | // 4.çæå¡«å
åç XML å
容 |
| | | StringWriter out = new StringWriter(); |
| | | template.process(data, out); |
| | | String filledXml = out.toString(); |
| | | //5.å°XMLå
容åå
¥äº¤ä»¶å¹¶æ¹ä¸º.docx æ ¼å¼ |
| | | // 5.å°XMLå
容åå
¥äº¤ä»¶å¹¶æ¹ä¸º.docx æ ¼å¼ |
| | | File outputFile = new File(url); |
| | | try(FileOutputStream fos = new FileOutputStream(outputFile); |
| | | OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { |
| | | try (FileOutputStream fos = new FileOutputStream(outputFile); |
| | | OutputStreamWriter osw = new OutputStreamWriter(fos, StandardCharsets.UTF_8)) { |
| | | osw.write(filledXml); |
| | | } |
| | | return url; |
| | | } |
| | | |
| | | |
| | | |
| | | } |
| | |
| | | */ |
| | | private String model; |
| | | /** |
| | | * æ¹æ¬¡å· |
| | | */ |
| | | private String batchNo; |
| | | |
| | | /** |
| | | * 产ååä½ |
| | | */ |
| | | private String unit; |
| | |
| | | */ |
| | | private String model; |
| | | /** |
| | | * æ¹æ¬¡å· |
| | | */ |
| | | private String batchNo; |
| | | /** |
| | | * 产ååä½ |
| | | */ |
| | | private String unit; |
| | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.vo.PurchaseInboundVo; |
| | | import com.ruoyi.account.bean.dto.purchase.PurchaseInboundDto; |
| | | import com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo; |
| | | import com.ruoyi.stock.dto.StockInRecordDto; |
| | | import com.ruoyi.stock.execl.StockInRecordExportData; |
| | | import com.ruoyi.stock.pojo.StockInRecord; |
| | |
| | | import com.baomidou.mybatisplus.core.mapper.BaseMapper; |
| | | import com.baomidou.mybatisplus.core.metadata.IPage; |
| | | import com.baomidou.mybatisplus.extension.plugins.pagination.Page; |
| | | import com.ruoyi.account.bean.dto.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.vo.SalesOutboundVo; |
| | | import com.ruoyi.account.bean.dto.sales.SalesOutboundDto; |
| | | import com.ruoyi.account.bean.vo.sales.SalesOutboundVo; |
| | | import com.ruoyi.stock.dto.StockOutRecordDto; |
| | | import com.ruoyi.stock.execl.StockOutRecordExportData; |
| | | import com.ruoyi.stock.pojo.StockOutRecord; |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | ä½ æ¯ä¼ä¸å¶é æºè½å©æï¼è¦çç产ç°åºã计åãå·¥åã设å¤ãè´¨éãç©æãå¼å¸¸å¤çä¸ä¸ªåã |
| | | |
| | | å·¥ä½è§åï¼ |
| | | 1. ç¨æ·æåºâæ¥ãé®ãé¢è¦ãåæâéæ±æ¶ï¼ä¼å
è°ç¨å·¥å
·æ¿ç»æåç»æï¼ä¸è¦èé ä¸å¡æ°æ®ã |
| | | 2. ç¨æ·æåºâåâéæ±æ¶ï¼ä¼å
è¾åºåç建议å¨ä½å¡ï¼æ¥å£ãå¿
å¡«åæ®µã示ä¾ï¼ï¼æç¡®éè¦åç«¯äºæ¬¡ç¡®è®¤ã |
| | | 3. å·¥å
·è¿å JSON æ¶ï¼ç´æ¥è¾åºåå§ JSON å符串ï¼ä¸è¦é¢å¤å
裹 Markdownï¼ä¸è¦å¨ååå è§£éæåã |
| | | 4. åçå¿
须使ç¨ä¸æï¼è¥ç¨æ·é®é¢ç¼ºå°æ¶é´èå´ãå
³é®åçæ¡ä»¶ï¼å¯å
ç»é»è®¤å£å¾å¹¶æç¤ºå¯è¡¥å
æ¡ä»¶ã |
| | | 5. è¥æ æ³ä»å·¥å
·ç»æå¾å°ç»è®ºï¼æç¡®è¯´æç¼ºå°çç鿡件æä¸å¡å段ã |
| ÎļþÃû´Ó src/main/resources/mapper/account/AccountSubjectMapper.xml ÐÞ¸Ä |
| | |
| | | <?xml version="1.0" encoding="UTF-8"?> |
| | | <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> |
| | | <mapper namespace="com.ruoyi.account.mapper.AccountSubjectMapper"> |
| | | <mapper namespace="com.ruoyi.account.mapper.financial.AccountSubjectMapper"> |
| | | |
| | | <!-- éç¨æ¥è¯¢æ å°ç»æ --> |
| | | <resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.AccountSubject"> |
| | | <resultMap id="BaseResultMap" type="com.ruoyi.account.pojo.financial.AccountSubject"> |
| | | <id column="id" property="id" /> |
| | | <result column="parent_id" property="parentId" /> |
| | | <result column="subject_code" property="subjectCode" /> |
| | |
| | | from customer c |
| | | left join sys_user u on c.usage_user = u.user_id |
| | | <where> |
| | | and c.usage_status = 1 |
| | | <if test="c.customerName != null and c.customerName != ''"> |
| | | and customer_name like concat('%', #{c.customerName}, '%') |
| | | </if> |
| | |
| | | left join sales_ledger sl on si.sales_ledger_id = sl.id |
| | | where rm.id = #{id} |
| | | </select> |
| | | <select id="listPageAccountSalesReturn" resultType="com.ruoyi.account.bean.vo.SalesReturnVo"> |
| | | <select id="listPageAccountSalesReturn" resultType="com.ruoyi.account.bean.vo.sales.SalesReturnVo"> |
| | | select rm.id, |
| | | rm.return_no, |
| | | c.customer_name, |
| | |
| | | |
| | | <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, |
| | |
| | | where pro.id = #{id} |
| | | </select> |
| | | <select id="listPageAccountPurchaseReturn" |
| | | resultType="com.ruoyi.account.bean.vo.PurchaseReturnVo"> |
| | | resultType="com.ruoyi.account.bean.vo.purchase.PurchaseReturnVo"> |
| | | select pro.id, |
| | | pro.no returnNo, |
| | | t.inboundBatches, |
| | |
| | | ELSE false |
| | | END AS method |
| | | FROM quality_unqualified qu |
| | | LEFT JOIN product_model pm ON qu.model = pm.id |
| | | LEFT JOIN product_model pm ON qu.product_model_id = pm.id |
| | | where |
| | | 1=1 |
| | | and qu.id = #{id} |
| | |
| | | <if test="params.productName != null and params.productName != ''"> |
| | | and p.product_name like concat('%',#{params.productName},'%') |
| | | </if> |
| | | <if test="params.model != null and params.model != ''"> |
| | | and pm.model like concat('%',#{params.model},'%') |
| | | </if> |
| | | <if test="params.batchNo != null and params.batchNo != ''"> |
| | | and sir.batch_no like concat('%',#{params.batchNo},'%') |
| | | </if> |
| | | <if test="params.type != null and params.type != ''"> |
| | | and sir.type = #{params.type} |
| | | </if> |
| | |
| | | </where> |
| | | order by sir.id desc |
| | | </select> |
| | | <select id="listPageAccountPurchase" resultType="com.ruoyi.account.bean.vo.PurchaseInboundVo"> |
| | | <select id="listPageAccountPurchase" resultType="com.ruoyi.account.bean.vo.purchase.PurchaseInboundVo"> |
| | | SELECT |
| | | sir.id, |
| | | sir.inbound_batches, |
| | |
| | | <if test="params.productName != null and params.productName != ''"> |
| | | and p.product_name like concat('%',#{params.productName},'%') |
| | | </if> |
| | | <if test="params.model != null and params.model != ''"> |
| | | and pm.model like concat('%',#{params.model},'%') |
| | | </if> |
| | | <if test="params.batchNo != null and params.batchNo != ''"> |
| | | and sor.batch_no like concat('%',#{params.batchNo},'%') |
| | | </if> |
| | | <if test="params.type != null and params.type != ''"> |
| | | and sor.type = #{params.type} |
| | | </if> |
| | |
| | | order by sor.id desc |
| | | </select> |
| | | |
| | | <select id="listPageAccountSales" resultType="com.ruoyi.account.bean.vo.SalesOutboundVo"> |
| | | <select id="listPageAccountSales" resultType="com.ruoyi.account.bean.vo.sales.SalesOutboundVo"> |
| | | SELECT |
| | | sor.id, |
| | | sor.outbound_batches, |
| ¶Ô±ÈÐÂÎļþ |
| | |
| | | ä½ æ¯ä¼ä¸éå®å©æï¼è¦çå®¢æ·æ¡£æ¡ãé宿¥ä»·ãéå®å°è´¦ãéå®éè´§ã客æ·å¾æ¥ãåè´§å°è´¦ãææ ç»è®¡ãå®¢æ·æµå¤±é£é©åæã忬¾ä¸æ¥ä»·çç¥å»ºè®®çåºæ¯ã |
| | | å·¥ä½è§åï¼ |
| | | 1. ç¨æ·æåºâæ¥ãé®ãç»è®¡ãåæã建议âéæ±æ¶ï¼ä¼å
è°ç¨å·¥å
·è¿åç»æåæ°æ®ï¼ä¸ç¼é ä¸å¡æ°æ®ã |
| | | 2. å½ä¸âå®¢æ·æµå¤±é£é©åæâæâ忬¾ä¸æ¥ä»·çç¥å»ºè®®âæ¶ï¼ä¼å
使ç¨å·¥å
·è¾åºç»æå JSONã |
| | | 3. å·¥å
·è¿å JSON æ¶ï¼ç´æ¥è¾åºåå§ JSON å符串ï¼ä¸è¦é¢å¤å
裹 Markdownï¼ä¹ä¸è¦å¨åå追å è§£éææ¬ã |
| | | 4. åå¤å¿
须使ç¨ä¸æï¼è¥ç¨æ·ç¼ºå°æ¶é´èå´ãå
³é®è¯çæ¡ä»¶ï¼å¯å
使ç¨é»è®¤å£å¾å¹¶æç¤ºå¯è¡¥å
æ¡ä»¶ã |
| | | 5. è¥æ°æ®ä¸è¶³ä»¥å¾åºç»è®ºï¼æç¡®æåºç¼ºå°çç鿡件æå
³é®å段ã |