From d8cf4b3db03a4a4c2d12ef21eaec78cb7d3b10d1 Mon Sep 17 00:00:00 2001
From: yuan <123@>
Date: 星期三, 20 五月 2026 09:22:07 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_New_pro' into dev_山西_晋和园_pro
---
src/main/java/com/ruoyi/ai/assistant/SalesAgent.java | 22
src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java | 32
src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java | 4
src/main/resources/mapper/basic/CustomerMapper.xml | 1
src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java | 4
src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java | 4
src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java | 185 +-
src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java | 199 ++
src/main/resources/manufacturing-agent-prompt.txt | 8
src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java | 8
src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java | 40
src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java | 4
src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java | 4
src/main/java/com/ruoyi/ai/controller/SalesAiController.java | 131 +
src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java | 4
doc/20260518_销售助手前端联调文档.md | 188 ++
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java | 2
src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java | 4
src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java | 1475 +++++++++++++++++++
src/main/resources/static/销售台账导入模板.xlsx | 0
src/main/java/com/ruoyi/account/bean/vo/sales/SalesOutboundVo.java | 2
src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java | 17
src/main/java/com/ruoyi/account/controller/sales/AccountSalesController.java | 12
src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java | 3
src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml | 2
src/main/resources/mapper/stock/StockOutRecordMapper.xml | 8
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java | 2
src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java | 4
src/main/java/com/ruoyi/account/service/sales/AccountSalesService.java | 10
src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java | 1035 +++++++++++++
src/main/resources/mapper/stock/StockInRecordMapper.xml | 8
src/main/java/com/ruoyi/account/pojo/financial/AccountSubject.java | 2
src/main/java/com/ruoyi/quality/pojo/QualityInspect.java | 10
src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.java | 10
src/main/resources/mapper/production/ProductionOperationTaskMapper.xml | 1
src/main/java/com/ruoyi/account/service/purchase/AccountSubjectService.java | 8
src/main/java/com/ruoyi/account/bean/dto/sales/SalesOutboundDto.java | 2
src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseReturnVo.java | 2
src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectDto.java | 4
src/main/java/com/ruoyi/account/bean/vo/financial/AccountSubjectVo.java | 4
src/main/java/com/ruoyi/account/controller/purchase/AccounPurchaseController.java | 12
src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java | 14
src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java | 8
src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java | 2
src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml | 2
src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseInboundDto.java | 2
src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml | 2
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java | 3
src/main/java/com/ruoyi/stock/mapper/StockInRecordMapper.java | 4
src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java | 6
src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java | 7
src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseInboundVo.java | 2
src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java | 20
src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseServiceImpl.java | 12
src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java | 102 +
src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java | 136 +
doc/20260516_制造智能助手前端联调文档.md | 258 +++
src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java | 2
src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java | 5
src/main/resources/sales-agent-prompt.txt | 7
src/main/java/com/ruoyi/ai/controller/XiaozhiController.java | 31
src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java | 13
src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesServiceImpl.java | 12
src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java | 21
src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java | 21
src/main/resources/mapper/account/financial/AccountSubjectMapper.xml | 4
src/main/java/com/ruoyi/ai/assistant/SalesIntentExecutor.java | 270 +++
67 files changed, 4,230 insertions(+), 213 deletions(-)
diff --git "a/doc/20260516_\345\210\266\351\200\240\346\231\272\350\203\275\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260516_\345\210\266\351\200\240\346\231\272\350\203\275\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..d4b9a1f
--- /dev/null
+++ "b/doc/20260516_\345\210\266\351\200\240\346\231\272\350\203\275\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,258 @@
+# 鍒堕�犳櫤鑳藉姪鎵嬪墠绔仈璋冩枃妗o紙`manufacturing-ai`锛�
+
+> 鏇存柊鏃ユ湡锛�2026-05-16
+> 閫傜敤妯″潡锛氱敓浜х幇鍦恒�佽鍒掋�佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞�
+> 鑳藉姏鑼冨洿锛氭煡銆侀棶銆佸姙銆侀璀︺�佸垎鏋�
+
+## 1. 鎺ュ彛鎬昏
+
+1. 娴佸紡瀵硅瘽锛歚POST /manufacturing-ai/chat`
+2. 浼氳瘽鍒楄〃锛歚GET /manufacturing-ai/history/sessions`
+3. 浼氳瘽娑堟伅锛歚GET /manufacturing-ai/history/messages/{memoryId}`
+4. 鍒犻櫎浼氳瘽锛歚DELETE /manufacturing-ai/history/{memoryId}`
+
+璇存槑锛�
+- `/chat` 涓� **SSE/娴佸紡鏂囨湰** 杩斿洖锛坄text/stream;charset=utf-8`锛夈��
+- 鍛戒腑鈥滄煡/棰勮/鍒嗘瀽/鍔炩�濆伐鍏锋椂锛屾祦寮忔渶缁堝唴瀹规槸 **JSON 瀛楃涓�**锛堜笉鏄� `AjaxResult`锛夈��
+- 鏈懡涓伐鍏锋椂锛岃繑鍥炴櫘閫氳嚜鐒惰瑷�鏂囨湰銆�
+
+## 2. 閴存潈涓庤姹傚ご
+
+- 缁熶竴浣跨敤绯荤粺鐧诲綍鎬侊紙`Authorization` 涓庣幇鏈夋帴鍙d竴鑷达級銆�
+- `POST /manufacturing-ai/chat` 璇锋眰澶达細`Content-Type: application/json`銆�
+
+## 3. 瀵硅瘽鎺ュ彛
+
+### 3.1 璇锋眰
+
+```http
+POST /manufacturing-ai/chat
+Content-Type: application/json
+```
+
+```json
+{
+ "memoryId": "mfg-ai-001",
+ "message": "鏌ヨ澶囪タ闂ㄥ瓙鍙橀鍣ㄧ殑缁翠慨鎯呭喌"
+}
+```
+
+瀛楁璇存槑锛�
+
+| 瀛楁 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+| --- | --- | --- | --- |
+| memoryId | string | 鏄� | 浼氳瘽 ID锛屽墠绔敓鎴愬苟澶嶇敤 |
+| message | string | 鏄� | 鐢ㄦ埛杈撳叆 |
+
+### 3.2 杩斿洖锛堟祦寮忥級
+
+```http
+Content-Type: text/stream;charset=utf-8
+```
+
+鍓嶇澶勭悊寤鸿锛�
+1. 鎸夋祦鎷兼帴瀹屾暣鏂囨湰銆�
+2. 灏濊瘯 `JSON.parse(fullText)`锛�
+ - 鎴愬姛锛氭寜缁撴瀯鍖栫粨鏋滄覆鏌撱��
+ - 澶辫触锛氭寜鏅�氳亰澶╂枃鏈覆鏌撱��
+
+## 4. 缁撴瀯鍖栧搷搴斿崗璁�
+
+### 4.1 閫氱敤缁撴瀯
+
+```json
+{
+ "success": true,
+ "type": "manufacturing_device_repair_list",
+ "description": "宸茶繑鍥炶澶囩淮淇褰曘��",
+ "summary": {},
+ "data": {},
+ "charts": {}
+}
+```
+
+### 4.2 `type` 鏋氫妇
+
+| type | 鍦烘櫙 |
+| --- | --- |
+| manufacturing_site_snapshot | 鐢熶骇鐜板満姒傝 |
+| manufacturing_plan_list | 鐢熶骇璁″垝鏌ヨ |
+| manufacturing_workorder_list | 宸ュ崟鏌ヨ |
+| manufacturing_device_list | 璁惧鍙拌处鏌ヨ |
+| manufacturing_device_repair_list | 璁惧缁翠慨璁板綍鏌ヨ |
+| manufacturing_quality_list | 璐ㄩ噺鏌ヨ |
+| manufacturing_material_list | 鐗╂枡搴撳瓨鏌ヨ |
+| manufacturing_exception_list | 寮傚父澶勭悊鏌ヨ |
+| manufacturing_warning | 棰勮鐪嬫澘 |
+| manufacturing_analysis | 缁忚惀鍒嗘瀽 |
+| manufacturing_action_plan | 鍔炵悊寤鸿锛堝姩浣滃崱锛� |
+
+## 5. 鈥滄煡鈥濊兘鍔涜仈璋冭鐐�
+
+### 5.1 璁惧鐩稿叧璺敱瑙勫垯锛堝叧閿級
+
+- 褰撶敤鎴疯緭鍏ュ寘鍚� `缁翠慨/鎶ヤ慨/妫�淇�/缁存姢`锛岃澶囧煙浼氳繑鍥� `manufacturing_device_repair_list`锛堟煡 `device_repair`锛夈��
+- 鏈寘鍚互涓婅瘝鏃讹紝杩斿洖 `manufacturing_device_list`锛堟煡璁惧鍙拌处锛夈��
+
+绀轰緥锛�
+- `鏌ヨ澶嘇-01` -> `manufacturing_device_list`
+- `鏌ヨ澶嘇-01缁翠慨鎯呭喌` -> `manufacturing_device_repair_list`
+
+### 5.2 缁翠慨璁板綍鏃堕棿杩囨护瑙勫垯锛堝叧閿級
+
+- 鐢ㄦ埛鏄庣‘甯︽椂闂存潯浠讹紙濡傗�滄湰鏈�/涓婂懆/杩�7澶�/2026-05-01 鍒� 2026-05-16鈥濓級鎵嶆寜鏃堕棿杩囨护缁翠慨璁板綍銆�
+- 鏈甫鏃堕棿鏉′欢鏃讹紝涓嶉粯璁ゆ寜杩� 30 澶╂埅鏂紝閬垮厤鍘嗗彶缁翠慨璁板綍琚杩囨护銆�
+
+### 5.3 鍏抽敭璇嶅鐞嗚鍒欙紙璁惧/缁翠慨锛�
+
+- 绯荤粺浼氭竻娲楀櫔闊宠瘝锛歚鏌ヨ/鏌ョ湅/璇�/璁惧/缁翠慨鎯呭喌/璁板綍/淇℃伅` 绛夈��
+- 鍚屾椂浼氶�氳繃璁惧鍙拌处鍖归厤 `deviceLedgerId` 鍏滃簳锛屽啀鍥炴煡缁翠慨璁板綍锛岄檷浣庘�滄湁鏁版嵁浣嗘煡涓嶅埌鈥濈殑姒傜巼銆�
+
+### 5.4 鍒楄〃缁撴灉绾﹀畾
+
+- 鍒楄〃鏁版嵁缁熶竴鍦� `data.items`
+- 缁熻鎽樿鍦� `summary`
+
+甯哥敤瀛楁锛�
+
+| type | 甯哥敤瀛楁 |
+| --- | --- |
+| manufacturing_plan_list | `mpsNo`, `requiredDate`, `status` |
+| manufacturing_workorder_list | `workOrderNo`, `planStartTime`, `planEndTime`, `status` |
+| manufacturing_device_list | `deviceName`, `deviceModel`, `pendingRepairCount` |
+| manufacturing_device_repair_list | `deviceName`, `deviceModel`, `repairTime`, `repairName`, `maintenanceName`, `status`, `createTime` |
+
+## 6. 鈥滈璀︹�濊仈璋冭鐐�
+
+- `type = manufacturing_warning`
+- 棰勮鏄庣粏鍦� `data.items`锛屾瘡椤瑰寘鍚細
+ - `level`锛歚high` / `medium`
+ - `title`
+ - `count`
+ - `detail`
+
+鐘舵�佸彛寰勶細
+- 璁惧鈥滃緟缁翠慨鈥濈粺璁℃寜 `status = 0` 璁$畻锛堜笉鍐嶆妸鍏朵粬鐘舵�佽鍏ュ緟缁翠慨锛夈��
+
+## 7. 鈥滃垎鏋愨�濊仈璋冭鐐�
+
+- `type = manufacturing_analysis`
+- 鍏抽敭鎸囨爣鍦� `summary`
+- 鎸囨爣鍗″湪 `data.coreMetrics`
+- 鍥捐〃閰嶇疆鍦� `charts`锛�
+ - `charts.domainBarOption`
+ - `charts.qualityPieOption`
+
+鍥捐〃閰嶇疆鍙洿鎺ョ粰 ECharts 浣跨敤銆�
+
+## 8. 鈥滃姙鈥濊兘鍔涜仈璋冭鐐�
+
+褰撳墠鈥滃姙鈥濅负 **鍔炵悊寤鸿妯″紡**锛圓I 杈撳嚭鍔ㄤ綔鍗★紝鍓嶇纭鍚庤皟鐢ㄧ洰鏍囦笟鍔℃帴鍙o級銆�
+
+- `type = manufacturing_action_plan`
+- 鍔ㄤ綔鍗℃暟缁勶細`data.actionCards`
+
+鍔ㄤ綔鍗″瓧娈碉細
+
+| 瀛楁 | 璇存槑 |
+| --- | --- |
+| code | 鍔ㄤ綔缂栫爜 |
+| name | 鍔ㄤ綔鍚嶇О |
+| method | 璇锋眰鏂规硶 |
+| targetApi | 鐩爣涓氬姟鎺ュ彛 |
+| requiredFields | 蹇呭~瀛楁 |
+| examplePayload | 绀轰緥鍙傛暟 |
+| description | 璇存槑 |
+
+鍐呯疆鍔ㄤ綔绀轰緥锛�
+1. `POST /productionOperationTask/assign`
+2. `POST /device/repair`
+3. `POST /quality/qualityUnqualified/deal`
+4. `POST /stockInventory/addstockInventory`
+5. `POST /procurementExceptionRecord/add`
+
+## 9. 浼氳瘽绠$悊鎺ュ彛
+
+### 9.1 浼氳瘽鍒楄〃
+
+```http
+GET /manufacturing-ai/history/sessions
+```
+
+`AjaxResult.data` 瀛楁锛�
+- `memoryId`
+- `title`
+- `lastMessage`
+- `messageCount`
+- `lastChatTime`
+
+### 9.2 浼氳瘽娑堟伅
+
+```http
+GET /manufacturing-ai/history/messages/{memoryId}
+```
+
+`AjaxResult.data` 瀛楁锛�
+- `role`锛歚user` / `assistant` / `system` / `tool`
+- `content`
+- `filePaths`
+
+### 9.3 鍒犻櫎浼氳瘽
+
+```http
+DELETE /manufacturing-ai/history/{memoryId}
+```
+
+杩斿洖鏍囧噯 `AjaxResult`銆�
+
+## 10. 閿欒涓庤竟鐣�
+
+`/chat` 甯歌杩斿洖鏂囨湰锛�
+- `memoryId涓嶈兘涓虹┖`
+- `message涓嶈兘涓虹┖`
+
+寤鸿鍓嶇鍙戦�佸墠鍏堝仛蹇呭~鏍¢獙銆�
+
+## 11. 鍓嶇鑱旇皟娴佺▼寤鸿
+
+1. 鐧诲綍鍚庡垱寤哄苟澶嶇敤 `memoryId`銆�
+2. 璋冪敤 `/manufacturing-ai/chat`锛屾寜 SSE 鎷兼帴瀹屾暣鏂囨湰銆�
+3. 鍏堝皾璇� JSON 瑙f瀽锛�
+ - 鎴愬姛锛氭寜 `type` 璺敱鍒板搴� UI锛堝垪琛�/棰勮/鍒嗘瀽/鍔ㄤ綔鍗★級銆�
+ - 澶辫触锛氭寜鏅�氳亰澶╂秷鎭睍绀恒��
+4. 鈥滃姙鈥濆満鏅敱鐢ㄦ埛纭鍔ㄤ綔鍗″悗锛屽墠绔皟鐢� `targetApi` 瀹屾垚涓氬姟鎻愪氦銆�
+5. 閫氳繃鍘嗗彶鎺ュ彛鍋氫細璇濆洖鏄句笌鍒犻櫎銆�
+
+## 12. 鍓嶇闆嗘垚绾︽潫锛堟湰娆¤ˉ鍏咃級
+
+### 12.1 鏅鸿兘浣撴柊澧炰笌寮圭獥鍚屾瑙勫垯锛堝己鍒讹級
+
+1. 褰� `src/views/aiIndustrialBrain/index.vue` 鏂板鏅鸿兘浣擄紙`agents`锛夐�昏緫鏃讹紝蹇呴』鍚屾纭寮圭獥鍔╂墜鍙敤鎬с��
+2. 寮圭獥鍔╂墜缁熶竴鐢� `src/components/AIChatSidebar/assistants/index.js` 鐨� `assistantRegistry` 娉ㄥ唽銆�
+3. 鏂板鏅鸿兘浣撶殑 `key` 鑻ヨ鍦ㄥ脊绐椾腑鍙敤锛屽繀椤诲湪 `assistantRegistry` 涓彁渚涘悓鍚嶉厤缃��
+4. 鏈湪 `assistantRegistry` 娉ㄥ唽鐨勬櫤鑳戒綋锛屽脊绐楁樉绀轰负 `pending`锛堝紑鍙戜腑锛夋�併��
+
+### 12.2 鐢熶骇鍔╂墜鎺ュ叆绾﹀畾
+
+1. 鐢熶骇鍔╂墜閰嶇疆浣嶄簬 `src/components/AIChatSidebar/assistants/productionAssistant.js`锛宍apiBase = /manufacturing-ai`銆�
+2. AI 宸ヤ笟澶ц剳涓敓浜ф櫤鑳戒綋杩涘叆寮圭獥鍚庯紝榛樿浣跨敤 `production` 鍔╂墜銆�
+3. 鍏ㄥ眬鍙充晶瀵硅瘽妗嗗姪鎵嬪垏鎹㈠垪琛ㄥ凡鍖呭惈锛�
+ - `general`锛堝緟鍔炲姪鐞嗭級
+ - `purchase`锛堥噰璐姪鐞嗭級
+ - `production`锛堢敓浜у姪鐞嗭級
+
+### 12.3 瀛楁涓枃鍖栧睍绀鸿鍒�
+
+1. 闈㈠悜涓氬姟鐢ㄦ埛鐨勫瓧娈靛悕銆佹爣绛俱�佸繀濉彁绀轰笉鐩存帴灞曠ず鑻辨枃 key銆�
+2. `requiredFields`銆乣missingFields` 鎻愮ず闇�杞崲涓轰腑鏂囪矾寰勬爣绛撅紙绀轰緥锛歚缂哄皯蹇呭~瀛楁锛氬伐鍗曞彿銆佽鍒掔粨鏉熸椂闂碻锛夈��
+3. 缁撴瀯鍖栧垪琛ㄥ垪鍚嶃�佹憳瑕佹寚鏍囥�佸姩浣滃崱瀛楁浼樺厛鏄剧ず涓枃锛涜嫳鏂� key 浠呯敤浜庢帴鍙i�氫俊涓庤皟璇曘��
+
+## 13. 鏈鏇存柊璁板綍锛�2026-05-16锛�
+
+1. 鏂板璁惧缁翠慨璁板綍杩斿洖绫诲瀷锛歚manufacturing_device_repair_list`銆�
+2. 淇璁惧鍩熸剰鍥惧垎娴侊細`缁翠慨/鎶ヤ慨/妫�淇�/缁存姢` 璧扮淮淇褰曪紝涓嶅啀璇蛋璁惧鍒楄〃銆�
+3. 淇缁翠慨璁板綍鏃堕棿杩囨护锛氫粎鍦ㄧ敤鎴锋槑纭椂闂存潯浠舵椂鐢熸晥銆�
+4. 淇寰呯淮淇粺璁″彛寰勶細鎸� `status = 0` 缁熻銆�
+5. 鏂板 AI 宸ヤ笟澶ц剳鏅鸿兘浣撲笌寮圭獥鍚屾缁存姢瑙勫垯锛氭柊澧炴櫤鑳戒綋蹇呴』鍚屾娉ㄥ唽寮圭獥鍔╂墜銆�
+6. 鐢熶骇鍔╂墜宸叉帴鍏ュ伐涓氬ぇ鑴戝脊绐椾笌鍏ㄥ眬鍙充晶瀵硅瘽妗嗗姪鎵嬪垏鎹€��
+7. 澧炲姞瀛楁涓枃鍖栧睍绀虹害鏉燂細閬垮厤鑻辨枃瀛楁瀵逛笟鍔$敤鎴风洿鍑恒��
diff --git "a/doc/20260518_\351\224\200\345\224\256\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260518_\351\224\200\345\224\256\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..d0c6e40
--- /dev/null
+++ "b/doc/20260518_\351\224\200\345\224\256\345\212\251\346\211\213\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,188 @@
+# 閿�鍞姪鎵嬪墠绔仈璋冩枃妗o紙`/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`锛圫SE 鏂囨湰娴侊級銆�
+- 鍛戒腑宸ュ叿鏃讹紝鏈�缁堝唴瀹逛负 **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` | 瀹㈡埛妗f锛堢娴�/鍏捣锛� |
+| `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. 瀹㈡埛妗f锛堢娴凤級锛氱ず渚嬫彁闂� `鏌ヨ绉佹捣瀹㈡埛妗f鍓�10鏉
+2. 瀹㈡埛妗f锛堝叕娴凤級锛氱ず渚嬫彁闂� `鏌ヨ鍏捣瀹㈡埛妗f`
+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 瑙f瀽澶辫触鏃堕〉闈㈢┖鐧姐��
+4. 涓氬姟灞曠ず瀛楁寤鸿涓枃鍖栵紝涓嶇洿鎺ュ睍绀鸿嫳鏂囧瓧娈� key銆�
+
+## 9. 鑱旇皟楠屾敹娓呭崟
+
+1. 鑳芥甯告祦寮忔帴鏀� `/sales-ai/chat` 鍝嶅簲骞舵嫾鎺ユ枃鏈��
+2. 鑳芥寜 `type` 姝g‘娓叉煋 9 绫荤粨鏋勫寲缁撴灉銆�
+3. 鑳芥纭睍绀衡�滃鎴锋祦澶遍闄╁垎鏋愨�濆拰鈥滃洖娆句笌鎶ヤ环绛栫暐寤鸿鈥濅袱涓噸鐐瑰満鏅��
+4. 浼氳瘽鍒楄〃銆佷細璇濇秷鎭�佸垹闄や細璇濆叏閾捐矾鍙敤銆�
+5. `memoryId` 澶嶇敤鍚庡彲鍥炵湅鍘嗗彶锛屼笉浼氫覆浼氳瘽銆�
diff --git a/src/main/java/com/ruoyi/account/bean/dto/AccountSubjectDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectDto.java
similarity index 60%
rename from src/main/java/com/ruoyi/account/bean/dto/AccountSubjectDto.java
rename to src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectDto.java
index e26844d..4d55666 100644
--- a/src/main/java/com/ruoyi/account/bean/dto/AccountSubjectDto.java
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectDto.java
@@ -1,6 +1,6 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/dto/AccountSubjectImportDto.java b/src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java
similarity index 94%
rename from src/main/java/com/ruoyi/account/bean/dto/AccountSubjectImportDto.java
rename to src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java
index 28c8ab9..7ea5225 100644
--- a/src/main/java/com/ruoyi/account/bean/dto/AccountSubjectImportDto.java
+++ b/src/main/java/com/ruoyi/account/bean/dto/financial/AccountSubjectImportDto.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java b/src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseInboundDto.java
similarity index 94%
rename from src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java
rename to src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseInboundDto.java
index 757e6b4..9e0a027 100644
--- a/src/main/java/com/ruoyi/account/bean/dto/PurchaseInboundDto.java
+++ b/src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseInboundDto.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java b/src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java
similarity index 94%
rename from src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java
rename to src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java
index c238990..f5adae5 100644
--- a/src/main/java/com/ruoyi/account/bean/dto/PurchaseReturnDto.java
+++ b/src/main/java/com/ruoyi/account/bean/dto/purchase/PurchaseReturnDto.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/dto/SalesOutboundDto.java b/src/main/java/com/ruoyi/account/bean/dto/sales/SalesOutboundDto.java
similarity index 94%
rename from src/main/java/com/ruoyi/account/bean/dto/SalesOutboundDto.java
rename to src/main/java/com/ruoyi/account/bean/dto/sales/SalesOutboundDto.java
index 33bc1b9..ba762e3 100644
--- a/src/main/java/com/ruoyi/account/bean/dto/SalesOutboundDto.java
+++ b/src/main/java/com/ruoyi/account/bean/dto/sales/SalesOutboundDto.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/dto/SalesReturnDto.java b/src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java
similarity index 94%
rename from src/main/java/com/ruoyi/account/bean/dto/SalesReturnDto.java
rename to src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java
index b7ebae2..1c6e266 100644
--- a/src/main/java/com/ruoyi/account/bean/dto/SalesReturnDto.java
+++ b/src/main/java/com/ruoyi/account/bean/dto/sales/SalesReturnDto.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java b/src/main/java/com/ruoyi/account/bean/vo/financial/AccountSubjectVo.java
similarity index 76%
rename from src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
rename to src/main/java/com/ruoyi/account/bean/vo/financial/AccountSubjectVo.java
index c6bb078..106eb17 100644
--- a/src/main/java/com/ruoyi/account/bean/vo/AccountSubjectVo.java
+++ b/src/main/java/com/ruoyi/account/bean/vo/financial/AccountSubjectVo.java
@@ -1,6 +1,6 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/vo/PurchaseInboundVo.java b/src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseInboundVo.java
similarity index 96%
rename from src/main/java/com/ruoyi/account/bean/vo/PurchaseInboundVo.java
rename to src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseInboundVo.java
index 934502a..2160674 100644
--- a/src/main/java/com/ruoyi/account/bean/vo/PurchaseInboundVo.java
+++ b/src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseInboundVo.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/vo/PurchaseReturnVo.java b/src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseReturnVo.java
similarity index 96%
rename from src/main/java/com/ruoyi/account/bean/vo/PurchaseReturnVo.java
rename to src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseReturnVo.java
index e912993..82a90a3 100644
--- a/src/main/java/com/ruoyi/account/bean/vo/PurchaseReturnVo.java
+++ b/src/main/java/com/ruoyi/account/bean/vo/purchase/PurchaseReturnVo.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/vo/SalesOutboundVo.java b/src/main/java/com/ruoyi/account/bean/vo/sales/SalesOutboundVo.java
similarity index 96%
rename from src/main/java/com/ruoyi/account/bean/vo/SalesOutboundVo.java
rename to src/main/java/com/ruoyi/account/bean/vo/sales/SalesOutboundVo.java
index 5ccdb5b..90d4d53 100644
--- a/src/main/java/com/ruoyi/account/bean/vo/SalesOutboundVo.java
+++ b/src/main/java/com/ruoyi/account/bean/vo/sales/SalesOutboundVo.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/bean/vo/SalesReturnVo.java b/src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java
similarity index 96%
rename from src/main/java/com/ruoyi/account/bean/vo/SalesReturnVo.java
rename to src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java
index c425737..980d7f8 100644
--- a/src/main/java/com/ruoyi/account/bean/vo/SalesReturnVo.java
+++ b/src/main/java/com/ruoyi/account/bean/vo/sales/SalesReturnVo.java
@@ -1,4 +1,4 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java b/src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java
similarity index 91%
rename from src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
rename to src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java
index 38dd0ce..19d333d 100644
--- a/src/main/java/com/ruoyi/account/controller/AccountSubjectController.java
+++ b/src/main/java/com/ruoyi/account/controller/financial/AccountSubjectController.java
@@ -1,10 +1,10 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/controller/AccounPurchaseController.java b/src/main/java/com/ruoyi/account/controller/purchase/AccounPurchaseController.java
similarity index 88%
rename from src/main/java/com/ruoyi/account/controller/AccounPurchaseController.java
rename to src/main/java/com/ruoyi/account/controller/purchase/AccounPurchaseController.java
index d1993ea..283e8fb 100644
--- a/src/main/java/com/ruoyi/account/controller/AccounPurchaseController.java
+++ b/src/main/java/com/ruoyi/account/controller/purchase/AccounPurchaseController.java
@@ -1,12 +1,12 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/controller/AccountSalesController.java b/src/main/java/com/ruoyi/account/controller/sales/AccountSalesController.java
similarity index 88%
rename from src/main/java/com/ruoyi/account/controller/AccountSalesController.java
rename to src/main/java/com/ruoyi/account/controller/sales/AccountSalesController.java
index bca90db..25d8096 100644
--- a/src/main/java/com/ruoyi/account/controller/AccountSalesController.java
+++ b/src/main/java/com/ruoyi/account/controller/sales/AccountSalesController.java
@@ -1,12 +1,12 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java b/src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java
similarity index 82%
rename from src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
rename to src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java
index 46a4968..a5dd4fc 100644
--- a/src/main/java/com/ruoyi/account/mapper/AccountSubjectMapper.java
+++ b/src/main/java/com/ruoyi/account/mapper/financial/AccountSubjectMapper.java
@@ -1,7 +1,7 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/pojo/AccountSubject.java b/src/main/java/com/ruoyi/account/pojo/financial/AccountSubject.java
similarity index 98%
rename from src/main/java/com/ruoyi/account/pojo/AccountSubject.java
rename to src/main/java/com/ruoyi/account/pojo/financial/AccountSubject.java
index 9616324..222ee6c 100644
--- a/src/main/java/com/ruoyi/account/pojo/AccountSubject.java
+++ b/src/main/java/com/ruoyi/account/pojo/financial/AccountSubject.java
@@ -1,4 +1,4 @@
-package com.ruoyi.account.pojo;
+package com.ruoyi.account.pojo.financial;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
diff --git a/src/main/java/com/ruoyi/account/service/AccountPurchaseService.java b/src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.java
similarity index 72%
rename from src/main/java/com/ruoyi/account/service/AccountPurchaseService.java
rename to src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.java
index 386f921..b7a6cfd 100644
--- a/src/main/java/com/ruoyi/account/service/AccountPurchaseService.java
+++ b/src/main/java/com/ruoyi/account/service/financial/AccountPurchaseService.java
@@ -1,11 +1,11 @@
-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;
/**
diff --git a/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java
similarity index 97%
rename from src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
rename to src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java
index 37bf64b..a6c4149 100644
--- a/src/main/java/com/ruoyi/account/service/impl/AccountSubjectServiceImpl.java
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/AccountSubjectServiceImpl.java
@@ -1,15 +1,15 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
index b7548ef..6859b52 100644
--- a/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
+++ b/src/main/java/com/ruoyi/account/service/impl/financial/FinVoucherServiceImpl.java
@@ -8,10 +8,10 @@
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;
diff --git a/src/main/java/com/ruoyi/account/service/impl/AccountPurchaseServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseServiceImpl.java
similarity index 85%
rename from src/main/java/com/ruoyi/account/service/impl/AccountPurchaseServiceImpl.java
rename to src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseServiceImpl.java
index 747f6cf..9547d05 100644
--- a/src/main/java/com/ruoyi/account/service/impl/AccountPurchaseServiceImpl.java
+++ b/src/main/java/com/ruoyi/account/service/impl/purchase/AccountPurchaseServiceImpl.java
@@ -1,12 +1,12 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/service/impl/AccountSalesServiceImpl.java b/src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesServiceImpl.java
similarity index 85%
rename from src/main/java/com/ruoyi/account/service/impl/AccountSalesServiceImpl.java
rename to src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesServiceImpl.java
index ddf4a57..814d294 100644
--- a/src/main/java/com/ruoyi/account/service/impl/AccountSalesServiceImpl.java
+++ b/src/main/java/com/ruoyi/account/service/impl/sales/AccountSalesServiceImpl.java
@@ -1,12 +1,12 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/service/AccountSubjectService.java b/src/main/java/com/ruoyi/account/service/purchase/AccountSubjectService.java
similarity index 78%
rename from src/main/java/com/ruoyi/account/service/AccountSubjectService.java
rename to src/main/java/com/ruoyi/account/service/purchase/AccountSubjectService.java
index bcbc57c..51d44ec 100644
--- a/src/main/java/com/ruoyi/account/service/AccountSubjectService.java
+++ b/src/main/java/com/ruoyi/account/service/purchase/AccountSubjectService.java
@@ -1,10 +1,10 @@
-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;
diff --git a/src/main/java/com/ruoyi/account/service/AccountSalesService.java b/src/main/java/com/ruoyi/account/service/sales/AccountSalesService.java
similarity index 73%
rename from src/main/java/com/ruoyi/account/service/AccountSalesService.java
rename to src/main/java/com/ruoyi/account/service/sales/AccountSalesService.java
index 7db5416..82f606c 100644
--- a/src/main/java/com/ruoyi/account/service/AccountSalesService.java
+++ b/src/main/java/com/ruoyi/account/service/sales/AccountSalesService.java
@@ -1,11 +1,11 @@
-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;
/**
diff --git a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
index 9f8499d..8d8ad45 100644
--- a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
@@ -14,8 +14,9 @@
@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+(澶﹟鍛▅涓湀|鏈坾骞�)");
@@ -34,54 +35,63 @@
}
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,
@@ -101,7 +111,82 @@
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;
}
@@ -130,6 +215,10 @@
}
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;
}
@@ -196,6 +285,8 @@
.replace("鍗曟嵁", "")
.replace("寰呭姙", "")
.replace("鍒楄〃", "")
+ .replace("娴佺▼缂栧彿", "")
+ .replace("娴佺▼鍙�", "")
.replace("鍓�10鏉�", "")
.replace("鏈�杩�10鏉�", "")
.trim();
@@ -203,7 +294,7 @@
}
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;
}
@@ -250,7 +341,7 @@
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;
}
@@ -264,21 +355,85 @@
}
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;
}
diff --git a/src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java b/src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
new file mode 100644
index 0000000..f0e8cf7
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/ManufacturingAgent.java
@@ -0,0 +1,21 @@
+package com.ruoyi.ai.assistant;
+
+import dev.langchain4j.service.MemoryId;
+import dev.langchain4j.service.SystemMessage;
+import dev.langchain4j.service.UserMessage;
+import dev.langchain4j.service.spring.AiService;
+import reactor.core.publisher.Flux;
+
+import static dev.langchain4j.service.spring.AiServiceWiringMode.EXPLICIT;
+
+@AiService(
+ wiringMode = EXPLICIT,
+ streamingChatModel = "qwenStreamingChatModel",
+ chatMemoryProvider = "chatMemoryProviderManufacturing",
+ tools = "manufacturingAgentTools"
+)
+public interface ManufacturingAgent {
+
+ @SystemMessage(fromResource = "manufacturing-agent-prompt.txt")
+ Flux<String> chat(@MemoryId String memoryId, @UserMessage String userMessage);
+}
diff --git a/src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java
new file mode 100644
index 0000000..e9d9396
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/ManufacturingIntentExecutor.java
@@ -0,0 +1,136 @@
+package com.ruoyi.ai.assistant;
+
+import com.ruoyi.ai.tools.ManufacturingAgentTools;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.util.regex.Matcher;
+import java.util.regex.Pattern;
+
+@Component
+public class ManufacturingIntentExecutor {
+
+ private static final Pattern LIMIT_PATTERN = Pattern.compile("(鍓峾鏈�杩�)?(\\d{1,2})鏉�");
+ private static final Pattern DATE_PATTERN = Pattern.compile("(\\d{4}-\\d{2}-\\d{2})");
+
+ private final ManufacturingAgentTools manufacturingAgentTools;
+
+ public ManufacturingIntentExecutor(ManufacturingAgentTools manufacturingAgentTools) {
+ this.manufacturingAgentTools = manufacturingAgentTools;
+ }
+
+ public String tryExecute(String memoryId, String message) {
+ if (!StringUtils.hasText(message)) {
+ return null;
+ }
+ String text = message.trim();
+ String keyword = extractKeyword(text);
+ Integer limit = extractLimit(text);
+ String startDate = extractStartDate(text);
+ String endDate = extractEndDate(text);
+
+ if (containsAny(text, "棰勮", "鍛婅", "椋庨櫓", "鎻愰啋")) {
+ return manufacturingAgentTools.getWarningBoard(memoryId, startDate, endDate, text);
+ }
+ if (containsAny(text, "鍒嗘瀽", "缁熻", "瓒嬪娍", "鐪嬫澘", "鎶ヨ〃", "鎬昏")) {
+ return manufacturingAgentTools.analyzeFactory(memoryId, startDate, endDate, text);
+ }
+ if (containsAny(text, "鍔�", "澶勭悊", "娲惧伐", "瀹夋帓", "闂幆", "璺熻繘", "澶勭疆")) {
+ return manufacturingAgentTools.planActions(memoryId, text);
+ }
+
+ if (containsAny(text, "鐢熶骇鐜板満", "鐜板満", "杞﹂棿")) {
+ return manufacturingAgentTools.queryDomain(memoryId, "site", keyword, limit, startDate, endDate, text);
+ }
+ if (containsAny(text, "璁″垝", "鎺掍骇", "mps")) {
+ return manufacturingAgentTools.queryDomain(memoryId, "plan", keyword, limit, startDate, endDate, text);
+ }
+ if (containsAny(text, "宸ュ崟", "浣滀笟鍗�", "浠诲姟鍗�", "浠诲姟")) {
+ return manufacturingAgentTools.queryDomain(memoryId, "workorder", keyword, limit, startDate, endDate, text);
+ }
+ if (containsAny(text, "璁惧", "缁翠慨", "淇濆吇", "鏁呴殰")) {
+ return manufacturingAgentTools.queryDomain(memoryId, "device", keyword, limit, startDate, endDate, text);
+ }
+ if (containsAny(text, "璐ㄩ噺", "璐ㄦ", "涓嶅悎鏍�", "妫�楠�")) {
+ return manufacturingAgentTools.queryDomain(memoryId, "quality", keyword, limit, startDate, endDate, text);
+ }
+ if (containsAny(text, "鐗╂枡", "搴撳瓨", "搴撲綅", "鍏ュ簱", "鍑哄簱")) {
+ return manufacturingAgentTools.queryDomain(memoryId, "material", keyword, limit, startDate, endDate, text);
+ }
+ if (containsAny(text, "寮傚父", "渚嬪", "鍋忓樊")) {
+ return manufacturingAgentTools.queryDomain(memoryId, "exception", keyword, limit, startDate, endDate, text);
+ }
+ return null;
+ }
+
+ private boolean containsAny(String text, String... keywords) {
+ for (String keyword : keywords) {
+ if (text.toLowerCase().contains(keyword.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private Integer extractLimit(String text) {
+ Matcher matcher = LIMIT_PATTERN.matcher(text);
+ return matcher.find() ? Integer.parseInt(matcher.group(2)) : 10;
+ }
+
+ private String extractStartDate(String text) {
+ Matcher matcher = DATE_PATTERN.matcher(text);
+ return matcher.find() ? matcher.group(1) : null;
+ }
+
+ private String extractEndDate(String text) {
+ Matcher matcher = DATE_PATTERN.matcher(text);
+ if (!matcher.find()) {
+ return null;
+ }
+ return matcher.find() ? matcher.group(1) : null;
+ }
+
+ private String extractKeyword(String text) {
+ String cleaned = text
+ .replace("鏌ヨ", "")
+ .replace("鏌ョ湅", "")
+ .replace("甯垜", "")
+ .replace("璇�", "")
+ .replace("涓�涓�", "")
+ .replace("鎵�鏈�", "")
+ .replace("鍏ㄩ儴", "")
+ .replace("浠婂勾", "")
+ .replace("鏈勾", "")
+ .replace("鍘诲勾", "")
+ .replace("鏈湀", "")
+ .replace("涓婃湀", "")
+ .replace("鏈懆", "")
+ .replace("涓婂懆", "")
+ .replace("浠婂ぉ", "")
+ .replace("鏄ㄥぉ", "")
+ .replace("杩�30澶�", "")
+ .replace("杩�7澶�", "")
+ .replace("杩�15澶�", "")
+ .replace("杩�60澶�", "")
+ .replace("鏈�杩�30澶�", "")
+ .replace("鏈�杩�7澶�", "")
+ .replace("鏈�杩�15澶�", "")
+ .replace("鏈�杩�60澶�", "")
+ .replace("鐢熶骇鐜板満", "")
+ .replace("鐜板満", "")
+ .replace("鐢熶骇宸ュ崟", "")
+ .replace("鐢熶骇", "")
+ .replace("璁″垝", "")
+ .replace("鎺掍骇", "")
+ .replace("宸ュ崟", "")
+ .replace("璁惧", "")
+ .replace("璐ㄩ噺", "")
+ .replace("鐗╂枡", "")
+ .replace("搴撳瓨", "")
+ .replace("寮傚父", "")
+ .replace("鍓�10鏉�", "")
+ .replace("鏈�杩�10鏉�", "")
+ .trim();
+ return cleaned.length() >= 2 ? cleaned : null;
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/assistant/SalesAgent.java b/src/main/java/com/ruoyi/ai/assistant/SalesAgent.java
new file mode 100644
index 0000000..1636239
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/SalesAgent.java
@@ -0,0 +1,22 @@
+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);
+}
+
diff --git a/src/main/java/com/ruoyi/ai/assistant/SalesIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/SalesIntentExecutor.java
new file mode 100644
index 0000000..4388a3c
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/assistant/SalesIntentExecutor.java
@@ -0,0 +1,270 @@
+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, "瀹㈡埛妗f", "绉佹捣", "鍏捣", "瀹㈡埛姹�")) {
+ 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 ("鏌ヨ绉佹捣瀹㈡埛妗f鍓�10鏉�".equals(normalized)) {
+ return salesAgentTools.listCustomerProfiles(memoryId, "private", null, 10);
+ }
+ if ("鏌ヨ鍏捣瀹㈡埛妗f".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("瀹㈡埛妗f", "")
+ .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) {
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java b/src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java
new file mode 100644
index 0000000..79aa222
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/config/ManufacturingAgentConfig.java
@@ -0,0 +1,20 @@
+package com.ruoyi.ai.config;
+
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import dev.langchain4j.memory.chat.ChatMemoryProvider;
+import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.context.annotation.Bean;
+import org.springframework.context.annotation.Configuration;
+
+@Configuration
+public class ManufacturingAgentConfig {
+
+ @Bean
+ ChatMemoryProvider chatMemoryProviderManufacturing(MongoChatMemoryStore mongoChatMemoryStore) {
+ return memoryId -> MessageWindowChatMemory.builder()
+ .id(memoryId)
+ .maxMessages(30)
+ .chatMemoryStore(mongoChatMemoryStore)
+ .build();
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java b/src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java
new file mode 100644
index 0000000..aed3104
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/config/SalesAgentConfig.java
@@ -0,0 +1,21 @@
+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();
+ }
+}
+
diff --git a/src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java b/src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
new file mode 100644
index 0000000..cb7c0ba
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/controller/ManufacturingAiController.java
@@ -0,0 +1,102 @@
+package com.ruoyi.ai.controller;
+
+import com.ruoyi.ai.assistant.ManufacturingAgent;
+import com.ruoyi.ai.assistant.ManufacturingIntentExecutor;
+import com.ruoyi.ai.bean.ChatForm;
+import com.ruoyi.ai.context.AiSessionUserContext;
+import com.ruoyi.ai.service.AiChatSessionService;
+import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.framework.web.controller.BaseController;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.UserMessage;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.web.bind.annotation.DeleteMapping;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PathVariable;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestBody;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+import reactor.core.publisher.Flux;
+
+import java.util.List;
+
+@Tag(name = "鍒堕�犳櫤鑳藉姪鎵�")
+@RestController
+@RequestMapping("/manufacturing-ai")
+public class ManufacturingAiController extends BaseController {
+
+ private final ManufacturingAgent manufacturingAgent;
+ private final ManufacturingIntentExecutor manufacturingIntentExecutor;
+ private final AiSessionUserContext aiSessionUserContext;
+ private final MongoChatMemoryStore mongoChatMemoryStore;
+ private final AiChatSessionService aiChatSessionService;
+
+ public ManufacturingAiController(ManufacturingAgent manufacturingAgent,
+ ManufacturingIntentExecutor manufacturingIntentExecutor,
+ AiSessionUserContext aiSessionUserContext,
+ MongoChatMemoryStore mongoChatMemoryStore,
+ AiChatSessionService aiChatSessionService) {
+ this.manufacturingAgent = manufacturingAgent;
+ this.manufacturingIntentExecutor = manufacturingIntentExecutor;
+ this.aiSessionUserContext = aiSessionUserContext;
+ this.mongoChatMemoryStore = mongoChatMemoryStore;
+ this.aiChatSessionService = aiChatSessionService;
+ }
+
+ @Operation(summary = "鍒堕�犲璇�")
+ @PostMapping(value = "/chat", produces = "text/stream;charset=utf-8")
+ public Flux<String> chat(@RequestBody ChatForm chatForm) {
+ if (!StringUtils.hasText(chatForm.getMemoryId())) {
+ return Flux.just("memoryId涓嶈兘涓虹┖");
+ }
+ if (!StringUtils.hasText(chatForm.getMessage())) {
+ return Flux.just("message涓嶈兘涓虹┖");
+ }
+
+ LoginUser loginUser = SecurityUtils.getLoginUser();
+ String memoryId = chatForm.getMemoryId();
+ String userMessage = chatForm.getMessage();
+
+ aiSessionUserContext.bind(memoryId, loginUser);
+ aiChatSessionService.touchSession(memoryId, loginUser, userMessage);
+
+ String directResponse = manufacturingIntentExecutor.tryExecute(memoryId, userMessage);
+ if (StringUtils.isNotEmpty(directResponse)) {
+ mongoChatMemoryStore.appendMessages(
+ memoryId,
+ List.of(UserMessage.from(userMessage), AiMessage.from(directResponse))
+ );
+ aiChatSessionService.refreshSessionStats(memoryId, loginUser);
+ return Flux.just(directResponse);
+ }
+
+ return manufacturingAgent.chat(memoryId, userMessage)
+ .doOnComplete(() -> aiChatSessionService.refreshSessionStats(memoryId, loginUser))
+ .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
+ }
+
+ @Operation(summary = "鍒堕�犱細璇濆垪琛�")
+ @GetMapping("/history/sessions")
+ public AjaxResult listSessions() {
+ return success(aiChatSessionService.listCurrentUserSessions(SecurityUtils.getLoginUser()));
+ }
+
+ @Operation(summary = "鍒堕�犱細璇濇秷鎭�")
+ @GetMapping("/history/messages/{memoryId}")
+ public AjaxResult listMessages(@PathVariable String memoryId) {
+ return success(aiChatSessionService.listCurrentUserMessages(memoryId, SecurityUtils.getLoginUser()));
+ }
+
+ @Operation(summary = "鍒犻櫎鍒堕�犱細璇�")
+ @DeleteMapping("/history/{memoryId}")
+ public AjaxResult deleteSession(@PathVariable String memoryId) {
+ aiSessionUserContext.remove(memoryId);
+ return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/controller/SalesAiController.java b/src/main/java/com/ruoyi/ai/controller/SalesAiController.java
new file mode 100644
index 0000000..c3a569b
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/controller/SalesAiController.java
@@ -0,0 +1,131 @@
+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,
+ "鏌ヨ", "鏌ョ湅", "缁熻", "鍒嗘瀽", "寤鸿", "瀹㈡埛妗f", "绉佹捣", "鍏捣",
+ "閿�鍞姤浠�", "閿�鍞彴璐�", "閿�鍞��璐�", "瀹㈡埛寰�鏉�", "鍙戣揣鍙拌处", "鍥炴", "鎶ヤ环", "椋庨櫓");
+ }
+
+ private boolean containsAny(String text, String... keywords) {
+ for (String keyword : keywords) {
+ if (text.contains(keyword)) {
+ return true;
+ }
+ }
+ return false;
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
index eac43b2..affa347 100644
--- a/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/XiaozhiController.java
@@ -90,6 +90,16 @@
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));
@@ -159,4 +169,25 @@
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;
+ }
}
diff --git a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
index cd3e933..fec0c21 100644
--- a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -76,12 +76,17 @@
@P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
@P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
@P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit,
- @P(value = "鏌ヨ鑼冨洿锛屽彲閫夊�硷細related銆乤pplicant銆乤pprover锛況elated 琛ㄧず褰撳墠鐢ㄦ埛鐩稿叧锛宎pplicant 琛ㄧず鎴戝彂璧风殑锛宎pprover 琛ㄧず寰呮垜澶勭悊鐨�", required = false) String scope) {
+ @P(value = "鏌ヨ鑼冨洿锛屽彲閫夊�硷細related銆乤pplicant銆乤pprover锛況elated 琛ㄧず褰撳墠鐢ㄦ埛鐩稿叧锛宎pplicant 琛ㄧず鎴戝彂璧风殑锛宎pprover 琛ㄧず寰呮垜澶勭悊鐨�", 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);
@@ -120,6 +125,11 @@
}
}
+ 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));
@@ -156,7 +166,10 @@
"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());
diff --git a/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java b/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
new file mode 100644
index 0000000..1ff96c1
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/tools/ManufacturingAgentTools.java
@@ -0,0 +1,1035 @@
+package com.ruoyi.ai.tools;
+
+import com.alibaba.fastjson2.JSON;
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.toolkit.support.SFunction;
+import com.ruoyi.ai.context.AiSessionUserContext;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.device.mapper.DeviceDefectRecordMapper;
+import com.ruoyi.device.mapper.DeviceLedgerMapper;
+import com.ruoyi.device.mapper.DeviceRepairMapper;
+import com.ruoyi.device.pojo.DeviceDefectRecord;
+import com.ruoyi.device.pojo.DeviceLedger;
+import com.ruoyi.device.pojo.DeviceRepair;
+import com.ruoyi.framework.security.LoginUser;
+import com.ruoyi.procurementrecord.mapper.ProcurementExceptionRecordMapper;
+import com.ruoyi.procurementrecord.pojo.ProcurementExceptionRecord;
+import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
+import com.ruoyi.production.mapper.ProductionOrderMapper;
+import com.ruoyi.production.mapper.ProductionPlanMapper;
+import com.ruoyi.production.mapper.ProductionProductMainMapper;
+import com.ruoyi.production.pojo.ProductionOperationTask;
+import com.ruoyi.production.pojo.ProductionOrder;
+import com.ruoyi.production.pojo.ProductionPlan;
+import com.ruoyi.production.pojo.ProductionProductMain;
+import com.ruoyi.quality.mapper.QualityInspectMapper;
+import com.ruoyi.quality.mapper.QualityUnqualifiedMapper;
+import com.ruoyi.quality.pojo.QualityInspect;
+import com.ruoyi.quality.pojo.QualityUnqualified;
+import com.ruoyi.stock.mapper.StockInventoryMapper;
+import com.ruoyi.stock.pojo.StockInventory;
+import dev.langchain4j.agent.tool.P;
+import dev.langchain4j.agent.tool.Tool;
+import dev.langchain4j.agent.tool.ToolMemoryId;
+import org.springframework.stereotype.Component;
+import org.springframework.util.StringUtils;
+
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.temporal.ChronoUnit;
+import java.util.ArrayList;
+import java.util.Date;
+import java.util.LinkedHashMap;
+import java.util.List;
+import java.util.Map;
+import java.util.Objects;
+import java.util.stream.Collectors;
+
+@Component
+public class ManufacturingAgentTools {
+
+ private static final DateTimeFormatter DATE_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
+ private static final int DEFAULT_LIMIT = 10;
+ private static final int MAX_LIMIT = 30;
+ private static final int DEVICE_REPAIR_STATUS_PENDING = 0;
+
+ private final ProductionPlanMapper productionPlanMapper;
+ private final ProductionOrderMapper productionOrderMapper;
+ private final ProductionOperationTaskMapper productionOperationTaskMapper;
+ private final ProductionProductMainMapper productionProductMainMapper;
+ private final DeviceLedgerMapper deviceLedgerMapper;
+ private final DeviceRepairMapper deviceRepairMapper;
+ private final DeviceDefectRecordMapper deviceDefectRecordMapper;
+ private final QualityInspectMapper qualityInspectMapper;
+ private final QualityUnqualifiedMapper qualityUnqualifiedMapper;
+ private final StockInventoryMapper stockInventoryMapper;
+ private final ProcurementExceptionRecordMapper procurementExceptionRecordMapper;
+ private final AiSessionUserContext aiSessionUserContext;
+
+ public ManufacturingAgentTools(ProductionPlanMapper productionPlanMapper,
+ ProductionOrderMapper productionOrderMapper,
+ ProductionOperationTaskMapper productionOperationTaskMapper,
+ ProductionProductMainMapper productionProductMainMapper,
+ DeviceLedgerMapper deviceLedgerMapper,
+ DeviceRepairMapper deviceRepairMapper,
+ DeviceDefectRecordMapper deviceDefectRecordMapper,
+ QualityInspectMapper qualityInspectMapper,
+ QualityUnqualifiedMapper qualityUnqualifiedMapper,
+ StockInventoryMapper stockInventoryMapper,
+ ProcurementExceptionRecordMapper procurementExceptionRecordMapper,
+ AiSessionUserContext aiSessionUserContext) {
+ this.productionPlanMapper = productionPlanMapper;
+ this.productionOrderMapper = productionOrderMapper;
+ this.productionOperationTaskMapper = productionOperationTaskMapper;
+ this.productionProductMainMapper = productionProductMainMapper;
+ this.deviceLedgerMapper = deviceLedgerMapper;
+ this.deviceRepairMapper = deviceRepairMapper;
+ this.deviceDefectRecordMapper = deviceDefectRecordMapper;
+ this.qualityInspectMapper = qualityInspectMapper;
+ this.qualityUnqualifiedMapper = qualityUnqualifiedMapper;
+ this.stockInventoryMapper = stockInventoryMapper;
+ this.procurementExceptionRecordMapper = procurementExceptionRecordMapper;
+ this.aiSessionUserContext = aiSessionUserContext;
+ }
+
+ @Tool(name = "鏌ヨ鍒堕�犱笟鍔″煙鏁版嵁", value = "鎸変笟鍔″煙鏌ヨ鐢熶骇鐜板満銆佽鍒掋�佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞嗙浉鍏虫暟鎹��")
+ public String queryDomain(@ToolMemoryId String memoryId,
+ @P(value = "涓氬姟鍩燂紝site/plan/workorder/device/quality/material/exception") String domain,
+ @P(value = "鍏抽敭瀛楋紝鍙笉浼�", required = false) String keyword,
+ @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+ @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡備粖骞淬�佹湰鏈堛�佽繎30澶�", required = false) String timeRange) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ int finalLimit = normalizeLimit(limit);
+ DateRange range = resolveDateRange(startDate, endDate, timeRange);
+ boolean hasTimeConstraint = hasTimeConstraint(startDate, endDate, timeRange);
+ String normalizedDomain = normalizeDomain(domain);
+
+ return switch (normalizedDomain) {
+ case "site" -> siteSnapshot(loginUser, range);
+ case "plan" -> listProductionPlans(loginUser, keyword, finalLimit, range);
+ case "workorder" -> listWorkOrders(loginUser, keyword, finalLimit, range);
+ case "device" -> isRepairIntent(keyword, timeRange)
+ ? listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint)
+ : listDevices(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit);
+ case "repair" -> listDeviceRepairs(loginUser, normalizeDeviceQueryKeyword(keyword, timeRange), finalLimit, range, hasTimeConstraint);
+ case "quality" -> listQualityIssues(loginUser, keyword, finalLimit, range);
+ case "material" -> listMaterialInventory(loginUser, keyword, finalLimit);
+ case "exception" -> listExceptions(loginUser, keyword, finalLimit, range);
+ default -> jsonResponse(false, "manufacturing_query", "涓嶆敮鎸佺殑涓氬姟鍩�: " + safe(domain), Map.of(), Map.of(), Map.of());
+ };
+ }
+
+ @Tool(name = "鍒堕�犻璀︾湅鏉�", value = "璁$畻璁″垝銆佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞嗙殑棰勮淇℃伅銆�")
+ public String getWarningBoard(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+ @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡備粖澶┿�佹湰鍛ㄣ�佹湰鏈堛�佽繎30澶�", required = false) String timeRange) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ DateRange range = resolveDateRange(startDate, endDate, timeRange);
+ LocalDate today = LocalDate.now();
+
+ long overduePlanCount = countOverduePlans(loginUser, today);
+ long overdueWorkOrderCount = countOverdueWorkOrders(loginUser, today);
+ long pendingRepairCount = countPendingRepairs(loginUser);
+ long qualityOpenCount = countOpenQualityIssues(loginUser, range);
+ long lowStockCount = countLowStock(loginUser);
+ long exceptionCount = countExceptionRecords(loginUser, range);
+
+ List<Map<String, Object>> warningItems = new ArrayList<>();
+ if (overduePlanCount > 0) {
+ warningItems.add(warningItem("high", "璁″垝閫炬湡", overduePlanCount, "鏈夌敓浜ц鍒掕秴杩囬渶姹傛棩鏈熶粛鏈畬鎴�"));
+ }
+ if (overdueWorkOrderCount > 0) {
+ warningItems.add(warningItem("high", "宸ュ崟閫炬湡", overdueWorkOrderCount, "鏈夊伐鍗曡鍒掔粨鏉熸棩鏈熷凡杩囦粛鏈畬宸�"));
+ }
+ if (pendingRepairCount > 0) {
+ warningItems.add(warningItem("medium", "璁惧寰呯淮淇�", pendingRepairCount, "瀛樺湪寰呯淮淇�/缁翠慨涓殑璁惧"));
+ }
+ if (qualityOpenCount > 0) {
+ warningItems.add(warningItem("high", "璐ㄩ噺鏈棴鐜�", qualityOpenCount, "瀛樺湪鏈鐞嗗畬鎴愮殑涓嶅悎鏍艰褰�"));
+ }
+ if (lowStockCount > 0) {
+ warningItems.add(warningItem("medium", "鐗╂枡浣庡簱瀛�", lowStockCount, "搴撳瓨鏁伴噺浣庝簬鎴栫瓑浜庨璀﹂槇鍊�"));
+ }
+ if (exceptionCount > 0) {
+ warningItems.add(warningItem("medium", "寮傚父璁板綍", exceptionCount, "鏃堕棿鑼冨洿鍐呭瓨鍦ㄥ紓甯稿鐞嗚褰�"));
+ }
+
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("timeRange", range.label());
+ summary.put("startDate", range.start().toString());
+ summary.put("endDate", range.end().toString());
+ summary.put("warningCount", warningItems.size());
+ summary.put("overduePlanCount", overduePlanCount);
+ summary.put("overdueWorkOrderCount", overdueWorkOrderCount);
+ summary.put("pendingRepairCount", pendingRepairCount);
+ summary.put("qualityOpenCount", qualityOpenCount);
+ summary.put("lowStockCount", lowStockCount);
+ summary.put("exceptionCount", exceptionCount);
+
+ return jsonResponse(true, "manufacturing_warning", "宸茶繑鍥炲埗閫犻璀︾湅鏉裤��", summary,
+ Map.of("items", warningItems), Map.of());
+ }
+
+ @Tool(name = "鍒堕�犵粡钀ュ垎鏋�", value = "鎸夋椂闂磋寖鍥磋緭鍑哄埗閫犲叧閿寚鏍囷紝鏀寔鏌ャ�侀棶銆佸垎鏋愬満鏅��")
+ public String analyzeFactory(@ToolMemoryId String memoryId,
+ @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd锛屽彲涓嶄紶", required = false) String startDate,
+ @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd锛屽彲涓嶄紶", required = false) String endDate,
+ @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡傛湰鏈堛�佽繎30澶�", required = false) String timeRange) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ DateRange range = resolveDateRange(startDate, endDate, timeRange);
+
+ long planTotal = countPlans(loginUser, range);
+ long planCompleted = countPlansByStatus(loginUser, range, 2);
+ long workOrderTotal = countWorkOrders(loginUser, range);
+ long workOrderCompleted = countWorkOrdersByStatus(loginUser, range, 2);
+ long workOrderInProgress = countWorkOrdersByStatus(loginUser, range, 1);
+
+ long outputCount = countOutputs(loginUser, range);
+ long deviceTotal = countDevices(loginUser);
+ long pendingRepairCount = countPendingRepairs(loginUser);
+ long qualityInspectTotal = countQualityInspect(loginUser, range);
+ long qualityNgCount = countOpenQualityIssues(loginUser, range);
+ long materialSkuCount = countInventorySku(loginUser);
+ long lowStockCount = countLowStock(loginUser);
+ long exceptionCount = countExceptionRecords(loginUser, range);
+
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("timeRange", range.label());
+ summary.put("startDate", range.start().toString());
+ summary.put("endDate", range.end().toString());
+ summary.put("planTotal", planTotal);
+ summary.put("planCompleted", planCompleted);
+ summary.put("planCompletionRate", toRate(planCompleted, planTotal));
+ summary.put("workOrderTotal", workOrderTotal);
+ summary.put("workOrderCompleted", workOrderCompleted);
+ summary.put("workOrderInProgress", workOrderInProgress);
+ summary.put("workOrderCompletionRate", toRate(workOrderCompleted, workOrderTotal));
+ summary.put("outputCount", outputCount);
+ summary.put("deviceTotal", deviceTotal);
+ summary.put("pendingRepairCount", pendingRepairCount);
+ summary.put("qualityInspectTotal", qualityInspectTotal);
+ summary.put("qualityNgCount", qualityNgCount);
+ summary.put("qualityIssueRate", toRate(qualityNgCount, qualityInspectTotal));
+ summary.put("materialSkuCount", materialSkuCount);
+ summary.put("lowStockCount", lowStockCount);
+ summary.put("exceptionCount", exceptionCount);
+
+ List<Map<String, Object>> coreMetrics = List.of(
+ metric("璁″垝瀹屾垚鐜�", toRate(planCompleted, planTotal)),
+ metric("宸ュ崟瀹屾垚鐜�", toRate(workOrderCompleted, workOrderTotal)),
+ metric("璐ㄩ噺寮傚父鐜�", toRate(qualityNgCount, qualityInspectTotal)),
+ metric("浣庡簱瀛樺崰姣�", toRate(lowStockCount, materialSkuCount))
+ );
+
+ Map<String, Object> charts = new LinkedHashMap<>();
+ charts.put("domainBarOption", buildDomainBarOption(summary));
+ charts.put("qualityPieOption", buildQualityPieOption(qualityInspectTotal, qualityNgCount));
+
+ return jsonResponse(true, "manufacturing_analysis", "宸茶繑鍥炲埗閫犲垎鏋愮粨鏋溿��", summary,
+ Map.of("coreMetrics", coreMetrics), charts);
+ }
+
+ @Tool(name = "鐢熸垚鍒堕�犲姙鐞嗗缓璁�", value = "鏍规嵁鐢ㄦ埛闂杈撳嚭鍙墽琛岀殑鍔炵悊鍔ㄤ綔寤鸿锛屽寘鎷洰鏍囦笟鍔℃帴鍙c�佸繀濉瓧娈靛拰绀轰緥銆�")
+ public String planActions(@ToolMemoryId String memoryId,
+ @P("鐢ㄦ埛璇夋眰鍘熸枃") String userQuery) {
+ LoginUser loginUser = currentLoginUser(memoryId);
+ List<Map<String, Object>> actionCards = new ArrayList<>();
+
+ if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "宸ュ崟", "娲惧伐", "浣滀笟")) {
+ actionCards.add(actionCard(
+ "workorder_assign",
+ "宸ュ崟娲惧伐",
+ "POST",
+ "/productionOperationTask/assign",
+ List.of("id", "userIds"),
+ Map.of("id", 10001, "userIds", "12,13"),
+ "灏嗗伐鍗曞垎閰嶇粰鎸囧畾浜哄憳锛岄�傜敤浜庣幇鍦鸿皟搴︺��"));
+ }
+ if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "璁惧", "缁翠慨", "鏁呴殰")) {
+ actionCards.add(actionCard(
+ "device_repair_create",
+ "鍒涘缓璁惧缁翠慨鍗�",
+ "POST",
+ "/device/repair",
+ List.of("deviceLedgerId", "deviceName", "repairName", "remark"),
+ Map.of("deviceLedgerId", 1001, "deviceName", "绌哄帇鏈篈-01", "repairName", "寮犱笁", "remark", "寮傚搷骞朵即闅忔俯鍗�"),
+ "鏂板缓缁翠慨鍗曪紝杩涘叆璁惧寮傚父澶勭悊闂幆銆�"));
+ }
+ if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "璐ㄩ噺", "涓嶅悎鏍�", "闂幆")) {
+ actionCards.add(actionCard(
+ "quality_unqualified_deal",
+ "澶勭悊涓嶅悎鏍煎崟",
+ "POST",
+ "/quality/qualityUnqualified/deal",
+ List.of("id", "dealResult", "dealName"),
+ Map.of("id", 3001, "dealResult", "杩斿伐鍚庡妫�", "dealName", "鏉庡洓"),
+ "瀵逛笉鍚堟牸璁板綍鎵ц澶勭疆骞堕棴鐜��"));
+ }
+ if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "鐗╂枡", "搴撳瓨", "琛ユ枡")) {
+ actionCards.add(actionCard(
+ "material_inbound",
+ "琛ュ厖搴撳瓨",
+ "POST",
+ "/stockInventory/addstockInventory",
+ List.of("productModelId", "batchNo", "qualitity"),
+ Map.of("productModelId", 5001, "batchNo", "B2026051601", "qualitity", 120),
+ "褰撲綆搴撳瓨棰勮瑙﹀彂鏃讹紝澧炲姞搴撳瓨鏁伴噺銆�"));
+ }
+ if (!StringUtils.hasText(userQuery) || containsAny(userQuery, "寮傚父", "閲囪喘寮傚父", "鏉ユ枡寮傚父")) {
+ actionCards.add(actionCard(
+ "procurement_exception_add",
+ "鐧昏寮傚父璁板綍",
+ "POST",
+ "/procurementExceptionRecord/add",
+ List.of("purchaseLedgerId", "exceptionReason", "exceptionNum"),
+ Map.of("purchaseLedgerId", 888, "exceptionReason", "鍒版枡鐭己", "exceptionNum", 24),
+ "鐧昏閲囪喘/鏉ユ枡寮傚父锛屼究浜庡悗缁拷韪拰鍒嗘瀽銆�"));
+ }
+
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("actionCount", actionCards.size());
+ summary.put("userId", loginUser.getUserId());
+ summary.put("tenantId", loginUser.getTenantId());
+
+ return jsonResponse(true, "manufacturing_action_plan", "宸茬敓鎴愬姙鐞嗗缓璁紝璇峰墠绔紩瀵肩敤鎴风‘璁ゅ悗璋冪敤鐩爣涓氬姟鎺ュ彛銆�",
+ summary, Map.of("actionCards", actionCards), Map.of());
+ }
+
+ private String siteSnapshot(LoginUser loginUser, DateRange range) {
+ long planTotal = countPlans(loginUser, range);
+ long workOrderTotal = countWorkOrders(loginUser, range);
+ long outputCount = countOutputs(loginUser, range);
+ long deviceTotal = countDevices(loginUser);
+ long pendingRepairCount = countPendingRepairs(loginUser);
+ long qualityOpenCount = countOpenQualityIssues(loginUser, range);
+ long lowStockCount = countLowStock(loginUser);
+ long exceptionCount = countExceptionRecords(loginUser, range);
+
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("timeRange", range.label());
+ summary.put("startDate", range.start().toString());
+ summary.put("endDate", range.end().toString());
+ summary.put("planTotal", planTotal);
+ summary.put("workOrderTotal", workOrderTotal);
+ summary.put("outputCount", outputCount);
+ summary.put("deviceTotal", deviceTotal);
+ summary.put("pendingRepairCount", pendingRepairCount);
+ summary.put("qualityOpenCount", qualityOpenCount);
+ summary.put("lowStockCount", lowStockCount);
+ summary.put("exceptionCount", exceptionCount);
+
+ return jsonResponse(true, "manufacturing_site_snapshot", "宸茶繑鍥炵敓浜х幇鍦烘瑙堛��", summary, Map.of(), Map.of());
+ }
+
+ private String listProductionPlans(LoginUser loginUser, String keyword, int limit, DateRange range) {
+ LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+ wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(ProductionPlan::getMpsNo, keyword)
+ .or().like(ProductionPlan::getRemark, keyword)
+ .or().like(ProductionPlan::getSource, keyword));
+ }
+ wrapper.orderByDesc(ProductionPlan::getRequiredDate, ProductionPlan::getId).last("limit " + limit);
+
+ List<Map<String, Object>> items = defaultList(productionPlanMapper.selectList(wrapper)).stream()
+ .map(this::toPlanItem)
+ .collect(Collectors.toList());
+ return jsonResponse(true, "manufacturing_plan_list", "宸茶繑鍥炵敓浜ц鍒掑垪琛ㄣ��",
+ rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+ }
+
+ private String listWorkOrders(LoginUser loginUser, String keyword, int limit, DateRange range) {
+ LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+ wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
+ .le(ProductionOperationTask::getPlanEndTime, range.end());
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(ProductionOperationTask::getWorkOrderNo, keyword)
+ .or().like(ProductionOperationTask::getUserIds, keyword));
+ }
+ wrapper.orderByDesc(ProductionOperationTask::getPlanEndTime, ProductionOperationTask::getId)
+ .last("limit " + limit);
+
+ List<Map<String, Object>> items = defaultList(productionOperationTaskMapper.selectList(wrapper)).stream()
+ .map(this::toWorkOrderItem)
+ .collect(Collectors.toList());
+ return jsonResponse(true, "manufacturing_workorder_list", "宸茶繑鍥炲伐鍗曞垪琛ㄣ��",
+ rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+ }
+
+ private String listDevices(LoginUser loginUser, String keyword, int limit) {
+ LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
+ .or().like(DeviceLedger::getDeviceModel, keyword)
+ .or().like(DeviceLedger::getDeviceBrand, keyword));
+ }
+ wrapper.orderByDesc(DeviceLedger::getId).last("limit " + limit);
+
+ Map<Long, Long> pendingRepairMap = pendingRepairCountByDevice(loginUser);
+ List<Map<String, Object>> items = defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
+ .map(item -> toDeviceItem(item, pendingRepairMap.getOrDefault(item.getId(), 0L)))
+ .collect(Collectors.toList());
+ return jsonResponse(true, "manufacturing_device_list", "宸茶繑鍥炶澶囧垪琛ㄣ��",
+ Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
+ }
+
+ private String listDeviceRepairs(LoginUser loginUser, String keyword, int limit, DateRange range, boolean hasTimeConstraint) {
+ LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
+ Long currentDeptId = loginUser.getCurrentDeptId();
+ if (currentDeptId != null) {
+ wrapper.and(w -> w.eq(DeviceRepair::getDeptId, currentDeptId).or().isNull(DeviceRepair::getDeptId));
+ }
+ if (hasTimeConstraint) {
+ wrapper.ge(DeviceRepair::getCreateTime, range.start().atStartOfDay())
+ .lt(DeviceRepair::getCreateTime, range.end().plusDays(1).atStartOfDay());
+ }
+ if (StringUtils.hasText(keyword)) {
+ List<Long> matchedDeviceIds = findDeviceLedgerIdsByKeyword(loginUser, keyword);
+ wrapper.and(w -> {
+ w.like(DeviceRepair::getDeviceName, keyword)
+ .or().like(DeviceRepair::getDeviceModel, keyword)
+ .or().like(DeviceRepair::getRemark, keyword)
+ .or().like(DeviceRepair::getRepairName, keyword)
+ .or().like(DeviceRepair::getMaintenanceName, keyword);
+ if (!matchedDeviceIds.isEmpty()) {
+ w.or().in(DeviceRepair::getDeviceLedgerId, matchedDeviceIds);
+ }
+ });
+ }
+ wrapper.orderByDesc(DeviceRepair::getCreateTime, DeviceRepair::getId).last("limit " + limit);
+
+ List<Map<String, Object>> items = defaultList(deviceRepairMapper.selectList(wrapper)).stream()
+ .map(this::toDeviceRepairItem)
+ .collect(Collectors.toList());
+ return jsonResponse(true, "manufacturing_device_repair_list", "宸茶繑鍥炶澶囩淮淇褰曘��",
+ rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+ }
+
+ private String listQualityIssues(LoginUser loginUser, String keyword, int limit, DateRange range) {
+ LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
+ wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
+ .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()));
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(QualityUnqualified::getProductName, keyword)
+ .or().like(QualityUnqualified::getDefectivePhenomena, keyword)
+ .or().like(QualityUnqualified::getDealResult, keyword));
+ }
+ wrapper.orderByDesc(QualityUnqualified::getCheckTime, QualityUnqualified::getId).last("limit " + limit);
+
+ List<Map<String, Object>> items = defaultList(qualityUnqualifiedMapper.selectList(wrapper)).stream()
+ .map(this::toQualityItem)
+ .collect(Collectors.toList());
+ return jsonResponse(true, "manufacturing_quality_list", "宸茶繑鍥炶川閲忓紓甯稿垪琛ㄣ��",
+ rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+ }
+
+ private String listMaterialInventory(LoginUser loginUser, String keyword, int limit) {
+ LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
+ if (StringUtils.hasText(keyword)) {
+ wrapper.and(w -> w.like(StockInventory::getBatchNo, keyword)
+ .or().like(StockInventory::getProductModelId, keyword));
+ }
+ wrapper.orderByDesc(StockInventory::getId).last("limit " + limit);
+
+ List<Map<String, Object>> items = defaultList(stockInventoryMapper.selectList(wrapper)).stream()
+ .map(this::toMaterialItem)
+ .collect(Collectors.toList());
+ return jsonResponse(true, "manufacturing_material_list", "宸茶繑鍥炵墿鏂欏簱瀛樺垪琛ㄣ��",
+ Map.of("count", items.size(), "keyword", safe(keyword)), Map.of("items", items), Map.of());
+ }
+
+ private String listExceptions(LoginUser loginUser, String keyword, int limit, DateRange range) {
+ LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
+ wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
+ .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
+ if (StringUtils.hasText(keyword)) {
+ wrapper.like(ProcurementExceptionRecord::getExceptionReason, keyword);
+ }
+ wrapper.orderByDesc(ProcurementExceptionRecord::getCreateTime, ProcurementExceptionRecord::getId)
+ .last("limit " + limit);
+
+ List<Map<String, Object>> items = defaultList(procurementExceptionRecordMapper.selectList(wrapper)).stream()
+ .map(this::toExceptionItem)
+ .collect(Collectors.toList());
+ return jsonResponse(true, "manufacturing_exception_list", "宸茶繑鍥炲紓甯稿鐞嗗垪琛ㄣ��",
+ rangeSummary(range, items.size(), keyword), Map.of("items", items), Map.of());
+ }
+
+ private long countPlans(LoginUser loginUser, DateRange range) {
+ LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+ wrapper.ge(ProductionPlan::getRequiredDate, range.start()).le(ProductionPlan::getRequiredDate, range.end());
+ return productionPlanMapper.selectCount(wrapper);
+ }
+
+ private long countPlansByStatus(LoginUser loginUser, DateRange range, int status) {
+ LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+ wrapper.ge(ProductionPlan::getRequiredDate, range.start())
+ .le(ProductionPlan::getRequiredDate, range.end())
+ .eq(ProductionPlan::getStatus, status);
+ return productionPlanMapper.selectCount(wrapper);
+ }
+
+ private long countWorkOrders(LoginUser loginUser, DateRange range) {
+ LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+ wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
+ .le(ProductionOperationTask::getPlanEndTime, range.end());
+ return productionOperationTaskMapper.selectCount(wrapper);
+ }
+
+ private long countWorkOrdersByStatus(LoginUser loginUser, DateRange range, int status) {
+ LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+ wrapper.ge(ProductionOperationTask::getPlanStartTime, range.start())
+ .le(ProductionOperationTask::getPlanEndTime, range.end())
+ .eq(ProductionOperationTask::getStatus, status);
+ return productionOperationTaskMapper.selectCount(wrapper);
+ }
+
+ private long countOutputs(LoginUser loginUser, DateRange range) {
+ LambdaQueryWrapper<ProductionProductMain> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionProductMain::getDeptId);
+ wrapper.ge(ProductionProductMain::getCreateTime, range.start().atStartOfDay())
+ .lt(ProductionProductMain::getCreateTime, range.end().plusDays(1).atStartOfDay());
+ return productionProductMainMapper.selectCount(wrapper);
+ }
+
+ private long countDevices(LoginUser loginUser) {
+ LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceLedger::getDeptId);
+ return deviceLedgerMapper.selectCount(wrapper);
+ }
+
+ private long countPendingRepairs(LoginUser loginUser) {
+ LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
+ wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
+ return deviceRepairMapper.selectCount(wrapper);
+ }
+
+ private long countQualityInspect(LoginUser loginUser, DateRange range) {
+ LambdaQueryWrapper<QualityInspect> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), QualityInspect::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityInspect::getDeptId);
+ wrapper.ge(QualityInspect::getCheckTime, toDate(range.start()))
+ .lt(QualityInspect::getCheckTime, toExclusiveEndDate(range.end()));
+ return qualityInspectMapper.selectCount(wrapper);
+ }
+
+ private long countOpenQualityIssues(LoginUser loginUser, DateRange range) {
+ LambdaQueryWrapper<QualityUnqualified> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), QualityUnqualified::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), QualityUnqualified::getDeptId);
+ wrapper.ge(QualityUnqualified::getCheckTime, toDate(range.start()))
+ .lt(QualityUnqualified::getCheckTime, toExclusiveEndDate(range.end()))
+ .ne(QualityUnqualified::getInspectState, 2);
+ return qualityUnqualifiedMapper.selectCount(wrapper);
+ }
+
+ private long countInventorySku(LoginUser loginUser) {
+ LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
+ return stockInventoryMapper.selectCount(wrapper);
+ }
+
+ private long countLowStock(LoginUser loginUser) {
+ LambdaQueryWrapper<StockInventory> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), StockInventory::getDeptId);
+ wrapper.isNotNull(StockInventory::getWarnNum);
+ List<StockInventory> stocks = defaultList(stockInventoryMapper.selectList(wrapper));
+ return stocks.stream()
+ .filter(this::isLowStock)
+ .count();
+ }
+
+ private long countExceptionRecords(LoginUser loginUser, DateRange range) {
+ LambdaQueryWrapper<ProcurementExceptionRecord> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), ProcurementExceptionRecord::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProcurementExceptionRecord::getDeptId);
+ wrapper.ge(ProcurementExceptionRecord::getCreateTime, range.start().atStartOfDay())
+ .lt(ProcurementExceptionRecord::getCreateTime, range.end().plusDays(1).atStartOfDay());
+ return procurementExceptionRecordMapper.selectCount(wrapper);
+ }
+
+ private long countOverduePlans(LoginUser loginUser, LocalDate today) {
+ LambdaQueryWrapper<ProductionPlan> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionPlan::getDeptId);
+ wrapper.lt(ProductionPlan::getRequiredDate, today).ne(ProductionPlan::getStatus, 2);
+ return productionPlanMapper.selectCount(wrapper);
+ }
+
+ private long countOverdueWorkOrders(LoginUser loginUser, LocalDate today) {
+ LambdaQueryWrapper<ProductionOperationTask> wrapper = new LambdaQueryWrapper<>();
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), ProductionOperationTask::getDeptId);
+ wrapper.lt(ProductionOperationTask::getPlanEndTime, today).ne(ProductionOperationTask::getStatus, 2);
+ return productionOperationTaskMapper.selectCount(wrapper);
+ }
+
+ private Map<Long, Long> pendingRepairCountByDevice(LoginUser loginUser) {
+ LambdaQueryWrapper<DeviceRepair> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceRepair::getTenantId);
+ applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), DeviceRepair::getDeptId);
+ wrapper.eq(DeviceRepair::getStatus, DEVICE_REPAIR_STATUS_PENDING);
+ return defaultList(deviceRepairMapper.selectList(wrapper)).stream()
+ .filter(item -> item.getDeviceLedgerId() != null)
+ .collect(Collectors.groupingBy(DeviceRepair::getDeviceLedgerId, Collectors.counting()));
+ }
+
+ private Map<String, Object> toPlanItem(ProductionPlan item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("mpsNo", safe(item.getMpsNo()));
+ map.put("requiredDate", formatDate(item.getRequiredDate()));
+ map.put("promisedDeliveryDate", formatDate(item.getPromisedDeliveryDate()));
+ map.put("qtyRequired", item.getQtyRequired());
+ map.put("quantityIssued", item.getQuantityIssued());
+ map.put("status", item.getStatus());
+ map.put("source", safe(item.getSource()));
+ map.put("remark", safe(item.getRemark()));
+ return map;
+ }
+
+ private Map<String, Object> toWorkOrderItem(ProductionOperationTask item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("workOrderNo", safe(item.getWorkOrderNo()));
+ map.put("productionOrderId", item.getProductionOrderId());
+ map.put("planStartTime", formatDate(item.getPlanStartTime()));
+ map.put("planEndTime", formatDate(item.getPlanEndTime()));
+ map.put("actualStartTime", formatDate(item.getActualStartTime()));
+ map.put("actualEndTime", formatDate(item.getActualEndTime()));
+ map.put("planQuantity", item.getPlanQuantity());
+ map.put("completeQuantity", item.getCompleteQuantity());
+ map.put("status", item.getStatus());
+ map.put("userIds", safe(item.getUserIds()));
+ return map;
+ }
+
+ private Map<String, Object> toDeviceItem(DeviceLedger item, long pendingRepairCount) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("deviceName", safe(item.getDeviceName()));
+ map.put("deviceModel", safe(item.getDeviceModel()));
+ map.put("deviceBrand", safe(item.getDeviceBrand()));
+ map.put("status", safe(item.getStatus()));
+ map.put("storageLocation", safe(item.getStorageLocation()));
+ map.put("supplierName", safe(item.getSupplierName()));
+ map.put("pendingRepairCount", pendingRepairCount);
+ return map;
+ }
+
+ private Map<String, Object> toDeviceRepairItem(DeviceRepair item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("deviceLedgerId", item.getDeviceLedgerId());
+ map.put("deviceName", safe(item.getDeviceName()));
+ map.put("deviceModel", safe(item.getDeviceModel()));
+ map.put("repairTime", formatDate(item.getRepairTime()));
+ map.put("repairName", safe(item.getRepairName()));
+ map.put("maintenanceName", safe(item.getMaintenanceName()));
+ map.put("maintenanceTime", formatDateTime(item.getMaintenanceTime()));
+ map.put("maintenanceResult", safe(item.getMaintenanceResult()));
+ map.put("acceptanceName", safe(item.getAcceptanceName()));
+ map.put("acceptanceTime", formatDateTime(item.getAcceptanceTime()));
+ map.put("status", item.getStatus());
+ map.put("remark", safe(item.getRemark()));
+ map.put("createTime", formatDateTime(item.getCreateTime()));
+ return map;
+ }
+
+ private Map<String, Object> toQualityItem(QualityUnqualified item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("checkTime", formatDate(item.getCheckTime()));
+ map.put("inspectState", item.getInspectState());
+ map.put("productId", item.getProductId());
+ map.put("productName", safe(item.getProductName()));
+ map.put("model", safe(item.getModel()));
+ map.put("quantity", item.getQuantity());
+ map.put("defectivePhenomena", safe(item.getDefectivePhenomena()));
+ map.put("dealResult", safe(item.getDealResult()));
+ map.put("dealName", safe(item.getDealName()));
+ map.put("dealTime", formatDate(item.getDealTime()));
+ return map;
+ }
+
+ private Map<String, Object> toMaterialItem(StockInventory item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("productModelId", item.getProductModelId());
+ map.put("batchNo", safe(item.getBatchNo()));
+ map.put("qualitity", item.getQualitity());
+ map.put("lockedQuantity", item.getLockedQuantity());
+ map.put("warnNum", item.getWarnNum());
+ map.put("lowStock", isLowStock(item));
+ map.put("remark", safe(item.getRemark()));
+ return map;
+ }
+
+ private Map<String, Object> toExceptionItem(ProcurementExceptionRecord item) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("id", item.getId());
+ map.put("purchaseLedgerId", item.getPurchaseLedgerId());
+ map.put("exceptionReason", safe(item.getExceptionReason()));
+ map.put("exceptionNum", item.getExceptionNum());
+ map.put("createTime", formatDateTime(item.getCreateTime()));
+ return map;
+ }
+
+ private boolean isLowStock(StockInventory item) {
+ BigDecimal quantity = item.getQualitity();
+ BigDecimal warnNum = item.getWarnNum();
+ if (quantity == null || warnNum == null) {
+ return false;
+ }
+ return quantity.compareTo(warnNum) <= 0;
+ }
+
+ private Map<String, Object> warningItem(String level, String title, long count, String detail) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("level", level);
+ map.put("title", title);
+ map.put("count", count);
+ map.put("detail", detail);
+ return map;
+ }
+
+ private Map<String, Object> actionCard(String code,
+ String name,
+ String method,
+ String targetApi,
+ List<String> requiredFields,
+ Map<String, Object> examplePayload,
+ String description) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("code", code);
+ map.put("name", name);
+ map.put("method", method);
+ map.put("targetApi", targetApi);
+ map.put("requiredFields", requiredFields);
+ map.put("examplePayload", examplePayload);
+ map.put("description", description);
+ return map;
+ }
+
+ private Map<String, Object> metric(String label, String value) {
+ Map<String, Object> map = new LinkedHashMap<>();
+ map.put("label", label);
+ map.put("value", value);
+ return map;
+ }
+
+ private Map<String, Object> rangeSummary(DateRange range, int count, String keyword) {
+ Map<String, Object> summary = new LinkedHashMap<>();
+ summary.put("timeRange", range.label());
+ summary.put("startDate", range.start().toString());
+ summary.put("endDate", range.end().toString());
+ summary.put("count", count);
+ summary.put("keyword", safe(keyword));
+ return summary;
+ }
+
+ private Map<String, Object> buildDomainBarOption(Map<String, Object> summary) {
+ List<String> xData = List.of("璁″垝", "宸ュ崟", "璁惧", "璐ㄩ噺", "鐗╂枡", "寮傚父");
+ List<Number> yData = List.of(
+ numberValue(summary.get("planTotal")),
+ numberValue(summary.get("workOrderTotal")),
+ numberValue(summary.get("deviceTotal")),
+ numberValue(summary.get("qualityNgCount")),
+ numberValue(summary.get("lowStockCount")),
+ numberValue(summary.get("exceptionCount"))
+ );
+ Map<String, Object> option = new LinkedHashMap<>();
+ option.put("title", Map.of("text", "鍒堕�犲煙鍏抽敭鏁伴噺", "left", "center"));
+ option.put("tooltip", Map.of("trigger", "axis"));
+ option.put("xAxis", Map.of("type", "category", "data", xData));
+ option.put("yAxis", Map.of("type", "value"));
+ option.put("series", List.of(Map.of("name", "鏁伴噺", "type", "bar", "data", yData)));
+ return option;
+ }
+
+ private Map<String, Object> buildQualityPieOption(long inspectTotal, long ngCount) {
+ long passCount = Math.max(inspectTotal - ngCount, 0);
+ List<Map<String, Object>> data = List.of(
+ Map.of("name", "涓嶅悎鏍�", "value", ngCount),
+ Map.of("name", "闈炰笉鍚堟牸", "value", passCount)
+ );
+ Map<String, Object> option = new LinkedHashMap<>();
+ option.put("title", Map.of("text", "璐ㄩ噺缁撴灉鍒嗗竷", "left", "center"));
+ option.put("tooltip", Map.of("trigger", "item"));
+ option.put("series", List.of(Map.of("name", "璐ㄩ噺", "type", "pie", "radius", "60%", "data", data)));
+ return option;
+ }
+
+ private int numberValue(Object value) {
+ if (value instanceof Number number) {
+ return number.intValue();
+ }
+ return 0;
+ }
+
+ private String toRate(long numerator, long denominator) {
+ if (denominator <= 0) {
+ return "0.00%";
+ }
+ BigDecimal rate = new BigDecimal(numerator)
+ .multiply(new BigDecimal("100"))
+ .divide(new BigDecimal(denominator), 2, RoundingMode.HALF_UP);
+ return rate.toPlainString() + "%";
+ }
+
+ private String normalizeDomain(String domain) {
+ if (!StringUtils.hasText(domain)) {
+ return "";
+ }
+ String value = domain.trim().toLowerCase();
+ return switch (value) {
+ case "鐢熶骇鐜板満", "site", "factory", "workshop" -> "site";
+ case "璁″垝", "plan", "schedule" -> "plan";
+ case "宸ュ崟", "workorder", "work_order", "task" -> "workorder";
+ case "璁惧", "device", "equipment" -> "device";
+ case "缁翠慨", "repair", "maintenance" -> "repair";
+ case "璐ㄩ噺", "quality", "qc" -> "quality";
+ case "鐗╂枡", "material", "inventory", "stock" -> "material";
+ case "寮傚父", "exception", "abnormal" -> "exception";
+ default -> value;
+ };
+ }
+
+ private boolean isRepairIntent(String keyword, String userQuery) {
+ String query = safe(userQuery);
+ return containsAny(safe(keyword), "缁翠慨", "鎶ヤ慨", "妫�淇�", "缁存姢")
+ || containsAny(query, "缁翠慨", "鎶ヤ慨", "妫�淇�", "缁存姢");
+ }
+
+ private String normalizeDeviceQueryKeyword(String keyword, String userQuery) {
+ String source = StringUtils.hasText(keyword) ? keyword : userQuery;
+ if (!StringUtils.hasText(source)) {
+ return null;
+ }
+ String cleaned = source
+ .replace("鏌ヨ", "")
+ .replace("鏌ョ湅", "")
+ .replace("甯垜", "")
+ .replace("璇�", "")
+ .replace("鏌�", "")
+ .replace("璁惧", "")
+ .replace("缁翠慨璁板綍", "")
+ .replace("缁翠慨鎯呭喌", "")
+ .replace("鎶ヤ慨璁板綍", "")
+ .replace("鎶ヤ慨鎯呭喌", "")
+ .replace("缁翠慨", "")
+ .replace("鎶ヤ慨", "")
+ .replace("鎯呭喌", "")
+ .replace("璁板綍", "")
+ .replace("淇℃伅", "")
+ .replace("鐨�", "")
+ .replace("涓�涓�", "")
+ .trim();
+ return cleaned.length() >= 2 ? cleaned : null;
+ }
+
+ private List<Long> findDeviceLedgerIdsByKeyword(LoginUser loginUser, String keyword) {
+ if (!StringUtils.hasText(keyword)) {
+ return List.of();
+ }
+ LambdaQueryWrapper<DeviceLedger> wrapper = new LambdaQueryWrapper<>();
+ applyTenantFilter(wrapper, loginUser.getTenantId(), DeviceLedger::getTenantId);
+ Long currentDeptId = loginUser.getCurrentDeptId();
+ if (currentDeptId != null) {
+ wrapper.and(w -> w.eq(DeviceLedger::getDeptId, currentDeptId).or().isNull(DeviceLedger::getDeptId));
+ }
+ wrapper.and(w -> w.like(DeviceLedger::getDeviceName, keyword)
+ .or().like(DeviceLedger::getDeviceModel, keyword)
+ .or().like(DeviceLedger::getDeviceBrand, keyword));
+ wrapper.orderByDesc(DeviceLedger::getId).last("limit 200");
+ return defaultList(deviceLedgerMapper.selectList(wrapper)).stream()
+ .map(DeviceLedger::getId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toList());
+ }
+
+ private boolean hasTimeConstraint(String startDate, String endDate, String userQuery) {
+ if (StringUtils.hasText(startDate) || StringUtils.hasText(endDate)) {
+ return true;
+ }
+ if (!StringUtils.hasText(userQuery)) {
+ return false;
+ }
+ String text = userQuery.trim();
+ return containsAny(text, "浠婂ぉ", "鏄ㄥぉ", "鏈懆", "涓婂懆", "鏈湀", "涓婃湀", "浠婂勾", "鍘诲勾", "杩�", "鏈�杩�");
+ }
+
+ private DateRange resolveDateRange(String startDate, String endDate, String timeRange) {
+ LocalDate today = LocalDate.now();
+ LocalDate start = parseLocalDate(startDate);
+ LocalDate end = parseLocalDate(endDate);
+ if (start != null || end != null) {
+ LocalDate s = start != null ? start : end;
+ LocalDate e = end != null ? end : start;
+ if (s.isAfter(e)) {
+ LocalDate temp = s;
+ s = e;
+ e = temp;
+ }
+ return new DateRange(s, e, s + "鑷�" + e);
+ }
+ if (!StringUtils.hasText(timeRange)) {
+ return new DateRange(today.minusDays(29), today, "杩�30澶�");
+ }
+ String text = timeRange.trim();
+ if (text.contains("浠婂ぉ")) {
+ return new DateRange(today, today, "浠婂ぉ");
+ }
+ if (text.contains("鏈懆")) {
+ LocalDate startOfWeek = today.minusDays(today.getDayOfWeek().getValue() - 1L);
+ return new DateRange(startOfWeek, today, "鏈懆");
+ }
+ if (text.contains("鏈湀")) {
+ return new DateRange(today.withDayOfMonth(1), today, "鏈湀");
+ }
+ if (text.contains("鏈勾") || text.contains("浠婂勾")) {
+ return new DateRange(today.withDayOfYear(1), today, "浠婂勾");
+ }
+ if (text.contains("鍘诲勾")) {
+ LocalDate firstDay = today.minusYears(1).withDayOfYear(1);
+ LocalDate lastDay = today.minusYears(1).withMonth(12).withDayOfMonth(31);
+ return new DateRange(firstDay, lastDay, "鍘诲勾");
+ }
+ if (text.contains("涓婃湀")) {
+ LocalDate startOfLastMonth = today.minusMonths(1).withDayOfMonth(1);
+ return new DateRange(startOfLastMonth, startOfLastMonth.withDayOfMonth(startOfLastMonth.lengthOfMonth()), "涓婃湀");
+ }
+ java.util.regex.Matcher matcher = java.util.regex.Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text);
+ if (matcher.find()) {
+ int amount = Integer.parseInt(matcher.group(2));
+ String unit = matcher.group(3);
+ LocalDate relativeStart = switch (unit) {
+ case "澶�" -> today.minusDays(Math.max(amount - 1L, 0));
+ case "鍛�" -> today.minusWeeks(Math.max(amount, 1)).plusDays(1);
+ case "涓湀", "鏈�" -> today.minusMonths(Math.max(amount, 1)).plusDays(1);
+ case "骞�" -> today.minusYears(Math.max(amount, 1)).plusDays(1);
+ default -> today.minusDays(29);
+ };
+ return new DateRange(relativeStart, today, "杩�" + amount + unit);
+ }
+ return new DateRange(today.minusDays(29), today, "杩�30澶�");
+ }
+
+ private LocalDate parseLocalDate(String text) {
+ if (!StringUtils.hasText(text)) {
+ return null;
+ }
+ return LocalDate.parse(text.trim(), DATE_FMT);
+ }
+
+ private Date toDate(LocalDate date) {
+ return Date.from(date.atStartOfDay(ZoneId.systemDefault()).toInstant());
+ }
+
+ private Date toExclusiveEndDate(LocalDate date) {
+ return Date.from(date.plusDays(1).atStartOfDay(ZoneId.systemDefault()).toInstant());
+ }
+
+ private String formatDate(LocalDate date) {
+ return date == null ? "" : DATE_FMT.format(date);
+ }
+
+ private String formatDate(Date date) {
+ if (date == null) {
+ return "";
+ }
+ return DATE_FMT.format(date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate());
+ }
+
+ private String formatDateTime(LocalDateTime time) {
+ if (time == null) {
+ return "";
+ }
+ return time.truncatedTo(ChronoUnit.SECONDS).toString().replace('T', ' ');
+ }
+
+ private int normalizeLimit(Integer limit) {
+ if (limit == null || limit <= 0) {
+ return DEFAULT_LIMIT;
+ }
+ return Math.min(limit, MAX_LIMIT);
+ }
+
+ private boolean containsAny(String text, String... values) {
+ for (String value : values) {
+ if (text.contains(value)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private <T> void applyTenantFilter(LambdaQueryWrapper<T> wrapper, Long tenantId, SFunction<T, Long> field) {
+ if (tenantId != null) {
+ wrapper.eq(field, tenantId);
+ }
+ }
+
+ private <T> void applyDeptFilter(LambdaQueryWrapper<T> wrapper, Long deptId, SFunction<T, Long> field) {
+ if (deptId != null) {
+ wrapper.eq(field, deptId);
+ }
+ }
+
+ private LoginUser currentLoginUser(String memoryId) {
+ LoginUser loginUser = aiSessionUserContext.get(memoryId);
+ if (loginUser != null) {
+ return loginUser;
+ }
+ return SecurityUtils.getLoginUser();
+ }
+
+ private String safe(Object value) {
+ return value == null ? "" : String.valueOf(value).replace('\n', ' ').replace('\r', ' ');
+ }
+
+ private <T> List<T> defaultList(List<T> list) {
+ return list == null ? List.of() : list;
+ }
+
+ private String jsonResponse(boolean success,
+ String type,
+ String description,
+ Map<String, Object> summary,
+ Map<String, Object> data,
+ Map<String, Object> charts) {
+ Map<String, Object> result = new LinkedHashMap<>();
+ result.put("success", success);
+ result.put("type", type);
+ result.put("description", description);
+ result.put("summary", summary == null ? Map.of() : summary);
+ result.put("data", data == null ? Map.of() : data);
+ result.put("charts", charts == null ? Map.of() : charts);
+ return JSON.toJSONString(result);
+ }
+
+ private record DateRange(LocalDate start, LocalDate end, String label) {
+ }
+}
diff --git a/src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java b/src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java
new file mode 100644
index 0000000..b56144b
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/tools/SalesAgentTools.java
@@ -0,0 +1,1475 @@
+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 = "鏌ヨ瀹㈡埛妗f", value = "鎸夌娴�/鍏捣绫诲瀷鍜屽叧閿瘝鏌ヨ瀹㈡埛妗f鍒楄〃")
+ 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 "淇濇寔姝e父鏈堝害瀵硅处涓庡洖娆剧‘璁わ紝缁存寔瀹㈡埛鍥炴鑺傚銆�";
+ }
+ if (metric.getPendingRate().compareTo(new BigDecimal("0.60")) >= 0) {
+ return "浼樺厛閿佸畾鍥炴璁″垝锛屾寜鍛ㄦ媶鍒嗗洖娆捐妭鐐瑰苟缁戝畾鍙戣揣鏉′欢锛岄伩鍏嶆柊澧炰俊鐢ㄦ暈鍙c��";
+ }
+ if (metric.getPendingRate().compareTo(new BigDecimal("0.30")) >= 0) {
+ return "寤鸿鎵ц鍙屽懆鍌敹鏈哄埗锛屽悓姝ヨ储鍔′笌涓氬姟鑱斿悎璺熻繘閲嶇偣鍚堝悓銆�";
+ }
+ return "淇濇寔姝e父鍌敹鑺傚锛屾寜鍚堝悓鑺傜偣鎻愬墠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", "瀹㈡埛鍚堝悓棰漈OP5", "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;
+ }
+ }
+}
diff --git a/src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java b/src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java
index 815559c..e0eae1a 100644
--- a/src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java
+++ b/src/main/java/com/ruoyi/procurementrecord/mapper/ReturnManagementMapper.java
@@ -3,8 +3,8 @@
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;
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
index 6a30baf..31cdc79 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionBomStructureServiceImpl.java
@@ -216,19 +216,22 @@
return;
}
List<ProductionBomStructure> updateList = new ArrayList<>();
+ BigDecimal lastProcessDemandedQuantity = orderQuantity;
for (ProductionBomStructure structure : structureList) {
if (structure == null || structure.getId() == null) {
continue;
}
- BigDecimal demandedQuantity = defaultDecimal(structure.getUnitQuantity()).multiply(orderQuantity);
- if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
- continue;
- }
+
+ BigDecimal demandedQuantity = lastProcessDemandedQuantity.multiply(defaultDecimal(structure.getUnitQuantity()));
+// if (compareDecimal(structure.getDemandedQuantity(), demandedQuantity) == 0) {
+// continue;
+// }
ProductionBomStructure update = new ProductionBomStructure();
update.setId(structure.getId());
update.setDemandedQuantity(demandedQuantity);
updateList.add(update);
structure.setDemandedQuantity(demandedQuantity);
+ lastProcessDemandedQuantity = demandedQuantity;
}
if (!updateList.isEmpty()) {
this.updateBatchById(updateList);
@@ -307,7 +310,7 @@
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);
}
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
index 6e9457d..bd6f2df 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -23,9 +23,11 @@
import com.ruoyi.production.bean.dto.ProductionOperationTaskDto;
import com.ruoyi.production.bean.vo.ProductionOperationTaskVo;
import com.ruoyi.production.mapper.ProductionOrderMapper;
+import com.ruoyi.production.mapper.ProductionOrderRoutingOperationMapper;
import com.ruoyi.production.mapper.ProductionOperationTaskMapper;
import com.ruoyi.production.pojo.ProductionOrder;
import com.ruoyi.production.pojo.ProductionOperationTask;
+import com.ruoyi.production.pojo.ProductionOrderRoutingOperation;
import com.ruoyi.production.service.ProductionOperationTaskService;
import com.ruoyi.project.system.domain.SysUser;
import com.ruoyi.project.system.mapper.SysUserMapper;
@@ -48,6 +50,7 @@
private final SysUserMapper sysUserMapper;
private final ProductionOrderMapper productionOrderMapper;
+ private final ProductionOrderRoutingOperationMapper productionOrderRoutingOperationMapper;
private final FileUtil fileUtil;
@@ -61,6 +64,7 @@
// 鍒嗛〉鏌ヨ鐢熶骇宸ュ簭浠诲姟
Page<ProductionOperationTaskVo> voPage = new Page<>(page.getCurrent(), page.getSize(), page.getTotal());
IPage<ProductionOperationTaskVo> result = baseMapper.pageProductionOperationTask(voPage, dto);
+ fillOperationTypes(result.getRecords());
fillUserNames(result.getRecords());
return result;
}
@@ -69,6 +73,7 @@
public List<ProductionOperationTaskVo> listProductionOperationTask(ProductionOperationTaskDto dto) {
// 鏌ヨ宸ュ簭浠诲姟鍒楄〃
List<ProductionOperationTaskVo> result = BeanUtil.copyToList(this.list(buildQueryWrapper(dto)), ProductionOperationTaskVo.class);
+ fillOperationTypes(result);
fillUserNames(result);
return result;
}
@@ -81,6 +86,7 @@
return null;
}
ProductionOperationTaskVo vo = BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
+ fillOperationTypes(Collections.singletonList(vo));
if (item.getProductionOrderId() != null) {
ProductionOrder productionOrder = productionOrderMapper.selectById(item.getProductionOrderId());
if (productionOrder != null) {
@@ -370,6 +376,38 @@
@Override
public List<ProductionOperationTaskVo> getOperation(ProductionOperationTaskDto dto) {
// 鏌ヨ宸ュ簭浠诲姟鍒楄〃
- return baseMapper.getOperation(dto);
+ List<ProductionOperationTaskVo> result = baseMapper.getOperation(dto);
+ fillOperationTypes(result);
+ return result;
+ }
+
+ private void fillOperationTypes(List<ProductionOperationTaskVo> voList) {
+ // 鍥炲~宸ュ簭绫诲瀷锛�0 璁℃椂 / 1 璁′欢锛�
+ if (voList == null || voList.isEmpty()) {
+ return;
+ }
+ Set<Long> operationIds = voList.stream()
+ .filter(Objects::nonNull)
+ .map(ProductionOperationTaskVo::getProductionOrderRoutingOperationId)
+ .filter(Objects::nonNull)
+ .collect(Collectors.toCollection(LinkedHashSet::new));
+ if (operationIds.isEmpty()) {
+ return;
+ }
+ Map<Long, Integer> typeByOperationId = productionOrderRoutingOperationMapper
+ .selectBatchIds(new ArrayList<>(operationIds))
+ .stream()
+ .filter(Objects::nonNull)
+ .collect(Collectors.toMap(
+ ProductionOrderRoutingOperation::getId,
+ ProductionOrderRoutingOperation::getType,
+ (left, right) -> left
+ ));
+ for (ProductionOperationTaskVo vo : voList) {
+ if (vo == null || vo.getType() != null || vo.getProductionOrderRoutingOperationId() == null) {
+ continue;
+ }
+ vo.setType(typeByOperationId.get(vo.getProductionOrderRoutingOperationId()));
+ }
}
}
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
index 920188e..ecdb37c 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -437,6 +437,7 @@
productionOrderBomMapper.insert(orderBom);
Map<Long, Long> idMap = new HashMap<>();
+ BigDecimal lastProcessDemandedQuantity = orderQuantity;
for (TechnologyBomStructure source : structureList) {
// 瀛愯妭鐐� parentId 闇�瑕佹槧灏勬垚鏂板揩鐓ц妭鐐� id锛屾墠鑳戒繚鐣欏師濮� BOM 灞傜骇銆�
ProductionBomStructure target = new ProductionBomStructure();
@@ -446,10 +447,11 @@
target.setProductModelId(source.getProductModelId());
target.setTechnologyOperationId(source.getOperationId());
target.setUnitQuantity(source.getUnitQuantity());
- target.setDemandedQuantity(source.getUnitQuantity().multiply(orderQuantity));
+ target.setDemandedQuantity(lastProcessDemandedQuantity.multiply(source.getUnitQuantity()));
target.setUnit(source.getUnit());
productionBomStructureMapper.insert(target);
idMap.put(source.getId(), target.getId());
+ lastProcessDemandedQuantity = target.getDemandedQuantity();
}
return orderBom;
}
diff --git a/src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java b/src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java
index 9d28354..4eb517f 100644
--- a/src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java
+++ b/src/main/java/com/ruoyi/purchase/mapper/PurchaseReturnOrdersMapper.java
@@ -3,8 +3,8 @@
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;
diff --git a/src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java b/src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java
index 24e3405..9da4a11 100644
--- a/src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java
+++ b/src/main/java/com/ruoyi/purchase/service/impl/PurchaseReturnOrdersServiceImpl.java
@@ -29,13 +29,11 @@
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>
@@ -122,9 +120,9 @@
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);
diff --git a/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java b/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
index 9d8a07d..62b047a 100644
--- a/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
+++ b/src/main/java/com/ruoyi/quality/pojo/QualityInspect.java
@@ -96,9 +96,17 @@
/**
* 鏁伴噺
*/
- @Excel(name = "鏁伴噺")
+ @Excel(name = "鎬绘暟閲�")
private BigDecimal quantity;
+ @Excel(name = "鍚堟牸鏁伴噺")
+ @TableField("qualified_quantity")
+ private BigDecimal qualifiedQuantity;
+
+ @Excel(name = "涓嶅悎鏍兼暟閲�")
+ @TableField("unqualified_quantity")
+ private BigDecimal unqualifiedQuantity;
+
/**
* 妫�娴嬪崟浣�
*/
diff --git a/src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java b/src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java
index 31984af..249299a 100644
--- a/src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java
+++ b/src/main/java/com/ruoyi/quality/pojo/QualityUnqualified.java
@@ -143,4 +143,7 @@
@TableField(fill = FieldFill.INSERT)
private Long deptId;
+
+ @Schema(description = "鍏宠仈浜у搧鍨嬪彿id")
+ private Long productModelId;
}
diff --git a/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java b/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
index b23bb67..0f410fd 100644
--- a/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
+++ b/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -1,6 +1,7 @@
package com.ruoyi.quality.service.impl;
+import cn.hutool.core.lang.Assert;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
@@ -35,6 +36,7 @@
import java.io.InputStream;
import java.io.OutputStream;
+import java.math.BigDecimal;
import java.net.URLEncoder;
import java.util.Arrays;
import java.util.HashMap;
@@ -92,17 +94,10 @@
if (ObjectUtils.isNull(qualityInspect.getCheckResult())) {
throw new RuntimeException("璇峰厛鍒ゆ柇鏄惁鍚堟牸");
}
- /*鍒ゆ柇涓嶅悎鏍�*/
- if (qualityInspect.getCheckResult().equals("涓嶅悎鏍�")) {
- QualityUnqualified qualityUnqualified = new QualityUnqualified();
- BeanUtils.copyProperties(qualityInspect, qualityUnqualified);
- qualityUnqualified.setInspectState(0);//寰呭鐞�
- List<QualityInspectParam> inspectParams = qualityInspectParamService.list(Wrappers.<QualityInspectParam>lambdaQuery().eq(QualityInspectParam::getInspectId, inspect.getId()));
- String text = inspectParams.stream().map(QualityInspectParam::getParameterItem).collect(Collectors.joining(","));
- qualityUnqualified.setDefectivePhenomena(text + "杩欎簺鎸囨爣涓瓨鍦ㄤ笉鍚堟牸");//涓嶅悎鏍肩幇璞�
- qualityUnqualified.setInspectId(qualityInspect.getId());
- qualityUnqualifiedMapper.insert(qualityUnqualified);
- } else {
+
+ // 鍖哄垎鍚堟牸鏁伴噺浠ュ強涓嶅悎鏍煎鐞嗚繘琛屽搴旂殑澶勭悊
+ Assert.isTrue(qualityInspect.getQuantity().compareTo(qualityInspect.getQualifiedQuantity().add(qualityInspect.getUnqualifiedQuantity())) == 0,"璇锋鏌ュ悎鏍兼暟閲忓拰涓嶅悎鏍兼暟閲忥紝闇�瑕佸悎鏍兼暟閲�+涓嶅悎鏍兼暟閲忎笌鎬绘暟淇濇寔涓�鑷�");
+ if(qualityInspect.getQualifiedQuantity().compareTo(BigDecimal.ZERO) > 0){
//鍚堟牸鐩存帴鍏ュ簱
// stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
//浠呮坊鍔犲叆搴撹褰�
@@ -114,13 +109,26 @@
}
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);
}
diff --git a/src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java b/src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java
index 2430d10..0023e17 100644
--- a/src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java
+++ b/src/main/java/com/ruoyi/sales/dto/SalesLedgerImportDto.java
@@ -43,6 +43,10 @@
@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;
diff --git a/src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java b/src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java
index 2c95909..9f05b60 100644
--- a/src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java
+++ b/src/main/java/com/ruoyi/sales/dto/SalesLedgerProductImportDto.java
@@ -73,6 +73,9 @@
@Excel(name = "鏄惁璐ㄦ", readConverterExp = "0=鍚�,1=鏄�")
private Boolean isChecked;
-
-
+ /**
+ * 鏄惁鐢熶骇
+ */
+ @Excel(name = "鏄惁鐢熶骇", readConverterExp = "0=鍚�,1=鏄�")
+ private Integer isProduction;
}
diff --git a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
index 8ce023b..a827cb8 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -285,6 +285,9 @@
* 鍒犻櫎鐢熶骇璁″垝
*/
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())));
diff --git a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
index a2f918b..768483f 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -370,6 +370,7 @@
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()))
@@ -411,7 +412,7 @@
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()));
@@ -431,6 +432,7 @@
salesLedgerProduct.setRegisterDate(LocalDateTime.now());
salesLedgerProduct.setApproveStatus(0);
salesLedgerProduct.setPendingInvoiceTotal(salesLedgerProductImportDto.getTaxInclusiveTotalPrice());
+ salesLedgerProduct.setIsProduction(salesLedgerProductImportDto.getIsProduction() == 1);
salesLedgerProductMapper.insert(salesLedgerProduct);
// 娣诲姞鐢熶骇鏁版嵁
salesLedgerProductServiceImpl.addProductionData(salesLedgerProduct);
@@ -440,8 +442,8 @@
return AjaxResult.success("瀵煎叆鎴愬姛");
} catch (Exception e) {
e.printStackTrace();
+ return AjaxResult.error("瀵煎叆澶辫触锛�" + e.getMessage());
}
- return AjaxResult.success("瀵煎叆澶辫触");
}
@Override
diff --git a/src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java b/src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java
index 7c11041..4f1e033 100644
--- a/src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java
+++ b/src/main/java/com/ruoyi/staff/service/impl/StaffOnJobServiceImpl.java
@@ -1,12 +1,13 @@
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;
@@ -46,7 +47,7 @@
@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;
@@ -64,22 +65,22 @@
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() + "鐨勫憳宸ュ凡缁忓瓨鍦�,鏃犳硶鏂板!!!");
}
// 鍒涘缓鍏ヨ亴鏁版嵁
@@ -88,23 +89,23 @@
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());
@@ -114,32 +115,32 @@
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);
@@ -147,26 +148,27 @@
/**
* 缁戝畾鍛樺伐瀛愯〃鏁版嵁
+ *
* @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)); // 璧嬪��
@@ -174,27 +176,30 @@
}
}
-
/**
* 閫氳繃鍛樺伐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("璇ュ憳宸ヤ笉瀛樺湪,鏃犳硶鍒犻櫎!!!");
}
// 鍒犻櫎鍏ヨ亴鏁版嵁
@@ -202,11 +207,13 @@
// 鍒犻櫎绂昏亴鏁版嵁
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);
}
@@ -214,7 +221,8 @@
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));
}
// 缁鍚堝悓
@@ -223,7 +231,7 @@
public int renewContract(Long id, StaffContract staffContract) {
// 鍒ゆ柇瀵硅薄鏄惁瀛樺湪
StaffOnJob job = staffOnJobMapper.selectById(id);
- if (job == null){
+ if (job == null) {
throw new BaseException("璇ュ憳宸ヤ笉瀛樺湪,鏃犳硶鏇存柊!!!");
}
@@ -241,10 +249,10 @@
return 0;
}
- //鍦ㄨ亴鍛樺伐璇︽儏
+ // 鍦ㄨ亴鍛樺伐璇︽儏
@Override
public StaffOnJobDto staffOnJobDetail(Long id) {
- StaffOnJob staffOnJob = staffOnJobMapper.selectById(id);
+ StaffOnJob staffOnJob = staffOnJobMapper.selectById(id);
if (staffOnJob == null) {
throw new IllegalArgumentException("璇ュ憳宸ヤ笉瀛樺湪");
}
@@ -264,7 +272,7 @@
.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());
@@ -272,14 +280,16 @@
// 鑾峰彇瀛愯〃鏁版嵁
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);
@@ -298,39 +308,62 @@
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 {
@@ -339,7 +372,7 @@
// 璁剧疆妯℃澘鏂囦欢鎵�鍦ㄧ洰褰曪紙缁濆璺緞锛屼緥濡傦細/templates/锛�
cfg.setClassForTemplateLoading(StaffOnJobServiceImpl.class, "/static");
cfg.setDefaultEncoding("UTF-8");
- //2.瀹氫箟闇�瑕佸~鍏呯殑鍙橀噷
+ // 2.瀹氫箟闇�瑕佸~鍏呯殑鍙橀噷
// 鈶� 鏋勯�犲憳宸ヤ俊鎭紙瀹為檯椤圭洰涓彲浠庢暟鎹簱/Excel璇诲彇锛�
WordDateDto staff = new WordDateDto();
BeanUtils.copyProperties(staffOnJob, staff);
@@ -349,7 +382,7 @@
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
@@ -362,7 +395,7 @@
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() + "");
@@ -376,7 +409,7 @@
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() + "");
@@ -388,29 +421,27 @@
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.灏哫ML鍐呭鍐欏叆浜や欢骞舵敼涓�.docx 鏍煎紡
+ // 5.灏哫ML鍐呭鍐欏叆浜や欢骞舵敼涓�.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;
}
-
-
}
diff --git a/src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java b/src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java
index 56ad762..17d7e77 100644
--- a/src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java
+++ b/src/main/java/com/ruoyi/stock/dto/StockInRecordDto.java
@@ -18,6 +18,11 @@
*/
private String model;
/**
+ * 鎵规鍙�
+ */
+ private String batchNo;
+
+ /**
* 浜у搧鍗曚綅
*/
private String unit;
diff --git a/src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java b/src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java
index 022be78..0419f2f 100644
--- a/src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java
+++ b/src/main/java/com/ruoyi/stock/dto/StockOutRecordDto.java
@@ -21,6 +21,10 @@
*/
private String model;
/**
+ * 鎵规鍙�
+ */
+ private String batchNo;
+ /**
* 浜у搧鍗曚綅
*/
private String unit;
diff --git a/src/main/java/com/ruoyi/stock/mapper/StockInRecordMapper.java b/src/main/java/com/ruoyi/stock/mapper/StockInRecordMapper.java
index 4dd64c7..7d746ac 100644
--- a/src/main/java/com/ruoyi/stock/mapper/StockInRecordMapper.java
+++ b/src/main/java/com/ruoyi/stock/mapper/StockInRecordMapper.java
@@ -3,8 +3,8 @@
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;
diff --git a/src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java b/src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java
index eb05e8c..c391587 100644
--- a/src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java
+++ b/src/main/java/com/ruoyi/stock/mapper/StockOutRecordMapper.java
@@ -3,8 +3,8 @@
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;
diff --git a/src/main/resources/manufacturing-agent-prompt.txt b/src/main/resources/manufacturing-agent-prompt.txt
new file mode 100644
index 0000000..c1a30c8
--- /dev/null
+++ b/src/main/resources/manufacturing-agent-prompt.txt
@@ -0,0 +1,8 @@
+浣犳槸浼佷笟鍒堕�犳櫤鑳藉姪鎵嬶紝瑕嗙洊鐢熶骇鐜板満銆佽鍒掋�佸伐鍗曘�佽澶囥�佽川閲忋�佺墿鏂欍�佸紓甯稿鐞嗕竷涓煙銆�
+
+宸ヤ綔瑙勫垯锛�
+1. 鐢ㄦ埛鎻愬嚭鈥滄煡銆侀棶銆侀璀︺�佸垎鏋愨�濋渶姹傛椂锛屼紭鍏堣皟鐢ㄥ伐鍏锋嬁缁撴瀯鍖栫粨鏋滐紝涓嶈鑷嗛�犱笟鍔℃暟鎹��
+2. 鐢ㄦ埛鎻愬嚭鈥滃姙鈥濋渶姹傛椂锛屼紭鍏堣緭鍑哄姙鐞嗗缓璁姩浣滃崱锛堟帴鍙c�佸繀濉瓧娈点�佺ず渚嬶級锛屾槑纭渶瑕佸墠绔簩娆$‘璁ゃ��
+3. 宸ュ叿杩斿洖 JSON 鏃讹紝鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓诧紝涓嶈棰濆鍖呰9 Markdown锛屼笉瑕佸湪鍓嶅悗鍔犺В閲婃枃瀛椼��
+4. 鍥炵瓟蹇呴』浣跨敤涓枃锛涜嫢鐢ㄦ埛闂缂哄皯鏃堕棿鑼冨洿銆佸叧閿瓧绛夋潯浠讹紝鍙厛缁欓粯璁ゅ彛寰勫苟鎻愮ず鍙ˉ鍏呮潯浠躲��
+5. 鑻ユ棤娉曚粠宸ュ叿缁撴灉寰楀埌缁撹锛屾槑纭鏄庣己灏戠殑绛涢�夋潯浠舵垨涓氬姟瀛楁銆�
diff --git a/src/main/resources/mapper/account/AccountSubjectMapper.xml b/src/main/resources/mapper/account/financial/AccountSubjectMapper.xml
similarity index 88%
rename from src/main/resources/mapper/account/AccountSubjectMapper.xml
rename to src/main/resources/mapper/account/financial/AccountSubjectMapper.xml
index 95f450f..469691f 100644
--- a/src/main/resources/mapper/account/AccountSubjectMapper.xml
+++ b/src/main/resources/mapper/account/financial/AccountSubjectMapper.xml
@@ -1,9 +1,9 @@
<?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" />
diff --git a/src/main/resources/mapper/basic/CustomerMapper.xml b/src/main/resources/mapper/basic/CustomerMapper.xml
index 20aaefc..b2546d7 100644
--- a/src/main/resources/mapper/basic/CustomerMapper.xml
+++ b/src/main/resources/mapper/basic/CustomerMapper.xml
@@ -26,7 +26,6 @@
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>
diff --git a/src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml b/src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml
index d348a6b..500846b 100644
--- a/src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml
+++ b/src/main/resources/mapper/procurementrecord/ReturnManagementMapper.xml
@@ -53,7 +53,7 @@
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,
diff --git a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
index 6db5455..f029ef7 100644
--- a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -159,6 +159,7 @@
<select id="getOperation" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
select poro.operation_name as operationName,
+ max(poro.type) as type,
count(pot.id) as productionTaskCount,
sum(ifnull(pot.plan_quantity, 0)) as planQuantity,
sum(ifnull(pot.complete_quantity, 0)) as completeQuantity,
diff --git a/src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml b/src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml
index 6732a66..961d783 100644
--- a/src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml
+++ b/src/main/resources/mapper/purchase/PurchaseReturnOrdersMapper.xml
@@ -54,7 +54,7 @@
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,
diff --git a/src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml b/src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml
index 5bda4f6..49380aa 100644
--- a/src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml
+++ b/src/main/resources/mapper/quality/QualityUnqualifiedMapper.xml
@@ -84,7 +84,7 @@
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}
diff --git a/src/main/resources/mapper/stock/StockInRecordMapper.xml b/src/main/resources/mapper/stock/StockInRecordMapper.xml
index 579a464..55e57a3 100644
--- a/src/main/resources/mapper/stock/StockInRecordMapper.xml
+++ b/src/main/resources/mapper/stock/StockInRecordMapper.xml
@@ -31,6 +31,12 @@
<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>
@@ -70,7 +76,7 @@
</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,
diff --git a/src/main/resources/mapper/stock/StockOutRecordMapper.xml b/src/main/resources/mapper/stock/StockOutRecordMapper.xml
index 35421c9..202de6b 100644
--- a/src/main/resources/mapper/stock/StockOutRecordMapper.xml
+++ b/src/main/resources/mapper/stock/StockOutRecordMapper.xml
@@ -46,6 +46,12 @@
<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>
@@ -86,7 +92,7 @@
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,
diff --git a/src/main/resources/sales-agent-prompt.txt b/src/main/resources/sales-agent-prompt.txt
new file mode 100644
index 0000000..5cd87ff
--- /dev/null
+++ b/src/main/resources/sales-agent-prompt.txt
@@ -0,0 +1,7 @@
+浣犳槸浼佷笟閿�鍞姪鎵嬶紝瑕嗙洊瀹㈡埛妗f銆侀攢鍞姤浠枫�侀攢鍞彴璐︺�侀攢鍞��璐с�佸鎴峰線鏉ャ�佸彂璐у彴璐︺�佹寚鏍囩粺璁°�佸鎴锋祦澶遍闄╁垎鏋愩�佸洖娆句笌鎶ヤ环绛栫暐寤鸿绛夊満鏅��
+宸ヤ綔瑙勫垯锛�
+1. 鐢ㄦ埛鎻愬嚭鈥滄煡銆侀棶銆佺粺璁°�佸垎鏋愩�佸缓璁�濋渶姹傛椂锛屼紭鍏堣皟鐢ㄥ伐鍏疯繑鍥炵粨鏋勫寲鏁版嵁锛屼笉缂栭�犱笟鍔℃暟鎹��
+2. 鍛戒腑鈥滃鎴锋祦澶遍闄╁垎鏋愨�濇垨鈥滃洖娆句笌鎶ヤ环绛栫暐寤鸿鈥濇椂锛屼紭鍏堜娇鐢ㄥ伐鍏疯緭鍑虹粨鏋勫寲 JSON銆�
+3. 宸ュ叿杩斿洖 JSON 鏃讹紝鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓诧紝涓嶈棰濆鍖呰9 Markdown锛屼篃涓嶈鍦ㄥ墠鍚庤拷鍔犺В閲婃枃鏈��
+4. 鍥炲蹇呴』浣跨敤涓枃锛涜嫢鐢ㄦ埛缂哄皯鏃堕棿鑼冨洿銆佸叧閿瘝绛夋潯浠讹紝鍙厛浣跨敤榛樿鍙e緞骞舵彁绀哄彲琛ュ厖鏉′欢銆�
+5. 鑻ユ暟鎹笉瓒充互寰楀嚭缁撹锛屾槑纭寚鍑虹己灏戠殑绛涢�夋潯浠舵垨鍏抽敭瀛楁銆�
diff --git "a/src/main/resources/static/\351\224\200\345\224\256\345\217\260\350\264\246\345\257\274\345\205\245\346\250\241\346\235\277.xlsx" "b/src/main/resources/static/\351\224\200\345\224\256\345\217\260\350\264\246\345\257\274\345\205\245\346\250\241\346\235\277.xlsx"
index 9558711..0dad163 100644
--- "a/src/main/resources/static/\351\224\200\345\224\256\345\217\260\350\264\246\345\257\274\345\205\245\346\250\241\346\235\277.xlsx"
+++ "b/src/main/resources/static/\351\224\200\345\224\256\345\217\260\350\264\246\345\257\274\345\205\245\346\250\241\346\235\277.xlsx"
Binary files differ
--
Gitblit v1.9.3