From a3c54410de03f0fd242e1a1118d6471300cf1eda Mon Sep 17 00:00:00 2001
From: zss <zss@example.com>
Date: 星期四, 30 四月 2026 17:36:25 +0800
Subject: [PATCH] Merge remote-tracking branch 'origin/dev_New_pro' into dev_New_pro

---
 src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java                                    |   15 
 src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java                                            |   44 
 src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java                                   |  137 -
 src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java                                                |   17 
 src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java                        |    9 
 src/main/resources/mapper/basic/StorageAttachmentMapper.xml                                                    |    8 
 src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java                                        |    3 
 doc/采购智能体多文件分析前端联调说明.md                                                                                        |  183 ++
 src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java                              |    2 
 src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java                                |  208 ++
 src/main/java/com/ruoyi/production/service/ProductionOrderService.java                                         |    5 
 src/main/resources/approve-todo-agent-prompt.txt                                                               |   17 
 src/main/java/com/ruoyi/basic/pojo/ProductModel.java                                                           |    4 
 src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java                                      |  107 +
 src/main/java/com/ruoyi/project/common/CommonController.java                                                   |   90 -
 src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java                                                         |   54 
 src/main/java/com/ruoyi/production/controller/ProductionOrderController.java                                   |   13 
 src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java       |  105 -
 src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java                                  |    3 
 src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java                                  |   18 
 src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java                                          |  114 -
 src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java                                 |    3 
 src/main/resources/mapper/production/ProductionOperationTaskMapper.xml                                         |    1 
 src/main/resources/mapper/basic/StorageBlobMapper.xml                                                          |   61 
 src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java                               |   87 -
 src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java                                               |   55 
 src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java                               |   66 
 src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java                                                    |    6 
 src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java                                                  |   26 
 FILE_UPLOAD_README.md                                                                                          |  734 ++++++++++
 src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java                            |  102 
 src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java                                      |    3 
 src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java                                                     |   12 
 src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java                                                |  803 +++++++++++
 src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java                                                       |  334 ++++
 src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java                                            |    4 
 src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java                                              |    4 
 src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java                             |    2 
 src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerRecordService.java          |    4 
 src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java                                         |   90 -
 src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java                                       |    3 
 src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java                                    |   22 
 src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java                          |   19 
 src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java                                                    |    5 
 src/main/java/com/ruoyi/sales/service/ICommonFileService.java                                                  |    6 
 src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java                                                 |  184 ++
 src/main/resources/mapper/production/ProductionOrderMapper.xml                                                 |    2 
 src/main/resources/application-dev-pro.yml                                                                     |    4 
 src/main/resources/purchase-agent-prompt.txt                                                                   |   11 
 /dev/null                                                                                                      |  184 --
 src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java                                     |   11 
 src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerRecordServiceImpl.java |   97 -
 src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java                                                    |   14 
 src/main/resources/mapper/system/SysUserMapper.xml                                                             |   19 
 src/main/java/com/ruoyi/production/pojo/ProductionOrder.java                                                   |    6 
 55 files changed, 3,078 insertions(+), 1,062 deletions(-)

diff --git a/FILE_UPLOAD_README.md b/FILE_UPLOAD_README.md
new file mode 100644
index 0000000..558dc85
--- /dev/null
+++ b/FILE_UPLOAD_README.md
@@ -0,0 +1,734 @@
+# 鏂囦欢涓婁紶鍔熻兘璇存槑
+
+鏈枃妗e熀浜庝互涓嬩唬鐮佹暣鐞嗭細
+
+- `src/main/java/com/ruoyi/basic/utils/FileUtil.java`
+- `src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java`
+- `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java`
+- `src/main/java/com/ruoyi/project/common/CommonController.java`
+- `src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java`
+
+鐢ㄤ簬璇存槑鏈」鐩腑鏂囦欢涓婁紶銆侀檮浠剁粦瀹氥�佹枃浠堕瑙�/涓嬭浇鐨勬暣浣撹璁★紝浠ュ強 `FileUtil` 涓瘡涓柟娉曠殑浣滅敤銆�
+
+## 1. 鏁翠綋璁捐
+
+鏈」鐩殑鏂囦欢浣撶郴鍒嗘垚涓ゅ眰锛�
+
+- `storage_blob`锛氬瓨鏂囦欢瀹炰綋淇℃伅
+  - 鍘熷鏂囦欢鍚�
+  - 鍞竴鏂囦欢鍚� `uidFilename`
+  - 鏂囦欢璺緞 `path`
+  - 鏂囦欢澶у皬 `byteSize`
+  - 鏂囦欢绫诲瀷 `contentType`
+  - 鍏叡璁块棶鏍囪瘑 `resourceKey`
+- `storage_attachment`锛氬瓨鏂囦欢鍜屼笟鍔¤褰曠殑鍏宠仈鍏崇郴
+  - `application`锛氭枃浠剁敤閫�
+  - `recordType`锛氫笟鍔¤褰曠被鍨�
+  - `recordId`锛氫笟鍔¤褰曚富閿�
+  - `storageBlobId`锛氬叧鑱旂殑鏂囦欢涓昏〃 id
+
+鍙互鐞嗚В涓猴細
+
+- `storage_blob` 璐熻矗鈥滄枃浠舵湰韬��
+- `storage_attachment` 璐熻矗鈥滄枃浠舵寕鍦ㄥ摢鏉′笟鍔℃暟鎹笂鈥�
+
+## 2. 涓婁紶娴佺▼
+
+### 2.1 鏅�氫笂浼�
+
+鎺ュ彛锛�
+
+- `POST /common/upload`
+
+鎺у埗鍣ㄤ綅缃細
+
+- `src/main/java/com/ruoyi/project/common/CommonController.java`
+
+鍏ュ弬锛�
+
+- 琛ㄥ崟瀛楁鍚嶏細`files`
+- 绫诲瀷锛歚List<MultipartFile>`
+
+浠g爜閫昏緫锛�
+
+1. 鍓嶇鍏堣皟鐢� `/common/upload`
+2. `CommonController.upload()` 璋冪敤 `storageBlobService.upload(files, false)`
+3. 鏈嶅姟灞備繚瀛樻枃浠跺厓鏁版嵁鍒� `storage_blob`
+4. 杩斿洖 `StorageBlobVO` 鍒楄〃锛岄噷闈㈤�氬父浼氬甫锛�
+   - 鏂囦欢 id
+   - 鍘熷鏂囦欢鍚�
+   - 鍞竴鏂囦欢鍚�
+   - 棰勮鍦板潃 `previewURL`
+   - 涓嬭浇鍦板潃 `downloadURL`
+
+璇存槑锛�
+
+- 姝ゆ椂鍙槸鈥滀笂浼犱簡鏂囦欢鈥�
+- 杩樻病鏈夊拰鍏蜂綋涓氬姟鍗曟嵁寤虹珛鍏崇郴
+
+### 2.2 鍏叡涓婁紶
+
+鎺ュ彛锛�
+
+- `POST /common/public/upload`
+
+浠g爜閫昏緫锛�
+
+- `CommonController.publicUpload()` 璋冪敤 `storageBlobService.upload(files, true)`
+
+璇存槑锛�
+
+- 璇ユ帴鍙d笂浼犵殑鏂囦欢璧扳�滃叕鍏辨枃浠垛�濇ā寮�
+- 鎺у埗鍣ㄦ敞閲婂凡鏄庣‘璇存槑锛氭案涔呮湁鏁堬紝鎱庣敤
+- 瀵瑰簲 URL 鏋勫缓鏃讹紝鍙兘璧� `publicKey` 鍙傛暟锛岃�屼笉鏄复鏃� `token`
+
+## 3. 闄勪欢缁戝畾娴佺▼
+
+涓婁紶瀹屾垚鍚庯紝濡傛灉闇�瑕佹妸鏂囦欢缁戝畾鍒版煇鏉′笟鍔¤褰曪紝闇�瑕佸啀璋冪敤闄勪欢鎺ュ彛銆�
+
+鎺ュ彛锛�
+
+- `POST /storageAttachment/add`
+
+鎺у埗鍣ㄤ綅缃細
+
+- `src/main/java/com/ruoyi/basic/controller/StorageAttachmentController.java`
+
+鏍稿績璇锋眰瀵硅薄锛�
+
+- `StorageAttachmentDTO`
+
+鍏朵腑缁ф壙浜� `StorageAttachment`锛屽苟棰濆鍖呭惈锛�
+
+- `storageBlobDTOs`锛氬緟缁戝畾鐨勬枃浠跺垪琛�
+
+甯哥敤瀛楁鍚箟锛�
+
+- `application`锛氭枃浠剁敤閫�
+- `recordType`锛氫笟鍔$被鍨�
+- `recordId`锛氫笟鍔′富閿�
+- `storageBlobDTOs[].id`锛氫笂浼犳垚鍔熷悗杩斿洖鐨勬枃浠� id
+
+绀轰緥璇锋眰浣擄細
+
+```json
+{
+  "application": "file",
+  "recordType": "common_file",
+  "recordId": 1001,
+  "storageBlobDTOs": [
+    {
+      "id": 12,
+      "application": "file"
+    },
+    {
+      "id": 13,
+      "application": "file"
+    }
+  ]
+}
+```
+
+缁戝畾閫昏緫璇存槑锛�
+
+1. 鍏堜笂浼犳枃浠讹紝鎷垮埌 `storage_blob.id`
+2. 鍐嶈皟鐢� `/storageAttachment/add`
+3. 鏈嶅姟灞傛渶缁堜細閫氳繃 `FileUtil` 淇濆瓨 `storage_attachment`
+4. 鍚庣画鍗冲彲鎸変笟鍔¤褰曟煡璇㈠嚭璇ヨ褰曚笅鐨勯檮浠�
+
+## 4. 鏌ヨ涓庡垹闄ら檮浠�
+
+### 4.1 鏌ヨ闄勪欢鍒楄〃
+
+鎺ュ彛锛�
+
+- `GET /storageAttachment/list`
+
+璇存槑锛�
+
+- 鎸� `StorageAttachmentDTO` 涓殑鏉′欢鏌ヨ
+- 甯歌鏉′欢鏄� `application`銆乣recordType`銆乣recordId`
+- 杩斿洖缁撴灉鏈川涓婃槸鍜屼笟鍔¤褰曞叧鑱斿悗鐨勬枃浠跺垪琛�
+
+### 4.2 鍒犻櫎闄勪欢
+
+鎺ュ彛锛�
+
+- `DELETE /storageAttachment/delete`
+
+璇锋眰浣擄細
+
+- `List<Long> ids`
+
+璇存槑锛�
+
+- 杩欓噷鐨� `ids` 鏄檮浠跺叧鑱旇〃 id锛屼竴鑸槸 `storage_attachment.id`
+- 鍒犻櫎鏃堕�氬父涓嶄粎浼氬垹鍏宠仈鍏崇郴锛屼篃浼氳繘涓�姝ュ垹闄ゅ搴旀枃浠惰褰�
+
+## 5. 棰勮涓庝笅杞芥祦绋�
+
+### 5.1 涓嬭浇鎺ュ彛
+
+鎺ュ彛锛�
+
+- `GET /common/download/{fileName}`
+
+鏀寔涓ょ璁块棶鏂瑰紡锛�
+
+- 涓存椂閾炬帴锛歚token`
+- 鍏叡閾炬帴锛歚publicKey`
+
+浠g爜閫昏緫锛�
+
+1. 濡傛灉璇锋眰閲屾湁 `publicKey`锛岃蛋 `storageBlobService.getPublicFile(fileName, publicKey)`
+2. 鍚﹀垯璧� `storageBlobService.getFileByToken(fileName, token)`
+3. 鍙栧埌瀹為檯鏂囦欢鍚庯紝璋冪敤 `fileUtil.compressFile(file)` 鍋氬浘鐗囧帇缂╁鐞�
+4. 璁剧疆涓嬭浇鍝嶅簲澶达紝杈撳嚭鏂囦欢娴�
+
+### 5.2 棰勮鎺ュ彛
+
+鎺ュ彛锛�
+
+- `GET /common/preview/{fileName}`
+
+鏀寔涓ょ璁块棶鏂瑰紡锛�
+
+- 涓存椂閾炬帴锛歚token`
+- 鍏叡閾炬帴锛歚publicKey`
+
+浠g爜閫昏緫锛�
+
+1. 鏍¢獙 `token` 鎴� `publicKey`
+2. 鑾峰彇鏂囦欢
+3. 璋冪敤 `fileUtil.compressFile(file)`
+4. 鏍规嵁鏂囦欢鍐呭绫诲瀷杩斿洖 inline 棰勮
+
+## 6. 鏋氫妇鍚箟
+
+### 6.1 `ApplicationTypeEnum`
+
+浣嶇疆锛�
+
+- `src/main/java/com/ruoyi/basic/enums/ApplicationTypeEnum.java`
+
+褰撳墠瀹氫箟鍊硷細
+
+| 鏋氫妇 | type | 璇存槑 |
+|---|---|---|
+| `IMAGE` | `image` | 鍥剧墖绫绘枃浠� |
+| `FILE` | `file` | 鏅�氭枃浠� |
+| `AFTER_FILE` | `after_file` | 鍞悗鐩稿叧鏂囦欢 |
+| `BEFORE_FILE` | `before_file` | 鍞墠/鍓嶇疆鐩稿叧鏂囦欢 |
+| `APK` | `apk` | 瀹夎鍖呮枃浠� |
+
+浣滅敤锛�
+
+- 鐢ㄤ簬鍖哄垎鍚屼竴鏉′笟鍔¤褰曚笅锛屼笉鍚岀敤閫旂殑鏂囦欢
+- `FileUtil` 鐨勫緢澶氭煡璇€�佸垹闄ゃ�佷繚瀛樻柟娉曢兘浼氱敤鍒拌瀛楁
+
+### 6.2 `RecordTypeEnum`
+
+浣嶇疆锛�
+
+- `src/main/java/com/ruoyi/basic/enums/RecordTypeEnum.java`
+
+浣滅敤锛�
+
+- 鐢ㄤ簬鏍囪鏂囦欢灞炰簬鍝被涓氬姟璁板綍
+- 渚嬪璐ㄦ銆侀噰璐�佸鎴枫�佸敭鍚庛�佸彴璐︺�侀�氱煡銆佽澶囩瓑妯″潡
+- 涓婁紶瀹屾垚鍚庯紝闄勪欢鏈�缁堥�氳繃 `recordType + recordId` 鍜屼笟鍔℃暟鎹叧鑱�
+
+璇存槑锛�
+
+- 璇ユ灇涓惧�煎緢澶氾紝鏂囨。涓嶉�愪釜灞曞紑
+- 瀹為檯浣跨敤鏃跺繀椤讳紶浠g爜涓凡瀹氫箟鐨� `type` 鍊�
+- 濡傦細
+  - `common_file`
+  - `after_sales_service`
+  - `quality_inspect`
+  - `product`
+  - `notice`
+
+## 7. `FileUtil` 鏂规硶璇存槑
+
+`FileUtil` 鏄湰濂楁枃浠朵笂浼犱綋绯荤殑鏍稿績宸ュ叿绫伙紝涓昏璐熻矗锛�
+
+- 鏂囦欢涓庝笟鍔¤褰曠粦瀹�
+- 鏂囦欢涓庨檮浠跺垹闄�
+- 闄勪欢鏌ヨ
+- 棰勮/涓嬭浇鍦板潃鐢熸垚
+- token 浣跨敤娆℃暟鎺у埗
+- 鍥剧墖鍘嬬缉
+
+涓嬮潰鎸夊姛鑳藉垎缁勮鏄庢瘡涓柟娉曘��
+
+### 7.1 淇濆瓨闄勪欢鍏崇郴
+
+#### 1. `saveStorageAttachment(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS)`
+
+浣滅敤锛�
+
+- 鎸夆�滄枃浠剁敤閫� + 璁板綍绫诲瀷 + 璁板綍 id鈥濅繚瀛橀檮浠跺叧绯�
+
+閫昏緫锛�
+
+1. 鏍¢獙 `application`銆乣recordType`銆乣recordId`
+2. 鍏堝垹闄よ繖缁勪笟鍔¤褰曚笅鐨勬棫闄勪欢
+3. 鎶婃柊鐨� `storageBlobDTOS` 杞垚 `storage_attachment` 璁板綍鍚庢壒閲忔彃鍏�
+
+閫傜敤鍦烘櫙锛�
+
+- 鏌愭潯涓氬姟鏁版嵁閲嶆柊淇濆瓨闄勪欢锛屾棫闄勪欢鏁翠綋鏇挎崲鎴愭柊闄勪欢
+
+#### 2. `saveStorageAttachmentByRecordTypeAndRecordId(String application, RecordTypeEnum recordType, Long recordId, List<StorageBlobDTO> storageBlobDTOS)`
+
+浣滅敤锛�
+
+- 鎸� `recordType + recordId` 淇濆瓨闄勪欢鍏崇郴锛宍application` 鍙寚瀹氾紝涔熷彲浠庢瘡涓枃浠跺璞¢噷璇诲彇
+
+閫昏緫鐗圭偣锛�
+
+- 濡傛灉 `application == null`锛屼細鏍规嵁 `storageBlobDTO.application` 鍒嗗埆鍒犻櫎鏃у叧绯�
+- 濡傛灉闄勪欢鍒楄〃涓虹┖锛屼細鐩存帴鍒犻櫎璇ヤ笟鍔¤褰曠殑闄勪欢鍏崇郴
+- 鎻掑叆鏃朵細鑷姩鍥炲~ `application`
+
+閫傜敤鍦烘櫙锛�
+
+- 涓�娆℃彁浜ら噷鍙兘鍖呭惈澶氱鐢ㄩ�旂殑闄勪欢
+- 鎴栬�呰皟鐢ㄦ柟涓嶆柟渚跨洿鎺ヤ紶鏋氫妇绫诲瀷
+
+### 7.2 鍒犻櫎鏂囦欢涓昏〃 `storage_blob`
+
+#### 3. `deleteStorageBlobs(List<Long> storageBlobIds)`
+
+浣滅敤锛�
+
+- 鎸夋枃浠朵富琛� id 鎵归噺鍒犻櫎鏂囦欢璁板綍
+
+#### 4. `deleteStorageBlobsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
+
+浣滅敤锛�
+
+- 鍏堟牴鎹檮浠跺叧鑱� id 鏌ュ埌 `storageBlobId`
+- 鍐嶅垹闄ゅ搴旂殑鏂囦欢涓昏〃璁板綍
+
+#### 5. `deleteStorageBlobsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds)`
+
+浣滅敤锛�
+
+- 鏍规嵁鐢ㄩ�斻�佽褰曠被鍨嬨�佸涓笟鍔� id锛屾壒閲忓垹闄ゅ搴旂殑鏂囦欢涓昏〃璁板綍
+
+閫傜敤鍦烘櫙锛�
+
+- 鎵归噺鍒犻櫎鏌愮被涓氬姟鏁版嵁鏃讹紝鍚屾椂娓呯悊闄勪欢
+
+#### 6. `deleteStorageBlobsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鏍规嵁 `recordType + recordId` 鍒犻櫎璇ヤ笟鍔¤褰曚笅鎵�鏈夋枃浠朵富琛ㄨ褰�
+
+### 7.3 鍒犻櫎闄勪欢鍏崇郴 `storage_attachment`
+
+#### 7. `deleteStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
+
+浣滅敤锛�
+
+- 鍏堝垹闄ら檮浠跺搴旂殑鏂囦欢涓昏〃璁板綍
+- 鍐嶅垹闄ら檮浠跺叧绯昏〃璁板綍
+
+#### 8. `deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鍒犻櫎鎸囧畾鐢ㄩ�斻�佹寚瀹氫笟鍔¤褰曚笅鐨勯檮浠跺叧绯�
+
+鐗圭偣锛�
+
+- 浼氬厛鍒� blob锛屽啀鍒� attachment
+
+#### 9. `deleteStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鍒犻櫎鎸囧畾涓氬姟璁板綍涓嬪叏閮ㄩ檮浠跺叧绯伙紝涓嶅尯鍒嗙敤閫�
+
+#### 10. `deleteStorageAttachmentsByApplicationAndRecordTypeAndRecordIds(ApplicationTypeEnum application, RecordTypeEnum recordType, List<Long> recordIds)`
+
+浣滅敤锛�
+
+- 鎸夊涓笟鍔� id 鎵归噺鍒犻櫎闄勪欢鍏崇郴
+
+### 7.4 鏌ヨ闄勪欢鍏崇郴
+
+#### 11. `getStorageAttachmentsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鏌ヨ `storage_attachment` 璁板綍
+
+#### 12. `getStorageAttachmentsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鎸夌敤閫斻�佷笟鍔$被鍨嬨�佷笟鍔� id 鏌ヨ闄勪欢鍏崇郴
+
+#### 13. `getStorageAttachmentsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$被鍨嬨�佷笟鍔� id 鏌ヨ闄勪欢鍏崇郴
+
+### 7.5 鏌ヨ鏂囦欢淇℃伅 `StorageBlobVO`
+
+#### 14. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(StorageAttachmentDTO storageAttachmentDTO)`
+
+浣滅敤锛�
+
+- 閫氳繃 `StorageAttachmentDTO` 鏉′欢鏌ヨ鏂囦欢鍒楄〃
+
+鐗圭偣锛�
+
+- `application` 鍙��
+- 鏈�缁堣繑鍥炵殑鏄甫棰勮/涓嬭浇鍦板潃鐨� `StorageBlobVO`
+
+#### 15. `getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鏌ヨ鏂囦欢鍒楄〃
+
+鐗圭偣锛�
+
+- 浼氳嚜鍔ㄦ瀯寤猴細
+  - `previewURL`
+  - `downloadURL`
+  - `storageAttachmentId`
+
+#### 16. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鎸夌敤閫斻�佷笟鍔$被鍨嬨�佷笟鍔� id 鏌ヨ鏂囦欢鍒楄〃
+
+#### 17. `getStorageBlobVOsByRecordTypeAndRecordId(RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$被鍨嬨�佷笟鍔� id 鏌ヨ鏂囦欢鍒楄〃
+
+#### 18. `getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鍜岀 16 涓柟娉曠被浼硷紝浣嗗彲浠ヨ嚜瀹氫箟閾炬帴杩囨湡鏃堕棿
+
+璇存槑锛�
+
+- `expired` 鍗曚綅鏄垎閽�
+- 杩斿洖鐨勯瑙�/涓嬭浇鍦板潃浼氭寜杩欎釜鏃堕棿鐢熸垚绛惧悕
+
+#### 19. `getStorageBlobVOsByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鏌ヨ鏂囦欢鍒楄〃锛屽苟鑷畾涔夐摼鎺ヨ繃鏈熸椂闂�
+
+### 7.6 鏌ヨ闄勪欢瑙嗗浘 `StorageAttachmentVO`
+
+#### 20. `getStorageAttachmentVOSByStorageAttachmentIds(List<Long> storageAttachmentIds)`
+
+浣滅敤锛�
+
+- 鏌ヨ闄勪欢瑙嗗浘瀵硅薄
+
+鐗圭偣锛�
+
+- 姣忔潯闄勪欢璁板綍閲屼細宓屽鑷繁鐨� `storageBlobVOS`
+
+#### 21. `getStorageAttachmentVOSByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鏌ヨ闄勪欢瑙嗗浘锛屽苟鑷畾涔夐摼鎺ヨ繃鏈熸椂闂�
+
+#### 22. `getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$淮搴︽煡璇㈤檮浠惰鍥�
+
+#### 23. `getStorageAttachmentVOSByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$淮搴︽煡璇㈤檮浠惰鍥撅紝骞惰嚜瀹氫箟閾炬帴杩囨湡鏃堕棿
+
+### 7.7 浠呰幏鍙栭瑙堝湴鍧�
+
+#### 24. `getFilePreviewURLByStorageAttachmentIds(List<Long> storageAttachmentIds)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鍒楄〃锛岃繑鍥為瑙堝湴鍧�鍒楄〃
+
+#### 25. `getFilePreviewURLByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鍒楄〃锛岃繑鍥炲甫鑷畾涔夎繃鏈熸椂闂寸殑棰勮鍦板潃鍒楄〃
+
+#### 26. `getFilePreviewURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$淮搴﹁繑鍥為瑙堝湴鍧�鍒楄〃
+
+#### 27. `getFilePreviewURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$淮搴﹁繑鍥炲甫鑷畾涔夎繃鏈熸椂闂寸殑棰勮鍦板潃鍒楄〃
+
+### 7.8 浠呰幏鍙栦笅杞藉湴鍧�
+
+#### 28. `getFileDownloadURLByStorageAttachmentIds(List<Long> storageAttachmentIds)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鍒楄〃锛岃繑鍥炰笅杞藉湴鍧�鍒楄〃
+
+#### 29. `getFileDownloadURLByStorageAttachmentIds(List<Long> storageAttachmentIds, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鏍规嵁闄勪欢鍏崇郴 id 鍒楄〃锛岃繑鍥炲甫鑷畾涔夎繃鏈熸椂闂寸殑涓嬭浇鍦板潃鍒楄〃
+
+#### 30. `getFileDownloadURLByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$淮搴﹁繑鍥炰笅杞藉湴鍧�鍒楄〃
+
+#### 31. `getFileDownloadURLByApplicationAndRecordTypeAndRecordIdAndExpired(ApplicationTypeEnum application, RecordTypeEnum recordType, Long recordId, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鎸変笟鍔$淮搴﹁繑鍥炲甫鑷畾涔夎繃鏈熸椂闂寸殑涓嬭浇鍦板潃鍒楄〃
+
+### 7.9 鏋勫缓绛惧悕 URL
+
+#### 32. `buildSignedPreviewUrl(StorageBlobVO storageBlob)`
+
+浣滅敤锛�
+
+- 浣跨敤绯荤粺榛樿杩囨湡鏃堕棿锛岀敓鎴愰瑙堥摼鎺�
+
+瀹為檯璋冪敤锛�
+
+- 鍐呴儴绛変环浜庤皟鐢� `buildSignedUrl(storageBlob, "/preview/", properties.getExpired())`
+
+#### 33. `buildSignedDownloadUrl(StorageBlobVO storageBlob)`
+
+浣滅敤锛�
+
+- 浣跨敤绯荤粺榛樿杩囨湡鏃堕棿锛岀敓鎴愪笅杞介摼鎺�
+
+瀹為檯璋冪敤锛�
+
+- 鍐呴儴绛変环浜庤皟鐢� `buildSignedUrl(storageBlob, "/download/", properties.getExpired())`
+
+#### 34. `buildSignedUrl(StorageBlobVO storageBlob, String actionPath, BigDecimal expired)`
+
+浣滅敤锛�
+
+- 鏋勫缓缁熶竴鐨勫甫绛惧悕棰勮/涓嬭浇鍦板潃
+
+鏀寔锛�
+
+- `actionPath = "/preview/"`
+- `actionPath = "/download/"`
+
+鏍稿績閫昏緫锛�
+
+1. 鏍¢獙璺緞鍙傛暟鍜屾枃浠朵俊鎭�
+2. 鎷兼帴鍩虹璁块棶鍦板潃
+3. 濡傛灉 `expired == -1`锛屼笉鐢熸垚 token锛岀洿鎺ヨ蛋 `publicKey`
+4. 鍚﹀垯鐢熸垚甯﹁繃鏈熸椂闂寸殑 JWT token
+5. 鎶� token 鐨勪娇鐢ㄦ鏁颁俊鎭啓鍏� Redis
+6. 杩斿洖鏈�缁� URL
+
+閲嶈璇存槑锛�
+
+- `expired` 鍗曚綅涓哄垎閽�
+- 榛樿杩囨湡鏃堕棿涓� 120 鍒嗛挓
+- 闈炴案涔呴摼鎺ヤ細鍙椻�滆繃鏈熸椂闂� + 浣跨敤娆℃暟闄愬埗鈥濆弻閲嶆帶鍒�
+
+### 7.10 token 浣跨敤鎺у埗
+
+#### 35. `cacheTokenUsage(String token, long expiredMillis)`
+
+浣滅敤锛�
+
+- 鎶� token 浣跨敤娆℃暟鍒濆鍖栧埌 Redis
+
+鐗圭偣锛�
+
+- 鍒濆鍊煎啓鍏ヤ负 `0`
+- TTL 涓� token 杩囨湡鏃堕棿淇濇寔涓�鑷�
+
+璇存槑锛�
+
+- 杩欐槸绉佹湁鏂规硶锛屼緵 `buildSignedUrl()` 鍐呴儴璋冪敤
+
+#### 36. `buildTokenUsageKey(String token)`
+
+浣滅敤锛�
+
+- 缁熶竴鐢熸垚 Redis key
+
+鏍煎紡锛�
+
+- `file:token:usage:{token}`
+
+璇存槑锛�
+
+- 杩欐槸绉佹湁鏂规硶
+
+#### 37. `validateTokenUsage(String token)`
+
+浣滅敤锛�
+
+- 鏍¢獙 token 鏄惁杩樿兘缁х画浣跨敤
+
+鏍稿績閫昏緫锛�
+
+1. 浠� Redis 璇诲彇褰撳墠浣跨敤娆℃暟
+2. 濡傛灉娌℃湁鍊硷紝璁や负閾炬帴宸茶繃鏈熸垨宸插け鏁�
+3. 濡傛灉杈惧埌涓婇檺锛岀珛鍗冲垹闄� Redis 璁板綍骞舵姤閿�
+4. 鍚﹀垯鑷涓�娆′娇鐢ㄦ鏁�
+5. 濡傛灉鑷鍚庤揪鍒颁笂闄愶紝鍐嶅垹闄� Redis 璁板綍
+
+璇存槑锛�
+
+- 璇ユ柟娉曢�氬父浼氬湪瀹為檯璁块棶鏂囦欢鏃剁敱鏈嶅姟灞傝皟鐢�
+
+#### 38. `resolveLimit()`
+
+浣滅敤锛�
+
+- 瑙f瀽 token 鍙娇鐢ㄦ鏁颁笂闄�
+
+瑙勫垯锛�
+
+- `properties.getUseLimit() <= 0` 鏃讹紝榛樿杩斿洖 `10`
+
+璇存槑锛�
+
+- 杩欐槸绉佹湁鏂规硶
+
+### 7.11 璺緞涓庡帇缂�
+
+#### 39. `buildRelativePath()`
+
+浣滅敤锛�
+
+- 鐢熸垚鏂囦欢瀛樺偍鐩稿璺緞
+
+鏍煎紡锛�
+
+- `yyyy/MMdd`
+
+渚嬪锛�
+
+- `2026/0430`
+
+鐢ㄩ�旓細
+
+- 涓�鑸敤浜庢寜鏃ユ湡鍒嗙洰褰曚繚瀛樹笂浼犳枃浠�
+
+#### 40. `compressFile(File file)`
+
+浣滅敤锛�
+
+- 瀵瑰浘鐗囪繘琛屽帇缂╋紝闈炲浘鐗囨垨涓嶆弧瓒虫潯浠舵椂杩斿洖鍘熸枃浠�
+
+鍘嬬缉鏉′欢锛�
+
+1. 寮�鍚簡 `properties.getCompress()`
+2. 鏂囦欢鏄浘鐗�
+3. 鏂囦欢澶у皬澶т簬 `properties.getNeedCompressSize()`
+
+澶勭悊閫昏緫锛�
+
+1. 鐩爣鏂囦欢鍚嶄负 `thumb_鍘熸枃浠跺悕`
+2. 濡傛灉鍘嬬缉鏂囦欢宸插瓨鍦紝鐩存帴澶嶇敤
+3. 浣跨敤 `Thumbnailator` 鎸夊師灏哄鍘嬬缉鐢昏川
+4. 濡傛灉鍘嬬缉澶辫触锛岄檷绾ц繑鍥炲師鏂囦欢
+
+璇存槑锛�
+
+- 褰撳墠涓嬭浇鍜岄瑙堟帴鍙i兘浼氳皟鐢ㄨ繖涓柟娉�
+
+#### 41. `isImage(String fileName)`
+
+浣滅敤锛�
+
+- 绠�鍗曞垽鏂枃浠舵槸鍚︽槸鍥剧墖
+
+鏀寔鍚庣紑锛�
+
+- `jpg`
+- `jpeg`
+- `png`
+
+璇存槑锛�
+
+- 杩欐槸绉佹湁鏂规硶锛屼緵 `compressFile()` 浣跨敤
+
+## 8. 鎺ㄨ崘浣跨敤椤哄簭
+
+涓氬姟涓婃渶甯歌鐨勬帴鍏ラ『搴忓涓嬶細
+
+1. 鍓嶇涓婁紶鏂囦欢鍒� `/common/upload`
+2. 鎷垮埌杩斿洖缁撴灉涓殑鏂囦欢 id
+3. 涓氬姟淇濆瓨鏃惰皟鐢� `/storageAttachment/add`
+4. 浼犲叆 `application + recordType + recordId + storageBlobDTOs`
+5. 鍚庣画椤甸潰鍥炴樉鏃舵寜涓氬姟鏉′欢璋冪敤闄勪欢鏌ヨ
+6. 鍓嶇浣跨敤杩斿洖鐨� `previewURL` 鎴� `downloadURL`
+
+## 9. 甯歌娉ㄦ剰鐐�
+
+### 9.1 鍏堜笂浼狅紝鍐嶇粦瀹�
+
+- `/common/upload` 鍙礋璐f枃浠跺叆搴�
+- `/storageAttachment/add` 鎵嶆槸鍜屼笟鍔℃暟鎹缓绔嬪叧绯�
+
+### 9.2 `application` 寰堥噸瑕�
+
+- 鍚屼竴鏉� `recordId` 涓嬪彲鑳芥湁澶氱粍涓嶅悓鐢ㄩ�旈檮浠�
+- 鍒犻櫎鍜屾煡璇㈡椂锛岀粡甯镐緷璧� `application`
+
+### 9.3 涓嬭浇閾炬帴涓嶆槸姘镐箙鏈夋晥
+
+- 鏅�氶摼鎺ヤ竴鑸�氳繃 JWT token 鎺у埗
+- 鍚屾椂鍙楄繃鏈熸椂闂村拰浣跨敤娆℃暟闄愬埗
+
+### 9.4 鍏叡鏂囦欢瑕佹厧鐢�
+
+- `public/upload` 涓婁紶鐨勬枃浠跺彲璧版案涔呭叕寮�璁块棶
+- 閫傚悎鍏紑璧勬簮锛屼笉閫傚悎鏁忔劅鏂囦欢
+
+### 9.5 鍥剧墖棰勮/涓嬭浇鍙兘杩斿洖鍘嬬缉鏂囦欢
+
+- 褰撳墠鎺у埗鍣ㄥ湪涓嬭浇鍜岄瑙堝墠閮戒細璋冪敤 `compressFile()`
+- 澶у浘鍦ㄨ闂椂鍙兘浣跨敤鍘嬬缉鍚庣殑鍓湰
+
+## 10. 涓�鍙ヨ瘽鎬荤粨
+
+鏈」鐩殑鏂囦欢涓婁紶鏂规鏄�滀袱闃舵妯″瀷鈥濓細
+
+- 绗竴闃舵涓婁紶鏂囦欢锛岀敓鎴� `storage_blob`
+- 绗簩闃舵缁戝畾涓氬姟锛岀敓鎴� `storage_attachment`
+
+鑰� `FileUtil` 鍒欒礋璐f妸鈥滀笂浼犲悗鐨勬枃浠垛�濆彉鎴愨�滃彲鏌ヨ銆佸彲棰勮銆佸彲涓嬭浇銆佸彲鍒犻櫎銆佸彲鎺ф椂鏁堚�濈殑瀹屾暣闄勪欢鑳藉姏銆�
diff --git "a/doc/\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md" "b/doc/\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md"
new file mode 100644
index 0000000..cb9e3cf
--- /dev/null
+++ "b/doc/\351\207\207\350\264\255\346\231\272\350\203\275\344\275\223\345\244\232\346\226\207\344\273\266\345\210\206\346\236\220\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md"
@@ -0,0 +1,183 @@
+# 閲囪喘鏅鸿兘浣撳鏂囦欢鍒嗘瀽鍓嶇鑱旇皟璇存槑
+
+## 娴佺▼璇存槑
+
+鍚庣宸叉柊澧為噰璐櫤鑳戒綋澶氭枃浠跺垎鏋愮‘璁ゆ祦绋嬶細
+
+1. 鍓嶇涓婁紶澶氫釜閲囪喘鐩稿叧鏂囦欢锛屽苟闄勫甫鐢ㄦ埛瑕佹眰銆�
+2. 鍚庣鎻愬彇鏂囦欢鍐呭锛屼氦缁欓噰璐櫤鑳戒綋鍒嗘瀽銆�
+3. 鏅鸿兘浣撹繑鍥炲緟瀹㈡埛纭鐨勭粨鏋勫寲 JSON銆�
+4. 鍓嶇灞曠ず鎽樿銆侀闄┿�佺己澶卞瓧娈靛拰寰呭鐞嗘暟鎹��
+5. 瀹㈡埛纭鎴栬ˉ鍏呮暟鎹悗锛屽墠绔皟鐢ㄧ‘璁ゆ帴鍙c��
+6. 鍚庣鏍规嵁纭鍚庣殑鏁版嵁鎵ц瀵瑰簲閲囪喘涓氬姟澶勭悊銆�
+
+鍒嗘瀽鎺ュ彛涓嶄細钀藉簱锛屽彧鏈夌‘璁ゆ帴鍙d細鎵ц涓氬姟澶勭悊銆�
+
+## 鎺ュ彛 1锛氶噰璐鏂囦欢鍒嗘瀽
+
+```http
+POST /purchase-ai/analyze-files
+Content-Type: multipart/form-data
+```
+
+璇锋眰鍙傛暟锛�
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+| --- | --- | --- | --- |
+| files | file[] | 鏄� | 澶氭枃浠朵笂浼犲瓧娈碉紝瀛楁鍚嶅繀椤绘槸 `files` |
+| message | string | 鍚� | 鐢ㄦ埛瑕佹眰锛屼緥濡傦細璇锋牴鎹繖浜涢噰璐悎鍚屽拰鏄庣粏鏁寸悊閲囪喘鍙拌处鏁版嵁 |
+| memoryId | string | 鍚� | 浼氳瘽 ID锛屼笉浼犳椂鍚庣浼氳嚜鍔ㄧ敓鎴愬唴閮ㄤ細璇� |
+
+杩斿洖锛�
+
+```http
+Content-Type: text/stream;charset=utf-8
+```
+
+鍓嶇闇�瑕佹嫾鎺ュ畬鏁存祦寮忔枃鏈悗鍐嶆墽琛� `JSON.parse`銆�
+
+杩斿洖 JSON 缁撴瀯绀轰緥锛�
+
+```json
+{
+  "success": true,
+  "businessType": "purchase_ledger",
+  "action": "confirm_required",
+  "description": "宸叉牴鎹枃浠舵暣鐞嗗嚭閲囪喘鍙拌处鑽夌锛岃纭銆�",
+  "confidence": 0.86,
+  "missingFields": [],
+  "warnings": [],
+  "payload": {},
+  "preview": []
+}
+```
+
+瀛楁璇存槑锛�
+
+| 瀛楁 | 璇存槑 |
+| --- | --- |
+| success | 鏄惁鍒嗘瀽鎴愬姛 |
+| businessType | 涓氬姟绫诲瀷锛歚purchase_ledger`銆乣payment_registration`銆乣purchase_return_order`銆乣unknown` |
+| action | 鍥哄畾涓� `confirm_required` |
+| description | 涓枃璇存槑 |
+| confidence | 缃俊搴︼紝0 鍒� 1 |
+| missingFields | 缂哄け瀛楁锛屽墠绔渶瑕佹彁绀虹敤鎴疯ˉ鍏� |
+| warnings | 椋庨櫓鎻愮ず |
+| payload | 寰呭鎴风‘璁ゅ苟鎻愪氦缁欑‘璁ゆ帴鍙g殑鏁版嵁 |
+| preview | 缁欏鎴风‘璁ょ敤鐨勪腑鏂囨憳瑕� |
+
+## 鎺ュ彛 2锛氱‘璁ゅ苟鎵ц涓氬姟澶勭悊
+
+```http
+POST /purchase-ai/analyze-files/confirm
+Content-Type: application/json
+```
+
+璇锋眰浣擄細
+
+```json
+{
+  "businessType": "purchase_ledger",
+  "payload": {
+  }
+}
+```
+
+褰撳墠鏀寔鐨� `businessType`锛�
+
+| businessType | 璇存槑 | 鍚庣澶勭悊 |
+| --- | --- | --- |
+| purchase_ledger | 閲囪喘鍙拌处 | 璋冪敤閲囪喘鍙拌处鏂板/缂栬緫 |
+| payment_registration | 浠樻鐧昏 | 璋冪敤浠樻鐧昏鏂板 |
+| purchase_return_order | 閲囪喘閫�璐у崟 | 璋冪敤閲囪喘閫�璐у崟鏂板 |
+
+纭鎺ュ彛杩斿洖鏅�� `AjaxResult`銆�
+
+## 閲囪喘鍙拌处 Payload 绾﹀畾
+
+閲囪喘鍙拌处纭鎺ㄨ崘浣跨敤涓や釜闆嗗悎锛�
+
+```json
+{
+ "businessType": "purchase_ledger",
+  "payload": {
+    "purchaseLedgers": []
+  }
+}
+```
+
+瀛楁绾﹀畾锛�
+
+- `purchaseLedgers` 鏀鹃噰璐鍗�/閲囪喘鍙拌处涓昏〃鏁版嵁锛屽瓧娈靛悕蹇呴』涓� `PurchaseLedgerDto` 淇濇寔涓�鑷淬��
+- 浜у搧鏄庣粏鏀惧湪姣忔潯 `purchaseLedgers[i].productData` 涓紝瀵瑰簲 `PurchaseLedgerDto` 鐨� `private List<SalesLedgerProduct> productData;`銆�
+- 椤跺眰 `payload.productData` 浠呬綔涓烘棫鏍煎紡鍏煎锛屼笉寤鸿鍓嶇缁х画浣跨敤銆�
+- 鏂囦欢涓殑鈥滈噰璐崟鍙封�濆氨鏄�滈噰璐悎鍚屽彿鈥濓紝鍓嶇鍙互缁熶竴鏄犲皠鎴� `purchaseContractNumber`銆�
+- 鏂囦欢涓殑鈥滈攢鍞崟鍙封�濆氨鏄�滈攢鍞悎鍚屽彿鈥濓紝鍓嶇鍙互缁熶竴鏄犲皠鎴� `salesContractNo`銆�
+- 鏃ユ湡瀛楁缁熶竴浣跨敤 `yyyy-MM-dd`锛屼緥濡� `2026-04-30`锛涗笉瑕佹彁浜� `4/30/26`銆乣2026/4/30`銆乣2026骞�4鏈�30鏃 鎴栧甫鏃跺垎绉掔殑鏍煎紡銆�
+- 閲囪喘鍙拌处涓嶉渶瑕佸墠绔紶瀹℃壒浜猴紝涓嶈鎻愪氦 `approveUserIds`銆乣approverId`銆�
+- `missingFields` 闈㈠悜瀹㈡埛灞曠ず锛屽彧鏀句腑鏂囩己澶遍」锛屼緥濡� `渚涘簲鍟嗗悕绉癭銆乣鍚◣鍗曚环`锛屼笉瑕佸睍绀鸿嫳鏂囧瓧娈靛悕銆�
+- 閲囪喘鍙拌处涓氬姟蹇呭~锛氶噰璐悎鍚屽彿銆佷緵搴斿晢鍚嶇О鎴栦緵搴斿晢ID銆�
+- 浜у搧鏄庣粏涓氬姟蹇呭~锛氫骇鍝佸悕绉般�佽鏍煎瀷鍙枫�佸崟浣嶃�佹暟閲忋�佸惈绋庡崟浠枫�佸惈绋庢�讳环锛涘鏋滃彧鏈夊惈绋庢�讳环鍜屾暟閲忥紝鍚庣浼氳嚜鍔ㄨ绠楀惈绋庡崟浠凤紱濡傛灉鍙湁鍚◣鍗曚环鍜屾暟閲忥紝鍚庣浼氳嚜鍔ㄨ绠楀惈绋庢�讳环銆�
+- 浜у搧鏄庣粏鍙�氳繃 `purchaseContractNumber`銆乣purchaseContractNo`銆乣閲囪喘鍚堝悓鍙穈銆乣閲囪喘鍗曞彿`銆乣閲囪喘璁㈠崟鍙穈 鍏宠仈瀵瑰簲閲囪喘璁㈠崟锛涗篃鍙�氳繃 `salesContractNo`銆乣salesContractNumber`銆乣閿�鍞悎鍚屽彿`銆乣閿�鍞崟鍙穈銆乣閿�鍞鍗曞彿` 杈呭姪鍖归厤銆�
+
+`purchaseLedgers` 鍗曟潯璁板綍鍏佽浣跨敤鐨� `PurchaseLedgerDto` 瀛楁锛�
+
+```text
+entryDateStart, entryDateEnd, id, purchaseContractNumber,
+supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo,
+salesContractNoId, projectName, entryDate, executionDate, remarks,
+attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type,
+productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId,
+productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId,
+contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type,
+paymentMethod, approvalStatus, templateName
+```
+
+绀轰緥锛�
+
+```json
+{
+  "purchaseLedgers": [
+    {
+      "purchaseContractNumber": "CG-2026-001",
+      "supplierName": "鍗楅�氱ず渚嬩緵搴斿晢",
+      "salesContractNo": "XS-2026-001",
+      "projectName": "绀轰緥椤圭洰",
+      "entryDate": "2026-04-30",
+      "executionDate": "2026-04-30",
+      "contractAmount": 120000,
+      "remarks": "鐢辨枃浠跺垎鏋愮敓鎴愶紝寰呯‘璁�",
+      "productData": [
+        {
+          "productCategory": "绀轰緥浜у搧",
+          "specificationModel": "鍨嬪彿A",
+          "unit": "浠�",
+          "quantity": 10,
+          "taxInclusiveUnitPrice": 12000,
+          "taxInclusiveTotalPrice": 120000,
+          "type": 2
+        }
+      ]
+    }
+  ]
+}
+```
+
+## 鍓嶇澶勭悊寤鸿
+
+1. 鐢ㄦ埛閫夋嫨澶氫釜鏂囦欢锛屽~鍐欏垎鏋愯姹傘��
+2. 浣跨敤 `multipart/form-data` 璋冪敤 `/purchase-ai/analyze-files`銆�
+3. 鎷兼帴娴佸紡杩斿洖鏂囨湰銆�
+4. 瀵瑰畬鏁存枃鏈墽琛� `JSON.parse`銆�
+5. 灞曠ず `preview`銆乣warnings`銆乣missingFields` 鍜� `payload`銆�
+6. 濡傛灉 `missingFields` 涓嶄负绌猴紝寮曞鐢ㄦ埛琛ュ厖鎴栫紪杈� `payload`銆�
+7. 鐢ㄦ埛纭鍚庯紝灏� `businessType` 鍜岀‘璁ゅ悗鐨� `payload` 鎻愪氦鍒� `/purchase-ai/analyze-files/confirm`銆�
+
+## 娉ㄦ剰浜嬮」
+
+- 鏂囦欢涓婁紶瀛楁鍚嶅繀椤绘槸 `files`銆�
+- 鍒嗘瀽鎺ュ彛鍙敓鎴愬緟纭鏁版嵁锛屼笉浼氭墽琛屼笟鍔¤惤搴撱��
+- 纭鎺ュ彛鎵嶄細鎵ц涓氬姟澶勭悊銆�
+- 濡傛灉 `payload` 缂哄皯蹇呰涓氬姟 ID锛岀‘璁ゆ帴鍙e彲鑳借繑鍥炰笟鍔℃牎楠岄敊璇��
+- 鍓嶇闇�瑕佹妸 `missingFields` 鏄庣‘灞曠ず缁欑敤鎴枫��
+- AI 杩斿洖鍐呭鎸夊悎娉� JSON 澶勭悊锛屼笉瑕佹寜鏅�氳嚜鐒惰瑷�灞曠ず銆�
diff --git a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
index 5911521..9f8499d 100644
--- a/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/ApproveTodoIntentExecutor.java
@@ -44,7 +44,7 @@
                     extractTimeRange(text)
             );
         }
-        if (containsAny(text, "娴佽浆", "杩涘害", "鑺傜偣", "鏃ュ織")) {
+        if (containsAny(text, "娴佽浆", "杩涘害", "鑺傜偣", "鏃ュ織", "鍗″湪", "鍗″埌", "褰撳墠瀹℃壒浜�", "澶勭悊璁板綍")) {
             return StringUtils.hasText(approveId)
                     ? approveTodoTools.getTodoProgress(memoryId, approveId)
                     : missingApproveId("todo_progress", "鏌ヨ瀹℃壒杩涘害闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
@@ -54,19 +54,20 @@
                     ? approveTodoTools.getTodoDetail(memoryId, approveId)
                     : missingApproveId("todo_detail", "鏌ヨ瀹℃壒璇︽儏闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
-        if (containsAny(text, "鍙栨秷瀹℃牳", "鎾ら攢瀹℃牳", "鍥為��瀹℃牳")) {
+        if (containsAny(text, "鍙栨秷瀹℃牳", "鎾ら攢瀹℃牳", "鍥為��瀹℃牳", "鎾ら攢瀹℃壒", "鎾ゅ洖瀹℃壒")
+                || (containsAny(text, "鎾ら攢", "鎾ゅ洖") && containsAny(text, "瀹℃壒鎿嶄綔", "瀹℃牳鎿嶄綔"))) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.cancelReviewTodo(memoryId, approveId, extractTail(text, "鍘熷洜"))
+                    ? approveTodoTools.cancelReviewTodo(memoryId, approveId, firstNonBlank(extractTail(text, "鍘熷洜"), extractTail(text, "澶囨敞")))
                     : missingApproveId("cancel_review_action", "鍙栨秷瀹℃牳闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
-        if (containsAny(text, "鍒犻櫎")) {
+        if (containsAny(text, "鍒犻櫎", "绉婚櫎")) {
             return StringUtils.hasText(approveId)
                     ? approveTodoTools.deleteTodo(memoryId, approveId)
                     : missingApproveId("delete_action", "鍒犻櫎瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (containsAny(text, "椹冲洖", "鎷掔粷")) {
             return StringUtils.hasText(approveId)
-                    ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", extractTail(text, "鍘熷洜"))
+                    ? approveTodoTools.reviewTodo(memoryId, approveId, "reject", firstNonBlank(extractTail(text, "鍘熷洜"), extractTail(text, "澶囨敞")))
                     : missingApproveId("review_action", "椹冲洖瀹℃壒闇�瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
         if (containsAny(text, "瀹℃牳閫氳繃", "瀹℃壒閫氳繃", "閫氳繃瀹℃壒", "鍚屾剰瀹℃壒", "瀹℃壒鍚屾剰")) {
@@ -79,7 +80,7 @@
                 && !containsAny(text, "鏈�氳繃", "閫氳繃鐜�", "瀹℃壒閫氳繃鐜�", "瀹℃牳閫氳繃鐜�")) {
             return approveTodoTools.reviewTodo(memoryId, approveId, "approve", extractTail(text, "澶囨敞"));
         }
-        if (containsAny(text, "淇敼")) {
+        if (containsAny(text, "淇敼", "鏇存柊", "鍙樻洿")) {
             return StringUtils.hasText(approveId)
                     ? approveTodoTools.updateTodo(
                     memoryId,
@@ -93,19 +94,20 @@
                     extractValue(text, "澶囨敞"))
                     : missingApproveId("update_action", "淇敼瀹℃壒鍗曢渶瑕佹彁渚涙祦绋嬬紪鍙枫��");
         }
-        if (containsAny(text, "鍒楄〃", "寰呭姙", "鏌ヨ瀹℃壒")) {
+        if (containsAny(text, "鍒楄〃", "寰呭姙", "鏌ヨ瀹℃壒", "鍗曟嵁", "娴佺▼", "瀹℃壒鎵�")) {
             return approveTodoTools.listTodos(
                     memoryId,
                     extractStatus(text),
                     extractApproveType(text),
                     extractKeyword(text),
-                    extractLimit(text));
+                    extractLimit(text),
+                    extractScope(text));
         }
         return null;
     }
 
     private boolean isStatsIntent(String text) {
-        if (containsAny(text, "缁熻", "鍒嗘瀽", "鍥捐〃", "瓒嬪娍", "鍗犳瘮", "姹囨��", "鎬婚噺")) {
+        if (containsAny(text, "缁熻", "鍒嗘瀽", "鍥捐〃", "瓒嬪娍", "鍗犳瘮", "姹囨��", "鎬婚噺", "鍒嗗竷", "鍚勬湁澶氬皯", "鏈夊灏�")) {
             return true;
         }
         boolean hasQueryWord = containsAny(text, "鏌ヨ", "鏌ョ湅", "鐪嬩笅", "鐪嬬湅", "鑾峰彇");
@@ -141,13 +143,13 @@
         if (containsAny(text, "寰呭鏍�", "寰呭鎵�")) {
             return "pending";
         }
-        if (containsAny(text, "瀹℃牳涓�")) {
+        if (containsAny(text, "瀹℃牳涓�", "澶勭悊涓�", "澶勭悊涓殑", "鍔炵悊涓�")) {
             return "processing";
         }
-        if (containsAny(text, "宸查�氳繃", "瀹℃牳瀹屾垚")) {
+        if (containsAny(text, "宸查�氳繃", "閫氳繃", "瀹℃牳瀹屾垚", "瀹℃壒瀹屾垚")) {
             return "approved";
         }
-        if (containsAny(text, "鏈�氳繃", "椹冲洖")) {
+        if (containsAny(text, "鏈�氳繃", "椹冲洖", "宸查┏鍥�", "鎷掔粷")) {
             return "rejected";
         }
         if (containsAny(text, "閲嶆柊鎻愪氦")) {
@@ -187,7 +189,11 @@
     private String extractKeyword(String text) {
         String cleaned = text
                 .replace("鏌ヨ", "")
+                .replace("鏌ョ湅", "")
+                .replace("鍒楀嚭", "")
+                .replace("甯垜", "")
                 .replace("瀹℃壒", "")
+                .replace("鍗曟嵁", "")
                 .replace("寰呭姙", "")
                 .replace("鍒楄〃", "")
                 .replace("鍓�10鏉�", "")
@@ -263,6 +269,20 @@
         return matcher.find() ? matcher.group(2).trim() : null;
     }
 
+    private String extractScope(String text) {
+        if (containsAny(text, "鎴戝彂璧�", "鎴戞彁浜�", "鎴戠敵璇�", "鐢宠浜烘槸鎴�")) {
+            return "applicant";
+        }
+        if (containsAny(text, "寰呮垜瀹℃壒", "寰呮垜瀹℃牳", "鎴戝鐞�", "鎴戝鎵�", "褰撳墠寰呮垜", "闇�瑕佹垜澶勭悊")) {
+            return "approver";
+        }
+        return "related";
+    }
+
+    private String firstNonBlank(String first, String second) {
+        return StringUtils.hasText(first) ? first : second;
+    }
+
     private String missingApproveId(String type, String description) {
         Map<String, Object> result = new LinkedHashMap<>();
         result.put("success", false);
diff --git a/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java b/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
index 2d4e6b8..5991fc3 100644
--- a/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
+++ b/src/main/java/com/ruoyi/ai/assistant/PurchaseIntentExecutor.java
@@ -26,6 +26,51 @@
         }
         String text = message.trim();
 
+        if (containsAny(text, "鎺掕", "鎺掑悕", "鍓嶅嚑", "鍓嶄簲", "鍓嶅崄") && containsAny(text, "鐗╂枡", "浜у搧", "鍘熸潗鏂�", "閲囪喘閲戦", "閲戦")) {
+            return purchaseAgentTools.rankPurchaseMaterials(
+                    memoryId,
+                    extractStartDate(text),
+                    extractEndDate(text),
+                    text,
+                    extractLimit(text)
+            );
+        }
+        if (containsAny(text, "鏈叆搴�", "寰呭叆搴�", "娌℃湁鍏ュ簱", "杩樻湭鍏ュ簱")) {
+            return purchaseAgentTools.listUnstockedPurchaseOrders(
+                    memoryId,
+                    extractStartDate(text),
+                    extractEndDate(text),
+                    extractKeyword(text),
+                    extractLimit(text)
+            );
+        }
+        if (containsAny(text, "鍒拌揣寮傚父", "鍒拌揣鏈夊紓甯�", "寮傚父鍒拌揣", "鍒拌揣闂", "渚涘簲鍟嗗埌璐у紓甯�")) {
+            return purchaseAgentTools.listArrivalExceptions(
+                    memoryId,
+                    extractStartDate(text),
+                    extractEndDate(text),
+                    text,
+                    extractLimit(text)
+            );
+        }
+        if (containsAny(text, "寰呬粯娆�", "鏈粯娆�", "鏈粯娓�", "寰呮敮浠�", "搴斾粯")) {
+            return purchaseAgentTools.listPendingPaymentOrders(
+                    memoryId,
+                    extractStartDate(text),
+                    extractEndDate(text),
+                    extractKeyword(text),
+                    extractLimit(text)
+            );
+        }
+        if (containsAny(text, "閫�璐�", "閫�鏂�", "鎷掓敹")) {
+            return purchaseAgentTools.listPurchaseReturns(
+                    memoryId,
+                    extractStartDate(text),
+                    extractEndDate(text),
+                    extractKeyword(text),
+                    extractLimit(text)
+            );
+        }
         if (isStatsIntent(text)) {
             return purchaseAgentTools.getPurchaseStats(
                     memoryId,
@@ -37,7 +82,7 @@
         if (containsAny(text, "璇︽儏", "鏄庣粏") && extractId(text) != null) {
             return purchaseAgentTools.getPurchaseLedgerDetail(memoryId, extractId(text));
         }
-        if (containsAny(text, "鍙拌处", "閲囪喘鍗�", "鍚堝悓", "鍒楄〃", "鏌ヨ")) {
+        if (containsAny(text, "鍙拌处", "閲囪喘鍗�", "閲囪喘璁㈠崟", "璁㈠崟", "鍚堝悓", "鍒楄〃", "鏌ヨ")) {
             return purchaseAgentTools.listPurchaseLedgers(
                     memoryId,
                     extractKeyword(text),
@@ -50,7 +95,7 @@
     }
 
     private boolean isStatsIntent(String text) {
-        if (containsAny(text, "缁熻", "鍒嗘瀽", "鎶ヨ〃", "姹囨��", "瓒嬪娍", "鏁版嵁鐪嬫澘")) {
+        if (containsAny(text, "缁熻", "鍒嗘瀽", "鎶ヨ〃", "姹囨��", "瓒嬪娍", "鏁版嵁鐪嬫澘", "鎯呭喌", "鏈夊灏�")) {
             return true;
         }
         boolean queryWord = containsAny(text, "鏌ヨ", "鏌ョ湅", "鐪嬩笅", "鐪嬬湅", "鑾峰彇");
@@ -100,8 +145,14 @@
                 .replace("鏌ヨ", "")
                 .replace("鏌ョ湅", "")
                 .replace("閲囪喘", "")
+                .replace("閲囪喘鍗�", "")
+                .replace("閲囪喘璁㈠崟", "")
+                .replace("璁㈠崟", "")
                 .replace("鍙拌处", "")
                 .replace("鍒楄〃", "")
+                .replace("鍝簺", "")
+                .replace("鍒楀嚭", "")
+                .replace("甯垜", "")
                 .replace("鏈�杩�10鏉�", "")
                 .replace("鍓�10鏉�", "")
                 .trim();
diff --git a/src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java b/src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java
new file mode 100644
index 0000000..f727981
--- /dev/null
+++ b/src/main/java/com/ruoyi/ai/bean/PurchaseAiConfirmRequest.java
@@ -0,0 +1,26 @@
+package com.ruoyi.ai.bean;
+
+import java.util.Map;
+
+public class PurchaseAiConfirmRequest {
+
+    private String businessType;
+
+    private Map<String, Object> payload;
+
+    public String getBusinessType() {
+        return businessType;
+    }
+
+    public void setBusinessType(String businessType) {
+        this.businessType = businessType;
+    }
+
+    public Map<String, Object> getPayload() {
+        return payload;
+    }
+
+    public void setPayload(Map<String, Object> payload) {
+        this.payload = payload;
+    }
+}
diff --git a/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java b/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
index 6ddf304..62061ee 100644
--- a/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
+++ b/src/main/java/com/ruoyi/ai/config/PurchaseAgentConfig.java
@@ -1,8 +1,10 @@
 package com.ruoyi.ai.config;
 
 import com.ruoyi.ai.store.MongoChatMemoryStore;
+import dev.langchain4j.community.model.dashscope.QwenStreamingChatModel;
 import dev.langchain4j.memory.chat.ChatMemoryProvider;
 import dev.langchain4j.memory.chat.MessageWindowChatMemory;
+import org.springframework.beans.factory.annotation.Value;
 import org.springframework.context.annotation.Bean;
 import org.springframework.context.annotation.Configuration;
 
@@ -17,4 +19,14 @@
                 .chatMemoryStore(mongoChatMemoryStore)
                 .build();
     }
+
+    @Bean("purchaseVisionStreamingChatModel")
+    QwenStreamingChatModel purchaseVisionStreamingChatModel(
+            @Value("${langchain4j.community.dashscope.streaming-chat-model.api-key}") String apiKey) {
+        return QwenStreamingChatModel.builder()
+                .apiKey(apiKey)
+                .modelName("qwen-vl-max")
+                .isMultimodalModel(true)
+                .build();
+    }
 }
diff --git a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
index f3e5aec..2bb0625 100644
--- a/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
+++ b/src/main/java/com/ruoyi/ai/controller/PurchaseAiController.java
@@ -1,20 +1,45 @@
 package com.ruoyi.ai.controller;
 
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.fasterxml.jackson.core.type.TypeReference;
+import com.fasterxml.jackson.databind.ObjectMapper;
 import com.ruoyi.ai.assistant.PurchaseAgent;
 import com.ruoyi.ai.assistant.PurchaseIntentExecutor;
 import com.ruoyi.ai.bean.ChatForm;
+import com.ruoyi.ai.bean.PurchaseAiConfirmRequest;
 import com.ruoyi.ai.context.AiSessionUserContext;
 import com.ruoyi.ai.service.AiChatSessionService;
+import com.ruoyi.ai.service.AiFileTextExtractor;
 import com.ruoyi.ai.store.MongoChatMemoryStore;
+import com.ruoyi.basic.mapper.SupplierManageMapper;
+import com.ruoyi.basic.pojo.SupplierManage;
 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 com.ruoyi.purchase.dto.PurchaseLedgerDto;
+import com.ruoyi.purchase.dto.PurchaseReturnOrderDto;
+import com.ruoyi.purchase.pojo.PaymentRegistration;
+import com.ruoyi.purchase.service.IPaymentRegistrationService;
+import com.ruoyi.purchase.service.IPurchaseLedgerService;
+import com.ruoyi.purchase.service.PurchaseReturnOrdersService;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
+import dev.langchain4j.data.image.Image;
 import dev.langchain4j.data.message.AiMessage;
+import dev.langchain4j.data.message.ChatMessage;
+import dev.langchain4j.data.message.Content;
+import dev.langchain4j.data.message.ImageContent;
+import dev.langchain4j.data.message.SystemMessage;
+import dev.langchain4j.data.message.TextContent;
 import dev.langchain4j.data.message.UserMessage;
+import dev.langchain4j.model.chat.StreamingChatLanguageModel;
+import dev.langchain4j.model.chat.response.ChatResponse;
+import dev.langchain4j.model.chat.response.StreamingChatResponseHandler;
 import io.swagger.v3.oas.annotations.Operation;
 import io.swagger.v3.oas.annotations.tags.Tag;
+import org.springframework.beans.factory.annotation.Qualifier;
+import org.springframework.web.bind.annotation.RequestParam;
 import org.springframework.web.bind.annotation.DeleteMapping;
 import org.springframework.web.bind.annotation.GetMapping;
 import org.springframework.web.bind.annotation.PathVariable;
@@ -22,31 +47,73 @@
 import org.springframework.web.bind.annotation.RequestBody;
 import org.springframework.web.bind.annotation.RequestMapping;
 import org.springframework.web.bind.annotation.RestController;
+import org.springframework.web.multipart.MultipartFile;
 import reactor.core.publisher.Flux;
 
+import java.io.IOException;
+import java.math.BigDecimal;
+import java.math.RoundingMode;
+import java.util.Base64;
+import java.time.LocalDate;
+import java.time.ZoneId;
+import java.time.format.DateTimeFormatter;
+import java.time.format.DateTimeParseException;
+import java.util.ArrayList;
+import java.util.Collections;
+import java.util.Date;
+import java.util.LinkedHashMap;
 import java.util.List;
+import java.util.Map;
+import java.util.NoSuchElementException;
+import java.util.UUID;
 
 @Tag(name = "閲囪喘鏅鸿兘浣�")
 @RestController
 @RequestMapping("/purchase-ai")
 public class PurchaseAiController extends BaseController {
 
+    private static final String PURCHASE_FILE_ANALYZE_MEMORY_PREFIX = "purchase-file-analyze::";
+    private static final int MAX_FILE_COUNT = 10;
+    private static final int MAX_SINGLE_FILE_TEXT_LENGTH = 8000;
+    private static final int MAX_TOTAL_FILE_TEXT_LENGTH = 30000;
+
     private final PurchaseAgent purchaseAgent;
     private final PurchaseIntentExecutor purchaseIntentExecutor;
     private final AiSessionUserContext aiSessionUserContext;
     private final MongoChatMemoryStore mongoChatMemoryStore;
     private final AiChatSessionService aiChatSessionService;
+    private final AiFileTextExtractor aiFileTextExtractor;
+    private final ObjectMapper objectMapper;
+    private final IPurchaseLedgerService purchaseLedgerService;
+    private final IPaymentRegistrationService paymentRegistrationService;
+    private final PurchaseReturnOrdersService purchaseReturnOrdersService;
+    private final SupplierManageMapper supplierManageMapper;
+    private final StreamingChatLanguageModel purchaseVisionStreamingChatModel;
 
     public PurchaseAiController(PurchaseAgent purchaseAgent,
                                 PurchaseIntentExecutor purchaseIntentExecutor,
                                 AiSessionUserContext aiSessionUserContext,
                                 MongoChatMemoryStore mongoChatMemoryStore,
-                                AiChatSessionService aiChatSessionService) {
+                                AiChatSessionService aiChatSessionService,
+                                AiFileTextExtractor aiFileTextExtractor,
+                                ObjectMapper objectMapper,
+                                IPurchaseLedgerService purchaseLedgerService,
+                                IPaymentRegistrationService paymentRegistrationService,
+                                PurchaseReturnOrdersService purchaseReturnOrdersService,
+                                SupplierManageMapper supplierManageMapper,
+                                @Qualifier("purchaseVisionStreamingChatModel") StreamingChatLanguageModel purchaseVisionStreamingChatModel) {
         this.purchaseAgent = purchaseAgent;
         this.purchaseIntentExecutor = purchaseIntentExecutor;
         this.aiSessionUserContext = aiSessionUserContext;
         this.mongoChatMemoryStore = mongoChatMemoryStore;
         this.aiChatSessionService = aiChatSessionService;
+        this.aiFileTextExtractor = aiFileTextExtractor;
+        this.objectMapper = objectMapper;
+        this.purchaseLedgerService = purchaseLedgerService;
+        this.paymentRegistrationService = paymentRegistrationService;
+        this.purchaseReturnOrdersService = purchaseReturnOrdersService;
+        this.supplierManageMapper = supplierManageMapper;
+        this.purchaseVisionStreamingChatModel = purchaseVisionStreamingChatModel;
     }
 
     @Operation(summary = "閲囪喘瀵硅瘽")
@@ -81,6 +148,84 @@
                 .doOnError(ex -> aiChatSessionService.refreshSessionStats(memoryId, loginUser));
     }
 
+    @Operation(summary = "閲囪喘澶氭枃浠跺垎鏋�")
+    @PostMapping(value = "/analyze-files", consumes = "multipart/form-data", produces = "text/stream;charset=utf-8")
+    public Flux<String> analyzeFiles(@RequestParam("files") MultipartFile[] files,
+                                     @RequestParam(value = "message", required = false) String message,
+                                     @RequestParam(value = "memoryId", required = false) String memoryId) {
+        if (files == null || files.length == 0) {
+            return Flux.just("files涓嶈兘涓虹┖");
+        }
+        if (files.length > MAX_FILE_COUNT) {
+            return Flux.just("涓�娆℃渶澶氬垎鏋�" + MAX_FILE_COUNT + "涓枃浠�");
+        }
+
+        String rawMemoryId = StringUtils.hasText(memoryId) ? memoryId : UUID.randomUUID().toString();
+        String finalMemoryId = rawMemoryId.startsWith(PURCHASE_FILE_ANALYZE_MEMORY_PREFIX)
+                ? rawMemoryId
+                : PURCHASE_FILE_ANALYZE_MEMORY_PREFIX + rawMemoryId;
+
+        LoginUser loginUser = SecurityUtils.getLoginUser();
+        aiSessionUserContext.bind(finalMemoryId, loginUser);
+
+        String finalMessage = StringUtils.hasText(message)
+                ? message
+                : "璇峰垎鏋愯繖浜涢噰璐枃浠讹紝鎻愬彇鍙敤浜庝笟鍔″鐞嗙殑鏁版嵁锛屽苟鏁寸悊鎴愬緟瀹㈡埛纭鐨勬牸寮�";
+
+        String fileContent;
+        try {
+            fileContent = buildMultiFileContent(files);
+        } catch (IllegalArgumentException ex) {
+            return Flux.just(ex.getMessage());
+        } catch (IOException ex) {
+            return Flux.just("鏂囦欢璇诲彇澶辫触");
+        }
+
+        if (!StringUtils.hasText(fileContent)) {
+            return Flux.just("鏈彁鍙栧埌鏈夋晥鏂囦欢鍐呭");
+        }
+
+        String userPrompt = buildPurchaseFileAnalyzePrompt(finalMessage, fileContent);
+        aiChatSessionService.touchSession(finalMemoryId, loginUser, "閲囪喘澶氭枃浠跺垎鏋�: " + finalMessage);
+
+        if (containsImageFile(files)) {
+            return chatWithPurchaseVisionModel(finalMemoryId, userPrompt, files)
+                    .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+                    .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+        }
+
+        return Flux.defer(() -> purchaseAgent.chat(finalMemoryId, userPrompt))
+                .onErrorResume(NoSuchElementException.class, ex -> {
+                    mongoChatMemoryStore.deleteMessages(finalMemoryId);
+                    return purchaseAgent.chat(finalMemoryId, userPrompt);
+                })
+                .doOnComplete(() -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser))
+                .doOnError(ex -> aiChatSessionService.refreshSessionStats(finalMemoryId, loginUser));
+    }
+
+    @Operation(summary = "閲囪喘澶氭枃浠跺垎鏋愮‘璁ゅ鐞�")
+    @PostMapping("/analyze-files/confirm")
+    public AjaxResult confirmAnalyzeResult(@RequestBody PurchaseAiConfirmRequest request) {
+        if (request == null || !StringUtils.hasText(request.getBusinessType())) {
+            return AjaxResult.error("businessType涓嶈兘涓虹┖");
+        }
+        if (request.getPayload() == null || request.getPayload().isEmpty()) {
+            return AjaxResult.error("payload涓嶈兘涓虹┖");
+        }
+
+        try {
+            String businessType = request.getBusinessType().trim();
+            return switch (businessType) {
+                case "purchase_ledger" -> processPurchaseLedger(request.getPayload());
+                case "payment_registration" -> processPaymentRegistration(request.getPayload());
+                case "purchase_return_order" -> processPurchaseReturnOrder(request.getPayload());
+                default -> AjaxResult.error("鏆備笉鏀寔璇ヤ笟鍔$被鍨�: " + businessType);
+            };
+        } catch (Exception ex) {
+            return AjaxResult.error(toCustomerMessage(ex));
+        }
+    }
+
     @Operation(summary = "閲囪喘浼氳瘽鍒楄〃")
     @GetMapping("/history/sessions")
     public AjaxResult listSessions() {
@@ -99,4 +244,660 @@
         aiSessionUserContext.remove(memoryId);
         return toAjax(aiChatSessionService.deleteCurrentUserSession(memoryId, SecurityUtils.getLoginUser()));
     }
+
+    private String buildMultiFileContent(MultipartFile[] files) throws IOException {
+        StringBuilder builder = new StringBuilder();
+        int totalLength = 0;
+        for (MultipartFile file : files) {
+            String text = aiFileTextExtractor.extractText(file);
+            if (!StringUtils.hasText(text)) {
+                continue;
+            }
+            String limitedText = text.length() > MAX_SINGLE_FILE_TEXT_LENGTH
+                    ? text.substring(0, MAX_SINGLE_FILE_TEXT_LENGTH)
+                    : text;
+            if (totalLength + limitedText.length() > MAX_TOTAL_FILE_TEXT_LENGTH) {
+                int remain = MAX_TOTAL_FILE_TEXT_LENGTH - totalLength;
+                if (remain <= 0) {
+                    break;
+                }
+                limitedText = limitedText.substring(0, remain);
+            }
+            builder.append("\n--- 鏂囦欢: ")
+                    .append(file.getOriginalFilename())
+                    .append(" ---\n")
+                    .append(limitedText)
+                    .append('\n');
+            totalLength += limitedText.length();
+        }
+        return builder.toString();
+    }
+
+    private boolean containsImageFile(MultipartFile[] files) {
+        for (MultipartFile file : files) {
+            if (aiFileTextExtractor.isImageFile(file)) {
+                return true;
+            }
+        }
+        return false;
+    }
+
+    private Flux<String> chatWithPurchaseVisionModel(String memoryId, String userPrompt, MultipartFile[] files) {
+        return Flux.create(sink -> {
+            try {
+                List<Content> contents = new ArrayList<>();
+                contents.add(TextContent.from(userPrompt));
+                for (MultipartFile file : files) {
+                    if (!aiFileTextExtractor.isImageFile(file)) {
+                        continue;
+                    }
+                    contents.add(TextContent.from("涓嬮潰杩欏紶鍥剧墖鏂囦欢鍚嶏細" + file.getOriginalFilename()));
+                    contents.add(ImageContent.from(Image.builder()
+                            .base64Data(Base64.getEncoder().encodeToString(file.getBytes()))
+                            .mimeType(resolveImageMimeType(file))
+                            .build()));
+                }
+
+                List<ChatMessage> messages = List.of(
+                        SystemMessage.from("浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝浠庢枃鏈拰鍥剧墖涓瘑鍒噰璐彴璐︺�侀噰璐骇鍝佹槑缁嗐�佷粯娆炬垨閫�璐т俊鎭紝鍙緭鍑哄悎娉� JSON銆�"),
+                        UserMessage.from(contents)
+                );
+                purchaseVisionStreamingChatModel.chat(messages, new StreamingChatResponseHandler() {
+                    @Override
+                    public void onPartialResponse(String partialResponse) {
+                        sink.next(partialResponse);
+                    }
+
+                    @Override
+                    public void onCompleteResponse(ChatResponse completeResponse) {
+                        sink.complete();
+                    }
+
+                    @Override
+                    public void onError(Throwable error) {
+                        sink.error(error);
+                    }
+                });
+            } catch (Exception ex) {
+                sink.next("鍥剧墖鏂囦欢璇诲彇澶辫触锛岃纭鍥剧墖鏍煎紡涓� png銆乯pg銆乯peg銆亀ebp 鎴� bmp锛屼笖澶у皬涓嶈秴杩�10MB");
+                sink.complete();
+            }
+        });
+    }
+
+    private String resolveImageMimeType(MultipartFile file) {
+        String contentType = file.getContentType();
+        if (StringUtils.hasText(contentType) && contentType.startsWith("image/")) {
+            return contentType;
+        }
+        String filename = file.getOriginalFilename();
+        String ext = "";
+        if (StringUtils.hasText(filename) && filename.contains(".")) {
+            ext = filename.substring(filename.lastIndexOf('.') + 1).toLowerCase();
+        }
+        return switch (ext) {
+            case "jpg", "jpeg" -> "image/jpeg";
+            case "webp" -> "image/webp";
+            case "bmp" -> "image/bmp";
+            default -> "image/png";
+        };
+    }
+
+    private String buildPurchaseFileAnalyzePrompt(String message, String fileContent) {
+        return """
+                浣犳槸閲囪喘涓氬姟鏂囦欢鍒嗘瀽鍔╂墜銆傝涓ユ牸鏍规嵁鐢ㄦ埛涓婁紶鐨勫涓枃浠跺拰鐢ㄦ埛瑕佹眰鎻愬彇閲囪喘涓氬姟鏁版嵁銆�
+
+                鐢ㄦ埛瑕佹眰:
+                %s
+
+                杈撳嚭瑕佹眰:
+                1. 鍙緭鍑哄悎娉� JSON锛屼笉瑕� Markdown锛屼笉瑕侀澶栬В閲娿��
+                2. JSON 椤跺眰瀛楁鍥哄畾涓�:
+                   - success: boolean
+                   - businessType: purchase_ledger | payment_registration | purchase_return_order | unknown
+                   - action: confirm_required
+                   - description: 涓枃璇存槑
+                   - confidence: 0鍒�1鐨勫皬鏁�
+                   - missingFields: 缂哄け瀛楁涓枃鍚嶇О鏁扮粍锛岄潰鍚戝鎴峰睍绀猴紝涓嶈杈撳嚭鑻辨枃瀛楁鍚�
+                   - warnings: 椋庨櫓鎻愮ず鏁扮粍
+                   - payload: 寰呭鎴风‘璁ょ殑鏁版嵁锛屽瓧娈靛悕蹇呴』浣跨敤鍚庣 DTO 瀛楁鍚�
+                   - preview: 缁欏鎴风‘璁ょ敤鐨勪腑鏂囨憳瑕佹暟缁�
+                3. 濡傛灉鍙垽鏂负閲囪喘鍙拌处锛宐usinessType 浣跨敤 purchase_ledger锛宲ayload.purchaseLedgers 涓洪噰璐鍗�/閲囪喘鍙拌处鏁扮粍:
+                   - purchaseLedgers: 閲囪喘璁㈠崟/閲囪喘鍙拌处鏁扮粍锛屾瘡鏉¤褰曞瓧娈靛悕蹇呴』涓� PurchaseLedgerDto 淇濇寔涓�鑷�
+                   - 浜у搧鏄庣粏蹇呴』鏀惧湪姣忔潯閲囪喘鍙拌处璁板綍鐨� productData 瀛楁涓紝productData 绫诲瀷涓� List<SalesLedgerProduct>
+                   - 涓嶈浼樺厛浣跨敤 payload 椤跺眰 productData锛涢《灞� productData 浠呬綔涓烘棫鏍煎紡鍏煎
+                   - 鏂囦欢閲岀殑鈥滈噰璐崟鍙封�濆氨鏄�滈噰璐悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� purchaseContractNumber
+                   - 鏂囦欢閲岀殑鈥滈攢鍞崟鍙封�濆氨鏄�滈攢鍞悎鍚屽彿鈥濓紝缁熶竴鏄犲皠涓� salesContractNo
+                   - 鎵�鏈夋棩鏈熷瓧娈靛繀椤讳娇鐢� yyyy-MM-dd锛屼緥濡� 2026-04-30锛涗笉瑕佽緭鍑� 4/30/26銆�2026/4/30銆�2026骞�4鏈�30鏃� 鎴栧甫鏃跺垎绉掔殑鏍煎紡
+                   - 閲囪喘鍙拌处涓嶉渶瑕佸湪 payload 涓紶瀹℃壒浜猴紝涓嶈杈撳嚭 approveUserIds銆乤pproverId
+                   - missingFields 鍙~鍐欎笟鍔″繀濉絾鏃犳硶璇嗗埆鐨勫瓧娈碉紝涓嶈鎶� PurchaseLedgerDto 鐨勬墍鏈夌┖瀛楁閮藉垪涓虹己澶憋紱缂哄け椤瑰繀椤诲啓涓枃锛屼緥濡傗�滀緵搴斿晢鍚嶇О鈥濃�滃惈绋庡崟浠封�濓紝涓嶈鍐� supplierId銆乼axInclusiveUnitPrice
+                   - 閲囪喘鍙拌处涓昏〃蹇呭~瀛楁浠呮寜杩欎簺鍒ゆ柇: purchaseContractNumber銆乻upplierName 鎴� supplierId
+                   - productData 姣忔潯浜у搧蹇呭~瀛楁: productCategory銆乻pecificationModel銆乽nit銆乹uantity銆乼axInclusiveUnitPrice 鎴� taxInclusiveTotalPrice锛涘鏋滃彧鏈夊惈绋庢�讳环鍜屾暟閲忥紝蹇呴』璁$畻 taxInclusiveUnitPrice锛涘鏋滃彧鏈夊惈绋庡崟浠峰拰鏁伴噺锛屽繀椤昏绠� taxInclusiveTotalPrice
+                   - 浜у搧瀛楁鎸夐噰璐鍏ユ帴鍙� PurchaseLedgerProductImportDto 瀵归綈: 閲囪喘鍗曞彿銆佷骇鍝佸ぇ绫汇�佽鏍煎瀷鍙枫�佸崟浣嶃�佹暟閲忋�佺◣鐜囥�佸惈绋庡崟浠枫�佸惈绋庢�讳环銆佸彂绁ㄧ被鍨嬨�佹槸鍚﹁川妫�
+                   - 閲囪喘浜у搧 type 鍥哄畾涓� 2
+                   - purchaseLedgers 姣忔潯璁板綍鍙娇鐢ㄨ繖浜� PurchaseLedgerDto 瀛楁鍚�:
+                     entryDateStart, entryDateEnd, id, purchaseContractNumber, supplierId, supplierName, isWhite, recorderId, recorderName, salesContractNo, salesContractNoId, projectName, entryDate, executionDate, remarks, attachmentMaterials, createdAt, updatedAt, salesLedgerId, hasChildren, Type, productData, tempFileIds, SalesLedgerFiles, phoneNumber, businessPersonId, productId, productModelId, invoiceNumber, invoiceAmount, ticketRegistrationId, contractAmount, receiptPaymentAmount, unReceiptPaymentAmount, type, paymentMethod, approvalStatus, templateName
+                   - productData 姣忔潯浜у搧鍙娇鐢ㄨ繖浜� SalesLedgerProduct 瀛楁鍚�:
+                     productCategory, specificationModel, unit, quantity, taxRate, taxInclusiveUnitPrice, taxInclusiveTotalPrice, taxExclusiveTotalPrice, invoiceType, productId, productModelId, isChecked, type
+                4. 濡傛灉鍙垽鏂负浠樻鐧昏锛宐usinessType 浣跨敤 payment_registration锛宲ayload.records 涓轰粯娆剧櫥璁版暟缁勶紝瀛楁灏介噺鍖呭惈 purchaseLedgerId銆乻alesLedgerProductId銆乧urrentPaymentAmount銆乸aymentMethod銆乸aymentDate銆�
+                5. 濡傛灉鍙垽鏂负閲囪喘閫�璐э紝businessType 浣跨敤 purchase_return_order锛宲ayload 鎸� PurchaseReturnOrderDto 缁勭粐锛屾槑缁嗘斁 purchaseReturnOrderProductsDtos銆�
+                6. 缂哄皯涓氬姟澶勭悊蹇呴』瀛楁鏃讹紝涓嶈缂栭�� ID锛屾妸瀛楁鏀惧叆 missingFields锛屽苟浠嶈繑鍥炲彲纭鐨勮崏绋挎暟鎹��
+                7. 鎵�鏈変腑鏂囧唴瀹圭洿鎺ヤ繚鐣欙紝涓嶈杞箟鎴� Unicode銆�
+
+                鏂囦欢鍐呭:
+                %s
+                """.formatted(message, fileContent);
+    }
+
+    private AjaxResult processPurchaseLedger(Map<String, Object> payload) throws Exception {
+        if (payload.containsKey("purchaseLedgers")) {
+            return processPurchaseLedgerBatch(payload);
+        }
+
+        Map<String, Object> normalizedPayload = normalizePurchaseLedgerMap(payload);
+        PurchaseLedgerDto dto = objectMapper.convertValue(normalizedPayload, PurchaseLedgerDto.class);
+        AjaxResult ledgerResult = validatePurchaseLedger(dto, 0);
+        if (ledgerResult != null) {
+            return ledgerResult;
+        }
+        AjaxResult supplierResult = fillSupplierIdByName(dto);
+        if (supplierResult != null) {
+            return supplierResult;
+        }
+        AjaxResult productResult = validatePurchaseProducts(dto.getProductData(), 0);
+        if (productResult != null) {
+            return productResult;
+        }
+        int result = purchaseLedgerService.addOrEditPurchase(dto);
+        return AjaxResult.success("閲囪喘鍙拌处宸插鐞�", result);
+    }
+
+    private AjaxResult processPurchaseLedgerBatch(Map<String, Object> payload) throws Exception {
+        List<Map<String, Object>> purchaseLedgers = toMapList(payload.get("purchaseLedgers"));
+        if (purchaseLedgers.isEmpty()) {
+            return AjaxResult.error("purchaseLedgers涓嶈兘涓虹┖");
+        }
+
+        List<Map<String, Object>> topLevelProductData = toMapList(payload.get("productData"));
+        List<Map<String, Object>> results = new ArrayList<>();
+        for (int i = 0; i < purchaseLedgers.size(); i++) {
+            Map<String, Object> ledgerMap = normalizePurchaseLedgerMap(purchaseLedgers.get(i));
+            PurchaseLedgerDto dto = objectMapper.convertValue(ledgerMap, PurchaseLedgerDto.class);
+            AjaxResult ledgerResult = validatePurchaseLedger(dto, i);
+            if (ledgerResult != null) {
+                return ledgerResult;
+            }
+            AjaxResult supplierResult = fillSupplierIdByName(dto);
+            if (supplierResult != null) {
+                return supplierResult;
+            }
+
+            List<SalesLedgerProduct> products = dto.getProductData();
+            if (products == null || products.isEmpty()) {
+                products = matchProductsForLedger(ledgerMap, dto, topLevelProductData, purchaseLedgers.size() == 1);
+                dto.setProductData(products);
+            }
+            AjaxResult productResult = validatePurchaseProducts(products, i);
+            if (productResult != null) {
+                return productResult;
+            }
+            int result = purchaseLedgerService.addOrEditPurchase(dto);
+
+            Map<String, Object> item = new LinkedHashMap<>();
+            item.put("index", i);
+            item.put("purchaseContractNumber", dto.getPurchaseContractNumber());
+            item.put("supplierId", dto.getSupplierId());
+            item.put("supplierName", dto.getSupplierName());
+            item.put("productCount", products.size());
+            item.put("result", result);
+            results.add(item);
+        }
+        return AjaxResult.success("閲囪喘鍙拌处宸叉壒閲忓鐞�", results);
+    }
+
+    private List<SalesLedgerProduct> matchProductsForLedger(Map<String, Object> ledgerMap,
+                                                            PurchaseLedgerDto dto,
+                                                            List<Map<String, Object>> productData,
+                                                            boolean onlyOneLedger) {
+        List<SalesLedgerProduct> products = new ArrayList<>();
+        for (Map<String, Object> productMap : productData) {
+            if (onlyOneLedger || productBelongsToLedger(productMap, ledgerMap, dto)) {
+                products.add(objectMapper.convertValue(normalizeSalesLedgerProductMap(productMap), SalesLedgerProduct.class));
+            }
+        }
+        return products;
+    }
+
+    private boolean productBelongsToLedger(Map<String, Object> productMap, Map<String, Object> ledgerMap, PurchaseLedgerDto dto) {
+        Long productPurchaseLedgerId = longValue(productMap, "purchaseLedgerId", "purchaseId", "閲囪喘璁㈠崟id", "閲囪喘鍙拌处id");
+        if (productPurchaseLedgerId != null && dto.getId() != null && productPurchaseLedgerId.equals(dto.getId())) {
+            return true;
+        }
+
+        Long productSalesLedgerId = longValue(productMap, "salesLedgerId");
+        if (productSalesLedgerId != null && dto.getId() != null && productSalesLedgerId.equals(dto.getId())) {
+            return true;
+        }
+
+        String productContractNo = stringValue(productMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+        if (StringUtils.hasText(productContractNo)
+                && StringUtils.hasText(dto.getPurchaseContractNumber())
+                && productContractNo.trim().equals(dto.getPurchaseContractNumber().trim())) {
+            return true;
+        }
+
+        String ledgerContractNo = stringValue(ledgerMap, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+        if (StringUtils.hasText(productContractNo)
+                && StringUtils.hasText(ledgerContractNo)
+                && productContractNo.trim().equals(ledgerContractNo.trim())) {
+            return true;
+        }
+
+        String productSalesContractNo = stringValue(productMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+        if (StringUtils.hasText(productSalesContractNo)
+                && StringUtils.hasText(dto.getSalesContractNo())
+                && productSalesContractNo.trim().equals(dto.getSalesContractNo().trim())) {
+            return true;
+        }
+
+        String ledgerSalesContractNo = stringValue(ledgerMap, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+        if (StringUtils.hasText(productSalesContractNo)
+                && StringUtils.hasText(ledgerSalesContractNo)
+                && productSalesContractNo.trim().equals(ledgerSalesContractNo.trim())) {
+            return true;
+        }
+
+        String productSupplierName = stringValue(productMap, "supplierName", "渚涘簲鍟嗗悕绉�");
+        return StringUtils.hasText(productSupplierName)
+                && StringUtils.hasText(dto.getSupplierName())
+                && productSupplierName.trim().equals(dto.getSupplierName().trim());
+    }
+
+    private Map<String, Object> normalizePurchaseLedgerMap(Map<String, Object> source) {
+        Map<String, Object> target = new LinkedHashMap<>();
+        copyPurchaseLedgerDtoFields(source, target);
+        putDtoFieldIfPresent(source, target, "entryDateStart", "褰曞叆寮�濮嬫棩鏈�", "褰曞叆鏃ユ湡寮�濮�");
+        putDtoFieldIfPresent(source, target, "entryDateEnd", "褰曞叆缁撴潫鏃ユ湡", "褰曞叆鏃ユ湡缁撴潫");
+        putDtoFieldIfPresent(source, target, "id", "閲囪喘鍙拌处id", "閲囪喘璁㈠崟id", "涓婚敭");
+        putDtoFieldIfPresent(source, target, "purchaseContractNumber", "purchaseContractNo", "閲囪喘鍚堝悓鍙�", "閲囪喘鍗曞彿", "閲囪喘璁㈠崟鍙�");
+        putDtoFieldIfPresent(source, target, "supplierId", "渚涘簲鍟唅d", "渚涘簲鍟咺D", "渚涘簲鍟嗗悕绉癷d", "渚涘簲鍟嗗悕绉癐D");
+        putDtoFieldIfPresent(source, target, "supplierName", "渚涘簲鍟�", "渚涘簲鍟嗗悕绉�");
+        putDtoFieldIfPresent(source, target, "isWhite", "鏄惁鐧藉悕鍗�");
+        putDtoFieldIfPresent(source, target, "recorderId", "褰曞叆浜篿d", "褰曞叆浜篒D", "褰曞叆浜哄鍚峣d", "褰曞叆浜哄鍚岻D");
+        putDtoFieldIfPresent(source, target, "recorderName", "褰曞叆浜�", "褰曞叆浜哄鍚�");
+        putDtoFieldIfPresent(source, target, "salesContractNo", "salesContractNumber", "閿�鍞悎鍚屽彿", "閿�鍞崟鍙�", "閿�鍞鍗曞彿");
+        putDtoFieldIfPresent(source, target, "salesContractNoId", "閿�鍞悎鍚屽彿id", "閿�鍞悎鍚屽彿ID", "閿�鍞崟鍙穒d", "閿�鍞崟鍙稩D");
+        putDtoFieldIfPresent(source, target, "projectName", "椤圭洰", "椤圭洰鍚嶇О");
+        putDtoFieldIfPresent(source, target, "entryDate", "褰曞叆鏃ユ湡");
+        putDtoFieldIfPresent(source, target, "executionDate", "绛捐鏃ユ湡", "鍚堝悓绛捐鏃ユ湡");
+        putDtoFieldIfPresent(source, target, "remarks", "澶囨敞", "璇存槑");
+        putDtoFieldIfPresent(source, target, "attachmentMaterials", "闄勪欢鏉愭枡", "闄勪欢鏉愭枡璺緞鎴栧悕绉�");
+        putDtoFieldIfPresent(source, target, "createdAt", "鍒涘缓鏃堕棿", "璁板綍鍒涘缓鏃堕棿");
+        putDtoFieldIfPresent(source, target, "updatedAt", "鏇存柊鏃堕棿", "璁板綍鏈�鍚庢洿鏂版椂闂�");
+        putDtoFieldIfPresent(source, target, "salesLedgerId", "閿�鍞彴璐d", "閿�鍞彴璐D", "鍏宠仈閿�鍞彴璐︿富琛ㄤ富閿�");
+        putDtoFieldIfPresent(source, target, "hasChildren", "鏄惁鏈夊瓙绾�", "鏄惁鏈夋槑缁�");
+        putDtoFieldIfPresent(source, target, "Type", "鍙拌处绫诲瀷", "涓氬姟绫诲瀷");
+        putDtoFieldIfPresent(source, target, "productData", "products", "浜у搧鏄庣粏", "閲囪喘浜у搧鏄庣粏");
+        putDtoFieldIfPresent(source, target, "tempFileIds", "涓存椂鏂囦欢id", "涓存椂鏂囦欢ID", "涓存椂鏂囦欢ids");
+        putDtoFieldIfPresent(source, target, "SalesLedgerFiles", "闄勪欢鍒楄〃", "閿�鍞彴璐﹂檮浠�");
+        putDtoFieldIfPresent(source, target, "phoneNumber", "涓氬姟鍛樻墜鏈哄彿", "鎵嬫満鍙�");
+        putDtoFieldIfPresent(source, target, "businessPersonId", "涓氬姟鍛榠d", "涓氬姟鍛業D");
+        putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
+        putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID");
+        putDtoFieldIfPresent(source, target, "invoiceNumber", "鍙戠エ鍙�", "鍙戠エ鍙风爜");
+        putDtoFieldIfPresent(source, target, "invoiceAmount", "鍙戠エ閲戦", "鍙戠エ閲戦锛堝厓锛�");
+        putDtoFieldIfPresent(source, target, "ticketRegistrationId", "鏉ョエ鐧昏id", "鏉ョエ鐧昏ID");
+        putDtoFieldIfPresent(source, target, "contractAmount", "鍚堝悓閲戦", "鍚堝悓閲戦锛堜骇鍝佸惈绋庢�讳环锛�");
+        putDtoFieldIfPresent(source, target, "receiptPaymentAmount", "鏉ョエ閲戦", "宸叉潵绁ㄩ噾棰�", "宸叉潵绁ㄩ噾棰�(鍏�)");
+        putDtoFieldIfPresent(source, target, "unReceiptPaymentAmount", "鏈潵绁ㄩ噾棰�", "鏈潵绁ㄩ噾棰�(鍏�)");
+        putDtoFieldIfPresent(source, target, "type", "鏂囦欢绫诲瀷");
+        putDtoFieldIfPresent(source, target, "paymentMethod", "浠樻鏂瑰紡");
+        putDtoFieldIfPresent(source, target, "approvalStatus", "瀹℃壒鐘舵��");
+        putDtoFieldIfPresent(source, target, "templateName", "妯℃澘鍚嶇О");
+        target.remove("approveUserIds");
+        target.remove("approverId");
+        normalizeNestedProductData(target);
+        attachImportStyleProductData(source, target);
+        if (target.get("type") == null) {
+            target.put("type", 2);
+        }
+        target.putIfAbsent("entryDate", LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE));
+        normalizePurchaseLedgerDateFields(target);
+        return target;
+    }
+
+    private void attachImportStyleProductData(Map<String, Object> source, Map<String, Object> target) {
+        if (target.get("productData") != null) {
+            return;
+        }
+        Map<String, Object> productMap = normalizeSalesLedgerProductMap(source);
+        if (hasImportStyleProductData(productMap)) {
+            target.put("productData", List.of(productMap));
+        }
+    }
+
+    private boolean hasImportStyleProductData(Map<String, Object> productMap) {
+        return hasMapText(productMap, "productCategory")
+                || hasMapText(productMap, "specificationModel")
+                || productMap.get("quantity") != null
+                || productMap.get("taxInclusiveUnitPrice") != null
+                || productMap.get("taxInclusiveTotalPrice") != null;
+    }
+
+    private boolean hasMapText(Map<String, Object> map, String key) {
+        Object value = map.get(key);
+        return value != null && StringUtils.hasText(String.valueOf(value));
+    }
+
+    private void normalizeNestedProductData(Map<String, Object> target) {
+        Object productDataValue = target.get("productData");
+        if (productDataValue == null) {
+            return;
+        }
+        List<Map<String, Object>> productMaps = toMapList(productDataValue);
+        List<Map<String, Object>> normalizedProducts = new ArrayList<>();
+        for (Map<String, Object> productMap : productMaps) {
+            normalizedProducts.add(normalizeSalesLedgerProductMap(productMap));
+        }
+        target.put("productData", normalizedProducts);
+    }
+
+    private Map<String, Object> normalizeSalesLedgerProductMap(Map<String, Object> source) {
+        Map<String, Object> target = new LinkedHashMap<>();
+        copySalesLedgerProductFields(source, target);
+        putDtoFieldIfPresent(source, target, "productCategory", "浜у搧澶х被", "浜у搧鍚嶇О", "浜у搧", "鍝佸悕", "鐗╂枡鍚嶇О");
+        putDtoFieldIfPresent(source, target, "specificationModel", "瑙勬牸鍨嬪彿", "鍨嬪彿", "瑙勬牸", "浜у搧瑙勬牸");
+        putDtoFieldIfPresent(source, target, "unit", "鍗曚綅");
+        putDtoFieldIfPresent(source, target, "quantity", "鏁伴噺", "閲囪喘鏁伴噺");
+        putDtoFieldIfPresent(source, target, "taxRate", "绋庣巼");
+        putDtoFieldIfPresent(source, target, "taxInclusiveUnitPrice", "鍚◣鍗曚环", "鍗曚环", "閲囪喘鍗曚环", "鍚◣浠锋牸");
+        putDtoFieldIfPresent(source, target, "taxInclusiveTotalPrice", "鍚◣鎬讳环", "鎬讳环", "閲囪喘閲戦", "閲戦", "鍚堝悓閲戦");
+        putDtoFieldIfPresent(source, target, "taxExclusiveTotalPrice", "涓嶅惈绋庢�讳环");
+        putDtoFieldIfPresent(source, target, "invoiceType", "鍙戠エ绫诲瀷", "鍙戠エ绫诲埆");
+        putDtoFieldIfPresent(source, target, "productId", "浜у搧id", "浜у搧ID");
+        putDtoFieldIfPresent(source, target, "productModelId", "浜у搧瑙勬牸id", "浜у搧瑙勬牸ID", "鍨嬪彿id", "鍨嬪彿ID");
+        putDtoFieldIfPresent(source, target, "isChecked", "鏄惁璐ㄦ", "鏄惁璐ㄦ楠�", "璐ㄦ");
+        putDtoFieldIfPresent(source, target, "type", "鍙拌处绫诲瀷");
+        normalizeProductAmounts(target);
+        target.putIfAbsent("type", 2);
+        return target;
+    }
+
+    private void copySalesLedgerProductFields(Map<String, Object> source, Map<String, Object> target) {
+        String[] productFields = {
+                "id", "salesLedgerId", "warnNum", "productCategory", "specificationModel", "unit",
+                "speculativeTradingName", "quantity", "minStock", "taxRate", "taxInclusiveUnitPrice",
+                "taxInclusiveTotalPrice", "taxExclusiveTotalPrice", "invoiceType", "type", "ticketsNum",
+                "ticketsAmount", "futureTickets", "futureTicketsAmount", "invoiceNum", "noInvoiceNum",
+                "invoiceAmount", "noInvoiceAmount", "productId", "productModelId", "register", "registerDate",
+                "approveStatus", "pendingInvoiceTotal", "invoiceTotal", "pendingTicketsTotal", "ticketsTotal",
+                "isChecked", "isProduction"
+        };
+        for (String field : productFields) {
+            if (source.containsKey(field)) {
+                target.put(field, source.get(field));
+            }
+        }
+    }
+
+    private void normalizeProductAmounts(Map<String, Object> target) {
+        BigDecimal quantity = decimalValue(target.get("quantity"));
+        BigDecimal unitPrice = decimalValue(target.get("taxInclusiveUnitPrice"));
+        BigDecimal totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
+        if (unitPrice == null && totalPrice != null && quantity != null && quantity.compareTo(BigDecimal.ZERO) != 0) {
+            target.put("taxInclusiveUnitPrice", totalPrice.divide(quantity, 6, RoundingMode.HALF_UP));
+        }
+        if (totalPrice == null && unitPrice != null && quantity != null) {
+            target.put("taxInclusiveTotalPrice", unitPrice.multiply(quantity));
+        }
+        BigDecimal taxRate = decimalValue(target.get("taxRate"));
+        totalPrice = decimalValue(target.get("taxInclusiveTotalPrice"));
+        if (target.get("taxExclusiveTotalPrice") == null && totalPrice != null && taxRate != null) {
+            BigDecimal divisor = BigDecimal.ONE.add(taxRate.divide(new BigDecimal("100"), 6, RoundingMode.HALF_UP));
+            target.put("taxExclusiveTotalPrice", totalPrice.divide(divisor, 2, RoundingMode.HALF_UP));
+        }
+    }
+
+    private AjaxResult validatePurchaseProducts(List<SalesLedgerProduct> products, int ledgerIndex) {
+        if (products == null || products.isEmpty()) {
+            return null;
+        }
+        for (int i = 0; i < products.size(); i++) {
+            SalesLedgerProduct product = products.get(i);
+            String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐︾殑绗�" + (i + 1) + "鏉′骇鍝�";
+            if (!StringUtils.hasText(product.getProductCategory())) {
+                return AjaxResult.error(prefix + "缂哄皯浜у搧鍚嶇О锛岃琛ュ厖鍚庡啀纭");
+            }
+            if (!StringUtils.hasText(product.getSpecificationModel())) {
+                return AjaxResult.error(prefix + "缂哄皯瑙勬牸鍨嬪彿锛岃琛ュ厖鍚庡啀纭");
+            }
+            if (!StringUtils.hasText(product.getUnit())) {
+                return AjaxResult.error(prefix + "缂哄皯鍗曚綅锛岃琛ュ厖鍚庡啀纭");
+            }
+            if (product.getQuantity() == null) {
+                return AjaxResult.error(prefix + "缂哄皯鏁伴噺");
+            }
+            if (product.getTaxInclusiveUnitPrice() == null) {
+                return AjaxResult.error(prefix + "缂哄皯鍚◣鍗曚环锛岃琛ュ厖鍚庡啀纭");
+            }
+            if (product.getTaxInclusiveTotalPrice() == null) {
+                return AjaxResult.error(prefix + "缂哄皯鍚◣鎬讳环锛岃琛ュ厖鍚庡啀纭");
+            }
+        }
+        return null;
+    }
+
+    private AjaxResult validatePurchaseLedger(PurchaseLedgerDto dto, int ledgerIndex) {
+        String prefix = "绗�" + (ledgerIndex + 1) + "涓噰璐彴璐�";
+        if (!StringUtils.hasText(dto.getPurchaseContractNumber())) {
+            return AjaxResult.error(prefix + "缂哄皯閲囪喘鍚堝悓鍙凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
+        }
+        if (dto.getSupplierId() == null && !StringUtils.hasText(dto.getSupplierName())) {
+            return AjaxResult.error(prefix + "缂哄皯渚涘簲鍟嗗悕绉帮紝璇疯ˉ鍏呭悗鍐嶇‘璁�");
+        }
+        return null;
+    }
+
+    private void normalizePurchaseLedgerDateFields(Map<String, Object> target) {
+        normalizeDateField(target, "entryDate");
+        normalizeDateField(target, "executionDate");
+        normalizeDateField(target, "createdAt");
+        normalizeDateField(target, "updatedAt");
+    }
+
+    private void normalizeDateField(Map<String, Object> target, String fieldName) {
+        Object value = target.get(fieldName);
+        if (value == null) {
+            return;
+        }
+        String normalizedDate = normalizeDateValue(value);
+        if (StringUtils.hasText(normalizedDate)) {
+            target.put(fieldName, normalizedDate);
+        }
+    }
+
+    private String normalizeDateValue(Object value) {
+        if (value instanceof Date date) {
+            return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate().format(DateTimeFormatter.ISO_LOCAL_DATE);
+        }
+        if (value instanceof Number number) {
+            return LocalDate.of(1899, 12, 30)
+                    .plusDays(number.longValue())
+                    .format(DateTimeFormatter.ISO_LOCAL_DATE);
+        }
+
+        String text = String.valueOf(value).trim();
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        if (text.length() >= 10 && text.charAt(4) == '-' && text.charAt(7) == '-') {
+            return text.substring(0, 10);
+        }
+
+        String normalizedText = text.replace("骞�", "-")
+                .replace("鏈�", "-")
+                .replace("鏃�", "")
+                .replace(".", "-")
+                .replace("/", "-")
+                .trim();
+        DateTimeFormatter[] formatters = {
+                DateTimeFormatter.ofPattern("yyyy-M-d"),
+                DateTimeFormatter.ofPattern("M-d-yyyy"),
+                DateTimeFormatter.ofPattern("M-d-yy")
+        };
+        for (DateTimeFormatter formatter : formatters) {
+            try {
+                return LocalDate.parse(normalizedText, formatter).format(DateTimeFormatter.ISO_LOCAL_DATE);
+            } catch (DateTimeParseException ignored) {
+                // Try the next supported input pattern.
+            }
+        }
+        return text;
+    }
+
+    private void copyPurchaseLedgerDtoFields(Map<String, Object> source, Map<String, Object> target) {
+        String[] dtoFields = {
+                "entryDateStart", "entryDateEnd", "id", "purchaseContractNumber",
+                "supplierId", "supplierName", "isWhite", "recorderId", "recorderName", "salesContractNo",
+                "salesContractNoId", "projectName", "entryDate", "executionDate", "remarks", "attachmentMaterials",
+                "createdAt", "updatedAt", "salesLedgerId", "hasChildren", "Type", "productData", "tempFileIds",
+                "SalesLedgerFiles", "phoneNumber", "businessPersonId", "productId", "productModelId", "invoiceNumber",
+                "invoiceAmount", "ticketRegistrationId", "contractAmount", "receiptPaymentAmount",
+                "unReceiptPaymentAmount", "type", "paymentMethod", "approvalStatus", "templateName"
+        };
+        for (String field : dtoFields) {
+            if (source.containsKey(field)) {
+                target.put(field, source.get(field));
+            }
+        }
+    }
+
+    private void putDtoFieldIfPresent(Map<String, Object> source, Map<String, Object> target, String dtoField, String... aliases) {
+        if (target.containsKey(dtoField) && target.get(dtoField) != null) {
+            return;
+        }
+        for (String alias : aliases) {
+            Object value = source.get(alias);
+            if (value != null && StringUtils.hasText(String.valueOf(value))) {
+                target.put(dtoField, value);
+                return;
+            }
+        }
+    }
+
+    private List<Map<String, Object>> toMapList(Object value) {
+        if (value == null) {
+            return List.of();
+        }
+        return objectMapper.convertValue(value, new TypeReference<List<Map<String, Object>>>() {
+        });
+    }
+
+    private String stringValue(Map<String, Object> map, String... keys) {
+        for (String key : keys) {
+            Object value = map.get(key);
+            if (value != null && StringUtils.hasText(String.valueOf(value))) {
+                return String.valueOf(value);
+            }
+        }
+        return null;
+    }
+
+    private Long longValue(Map<String, Object> map, String... keys) {
+        String value = stringValue(map, keys);
+        if (!StringUtils.hasText(value)) {
+            return null;
+        }
+        try {
+            return Long.parseLong(value.trim());
+        } catch (NumberFormatException ignored) {
+            return null;
+        }
+    }
+
+    private BigDecimal decimalValue(Object value) {
+        if (value == null) {
+            return null;
+        }
+        if (value instanceof BigDecimal decimal) {
+            return decimal;
+        }
+        if (value instanceof Number number) {
+            return new BigDecimal(String.valueOf(number));
+        }
+        String text = String.valueOf(value)
+                .replace(",", "")
+                .replace("锛�", "")
+                .replace("鍏�", "")
+                .replace("锟�", "")
+                .trim();
+        if (!StringUtils.hasText(text)) {
+            return null;
+        }
+        try {
+            return new BigDecimal(text);
+        } catch (NumberFormatException ignored) {
+            return null;
+        }
+    }
+
+    private String toCustomerMessage(Exception ex) {
+        String message = ex.getMessage();
+        if (!StringUtils.hasText(message)) {
+            return "澶勭悊澶辫触锛岃妫�鏌ョ‘璁ゆ暟鎹悗閲嶈瘯";
+        }
+        if (message.contains("tax_inclusive_unit_price")) {
+            return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庡崟浠凤紝璇疯ˉ鍏呭悗鍐嶇‘璁�";
+        }
+        if (message.contains("tax_inclusive_total_price")) {
+            return "澶勭悊澶辫触锛氫骇鍝佹槑缁嗙己灏戝惈绋庢�讳环锛岃琛ュ厖鍚庡啀纭";
+        }
+        if (message.contains("entryDate")) {
+            return "澶勭悊澶辫触锛氬綍鍏ユ棩鏈熸牸寮忎笉姝g‘锛岃浣跨敤 yyyy-MM-dd锛屼緥濡� 2026-04-30";
+        }
+        if (message.contains("supplier")) {
+            return "澶勭悊澶辫触锛氫緵搴斿晢淇℃伅涓嶅畬鏁达紝璇风‘璁や緵搴斿晢鍚嶇О鎴栦緵搴斿晢ID";
+        }
+        if (message.contains("SQL") || message.contains("java.") || message.contains("Exception")) {
+            return "澶勭悊澶辫触锛氱‘璁ゆ暟鎹笉瀹屾暣鎴栨牸寮忎笉姝g‘锛岃妫�鏌ュ繀濉瓧娈靛悗閲嶈瘯";
+        }
+        return "澶勭悊澶辫触锛�" + message;
+    }
+
+    private AjaxResult fillSupplierIdByName(PurchaseLedgerDto dto) {
+        if (dto.getSupplierId() != null) {
+            return null;
+        }
+        if (!StringUtils.hasText(dto.getSupplierName())) {
+            return AjaxResult.error("渚涘簲鍟咺D涓嶈兘涓虹┖锛涙湭璇嗗埆鍒颁緵搴斿晢鍚嶇О锛屾棤娉曡嚜鍔ㄥ尮閰嶄緵搴斿晢ID");
+        }
+
+        SupplierManage supplier = supplierManageMapper.selectOne(new LambdaQueryWrapper<SupplierManage>()
+                .eq(SupplierManage::getSupplierName, dto.getSupplierName().trim())
+                .last("limit 1"));
+        if (supplier == null) {
+            return AjaxResult.error("鏈壘鍒颁緵搴斿晢锛�" + dto.getSupplierName() + "锛岃鍏堢淮鎶や緵搴斿晢鎴栨墜鍔ㄩ�夋嫨渚涘簲鍟咺D");
+        }
+        dto.setSupplierId(supplier.getId());
+        return null;
+    }
+
+    private AjaxResult processPaymentRegistration(Map<String, Object> payload) {
+        Object recordsValue = payload.get("records");
+        List<PaymentRegistration> records;
+        if (recordsValue == null) {
+            records = Collections.singletonList(objectMapper.convertValue(payload, PaymentRegistration.class));
+        } else {
+            records = objectMapper.convertValue(recordsValue, new TypeReference<List<PaymentRegistration>>() {
+            });
+        }
+        int result = paymentRegistrationService.insertPaymentRegistration(records);
+        return AjaxResult.success("浠樻鐧昏宸插鐞�", result);
+    }
+
+    private AjaxResult processPurchaseReturnOrder(Map<String, Object> payload) {
+        PurchaseReturnOrderDto dto = objectMapper.convertValue(payload, PurchaseReturnOrderDto.class);
+        Boolean result = purchaseReturnOrdersService.add(dto);
+        return AjaxResult.success("閲囪喘閫�璐у崟宸插鐞�", result);
+    }
 }
diff --git a/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
index 82cf1eb..6e37451 100644
--- a/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
+++ b/src/main/java/com/ruoyi/ai/service/AiFileTextExtractor.java
@@ -44,7 +44,17 @@
         if ("xls".equals(ext)) {
             return extractXls(bytes);
         }
+        if (isImage(ext)) {
+            return "鍥剧墖鏂囦欢锛�" + filename + "锛屽凡涓婁紶锛岃缁撳悎鍥剧墖鍐呭璇嗗埆閲囪喘鍗曟嵁銆佽〃鏍煎拰浜у搧鏄庣粏銆�";
+        }
         throw new IllegalArgumentException("鏆備笉鏀寔璇ユ枃浠剁被鍨�: " + ext);
+    }
+
+    public boolean isImageFile(MultipartFile file) {
+        if (file == null) {
+            return false;
+        }
+        return isImage(getExtension(file.getOriginalFilename()));
     }
 
     private String extractDocx(byte[] bytes) throws IOException {
@@ -114,4 +124,8 @@
                 "txt", "md", "markdown", "json", "xml", "yaml", "yml", "csv", "log", "properties",
                 "java", "js", "ts", "vue", "html", "css", "sql", "py", "go", "sh", "bat");
     }
+
+    private boolean isImage(String ext) {
+        return StringUtils.inStringIgnoreCase(ext, "png", "jpg", "jpeg", "webp", "bmp");
+    }
 }
diff --git a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
index 8215ce8..233c459 100644
--- a/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/ApproveTodoTools.java
@@ -70,36 +70,54 @@
         this.aiSessionUserContext = aiSessionUserContext;
     }
 
-    @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ褰撳墠鐧诲綍浜虹浉鍏崇殑瀹℃壒寰呭姙锛屼紭鍏堣繑鍥炶嚜宸卞緟澶勭悊鐨勫鎵癸紝鏀寔鎸夌姸鎬併�佺被鍨嬨�佸叧閿瓧杩囨护銆�")
+    @Tool(name = "鏌ヨ瀹℃壒寰呭姙鍒楄〃", value = "鏌ヨ褰撳墠鐧诲綍浜虹浉鍏崇殑瀹℃壒寰呭姙锛屼紭鍏堣繑鍥炶嚜宸卞緟澶勭悊鐨勫鎵癸紝鏀寔鎸夌姸鎬併�佺被鍨嬨�佸叧閿瓧鍜岃寖鍥磋繃婊ゃ��")
     public String listTodos(@ToolMemoryId String memoryId,
                             @P(value = "瀹℃壒鐘舵�侊紝鍙�夊�硷細all銆乸ending銆乸rocessing銆乤pproved銆乺ejected銆乺esubmitted", required = false) String status,
                             @P(value = "瀹℃壒绫诲瀷缂栧彿锛屽彲涓嶄紶", required = false) Integer approveType,
                             @P(value = "鍏抽敭瀛楋紝鍙尮閰嶆祦绋嬬紪鍙枫�佹爣棰樸�佺敵璇蜂汉銆佸綋鍓嶅鎵逛汉", required = false) String keyword,
-                            @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit) {
+                            @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�20", required = false) Integer limit,
+                            @P(value = "鏌ヨ鑼冨洿锛屽彲閫夊�硷細related銆乤pplicant銆乤pprover锛況elated 琛ㄧず褰撳墠鐢ㄦ埛鐩稿叧锛宎pplicant 琛ㄧず鎴戝彂璧风殑锛宎pprover 琛ㄧず寰呮垜澶勭悊鐨�", required = false) String scope) {
 
         LoginUser loginUser = currentLoginUser(memoryId);
         Long userId = loginUser.getUserId();
         Integer statusCode = parseStatus(status);
+        String normalizedScope = normalizeScope(scope);
 
         LambdaQueryWrapper<ApproveProcess> wrapper = new LambdaQueryWrapper<>();
-        wrapper.eq(ApproveProcess::getApproveDelete, 0)
-                .ne(ApproveProcess::getApproveStatus, 2);
+        wrapper.eq(ApproveProcess::getApproveDelete, 0);
+        if (statusCode == null) {
+            wrapper.ne(ApproveProcess::getApproveStatus, 2);
+        }
 
         if (approveType != null) {
             wrapper.eq(ApproveProcess::getApproveType, approveType);
         }
-//        if (StringUtils.hasText(keyword)) {
-//            wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
-//                    .or().like(ApproveProcess::getApproveReason, keyword)
-//                    .or().like(ApproveProcess::getApproveUserName, keyword)
-//                    .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
-//        }
-        if (statusCode != null && (statusCode == 0 || statusCode == 1)) {
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(ApproveProcess::getApproveId, keyword)
+                    .or().like(ApproveProcess::getApproveReason, keyword)
+                    .or().like(ApproveProcess::getApproveUserName, keyword)
+                    .or().like(ApproveProcess::getApproveUserCurrentName, keyword));
+        }
+        if ("applicant".equals(normalizedScope)) {
+            wrapper.eq(ApproveProcess::getApproveUser, userId);
+            if (statusCode != null) {
+                wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
+            }
+        } else if ("approver".equals(normalizedScope)) {
             wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
+            if (statusCode != null) {
+                wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
+            }
+        } else if (statusCode != null && (statusCode == 0 || statusCode == 1)) {
+            wrapper.eq(ApproveProcess::getApproveUserCurrentId, userId);
+            wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
         } else {
             wrapper.and(w -> w.eq(ApproveProcess::getApproveUser, userId)
                     .or().eq(ApproveProcess::getApproveUserCurrentId, userId)
                     .or().apply("FIND_IN_SET({0}, approve_user_ids)", userId));
+            if (statusCode != null) {
+                wrapper.eq(ApproveProcess::getApproveStatus, statusCode);
+            }
         }
 
         wrapper.orderByDesc(ApproveProcess::getCreateTime)
@@ -137,7 +155,8 @@
                         "count", items.size(),
                         "statusFilter", StringUtils.hasText(status) ? status : "all",
                         "approveType", approveType == null ? "" : approveType,
-                        "keyword", keyword == null ? "" : keyword
+                        "keyword", keyword == null ? "" : keyword,
+                        "scope", normalizedScope
                 ),
                 Map.of("columns", todoColumns(), "items", items),
                 Map.of());
@@ -638,6 +657,17 @@
         };
     }
 
+    private String normalizeScope(String scope) {
+        if (!StringUtils.hasText(scope)) {
+            return "related";
+        }
+        return switch (scope.trim().toLowerCase()) {
+            case "applicant", "mine", "created", "initiated" -> "applicant";
+            case "approver", "handler", "todo", "pending" -> "approver";
+            default -> "related";
+        };
+    }
+
     private String approveStatusName(Integer status) {
         if (status == null) {
             return "鏈煡";
diff --git a/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
index 92b739f..17b6868 100644
--- a/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
+++ b/src/main/java/com/ruoyi/ai/tools/PurchaseAgentTools.java
@@ -13,6 +13,12 @@
 import com.ruoyi.purchase.pojo.PaymentRegistration;
 import com.ruoyi.purchase.pojo.PurchaseLedger;
 import com.ruoyi.purchase.pojo.PurchaseReturnOrders;
+import com.ruoyi.procurementrecord.mapper.InboundManagementMapper;
+import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
+import com.ruoyi.procurementrecord.pojo.InboundManagement;
+import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
+import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
 import dev.langchain4j.agent.tool.P;
 import dev.langchain4j.agent.tool.Tool;
 import dev.langchain4j.agent.tool.ToolMemoryId;
@@ -29,6 +35,7 @@
 import java.util.List;
 import java.util.Map;
 import java.util.Objects;
+import java.util.Comparator;
 import java.util.stream.Collectors;
 
 @Component
@@ -42,17 +49,26 @@
     private final PaymentRegistrationMapper paymentRegistrationMapper;
     private final InvoicePurchaseMapper invoicePurchaseMapper;
     private final PurchaseReturnOrdersMapper purchaseReturnOrdersMapper;
+    private final SalesLedgerProductMapper salesLedgerProductMapper;
+    private final ProcurementRecordMapper procurementRecordMapper;
+    private final InboundManagementMapper inboundManagementMapper;
     private final AiSessionUserContext aiSessionUserContext;
 
     public PurchaseAgentTools(PurchaseLedgerMapper purchaseLedgerMapper,
                               PaymentRegistrationMapper paymentRegistrationMapper,
                               InvoicePurchaseMapper invoicePurchaseMapper,
                               PurchaseReturnOrdersMapper purchaseReturnOrdersMapper,
+                              SalesLedgerProductMapper salesLedgerProductMapper,
+                              ProcurementRecordMapper procurementRecordMapper,
+                              InboundManagementMapper inboundManagementMapper,
                               AiSessionUserContext aiSessionUserContext) {
         this.purchaseLedgerMapper = purchaseLedgerMapper;
         this.paymentRegistrationMapper = paymentRegistrationMapper;
         this.invoicePurchaseMapper = invoicePurchaseMapper;
         this.purchaseReturnOrdersMapper = purchaseReturnOrdersMapper;
+        this.salesLedgerProductMapper = salesLedgerProductMapper;
+        this.procurementRecordMapper = procurementRecordMapper;
+        this.inboundManagementMapper = inboundManagementMapper;
         this.aiSessionUserContext = aiSessionUserContext;
     }
 
@@ -78,7 +94,7 @@
             wrapper.ge(PurchaseLedger::getEntryDate, toDate(start));
         }
         if (end != null) {
-            wrapper.le(PurchaseLedger::getEntryDate, toDate(end));
+            wrapper.lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(end));
         }
         wrapper.orderByDesc(PurchaseLedger::getEntryDate, PurchaseLedger::getId).last("limit " + finalLimit);
 
@@ -151,19 +167,290 @@
         return jsonResponse(true, "purchase_stats", "宸茶繑鍥為噰璐粺璁℃暟鎹�", summary, Map.of(), Map.of());
     }
 
+    @Tool(name = "閲囪喘鐗╂枡閲戦鎺掕", value = "鎸夋椂闂磋寖鍥寸粺璁¢噰璐墿鏂欓噾棰濇帓琛岋紝鍙洖绛旀湰鏈堥噰璐噾棰濇帓鍚嶉潬鍓嶇殑鐗╂枡銆�")
+    public String rankPurchaseMaterials(@ToolMemoryId String memoryId,
+                                        @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                        @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                        @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡傛湰鏈堛�佽繎7澶┿�佽繎30澶�", required = false) String timeRange,
+                                        @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, timeRange);
+        List<Long> ledgerIds = queryLedgers(loginUser, range).stream()
+                .map(PurchaseLedger::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        if (ledgerIds.isEmpty()) {
+            return jsonResponse(true, "purchase_material_rank", "褰撳墠鏃堕棿鑼冨洿鍐呮病鏈夐噰璐墿鏂欐暟鎹��",
+                    rangeSummary(range, 0), Map.of("items", List.of()), Map.of());
+        }
+
+        List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>()
+                .eq(SalesLedgerProduct::getType, 2)
+                .in(SalesLedgerProduct::getSalesLedgerId, ledgerIds)));
+
+        Map<String, MaterialRankItem> grouped = new LinkedHashMap<>();
+        for (SalesLedgerProduct product : products) {
+            String name = safe(product.getProductCategory());
+            String model = safe(product.getSpecificationModel());
+            String key = name + "|" + model;
+            MaterialRankItem item = grouped.computeIfAbsent(key, ignored -> new MaterialRankItem(name, model, safe(product.getUnit())));
+            item.quantity = item.quantity.add(defaultDecimal(product.getQuantity()));
+            item.amount = item.amount.add(defaultDecimal(product.getTaxInclusiveTotalPrice()));
+        }
+
+        List<Map<String, Object>> items = grouped.values().stream()
+                .sorted(Comparator.comparing((MaterialRankItem item) -> item.amount).reversed())
+                .limit(normalizeLimit(limit))
+                .map(MaterialRankItem::toMap)
+                .collect(Collectors.toList());
+
+        return jsonResponse(true, "purchase_material_rank", "宸茶繑鍥為噰璐墿鏂欓噾棰濇帓琛屻��",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ鏈叆搴撻噰璐鍗�", value = "鏌ヨ閲囪喘璁㈠崟涓嬩粛鏈夊緟鍏ュ簱鏁伴噺鐨勭墿鏂欐槑缁嗐��")
+    public String listUnstockedPurchaseOrders(@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);
+        List<PurchaseLedger> ledgers = queryLedgers(loginUser, range).stream()
+                .filter(ledger -> matchLedgerKeyword(ledger, keyword))
+                .collect(Collectors.toList());
+        Map<Long, PurchaseLedger> ledgerMap = ledgers.stream()
+                .filter(ledger -> ledger.getId() != null)
+                .collect(Collectors.toMap(PurchaseLedger::getId, ledger -> ledger, (a, b) -> a, LinkedHashMap::new));
+        if (ledgerMap.isEmpty()) {
+            return jsonResponse(true, "purchase_unstocked_list", "鏈煡璇㈠埌绗﹀悎鏉′欢鐨勯噰璐鍗曘��",
+                    rangeSummary(range, 0), Map.of("items", List.of()), Map.of());
+        }
+
+        List<SalesLedgerProduct> products = defaultList(salesLedgerProductMapper.selectList(new LambdaQueryWrapper<SalesLedgerProduct>()
+                .eq(SalesLedgerProduct::getType, 2)
+                .in(SalesLedgerProduct::getSalesLedgerId, ledgerMap.keySet())));
+
+        List<Map<String, Object>> items = products.stream()
+                .filter(product -> matchProductKeyword(product, keyword))
+                .map(product -> toUnstockedItem(product, ledgerMap.get(product.getSalesLedgerId())))
+                .filter(Objects::nonNull)
+                .limit(normalizeLimit(limit))
+                .collect(Collectors.toList());
+
+        return jsonResponse(true, "purchase_unstocked_list", "宸茶繑鍥炴湭鍏ュ簱閲囪喘璁㈠崟銆�",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ閲囪喘鍒拌揣寮傚父", value = "鏌ヨ鍒拌揣鐘舵�佸紓甯告垨澶囨敞鍖呭惈寮傚父淇℃伅鐨勫埌璐ц褰曘��")
+    public String listArrivalExceptions(@ToolMemoryId String memoryId,
+                                        @P(value = "寮�濮嬫棩鏈� yyyy-MM-dd", required = false) String startDate,
+                                        @P(value = "缁撴潫鏃ユ湡 yyyy-MM-dd", required = false) String endDate,
+                                        @P(value = "鏃堕棿鑼冨洿鎻忚堪锛屼緥濡傝繎7澶┿�佹湰鏈�", required = false) String timeRange,
+                                        @P(value = "杩斿洖鏉℃暟锛岄粯璁�10锛屾渶澶�30", required = false) Integer limit) {
+        LoginUser loginUser = currentLoginUser(memoryId);
+        DateRange range = resolveDateRange(startDate, endDate, timeRange);
+        LambdaQueryWrapper<InboundManagement> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), InboundManagement::getTenantId);
+        wrapper.ge(InboundManagement::getArrivalTime, toDate(range.start()))
+                .lt(InboundManagement::getArrivalTime, toExclusiveEndDate(range.end()))
+                .and(w -> w.notLike(InboundManagement::getStatus, "姝e父")
+                        .notLike(InboundManagement::getStatus, "瀹屾垚")
+                        .notLike(InboundManagement::getStatus, "宸插埌璐�")
+                        .or().like(InboundManagement::getStatus, "寮傚父")
+                        .or().like(InboundManagement::getRemark, "寮傚父")
+                        .or().like(InboundManagement::getRemark, "闂")
+                        .or().like(InboundManagement::getRemark, "寤惰繜")
+                        .or().like(InboundManagement::getRemark, "鐭己"));
+        wrapper.orderByDesc(InboundManagement::getArrivalTime).last("limit " + normalizeLimit(limit));
+
+        List<Map<String, Object>> items = defaultList(inboundManagementMapper.selectList(wrapper)).stream()
+                .map(this::toArrivalItem)
+                .collect(Collectors.toList());
+        return jsonResponse(true, "purchase_arrival_exception_list", "宸茶繑鍥為噰璐埌璐у紓甯歌褰曘��",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ寰呬粯娆鹃噰璐崟", value = "鏌ヨ鍚堝悓閲戦澶т簬宸蹭粯娆鹃噾棰濈殑閲囪喘鍗曘��")
+    public String listPendingPaymentOrders(@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);
+        List<Map<String, Object>> items = queryLedgers(loginUser, range).stream()
+                .filter(ledger -> matchLedgerKeyword(ledger, keyword))
+                .map(ledger -> toPendingPaymentItem(loginUser, ledger))
+                .filter(Objects::nonNull)
+                .sorted(Comparator.comparing(item -> (BigDecimal) item.get("pendingAmount"), Comparator.reverseOrder()))
+                .limit(normalizeLimit(limit))
+                .collect(Collectors.toList());
+        return jsonResponse(true, "purchase_pending_payment_list", "宸茶繑鍥炲緟浠樻閲囪喘鍗曘��",
+                rangeSummary(range, items.size()), Map.of("items", items), Map.of());
+    }
+
+    @Tool(name = "鏌ヨ閲囪喘閫�璐ф儏鍐�", value = "鎸夋椂闂磋寖鍥存煡璇㈤噰璐��璐у崟鍒楄〃鍜岄��璐ч噾棰濄��")
+    public String listPurchaseReturns(@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<PurchaseReturnOrders> wrapper = new LambdaQueryWrapper<>();
+        applyDeptFilter(wrapper, loginUser.getCurrentDeptId(), PurchaseReturnOrders::getDeptId);
+        wrapper.ge(PurchaseReturnOrders::getPreparedAt, range.start())
+                .le(PurchaseReturnOrders::getPreparedAt, range.end());
+        if (StringUtils.hasText(keyword)) {
+            wrapper.and(w -> w.like(PurchaseReturnOrders::getNo, keyword)
+                    .or().like(PurchaseReturnOrders::getRemark, keyword)
+                    .or().like(PurchaseReturnOrders::getReturnUserName, keyword));
+        }
+        wrapper.orderByDesc(PurchaseReturnOrders::getPreparedAt).last("limit " + normalizeLimit(limit));
+
+        List<PurchaseReturnOrders> returns = defaultList(purchaseReturnOrdersMapper.selectList(wrapper));
+        BigDecimal totalAmount = returns.stream()
+                .map(PurchaseReturnOrders::getTotalAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+        Map<String, Object> summary = rangeSummary(range, returns.size());
+        summary.put("returnAmount", totalAmount);
+
+        return jsonResponse(true, "purchase_return_list", "宸茶繑鍥為噰璐��璐ф儏鍐点��",
+                summary,
+                Map.of("items", returns.stream().map(this::toReturnItem).collect(Collectors.toList())),
+                Map.of());
+    }
+
     private List<PurchaseLedger> queryLedgers(LoginUser loginUser, DateRange range) {
         LambdaQueryWrapper<PurchaseLedger> wrapper = new LambdaQueryWrapper<>();
         applyTenantFilter(wrapper, loginUser.getTenantId(), PurchaseLedger::getTenantId);
         wrapper.ge(PurchaseLedger::getEntryDate, toDate(range.start()))
-                .le(PurchaseLedger::getEntryDate, toDate(range.end()));
+                .lt(PurchaseLedger::getEntryDate, toExclusiveEndDate(range.end()));
         return defaultList(purchaseLedgerMapper.selectList(wrapper));
+    }
+
+    private Map<String, Object> rangeSummary(DateRange range, int count) {
+        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);
+        return summary;
+    }
+
+    private boolean matchLedgerKeyword(PurchaseLedger ledger, String keyword) {
+        if (!StringUtils.hasText(keyword)) {
+            return true;
+        }
+        String text = keyword.trim();
+        return safe(ledger.getPurchaseContractNumber()).contains(text)
+                || safe(ledger.getSupplierName()).contains(text)
+                || safe(ledger.getProjectName()).contains(text);
+    }
+
+    private boolean matchProductKeyword(SalesLedgerProduct product, String keyword) {
+        if (!StringUtils.hasText(keyword)) {
+            return true;
+        }
+        String text = keyword.trim();
+        return safe(product.getProductCategory()).contains(text)
+                || safe(product.getSpecificationModel()).contains(text);
+    }
+
+    private Map<String, Object> toUnstockedItem(SalesLedgerProduct product, PurchaseLedger ledger) {
+        if (product == null || ledger == null || product.getId() == null) {
+            return null;
+        }
+        BigDecimal orderedQuantity = defaultDecimal(product.getQuantity());
+        BigDecimal inboundQuantity = sumInboundQuantity(product.getId());
+        BigDecimal pendingQuantity = orderedQuantity.subtract(inboundQuantity);
+        if (pendingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+            return null;
+        }
+        Map<String, Object> item = new LinkedHashMap<>();
+        item.put("purchaseLedgerId", ledger.getId());
+        item.put("purchaseContractNumber", safe(ledger.getPurchaseContractNumber()));
+        item.put("supplierName", safe(ledger.getSupplierName()));
+        item.put("productCategory", safe(product.getProductCategory()));
+        item.put("specificationModel", safe(product.getSpecificationModel()));
+        item.put("unit", safe(product.getUnit()));
+        item.put("orderedQuantity", orderedQuantity);
+        item.put("inboundQuantity", inboundQuantity);
+        item.put("pendingInboundQuantity", pendingQuantity);
+        item.put("entryDate", formatDate(ledger.getEntryDate()));
+        return item;
+    }
+
+    private BigDecimal sumInboundQuantity(Long salesLedgerProductId) {
+        List<ProcurementRecordStorage> records = defaultList(procurementRecordMapper.selectList(new LambdaQueryWrapper<ProcurementRecordStorage>()
+                .eq(ProcurementRecordStorage::getType, 1)
+                .eq(ProcurementRecordStorage::getSalesLedgerProductId, salesLedgerProductId)));
+        return records.stream()
+                .map(ProcurementRecordStorage::getInboundNum)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    private Map<String, Object> toArrivalItem(InboundManagement item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("orderNo", safe(item.getOrderNo()));
+        map.put("arrivalNo", safe(item.getArrivalNo()));
+        map.put("supplierName", safe(item.getSupplierName()));
+        map.put("status", safe(item.getStatus()));
+        map.put("arrivalTime", formatDate(item.getArrivalTime()));
+        map.put("arrivalQuantity", safe(item.getArrivalQuantity()));
+        map.put("remark", safe(item.getRemark()));
+        return map;
+    }
+
+    private Map<String, Object> toPendingPaymentItem(LoginUser loginUser, PurchaseLedger ledger) {
+        BigDecimal contractAmount = defaultDecimal(ledger.getContractAmount());
+        BigDecimal paidAmount = sumPaymentAmount(loginUser, ledger.getId());
+        BigDecimal pendingAmount = contractAmount.subtract(paidAmount);
+        if (pendingAmount.compareTo(BigDecimal.ZERO) <= 0) {
+            return null;
+        }
+        Map<String, Object> item = toLedgerItem(ledger);
+        item.put("paidAmount", paidAmount);
+        item.put("pendingAmount", pendingAmount);
+        return item;
+    }
+
+    private BigDecimal sumPaymentAmount(LoginUser loginUser, Long purchaseLedgerId) {
+        LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
+        applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
+        wrapper.eq(PaymentRegistration::getPurchaseLedgerId, purchaseLedgerId);
+        return defaultList(paymentRegistrationMapper.selectList(wrapper)).stream()
+                .map(PaymentRegistration::getCurrentPaymentAmount)
+                .filter(Objects::nonNull)
+                .reduce(BigDecimal.ZERO, BigDecimal::add);
+    }
+
+    private Map<String, Object> toReturnItem(PurchaseReturnOrders item) {
+        Map<String, Object> map = new LinkedHashMap<>();
+        map.put("id", item.getId());
+        map.put("no", safe(item.getNo()));
+        map.put("returnType", item.getReturnType());
+        map.put("purchaseLedgerId", item.getPurchaseLedgerId());
+        map.put("preparedAt", item.getPreparedAt() == null ? "" : item.getPreparedAt().toString());
+        map.put("returnUserName", safe(item.getReturnUserName()));
+        map.put("totalAmount", item.getTotalAmount());
+        map.put("remark", safe(item.getRemark()));
+        return map;
+    }
+
+    private BigDecimal defaultDecimal(BigDecimal value) {
+        return value == null ? BigDecimal.ZERO : value;
     }
 
     private List<PaymentRegistration> queryPayments(LoginUser loginUser, DateRange range) {
         LambdaQueryWrapper<PaymentRegistration> wrapper = new LambdaQueryWrapper<>();
         applyTenantFilter(wrapper, loginUser.getTenantId(), PaymentRegistration::getTenantId);
         wrapper.ge(PaymentRegistration::getPaymentDate, toDate(range.start()))
-                .le(PaymentRegistration::getPaymentDate, toDate(range.end()));
+                .lt(PaymentRegistration::getPaymentDate, toExclusiveEndDate(range.end()));
         return defaultList(paymentRegistrationMapper.selectList(wrapper));
     }
 
@@ -231,6 +518,19 @@
         if (text.contains("杩戝崐涓湀") || text.contains("鏈�杩戝崐涓湀") || text.contains("鍗婁釜鏈�")) {
             return new DateRange(today.minusDays(14), today, "杩戝崐涓湀");
         }
+        java.util.regex.Matcher relativeMatcher = java.util.regex.Pattern.compile("(杩憒鏈�杩�)(\\d+)(澶﹟鍛▅涓湀|鏈坾骞�)").matcher(text);
+        if (relativeMatcher.find()) {
+            int amount = Integer.parseInt(relativeMatcher.group(2));
+            String unit = relativeMatcher.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澶�");
     }
 
@@ -243,6 +543,10 @@
 
     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 String formatDate(Date date) {
@@ -312,4 +616,28 @@
 
     private record DateRange(LocalDate start, LocalDate end, String label) {
     }
+
+    private static class MaterialRankItem {
+        private final String productCategory;
+        private final String specificationModel;
+        private final String unit;
+        private BigDecimal quantity = BigDecimal.ZERO;
+        private BigDecimal amount = BigDecimal.ZERO;
+
+        private MaterialRankItem(String productCategory, String specificationModel, String unit) {
+            this.productCategory = productCategory;
+            this.specificationModel = specificationModel;
+            this.unit = unit;
+        }
+
+        private Map<String, Object> toMap() {
+            Map<String, Object> map = new LinkedHashMap<>();
+            map.put("productCategory", productCategory);
+            map.put("specificationModel", specificationModel);
+            map.put("unit", unit);
+            map.put("quantity", quantity);
+            map.put("amount", amount);
+            return map;
+        }
+    }
 }
diff --git a/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java b/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
index b75fedf..d30fd2b 100644
--- a/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
+++ b/src/main/java/com/ruoyi/approve/service/impl/ApproveProcessServiceImpl.java
@@ -1,12 +1,12 @@
 package com.ruoyi.approve.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
 import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 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.approve.vo.ApproveProcessVo;
 import com.ruoyi.approve.bean.vo.ApproveGetAndUpdateVo;
 import com.ruoyi.approve.bean.vo.ApproveProcessConfigNodeVo;
 import com.ruoyi.approve.bean.vo.ApproveProcessVO;
@@ -19,7 +19,7 @@
 import com.ruoyi.approve.service.ApproveProcessConfigNodeService;
 import com.ruoyi.approve.service.IApproveNodeService;
 import com.ruoyi.approve.service.IApproveProcessService;
-import com.ruoyi.basic.enums.ApplicationTypeEnum;
+import com.ruoyi.approve.vo.ApproveProcessVo;
 import com.ruoyi.basic.enums.RecordTypeEnum;
 import com.ruoyi.basic.utils.FileUtil;
 import com.ruoyi.common.enums.FileNameType;
@@ -79,7 +79,15 @@
         List<ApproveProcessConfigNodeVo> list = approveProcessConfigNodeService.listNode( approveProcessVO.getApproveType());
         List<Long> nodeIds = list.stream()
                 .map(ApproveProcessConfigNodeVo::getApproverId)
+                .filter(Objects::nonNull)
                 .collect(Collectors.toList());
+        if(list.isEmpty()) {
+            throw new RuntimeException("娴佺▼涓嶅瓨鍦�");
+        }
+        if (CollectionUtils.isEmpty(nodeIds)) {
+            autoPassPurchaseApproveIfNoApprover(approveProcessVO);
+            return;
+        }
         List<SysUser> sysUsers = sysUserMapper.selectUserByIds(nodeIds);
         if (CollectionUtils.isEmpty(sysUsers)) throw new RuntimeException("瀹℃牳鐢ㄦ埛涓嶅瓨鍦�");
         if (sysDept == null) throw new RuntimeException("閮ㄩ棬涓嶅瓨鍦�");
@@ -147,6 +155,16 @@
         }
     }
 
+    private void autoPassPurchaseApproveIfNoApprover(ApproveProcessVO approveProcessVO) {
+        if (!Objects.equals(approveProcessVO.getApproveType(), 5)
+                || !StringUtils.hasText(approveProcessVO.getApproveReason())) {
+            throw new RuntimeException("瀹℃牳鐢ㄦ埛涓嶅瓨鍦�");
+        }
+        purchaseLedgerMapper.update(null, new LambdaUpdateWrapper<PurchaseLedger>()
+                .eq(PurchaseLedger::getPurchaseContractNumber, approveProcessVO.getApproveReason())
+                .set(PurchaseLedger::getApprovalStatus, 3));
+    }
+
     @Override
     public List<SysDept> selectDeptListByDeptIds(Long[] deptIds) {
         List<SysDept> sysDeptList = new ArrayList<SysDept>();
diff --git a/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java b/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java
index 756b4b9..5f84cb7 100644
--- a/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java
+++ b/src/main/java/com/ruoyi/basic/mapper/StorageBlobMapper.java
@@ -3,6 +3,7 @@
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.ruoyi.basic.pojo.StorageBlob;
 import org.apache.ibatis.annotations.Mapper;
+import org.apache.ibatis.annotations.Param;
 
 /**
  * <p>
@@ -15,4 +16,9 @@
 @Mapper
 public interface StorageBlobMapper extends BaseMapper<StorageBlob> {
 
+    java.util.List<StorageBlob> selectOrphanBlobsByIdRange(@Param("lastId") long lastId, @Param("limit") int limit);
+
+    int deleteByIdList(@Param("ids") java.util.List<Long> ids);
+
+    java.util.List<String> selectExistingUidFilenames(@Param("fileNames") java.util.List<String> fileNames);
 }
diff --git a/src/main/java/com/ruoyi/basic/pojo/ProductModel.java b/src/main/java/com/ruoyi/basic/pojo/ProductModel.java
index 0ec51b2..f0e9470 100644
--- a/src/main/java/com/ruoyi/basic/pojo/ProductModel.java
+++ b/src/main/java/com/ruoyi/basic/pojo/ProductModel.java
@@ -37,6 +37,10 @@
     @Excel(name = "瑙勬牸鍨嬪彿")
     private String model;
 
+    @Excel(name = "浜у搧缂栫爜")
+    @TableField("product_code")
+    private String productCode;
+
     /**
      * 鍗曚綅
      */
diff --git a/src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java b/src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java
new file mode 100644
index 0000000..d5ac456
--- /dev/null
+++ b/src/main/java/com/ruoyi/basic/task/StorageBlobCleanupTask.java
@@ -0,0 +1,184 @@
+package com.ruoyi.basic.task;
+
+import com.ruoyi.basic.mapper.StorageBlobMapper;
+import com.ruoyi.basic.pojo.StorageBlob;
+import com.ruoyi.common.config.FileProperties;
+import lombok.RequiredArgsConstructor;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.scheduling.annotation.Scheduled;
+import org.springframework.stereotype.Component;
+import org.springframework.util.CollectionUtils;
+import org.springframework.util.StringUtils;
+
+import java.io.File;
+import java.util.ArrayDeque;
+import java.util.ArrayList;
+import java.util.Deque;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Set;
+import java.util.concurrent.atomic.AtomicBoolean;
+
+/**
+ * 娓呯悊鏃犳晥鏂囦欢瀹氭椂浠诲姟銆�
+ */
+@Slf4j
+@Component
+@RequiredArgsConstructor
+public class StorageBlobCleanupTask {
+
+    private static final int DB_BATCH_SIZE = 500;
+    private static final int FILE_NAME_BATCH_SIZE = 1000;
+
+    private final StorageBlobMapper storageBlobMapper;
+    private final FileProperties fileProperties;
+
+    private final AtomicBoolean running = new AtomicBoolean(false);
+
+    /**
+     * 姣忔湀 1 鍙峰噷鏅� 2 鐐规墽琛屼竴娆★細
+     * 1. 鍒犻櫎 storage_blob 涓湭琚� storage_attachment 鍏宠仈鐨勮褰曞強鍏舵枃浠�
+     * 2. 鍒犻櫎纾佺洏涓婁笉瀛樺湪浜� storage_blob.uid_filename 鐨勬枃浠�
+     */
+    @Scheduled(cron = "0 0 2 1 * ?")
+    public void cleanupUnusedStorageFiles() {
+        if (!running.compareAndSet(false, true)) {
+            log.warn("鏂囦欢娓呯悊浠诲姟姝e湪鎵ц锛屾湰娆¤烦杩�");
+            return;
+        }
+
+        long start = System.currentTimeMillis();
+        log.info("鏂囦欢娓呯悊浠诲姟寮�濮嬫墽琛岋紝鏍圭洰褰曪細{}", fileProperties.getPath());
+        try {
+            int removedBlobCount = cleanupOrphanStorageBlobs();
+            int removedDiskFileCount = cleanupOrphanDiskFiles();
+            long cost = System.currentTimeMillis() - start;
+            log.info("鏂囦欢娓呯悊浠诲姟鎵ц瀹屾垚锛屽垹闄ゅ鍎� blob 璁板綍锛歿}锛屽垹闄ょ鐩樻棤鏁堟枃浠讹細{}锛岃�楁椂锛歿} ms",
+                    removedBlobCount, removedDiskFileCount, cost);
+        } catch (Exception e) {
+            log.error("鏂囦欢娓呯悊浠诲姟鎵ц澶辫触", e);
+        } finally {
+            running.set(false);
+        }
+    }
+
+    private int cleanupOrphanStorageBlobs() {
+        long lastId = 0L;
+        int removedCount = 0;
+
+        while (true) {
+            List<StorageBlob> orphanBlobs = storageBlobMapper.selectOrphanBlobsByIdRange(lastId, DB_BATCH_SIZE);
+            if (CollectionUtils.isEmpty(orphanBlobs)) {
+                break;
+            }
+
+            List<Long> ids = new ArrayList<>(orphanBlobs.size());
+            for (StorageBlob storageBlob : orphanBlobs) {
+                ids.add(storageBlob.getId());
+                deleteBlobFiles(storageBlob);
+            }
+            storageBlobMapper.deleteByIdList(ids);
+            removedCount += ids.size();
+            lastId = orphanBlobs.get(orphanBlobs.size() - 1).getId();
+
+            log.info("宸插垹闄や竴鎵瑰鍎� blob锛宐atchSize={}锛宭astId={}", ids.size(), lastId);
+        }
+
+        return removedCount;
+    }
+
+    private int cleanupOrphanDiskFiles() {
+        File rootDirectory = new File(fileProperties.getPath());
+        if (!rootDirectory.exists() || !rootDirectory.isDirectory()) {
+            log.warn("鏂囦欢鏍圭洰褰曚笉瀛樺湪鎴栦笉鏄洰褰曪紝璺宠繃纾佺洏娓呯悊锛歿}", fileProperties.getPath());
+            return 0;
+        }
+
+        int deletedCount = 0;
+        Deque<File> directories = new ArrayDeque<>();
+        directories.push(rootDirectory);
+
+        while (!directories.isEmpty()) {
+            File currentDirectory = directories.pop();
+            File[] children = currentDirectory.listFiles();
+            if (children == null || children.length == 0) {
+                continue;
+            }
+
+            List<File> filesInDirectory = new ArrayList<>();
+            for (File child : children) {
+                if (child.isDirectory()) {
+                    directories.push(child);
+                } else if (child.isFile()) {
+                    filesInDirectory.add(child);
+                }
+            }
+
+            deletedCount += cleanupFilesInDirectory(filesInDirectory);
+        }
+
+        return deletedCount;
+    }
+
+    private int cleanupFilesInDirectory(List<File> filesInDirectory) {
+        if (CollectionUtils.isEmpty(filesInDirectory)) {
+            return 0;
+        }
+
+        int deletedCount = 0;
+        for (int start = 0; start < filesInDirectory.size(); start += FILE_NAME_BATCH_SIZE) {
+            int end = Math.min(start + FILE_NAME_BATCH_SIZE, filesInDirectory.size());
+            List<File> batchFiles = filesInDirectory.subList(start, end);
+            List<String> fileNames = new ArrayList<>(batchFiles.size());
+            for (File file : batchFiles) {
+                fileNames.add(file.getName());
+            }
+
+            Set<String> existingFileNames = new HashSet<>(storageBlobMapper.selectExistingUidFilenames(fileNames));
+            for (File file : batchFiles) {
+                if (!existingFileNames.contains(file.getName()) && safeDelete(file)) {
+                    deletedCount++;
+                }
+            }
+        }
+        return deletedCount;
+    }
+
+    private void deleteBlobFiles(StorageBlob storageBlob) {
+        File originalFile = resolveBlobFile(storageBlob);
+        safeDelete(originalFile);
+
+        File compressedFile = resolveCompressedFile(originalFile);
+        safeDelete(compressedFile);
+    }
+
+    private File resolveBlobFile(StorageBlob storageBlob) {
+        String basePath = fileProperties.getPath();
+        if (!StringUtils.hasText(storageBlob.getPath())) {
+            return new File(basePath, storageBlob.getUidFilename());
+        }
+        return new File(new File(basePath, storageBlob.getPath()), storageBlob.getUidFilename());
+    }
+
+    private File resolveCompressedFile(File originalFile) {
+        if (originalFile == null) {
+            return null;
+        }
+        File parent = originalFile.getParentFile();
+        if (parent == null) {
+            return null;
+        }
+        return new File(parent, "thumb_" + originalFile.getName());
+    }
+
+    private boolean safeDelete(File file) {
+        if (file == null || !file.exists() || !file.isFile()) {
+            return false;
+        }
+        if (file.delete()) {
+            return true;
+        }
+        log.warn("鍒犻櫎鏂囦欢澶辫触锛歿}", file.getAbsolutePath());
+        return false;
+    }
+}
diff --git a/src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java b/src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java
index df0d011..6aecb29 100644
--- a/src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java
+++ b/src/main/java/com/ruoyi/common/enums/StockInQualifiedRecordTypeEnum.java
@@ -12,7 +12,8 @@
     PURCHASE_STOCK_IN("7", "閲囪喘-鍏ュ簱"),
     QUALITYINSPECT_STOCK_IN("6", "璐ㄦ-鍚堟牸鍏ュ簱"),
     DEFECTIVE_PASS("11", "涓嶅悎鏍�-璁╂鏀捐"),
-    RETURN_HE_IN("14", "閿�鍞��璐�-鍚堟牸鍏ュ簱");
+    RETURN_HE_IN("14", "閿�鍞��璐�-鍚堟牸鍏ュ簱"),
+    PICK_RETURN_IN("20", "閿�鍞��璐�-鍚堟牸鍏ュ簱");
 
 
     private final String code;
diff --git a/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerRecordService.java b/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerRecordService.java
index bdb1135..2d7d38c 100644
--- a/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerRecordService.java
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerRecordService.java
@@ -4,9 +4,7 @@
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
 import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedgerRecord;
-
 import jakarta.servlet.http.HttpServletResponse;
-import java.io.IOException;
 
 /**
  * @author :yys
@@ -25,5 +23,5 @@
 
     void export(HttpServletResponse response);
 
-    boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) throws IOException;
+    boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord);
 }
diff --git a/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerRecordServiceImpl.java b/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerRecordServiceImpl.java
index b178d75..7104c1a 100644
--- a/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerRecordServiceImpl.java
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerRecordServiceImpl.java
@@ -5,35 +5,21 @@
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ruoyi.common.enums.FileNameType;
-import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerMapper;
 import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerRecordMapper;
 import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedger;
 import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedgerRecord;
 import com.ruoyi.measuringinstrumentledger.service.MeasuringInstrumentLedgerRecordService;
-import com.ruoyi.other.mapper.TempFileMapper;
-import com.ruoyi.other.pojo.TempFile;
 import com.ruoyi.sales.mapper.CommonFileMapper;
 import com.ruoyi.sales.pojo.CommonFile;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FilenameUtils;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
 import java.util.Date;
 import java.util.List;
-import java.util.UUID;
 
 /**
  * @author :yys
@@ -47,10 +33,6 @@
     private final MeasuringInstrumentLedgerRecordMapper measuringInstrumentLedgerRecordMapper;
     private final MeasuringInstrumentLedgerMapper measuringInstrumentLedgerMapper;
     private final CommonFileMapper commonFileMapper;
-    private final TempFileMapper tempFileMapper;
-
-    @Value("${file.upload-dir}")
-    private String uploadDir;
 
     @Override
     public IPage<MeasuringInstrumentLedgerRecord> listPage(Page page, MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) {
@@ -73,7 +55,7 @@
     }
 
     @Override
-    public boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) throws IOException {
+    public boolean updateMeasuringInstrumentLedgerRecord(MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord) {
         MeasuringInstrumentLedgerRecord measuringInstrumentLedgerRecord1 = measuringInstrumentLedgerRecordMapper.selectById(measuringInstrumentLedgerRecord.getId());
         if (measuringInstrumentLedgerRecord1 == null) {
             return false;
@@ -88,83 +70,6 @@
             measuringInstrumentLedgerMapper.updateById(measuringInstrumentLedger);
         }
         measuringInstrumentLedgerRecordMapper.updateById(measuringInstrumentLedgerRecord);
-        // 璁板綍闄勪欢缁戝畾
-        migrateTempFilesToFormal(measuringInstrumentLedgerRecord.getId(), measuringInstrumentLedgerRecord.getTempFileIds(), FileNameType.MEASURINGRecord.getValue());
         return true;
-    }
-
-
-    /**
-     * 灏嗕复鏃舵枃浠惰縼绉诲埌姝e紡鐩綍
-     *
-     * @param businessId  涓氬姟ID锛堥攢鍞彴璐D锛�
-     * @param tempFileIds 涓存椂鏂囦欢ID鍒楄〃
-     * @throws IOException 鏂囦欢鎿嶄綔寮傚父
-     */
-    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds,Integer fileType) throws IOException {
-        if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(tempFileIds)) {
-            return;
-        }
-
-        // 鏋勫缓姝e紡鐩綍璺緞锛堟寜涓氬姟绫诲瀷鍜屾棩鏈熷垎缁勶級
-        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
-
-        Path formalDirPath = Paths.get(formalDir);
-
-        // 纭繚姝e紡鐩綍瀛樺湪锛堥�掑綊鍒涘缓锛�
-        if (!Files.exists(formalDirPath)) {
-            Files.createDirectories(formalDirPath);
-        }
-
-        for (String tempFileId : tempFileIds) {
-            // 鏌ヨ涓存椂鏂囦欢璁板綍
-            TempFile tempFile = tempFileMapper.selectById(tempFileId);
-            if (tempFile == null) {
-                log.warn("涓存椂鏂囦欢涓嶅瓨鍦紝璺宠繃澶勭悊: {}", tempFileId);
-                continue;
-            }
-
-            // 鏋勫缓姝e紡鏂囦欢鍚嶏紙鍖呭惈涓氬姟ID鍜屾椂闂存埑锛岄伩鍏嶅啿绐侊級
-            String originalFilename = tempFile.getOriginalName();
-            String fileExtension = FilenameUtils.getExtension(originalFilename);
-            String formalFilename = businessId + "_" +
-                    System.currentTimeMillis() + "_" +
-                    UUID.randomUUID().toString().substring(0, 8) +
-                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
-
-            Path formalFilePath = formalDirPath.resolve(formalFilename);
-
-            try {
-                // 鎵ц鏂囦欢杩佺Щ锛堜娇鐢ㄥ師瀛愭搷浣滅‘淇濆畨鍏ㄦ�э級
-//                Files.move(
-//                        Paths.get(tempFile.getTempPath()),
-//                        formalFilePath,
-//                        StandardCopyOption.REPLACE_EXISTING,
-//                        StandardCopyOption.ATOMIC_MOVE
-//                );
-                // 鍘熷瓙绉诲姩澶辫触锛屼娇鐢ㄥ鍒�+鍒犻櫎
-                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
-                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-
-                // 鏇存柊鏂囦欢璁板綍锛堝叧鑱斿埌涓氬姟ID锛�
-                CommonFile fileRecord = new CommonFile();
-                fileRecord.setCommonId(businessId);
-                fileRecord.setName(originalFilename);
-                fileRecord.setUrl(formalFilePath.toString());
-                fileRecord.setCreateTime(LocalDateTime.now());
-                fileRecord.setType(fileType);
-                commonFileMapper.insert(fileRecord);
-
-                // 鍒犻櫎涓存椂鏂囦欢璁板綍
-                tempFileMapper.deleteById(tempFile);
-
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-            } catch (IOException e) {
-                log.error("鏂囦欢杩佺Щ澶辫触: {}", tempFile.getTempPath(), e);
-                // 鍙�夋嫨鍥炴粴浜嬪姟鎴栬褰曞け璐ユ枃浠�
-                throw new IOException("鏂囦欢杩佺Щ寮傚父", e);
-            }
-        }
     }
 }
diff --git a/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java b/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java
index 72ddbaa..4025d75 100644
--- a/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java
@@ -6,7 +6,6 @@
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ruoyi.common.enums.FileNameType;
-import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.measuringinstrumentledger.dto.MeasuringInstrumentLedgerDto;
 import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerMapper;
@@ -14,8 +13,6 @@
 import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedger;
 import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedgerRecord;
 import com.ruoyi.measuringinstrumentledger.service.MeasuringInstrumentLedgerService;
-import com.ruoyi.other.mapper.TempFileMapper;
-import com.ruoyi.other.pojo.TempFile;
 import com.ruoyi.project.system.domain.SysUser;
 import com.ruoyi.project.system.mapper.SysUserMapper;
 import com.ruoyi.sales.mapper.CommonFileMapper;
@@ -23,22 +20,12 @@
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FilenameUtils;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 
 import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.Date;
 import java.util.List;
-import java.util.UUID;
 import java.util.stream.Collectors;
 
 /**
@@ -52,12 +39,7 @@
 
     private final MeasuringInstrumentLedgerMapper measuringInstrumentLedgerMapper;
     private final MeasuringInstrumentLedgerRecordMapper measuringInstrumentLedgerRecordMapper;
-    private final TempFileMapper tempFileMapper;
-    private final CommonFileMapper commonFileMapper;
     private final SysUserMapper sysUserMapper;
-
-    @Value("${file.upload-dir}")
-    private String uploadDir;
 
     @Override
     public IPage<MeasuringInstrumentLedger> listPage(Page page, MeasuringInstrumentLedger measuringInstrumentLedger) {
@@ -74,12 +56,6 @@
                 collect = measuringInstrumentLedgerRecords.stream().map(MeasuringInstrumentLedgerRecord::getId).collect(Collectors.toList());
             }
             collect.add(item.getId());
-            LambdaQueryWrapper<CommonFile> salesLedgerFileWrapper = new LambdaQueryWrapper<>();
-            salesLedgerFileWrapper.in(CommonFile::getCommonId, collect)
-                    .in(CommonFile::getType,types);
-            List<CommonFile> commonFiles = commonFileMapper.selectList(salesLedgerFileWrapper);
-            item.setCommonFiles(commonFiles);
-
         });
         return measuringInstrumentLedgerIPage;
     }
@@ -107,10 +83,6 @@
 //            if(!CollectionUtils.isEmpty(req.getTempFileIds())){
 //                migrateTempFilesToFormal(measuringInstrumentLedger.getId(), req.getTempFileIds(), FileNameType.MEASURING.getValue());
 //            }
-            // 鍙拌处璁板綍缁戝畾涓�娆�
-            if(!CollectionUtils.isEmpty(req.getTempFileIds())){
-                migrateTempFilesToFormal(measuringInstrumentLedgerRecord.getId(), req.getTempFileIds(), FileNameType.MEASURINGRecord.getValue());
-            }
             return true;
         }
         return false;
@@ -131,84 +103,7 @@
         }
         measuringInstrumentLedger.setUserName(sysUser.getUserName());
         measuringInstrumentLedgerMapper.insert(measuringInstrumentLedger);
-        if(!CollectionUtils.isEmpty(measuringInstrumentLedger.getTempFileIds())){
-            migrateTempFilesToFormal(measuringInstrumentLedger.getId(), measuringInstrumentLedger.getTempFileIds(), FileNameType.MEASURING.getValue());
-        }
         return true;
     }
 
-
-    /**
-     * 灏嗕复鏃舵枃浠惰縼绉诲埌姝e紡鐩綍
-     *
-     * @param businessId  涓氬姟ID锛堥攢鍞彴璐D锛�
-     * @param tempFileIds 涓存椂鏂囦欢ID鍒楄〃
-     * @throws IOException 鏂囦欢鎿嶄綔寮傚父
-     */
-    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds,Integer fileType) throws IOException {
-        if (CollectionUtils.isEmpty(tempFileIds)) {
-            return;
-        }
-
-        // 鏋勫缓姝e紡鐩綍璺緞锛堟寜涓氬姟绫诲瀷鍜屾棩鏈熷垎缁勶級
-        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
-
-        Path formalDirPath = Paths.get(formalDir);
-
-        // 纭繚姝e紡鐩綍瀛樺湪锛堥�掑綊鍒涘缓锛�
-        if (!Files.exists(formalDirPath)) {
-            Files.createDirectories(formalDirPath);
-        }
-
-        for (String tempFileId : tempFileIds) {
-            // 鏌ヨ涓存椂鏂囦欢璁板綍
-            TempFile tempFile = tempFileMapper.selectById(tempFileId);
-            if (tempFile == null) {
-                log.warn("涓存椂鏂囦欢涓嶅瓨鍦紝璺宠繃澶勭悊: {}", tempFileId);
-                continue;
-            }
-
-            // 鏋勫缓姝e紡鏂囦欢鍚嶏紙鍖呭惈涓氬姟ID鍜屾椂闂存埑锛岄伩鍏嶅啿绐侊級
-            String originalFilename = tempFile.getOriginalName();
-            String fileExtension = FilenameUtils.getExtension(originalFilename);
-            String formalFilename = businessId + "_" +
-                    System.currentTimeMillis() + "_" +
-                    UUID.randomUUID().toString().substring(0, 8) +
-                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
-
-            Path formalFilePath = formalDirPath.resolve(formalFilename);
-
-            try {
-                // 鎵ц鏂囦欢杩佺Щ锛堜娇鐢ㄥ師瀛愭搷浣滅‘淇濆畨鍏ㄦ�э級
-//                Files.move(
-//                        Paths.get(tempFile.getTempPath()),
-//                        formalFilePath,
-//                        StandardCopyOption.REPLACE_EXISTING,
-//                        StandardCopyOption.ATOMIC_MOVE
-//                );
-                // 鍘熷瓙绉诲姩澶辫触锛屼娇鐢ㄥ鍒�+鍒犻櫎
-                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
-                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-
-                // 鏇存柊鏂囦欢璁板綍锛堝叧鑱斿埌涓氬姟ID锛�
-                CommonFile fileRecord = new CommonFile();
-                fileRecord.setCommonId(businessId);
-                fileRecord.setName(originalFilename);
-                fileRecord.setUrl(formalFilePath.toString());
-                fileRecord.setCreateTime(LocalDateTime.now());
-                fileRecord.setType(fileType);
-                commonFileMapper.insert(fileRecord);
-
-                // 鍒犻櫎涓存椂鏂囦欢璁板綍
-                tempFileMapper.deleteById(tempFile);
-
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-            } catch (IOException e) {
-                log.error("鏂囦欢杩佺Щ澶辫触: {}", tempFile.getTempPath(), e);
-                // 鍙�夋嫨鍥炴粴浜嬪姟鎴栬褰曞け璐ユ枃浠�
-                throw new IOException("鏂囦欢杩佺Щ寮傚父", e);
-            }
-        }
-    }
 }
diff --git a/src/main/java/com/ruoyi/other/controller/TempFileController.java b/src/main/java/com/ruoyi/other/controller/TempFileController.java
deleted file mode 100644
index 1175eb0..0000000
--- a/src/main/java/com/ruoyi/other/controller/TempFileController.java
+++ /dev/null
@@ -1,57 +0,0 @@
-package com.ruoyi.other.controller;
-
-
-import com.ruoyi.framework.web.domain.AjaxResult;
-import com.ruoyi.other.service.TempFileService;
-import com.ruoyi.purchase.dto.ProductRecordDto;
-import com.ruoyi.purchase.dto.TicketRegistrationDto;
-import com.ruoyi.purchase.service.ITicketRegistrationService;
-import com.ruoyi.purchase.service.impl.TicketRegistrationServiceImpl;
-import lombok.AllArgsConstructor;
-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 org.springframework.web.multipart.MultipartFile;
-
-
-@RestController
-@RequestMapping("/file")
-@AllArgsConstructor
-public class TempFileController {
-
-    private TempFileService tempFileService;
-
-    private TicketRegistrationServiceImpl ticketRegistrationServiceImpl;
-
-    @PostMapping("/upload")
-    public AjaxResult uploadFile(MultipartFile file, Integer type) {
-        try {
-            return AjaxResult.success(tempFileService.uploadFile(file, type));
-        } catch (Exception e) {
-            return AjaxResult.error(e.getMessage());
-        }
-    }
-
-    @PostMapping("/uploadByCommon")
-    public AjaxResult uploadByCommon(MultipartFile file, Integer type, Long id) {
-        try {
-            return AjaxResult.success(tempFileService.uploadByCommon(file, type,id));
-        } catch (Exception e) {
-            return AjaxResult.error(e.getMessage());
-        }
-    }
-
-    @PostMapping("uploadFile")
-    public AjaxResult uploadFile(@RequestBody ProductRecordDto productRecordDto) {
-        try {
-            if (!productRecordDto.getTempFileIds().isEmpty()&&productRecordDto.getTicketRegistrationId() != null) {
-                ticketRegistrationServiceImpl.migrateTempFilesToFormal(productRecordDto.getTicketRegistrationId(), productRecordDto.getTempFileIds());
-            }
-        } catch (Exception e) {
-            return AjaxResult.error(e.getMessage());
-        }
-        return AjaxResult.success();
-    }
-
-}
diff --git a/src/main/java/com/ruoyi/other/service/TempFileService.java b/src/main/java/com/ruoyi/other/service/TempFileService.java
deleted file mode 100644
index 355e43c..0000000
--- a/src/main/java/com/ruoyi/other/service/TempFileService.java
+++ /dev/null
@@ -1,12 +0,0 @@
-package com.ruoyi.other.service;
-
-import com.ruoyi.other.pojo.TempFile;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.io.IOException;
-
-public interface TempFileService {
-    TempFile uploadFile(MultipartFile file,Integer type) throws IOException;
-
-    String uploadByCommon(MultipartFile file, Integer type, Long id) throws IOException;
-}
diff --git a/src/main/java/com/ruoyi/other/service/impl/TempFileServiceImpl.java b/src/main/java/com/ruoyi/other/service/impl/TempFileServiceImpl.java
deleted file mode 100644
index 093ddab..0000000
--- a/src/main/java/com/ruoyi/other/service/impl/TempFileServiceImpl.java
+++ /dev/null
@@ -1,184 +0,0 @@
-package com.ruoyi.other.service.impl;
-
-import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
-import com.ruoyi.common.utils.StringUtils;
-import com.ruoyi.other.mapper.TempFileMapper;
-import com.ruoyi.other.pojo.TempFile;
-import com.ruoyi.other.service.TempFileService;
-import com.ruoyi.sales.mapper.CommonFileMapper;
-import com.ruoyi.sales.pojo.CommonFile;
-import lombok.RequiredArgsConstructor;
-import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FilenameUtils;
-import org.springframework.beans.factory.annotation.Value;
-import org.springframework.stereotype.Service;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
-import java.util.Collections;
-import java.util.List;
-import java.util.UUID;
-
-@Service
-@Slf4j
-@RequiredArgsConstructor
-public class TempFileServiceImpl extends ServiceImpl<TempFileMapper, TempFile> implements TempFileService {
-
-    private final TempFileMapper tempFileMapper;
-    private final CommonFileMapper commonFileMapper;
-
-    @Value("${file.upload-dir}")
-    private String uploadDir;
-    @Value("${file.temp-dir}")
-    private String tempDir;
-
-    // 涓婁紶鍒颁复鏃剁洰褰�
-    @Override
-    public TempFile uploadFile(MultipartFile file,Integer type) throws IOException {
-        // 1. 鐢熸垚涓存椂鏂囦欢ID鍜岃矾寰�
-        String tempId = UUID.randomUUID().toString();
-        String originalFilename = file.getOriginalFilename();
-        if(originalFilename == null) throw new IOException("鏂囦欢鍚嶄笉鑳戒负绌�");
-//        URLEncoder urlEncoder = new URLEncoder();
-//        String encodedFilename = urlEncoder.encode(originalFilename, StandardCharsets.UTF_8);
-//        encodedFilename = encodedFilename.replaceAll("%2E",".");
-//        Path tempFilePath = Paths.get(tempDir, tempId + "_" + encodedFilename);
-
-        Path tempFilePath = Paths.get(tempDir, tempId + "_" + file.getOriginalFilename());
-
-        // 2. 纭繚鐩綍瀛樺湪
-        Path parentDir = tempFilePath.getParent();
-        if (parentDir != null) {
-            Files.createDirectories(parentDir); // 閫掑綊鍒涘缓鐩綍
-        }
-
-        // 3. 淇濆瓨鏂囦欢鍒颁复鏃剁洰褰�
-        file.transferTo(tempFilePath.toFile());
-
-        // 4. 淇濆瓨涓存椂鏂囦欢璁板綍
-        TempFile tempFileRecord = new TempFile();
-        tempFileRecord.setTempId(tempId);
-        tempFileRecord.setOriginalName(file.getOriginalFilename());
-        tempFileRecord.setTempPath(tempFilePath.toString());
-        tempFileRecord.setExpireTime(LocalDateTime.now().plusHours(2)); // 2灏忔椂鍚庤繃鏈�
-        tempFileRecord.setType(type);
-        tempFileRecord.setFileSize(file.getSize());
-        tempFileMapper.insert(tempFileRecord);
-        return tempFileRecord;
-    }
-
-    @Override
-    public String uploadByCommon(MultipartFile file, Integer type, Long id) throws  IOException{
-        TempFile tempFile = uploadFile(file, type);
-        if (tempFile != null) {
-            migrateTempFilesToFormal(id, Collections.singletonList(tempFile.getTempId()), type);
-            return tempFile.getTempPath();
-        }
-        return null;
-    }
-
-    /**
-     * 灏嗕复鏃舵枃浠惰縼绉诲埌姝e紡鐩綍
-     *
-     * @param businessId  涓氬姟ID锛堥攢鍞彴璐D锛�
-     * @param tempFileIds 涓存椂鏂囦欢ID鍒楄〃
-     * @param fileType     鏂囦欢绫诲瀷(鏉ヨ嚜FileNameType)
-     * @throws IOException 鏂囦欢鎿嶄綔寮傚父
-     */
-    public void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds, Integer fileType) throws IOException {
-        if (com.baomidou.mybatisplus.core.toolkit.CollectionUtils.isEmpty(tempFileIds)) {
-            return;
-        }
-
-        // 鏋勫缓姝e紡鐩綍璺緞锛堟寜涓氬姟绫诲瀷鍜屾棩鏈熷垎缁勶級
-        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
-
-        Path formalDirPath = Paths.get(formalDir);
-
-        // 纭繚姝e紡鐩綍瀛樺湪锛堥�掑綊鍒涘缓锛�
-        if (!Files.exists(formalDirPath)) {
-            Files.createDirectories(formalDirPath);
-        }
-
-        for (String tempFileId : tempFileIds) {
-            // 鏌ヨ涓存椂鏂囦欢璁板綍
-            TempFile tempFile = tempFileMapper.selectById(tempFileId);
-            if (tempFile == null) {
-                log.warn("涓存椂鏂囦欢涓嶅瓨鍦紝璺宠繃澶勭悊: {}", tempFileId);
-                continue;
-            }
-
-            // 鏋勫缓姝e紡鏂囦欢鍚嶏紙鍖呭惈涓氬姟ID鍜屾椂闂存埑锛岄伩鍏嶅啿绐侊級
-            String originalFilename = tempFile.getOriginalName();
-            String fileExtension = FilenameUtils.getExtension(originalFilename);
-            String formalFilename = businessId + "_" +
-                    System.currentTimeMillis() + "_" +
-                    UUID.randomUUID().toString().substring(0, 8) +
-                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
-
-            Path formalFilePath = formalDirPath.resolve(formalFilename);
-
-            try {
-                // 鎵ц鏂囦欢杩佺Щ锛堜娇鐢ㄥ師瀛愭搷浣滅‘淇濆畨鍏ㄦ�э級
-//                Files.move(
-//                        Paths.get(tempFile.getTempPath()),
-//                        formalFilePath,
-//                        StandardCopyOption.REPLACE_EXISTING,
-//                        StandardCopyOption.ATOMIC_MOVE
-//                );
-                // 鍘熷瓙绉诲姩澶辫触锛屼娇鐢ㄥ鍒�+鍒犻櫎
-                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
-                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-
-                // 鏇存柊鏂囦欢璁板綍锛堝叧鑱斿埌涓氬姟ID锛�
-                CommonFile fileRecord = new CommonFile();
-                fileRecord.setCommonId(businessId);
-                fileRecord.setName(originalFilename);
-                fileRecord.setUrl(formalFilePath.toString());
-                fileRecord.setCreateTime(LocalDateTime.now());
-                fileRecord.setType(fileType);
-                fileRecord.setFileSize(tempFile.getFileSize());
-                commonFileMapper.insert(fileRecord);
-
-                // 鍒犻櫎涓存椂鏂囦欢璁板綍
-                tempFileMapper.deleteById(tempFile);
-
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-            } catch (IOException e) {
-                log.error("鏂囦欢杩佺Щ澶辫触: {}", tempFile.getTempPath(), e);
-                // 鍙�夋嫨鍥炴粴浜嬪姟鎴栬褰曞け璐ユ枃浠�
-                throw new IOException("鏂囦欢杩佺Щ寮傚父", e);
-            }
-        }
-    }
-
-//    @Scheduled(cron = "0 0 3 * * ?") // 姣忓ぉ鍑屾櫒3鐐规墽琛�
-    public void cleanupExpiredTempFiles() {
-        LambdaQueryWrapper<TempFile> wrapper = new LambdaQueryWrapper<>();
-        wrapper.lt(TempFile::getExpireTime, LocalDateTime.now()); // expireTime < 褰撳墠鏃堕棿
-
-        List<TempFile> expiredFiles = tempFileMapper.selectList(wrapper);
-        for (TempFile file : expiredFiles) {
-            try {
-                // 鍒犻櫎鐗╃悊鏂囦欢
-                Files.deleteIfExists(Paths.get(file.getTempPath()));
-                // 鍒犻櫎鏁版嵁搴撹褰�
-                tempFileMapper.deleteById(file);
-                log.info("宸叉竻鐞嗚繃鏈熶复鏃舵枃浠�: {}", file.getTempPath());
-            } catch (IOException e) {
-                log.error("鍒犻櫎鏂囦欢澶辫触: {}", file.getTempPath(), e);
-                // 鍙�夋嫨璁板綍澶辫触鏃ュ織鎴栭噸璇�
-            }
-        }
-        log.info("杩囨湡涓存椂鏂囦欢娓呯悊瀹屾垚锛屽叡娓呯悊 {} 涓枃浠�", expiredFiles.size());
-    }
-}
diff --git a/src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java b/src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
index 5c4f384..1434e24 100644
--- a/src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
+++ b/src/main/java/com/ruoyi/procurementrecord/utils/StockUtils.java
@@ -4,7 +4,6 @@
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
 import com.ruoyi.procurementrecord.mapper.ProcurementRecordOutMapper;
-import com.ruoyi.stock.dto.StockInRecordDto;
 import com.ruoyi.stock.dto.StockInventoryDto;
 import com.ruoyi.stock.dto.StockUninventoryDto;
 import com.ruoyi.stock.mapper.StockInventoryMapper;
@@ -14,15 +13,11 @@
 import com.ruoyi.stock.service.StockInventoryService;
 import com.ruoyi.stock.service.StockOutRecordService;
 import com.ruoyi.stock.service.StockUninventoryService;
-import com.ruoyi.stock.service.impl.StockInRecordServiceImpl;
-import com.ruoyi.stock.service.impl.StockOutRecordServiceImpl;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Component;
 
 import java.math.BigDecimal;
 import java.util.Collections;
-import java.util.HashMap;
-import java.util.Map;
 
 @Component
 @RequiredArgsConstructor
@@ -37,12 +32,13 @@
 
     /**
      * 涓嶅悎鏍煎叆搴�
+     *
      * @param productModelId
      * @param quantity
      * @param recordType
      * @param recordId
      */
-    public void addUnStock(Long productModelId, BigDecimal quantity, String recordType,Long recordId) {
+    public void addUnStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
         StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
         stockUninventoryDto.setRecordId(recordId);
         stockUninventoryDto.setRecordType(String.valueOf(recordType));
@@ -53,12 +49,13 @@
 
     /**
      * 涓嶅悎鏍煎嚭搴�
+     *
      * @param productModelId
      * @param quantity
      * @param recordType
      * @param recordId
      */
-    public void subtractUnStock(Long productModelId, BigDecimal quantity, Integer recordType,Long recordId) {
+    public void subtractUnStock(Long productModelId, BigDecimal quantity, Integer recordType, Long recordId) {
         StockUninventoryDto stockUninventoryDto = new StockUninventoryDto();
         stockUninventoryDto.setRecordId(recordId);
         stockUninventoryDto.setRecordType(String.valueOf(recordType));
@@ -74,7 +71,7 @@
      * @param recordType
      * @param recordId
      */
-    public void addStock(Long productModelId, BigDecimal quantity, String recordType,Long recordId) {
+    public void addStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
         StockInventoryDto stockInventoryDto = new StockInventoryDto();
         stockInventoryDto.setRecordId(recordId);
         stockInventoryDto.setRecordType(String.valueOf(recordType));
@@ -85,12 +82,13 @@
 
     /**
      * 鍚堟牸鍑哄簱
+     *
      * @param productModelId
      * @param quantity
      * @param recordType
      * @param recordId
      */
-    public void substractStock(Long productModelId, BigDecimal quantity, String recordType,Long recordId) {
+    public void substractStock(Long productModelId, BigDecimal quantity, String recordType, Long recordId) {
         StockInventoryDto stockInventoryDto = new StockInventoryDto();
         stockInventoryDto.setRecordId(recordId);
         stockInventoryDto.setRecordType(String.valueOf(recordType));
@@ -115,6 +113,7 @@
         }
 
     }
+
     public void deleteStockOutRecord(Long recordId, String recordType) {
         StockOutRecord one = stockOutRecordService.getOne(new QueryWrapper<StockOutRecord>()
                 .lambda().eq(StockOutRecord::getRecordId, recordId)
diff --git a/src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java b/src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java
index df444bd..decdc39 100644
--- a/src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java
+++ b/src/main/java/com/ruoyi/production/bean/vo/ProductionOperationTaskVo.java
@@ -34,4 +34,7 @@
 
     @Schema(description = "鎶ュ伐浜哄憳鍚嶇О锛屽涓娇鐢ㄩ�楀彿鍒嗛殧")
     private String userNames;
+
+    @Schema(description = "鏄惁缁撴潫锛�")
+    private Boolean endOrder;
 }
diff --git a/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java b/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java
index 0596973..f3b48a6 100644
--- a/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java
+++ b/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderVo.java
@@ -6,6 +6,7 @@
 import lombok.Data;
 import lombok.EqualsAndHashCode;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 @EqualsAndHashCode(callSuper = true)
@@ -32,4 +33,7 @@
 
     @Schema(description = "bom缂栧彿")
     private String bomNo;
+
+    @Schema(description = "瀹屾垚杩涘害")
+    private BigDecimal completionStatus;
 }
diff --git a/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java b/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java
new file mode 100644
index 0000000..ecbb0f2
--- /dev/null
+++ b/src/main/java/com/ruoyi/production/bean/vo/ProductionOrderWorkOrderDetailVo.java
@@ -0,0 +1,66 @@
+package com.ruoyi.production.bean.vo;
+
+import com.ruoyi.production.pojo.ProductionOperationTask;
+import com.ruoyi.production.pojo.ProductionOrderRoutingOperationParam;
+import com.ruoyi.production.pojo.ProductionProductMain;
+import com.ruoyi.production.pojo.ProductionProductOutput;
+import com.ruoyi.quality.pojo.QualityInspect;
+import com.ruoyi.quality.pojo.QualityInspectFile;
+import com.ruoyi.quality.pojo.QualityInspectParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.util.List;
+
+@Data
+@Schema(name = "ProductionOrderWorkOrderDetailVo", description = "Production order work order/report/inspect detail")
+public class ProductionOrderWorkOrderDetailVo {
+
+    @Schema(description = "Production order info")
+    private ProductionOrderVo productionOrder;
+
+    @Schema(description = "Work order list")
+    private List<WorkOrderDetail> workOrderList;
+
+    @Data
+    @Schema(name = "WorkOrderDetail", description = "Work order detail")
+    public static class WorkOrderDetail {
+
+        @Schema(description = "Work order info")
+        private ProductionOperationTask workOrder;
+
+        @Schema(description = "Report list under current work order")
+        private List<ReportDetail> reportList;
+    }
+
+    @Data
+    @Schema(name = "ReportDetail", description = "Production report detail")
+    public static class ReportDetail {
+
+        @Schema(description = "Report main info")
+        private ProductionProductMain reportMain;
+
+        @Schema(description = "Report output list")
+        private List<ProductionProductOutput> reportOutputList;
+
+        @Schema(description = "Report process param list")
+        private List<ProductionOrderRoutingOperationParam> reportParamList;
+
+        @Schema(description = "Inspect list under current report")
+        private List<InspectDetail> inspectList;
+    }
+
+    @Data
+    @Schema(name = "InspectDetail", description = "Quality inspect detail")
+    public static class InspectDetail {
+
+        @Schema(description = "Inspect main info")
+        private QualityInspect inspect;
+
+        @Schema(description = "Inspect param list")
+        private List<QualityInspectParam> inspectParamList;
+
+        @Schema(description = "Inspect attachment list")
+        private List<QualityInspectFile> inspectFileList;
+    }
+}
diff --git a/src/main/java/com/ruoyi/production/controller/ProductionOrderController.java b/src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
index c152736..53a22a7 100644
--- a/src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
+++ b/src/main/java/com/ruoyi/production/controller/ProductionOrderController.java
@@ -7,6 +7,7 @@
 import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
 import com.ruoyi.production.bean.vo.ProductionOrderVo;
 import com.ruoyi.production.bean.vo.ProductionPlanVo;
+import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
 import com.ruoyi.production.pojo.ProductionOrder;
 import com.ruoyi.production.service.ProductionOrderService;
 import io.swagger.v3.oas.annotations.Operation;
@@ -82,4 +83,16 @@
     public R<List<ProductionOrderPickVo>> pick(@PathVariable Long productionOrderId) {
         return R.ok(productionOrderService.pick(productionOrderId));
     }
+
+    @GetMapping("/workOrder/detail/{productionOrderId}")
+    @Operation(summary = "Query work orders/reports/inspects by production order id")
+    public R<ProductionOrderWorkOrderDetailVo> getWorkOrderReportInspectDetail(@PathVariable Long productionOrderId) {
+        return R.ok(productionOrderService.getWorkOrderReportInspectDetail(productionOrderId));
+    }
+
+    @Operation(summary = "鏇存柊璁㈠崟鐘舵��")
+    @PostMapping("/updateOrder")
+    public R updateOrder(@RequestBody ProductionOrderDto productionOrderDto) {
+        return R.ok(productionOrderService.updateOrder(productionOrderDto));
+    }
 }
diff --git a/src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java b/src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java
index 97fb2df..926a59f 100644
--- a/src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java
+++ b/src/main/java/com/ruoyi/production/controller/ProductionProductMainController.java
@@ -9,6 +9,7 @@
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
+import org.springframework.security.access.prepost.PreAuthorize;
 import org.springframework.web.bind.annotation.*;
 
 import java.util.List;
@@ -50,6 +51,7 @@
      * @return
      */
     @PostMapping("/addProductMain")
+    @PreAuthorize("@ss.hasPermi('productionProductMain:add')")
     public R addProductMain(@RequestBody ProductionProductMainDto productionProductMainDto) {
         return R.ok(productionProductMainService.addProductMain(productionProductMainDto));
     }
diff --git a/src/main/java/com/ruoyi/production/pojo/ProductionOrder.java b/src/main/java/com/ruoyi/production/pojo/ProductionOrder.java
index c7415b5..1f882b1 100644
--- a/src/main/java/com/ruoyi/production/pojo/ProductionOrder.java
+++ b/src/main/java/com/ruoyi/production/pojo/ProductionOrder.java
@@ -74,6 +74,10 @@
     @DateTimeFormat(pattern = "yyyy-MM-dd")
     private LocalDate planCompleteTime;
 
-    @Schema(description = "鐘舵�侊紙1.寰呭紑濮� 2.杩涜涓� 3.宸插畬鎴� 4.宸插彇娑堬級")
+    @Schema(description = "鐘舵�侊紙1.寰呭紑濮� 2.杩涜涓� 3.宸插畬鎴� 4.宸插彇娑� 5.宸茬粨鏉燂級")
     private Integer status;
+
+    @Schema(description = "鏄惁缁撴潫锛�")
+    @TableField("is_end_order")
+    private Boolean endOrder;
 }
diff --git a/src/main/java/com/ruoyi/production/service/ProductionOrderService.java b/src/main/java/com/ruoyi/production/service/ProductionOrderService.java
index fa1186c..7e588dc 100644
--- a/src/main/java/com/ruoyi/production/service/ProductionOrderService.java
+++ b/src/main/java/com/ruoyi/production/service/ProductionOrderService.java
@@ -7,6 +7,7 @@
 import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
 import com.ruoyi.production.bean.vo.ProductionOrderVo;
 import com.ruoyi.production.bean.vo.ProductionPlanVo;
+import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
 import com.ruoyi.production.pojo.ProductionOrder;
 
 import java.util.List;
@@ -30,4 +31,8 @@
     List<ProductionPlanVo> getSource(Long id);
 
     List<ProductionOrderPickVo> pick(Long productionOrderId);
+
+    ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(Long productionOrderId);
+
+    int updateOrder(ProductionOrderDto productionOrderDto);
 }
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 e202ec2..aad1350 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOperationTaskServiceImpl.java
@@ -22,7 +22,9 @@
 import com.ruoyi.common.utils.StringUtils;
 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.ProductionOperationTaskMapper;
+import com.ruoyi.production.pojo.ProductionOrder;
 import com.ruoyi.production.pojo.ProductionOperationTask;
 import com.ruoyi.production.service.ProductionOperationTaskService;
 import com.ruoyi.project.system.domain.SysUser;
@@ -45,6 +47,7 @@
 public class ProductionOperationTaskServiceImpl extends ServiceImpl<ProductionOperationTaskMapper, ProductionOperationTask> implements ProductionOperationTaskService {
 
     private final SysUserMapper sysUserMapper;
+    private final ProductionOrderMapper productionOrderMapper;
 
     private final FileUtil fileUtil;
 
@@ -75,6 +78,12 @@
             return null;
         }
         ProductionOperationTaskVo vo = BeanUtil.copyProperties(item, ProductionOperationTaskVo.class);
+        if (item.getProductionOrderId() != null) {
+            ProductionOrder productionOrder = productionOrderMapper.selectById(item.getProductionOrderId());
+            if (productionOrder != null) {
+                vo.setEndOrder(productionOrder.getEndOrder());
+            }
+        }
         fillUserNames(Collections.singletonList(vo));
         return vo;
     }
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
index beedbb5..72a237e 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderPickServiceImpl.java
@@ -3,6 +3,7 @@
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
 import com.ruoyi.common.exception.ServiceException;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.production.bean.dto.ProductionOrderPickDto;
@@ -18,6 +19,7 @@
 import com.ruoyi.stock.dto.StockInventoryDto;
 import com.ruoyi.stock.mapper.StockInventoryMapper;
 import com.ruoyi.stock.pojo.StockInventory;
+import com.ruoyi.stock.service.StockInventoryService;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
@@ -46,6 +48,7 @@
     private final ProductionOperationTaskMapper productionOperationTaskMapper;
     private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
     private final StockInventoryMapper stockInventoryMapper;
+    private final StockInventoryService stockInventoryService;
 
     @Override
     @Transactional(rollbackFor = Exception.class)
@@ -59,7 +62,7 @@
             List<String> batchNoList = resolveBatchNoList(resolvedDto);
             String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
             String storedBatchNo = formatBatchNoStorage(batchNoList);
-            subtractInventory(resolvedDto.getProductModelId(), inventoryBatchNo, resolvedDto.getPickQuantity(), rowNo);
+            subtractInventory(resolvedDto.getProductModelId(), storedBatchNo, resolvedDto.getPickQuantity(), rowNo);
 
             ProductionOrderPick orderPick = new ProductionOrderPick();
             orderPick.setProductionOrderId(resolvedDto.getProductionOrderId());
@@ -234,7 +237,7 @@
         List<String> batchNoList = resolveBatchNoList(dto);
         String inventoryBatchNo = pickInventoryBatchNo(batchNoList);
         String storedBatchNo = formatBatchNoStorage(batchNoList);
-        subtractInventory(dto.getProductModelId(), inventoryBatchNo, dto.getPickQuantity(), rowNo);
+        subtractInventory(dto.getProductModelId(), storedBatchNo, dto.getPickQuantity(), rowNo);
 
         ProductionOrderPick orderPick = new ProductionOrderPick();
         orderPick.setProductionOrderId(dto.getProductionOrderId());
@@ -296,7 +299,7 @@
         List<String> batchNoList = resolveBatchNoList(dto);
         String inventoryBatchNo = batchNoList.isEmpty()
                 ? resolveInventoryBatchNoFromStored(oldPick.getBatchNo())
-                : pickInventoryBatchNo(batchNoList);
+                : formatBatchNoStorage(batchNoList);
         BigDecimal feedingQuantity = dto.getFeedingQuantity();
 
         subtractInventory(productModelId, inventoryBatchNo, feedingQuantity, rowNo);
@@ -388,13 +391,13 @@
         if (sameStockKey) {
             BigDecimal delta = newQuantity.subtract(oldQuantity);
             if (delta.compareTo(BigDecimal.ZERO) > 0) {
-                subtractInventory(newProductModelId, newBatchNo, delta, rowNo);
+                subtractInventory(newProductModelId, newStoredBatchNo, delta, rowNo);
             } else if (delta.compareTo(BigDecimal.ZERO) < 0) {
                 addInventory(oldProductModelId, oldBatchNo, delta.abs());
             }
         } else {
             addInventory(oldProductModelId, oldBatchNo, oldQuantity);
-            subtractInventory(newProductModelId, newBatchNo, newQuantity, rowNo);
+            subtractInventory(newProductModelId, newStoredBatchNo, newQuantity, rowNo);
         }
 
         oldPick.setProductModelId(newProductModelId);
@@ -457,22 +460,61 @@
     }
 
     private void subtractInventory(Long productModelId, String batchNo, BigDecimal quantity, int rowNo) {
-        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
-        if (stockInventory == null) {
-            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欏搴斿簱瀛樹笉瀛樺湪");
+        BigDecimal deductQuantity = defaultDecimal(quantity);
+        if (deductQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+            return;
         }
-        BigDecimal availableQuantity = defaultDecimal(stockInventory.getQualitity())
-                .subtract(defaultDecimal(stockInventory.getLockedQuantity()));
-        if (quantity.compareTo(availableQuantity) > 0) {
-            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欏彲鐢ㄥ簱瀛樹笉瓒�");
+
+        List<String> batchNoList = parseBatchNoValue(batchNo);
+        if (batchNoList.isEmpty()) {
+            batchNoList = Collections.singletonList(null);
         }
-        StockInventoryDto stockInventoryDto = new StockInventoryDto();
-        stockInventoryDto.setProductModelId(productModelId);
-        stockInventoryDto.setBatchNo(batchNo);
-        stockInventoryDto.setQualitity(quantity);
-        int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
-        if (affected <= 0) {
-            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐墸鍑忓簱瀛樺け璐�");
+
+        Map<String, BigDecimal> availableQuantityMap = new LinkedHashMap<>();
+        BigDecimal totalAvailableQuantity = BigDecimal.ZERO;
+        for (String currentBatchNo : batchNoList) {
+            StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, currentBatchNo));
+            BigDecimal availableQuantity = BigDecimal.ZERO;
+            if (stockInventory != null) {
+                availableQuantity = defaultDecimal(stockInventory.getQualitity())
+                        .subtract(defaultDecimal(stockInventory.getLockedQuantity()));
+                if (availableQuantity.compareTo(BigDecimal.ZERO) < 0) {
+                    availableQuantity = BigDecimal.ZERO;
+                }
+            }
+            availableQuantityMap.put(currentBatchNo, availableQuantity);
+            totalAvailableQuantity = totalAvailableQuantity.add(availableQuantity);
+        }
+
+        if (deductQuantity.compareTo(totalAvailableQuantity) > 0) {
+            BigDecimal shortQuantity = deductQuantity.subtract(totalAvailableQuantity);
+            throw new ServiceException("棰嗘枡鍙敤搴撳瓨涓嶈冻锛屽彲鐢ㄥ簱瀛樹负" + formatQuantity(totalAvailableQuantity)
+                    + "锛岃繕宸�" + formatQuantity(shortQuantity));
+        }
+
+        BigDecimal remainingQuantity = deductQuantity;
+        for (Map.Entry<String, BigDecimal> entry : availableQuantityMap.entrySet()) {
+            if (remainingQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+                break;
+            }
+            BigDecimal availableQuantity = defaultDecimal(entry.getValue());
+            if (availableQuantity.compareTo(BigDecimal.ZERO) <= 0) {
+                continue;
+            }
+            BigDecimal currentDeductQuantity = remainingQuantity.min(availableQuantity);
+            StockInventoryDto stockInventoryDto = new StockInventoryDto();
+            stockInventoryDto.setProductModelId(productModelId);
+            stockInventoryDto.setBatchNo(entry.getKey());
+            stockInventoryDto.setQualitity(currentDeductQuantity);
+            int affected = stockInventoryMapper.updateSubtractStockInventory(stockInventoryDto);
+            if (affected <= 0) {
+                throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐墸鍑忓簱瀛樺け璐�");
+            }
+            remainingQuantity = remainingQuantity.subtract(currentDeductQuantity);
+        }
+
+        if (remainingQuantity.compareTo(BigDecimal.ZERO) > 0) {
+            throw new ServiceException("绗�" + rowNo + "鏉¢鏂欐墸鍑忓簱瀛樺け璐ワ紝鍓╀綑寰呮墸鍑忔暟閲忎负" + formatQuantity(remainingQuantity));
         }
     }
 
@@ -481,25 +523,13 @@
         if (addQuantity.compareTo(BigDecimal.ZERO) <= 0) {
             return;
         }
-        StockInventory stockInventory = stockInventoryMapper.selectOne(buildStockWrapper(productModelId, batchNo));
-        if (stockInventory == null) {
-            StockInventory newStockInventory = new StockInventory();
-            newStockInventory.setProductModelId(productModelId);
-            newStockInventory.setBatchNo(batchNo);
-            newStockInventory.setQualitity(addQuantity);
-            newStockInventory.setLockedQuantity(BigDecimal.ZERO);
-            newStockInventory.setVersion(1);
-            stockInventoryMapper.insert(newStockInventory);
-            return;
-        }
         StockInventoryDto stockInventoryDto = new StockInventoryDto();
         stockInventoryDto.setProductModelId(productModelId);
         stockInventoryDto.setBatchNo(batchNo);
         stockInventoryDto.setQualitity(addQuantity);
-        int affected = stockInventoryMapper.updateAddStockInventory(stockInventoryDto);
-        if (affected <= 0) {
-            throw new ServiceException("搴撳瓨鍥為��澶辫触锛屼骇鍝佽鏍糏D=" + productModelId);
-        }
+        stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PICK_RETURN_IN.getCode()));
+        stockInventoryDto.setRecordId(0L);
+        stockInventoryService.addStockInRecordOnly(stockInventoryDto);
     }
 
     private List<ProductionOrderPickDto> resolvePickItems(ProductionOrderPickDto dto) {
@@ -882,4 +912,8 @@
     private BigDecimal defaultDecimal(BigDecimal value) {
         return value == null ? BigDecimal.ZERO : value;
     }
+
+    private String formatQuantity(BigDecimal value) {
+        return defaultDecimal(value).stripTrailingZeros().toPlainString();
+    }
 }
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 f994bb6..90c5e5c 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionOrderServiceImpl.java
@@ -19,9 +19,16 @@
 import com.ruoyi.production.bean.vo.ProductionOrderPickVo;
 import com.ruoyi.production.bean.vo.ProductionOrderVo;
 import com.ruoyi.production.bean.vo.ProductionPlanVo;
+import com.ruoyi.production.bean.vo.ProductionOrderWorkOrderDetailVo;
 import com.ruoyi.production.enums.ProductOrderStatusEnum;
 import com.ruoyi.production.mapper.*;
 import com.ruoyi.production.pojo.*;
+import com.ruoyi.quality.mapper.QualityInspectFileMapper;
+import com.ruoyi.quality.mapper.QualityInspectMapper;
+import com.ruoyi.quality.mapper.QualityInspectParamMapper;
+import com.ruoyi.quality.pojo.QualityInspect;
+import com.ruoyi.quality.pojo.QualityInspectFile;
+import com.ruoyi.quality.pojo.QualityInspectParam;
 import com.ruoyi.production.service.ProductionOrderService;
 import com.ruoyi.sales.mapper.SalesLedgerMapper;
 import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
@@ -51,8 +58,12 @@
     private final ProductionOrderBomMapper productionOrderBomMapper;
     private final ProductionBomStructureMapper productionBomStructureMapper;
     private final ProductionProductMainMapper productionProductMainMapper;
+    private final ProductionProductOutputMapper productionProductOutputMapper;
     private final ProductionOrderPickMapper productionOrderPickMapper;
     private final ProductionOrderPickRecordMapper productionOrderPickRecordMapper;
+    private final QualityInspectMapper qualityInspectMapper;
+    private final QualityInspectParamMapper qualityInspectParamMapper;
+    private final QualityInspectFileMapper qualityInspectFileMapper;
     private final ProductionPlanMapper productionPlanMapper;
     private final StockInventoryMapper stockInventoryMapper;
     private final StorageAttachmentMapper storageAttachmentMapper;
@@ -219,8 +230,8 @@
         List<TechnologyRoutingOperation> routingOperations = technologyRoutingOperationMapper.selectList(
                 Wrappers.<TechnologyRoutingOperation>lambdaQuery()
                         .eq(TechnologyRoutingOperation::getTechnologyRoutingId, technologyRouting.getId())
-                        .orderByAsc(TechnologyRoutingOperation::getDragSort)
-                        .orderByAsc(TechnologyRoutingOperation::getId));
+                        .orderByDesc(TechnologyRoutingOperation::getDragSort)
+                        .orderByDesc(TechnologyRoutingOperation::getId));
         Map<Long, String> operationNameMap = technologyOperationMapper.selectBatchIds(
                         routingOperations.stream()
                                 .map(TechnologyRoutingOperation::getTechnologyOperationId)
@@ -228,6 +239,11 @@
                                 .collect(Collectors.toSet()))
                 .stream()
                 .collect(Collectors.toMap(TechnologyOperation::getId, TechnologyOperation::getName, (a, b) -> a));
+        Integer lastDragSort = routingOperations.stream()
+                .map(TechnologyRoutingOperation::getDragSort)
+                .filter(Objects::nonNull)
+                .max(Integer::compareTo)
+                .orElse(null);
         for (TechnologyRoutingOperation sourceOperation : routingOperations) {
             // 璁㈠崟宸ュ簭淇濆瓨鐨勬槸宸ヨ壓宸ュ簭蹇収锛屽悗缁姤宸ュ彧渚濊禆蹇収锛屼笉鍐嶇洿鎺ュ紩鐢ㄥ伐鑹轰富鏁版嵁銆�
             ProductionOrderRoutingOperation targetOperation = new ProductionOrderRoutingOperation();
@@ -236,19 +252,23 @@
             targetOperation.setOrderRoutingId(orderRouting.getId());
             targetOperation.setProductModelId(sourceOperation.getProductModelId());
             targetOperation.setDragSort(sourceOperation.getDragSort());
+            targetOperation.setIsProduction(sourceOperation.getIsProduction());
             targetOperation.setIsQuality(sourceOperation.getIsQuality());
             targetOperation.setOperationName(operationNameMap.get(sourceOperation.getTechnologyOperationId()));
             targetOperation.setTechnologyOperationId(sourceOperation.getTechnologyOperationId());
             productionOrderRoutingOperationMapper.insert(targetOperation);
 
-            ProductionOperationTask task = new ProductionOperationTask();
-            task.setProductionOrderRoutingOperationId(targetOperation.getId());
-            task.setProductionOrderId(productionOrder.getId());
-            task.setPlanQuantity(defaultDecimal(productionOrder.getQuantity()));
-            task.setCompleteQuantity(BigDecimal.ZERO);
-            task.setWorkOrderNo(generateNextTaskNo());
-            task.setStatus(2);
-            productionOperationTaskMapper.insert(task);
+            boolean isLastOperation = lastDragSort != null && Objects.equals(sourceOperation.getDragSort(), lastDragSort);
+            if (isLastOperation || Boolean.TRUE.equals(targetOperation.getIsProduction())) {
+                ProductionOperationTask task = new ProductionOperationTask();
+                task.setProductionOrderRoutingOperationId(targetOperation.getId());
+                task.setProductionOrderId(productionOrder.getId());
+                task.setPlanQuantity(defaultDecimal(productionOrder.getQuantity()));
+                task.setCompleteQuantity(BigDecimal.ZERO);
+                task.setWorkOrderNo(generateNextTaskNo());
+                task.setStatus(2);
+                productionOperationTaskMapper.insert(task);
+            }
 
             List<TechnologyRoutingOperationParam> sourceParams = technologyRoutingOperationParamMapper.selectList(
                     Wrappers.<TechnologyRoutingOperationParam>lambdaQuery()
@@ -661,6 +681,168 @@
     }
 
     @Override
+    public ProductionOrderWorkOrderDetailVo getWorkOrderReportInspectDetail(Long productionOrderId) {
+        if (productionOrderId == null) {
+            throw new ServiceException("productionOrderId can not be null");
+        }
+        ProductionOrderVo orderInfo = getProductionOrderInfo(productionOrderId);
+        if (orderInfo == null) {
+            throw new ServiceException("production order not found");
+        }
+
+        ProductionOrderWorkOrderDetailVo detailVo = new ProductionOrderWorkOrderDetailVo();
+        detailVo.setProductionOrder(orderInfo);
+
+        List<ProductionOperationTask> workOrderList = productionOperationTaskMapper.selectList(
+                Wrappers.<ProductionOperationTask>lambdaQuery()
+                        .eq(ProductionOperationTask::getProductionOrderId, productionOrderId)
+                        .orderByAsc(ProductionOperationTask::getId));
+        if (workOrderList == null || workOrderList.isEmpty()) {
+            detailVo.setWorkOrderList(Collections.emptyList());
+            return detailVo;
+        }
+
+        List<Long> workOrderIdList = workOrderList.stream()
+                .map(ProductionOperationTask::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        List<ProductionProductMain> reportMainList = workOrderIdList.isEmpty()
+                ? Collections.emptyList()
+                : productionProductMainMapper.selectList(
+                Wrappers.<ProductionProductMain>lambdaQuery()
+                        .in(ProductionProductMain::getProductionOperationTaskId, workOrderIdList)
+                        .orderByAsc(ProductionProductMain::getId));
+        Map<Long, List<ProductionProductMain>> reportMainMap = new LinkedHashMap<>();
+        for (ProductionProductMain reportMain : reportMainList) {
+            if (reportMain == null || reportMain.getProductionOperationTaskId() == null) {
+                continue;
+            }
+            reportMainMap.computeIfAbsent(reportMain.getProductionOperationTaskId(), k -> new ArrayList<>()).add(reportMain);
+        }
+
+        List<Long> reportMainIdList = reportMainList.stream()
+                .map(ProductionProductMain::getId)
+                .filter(Objects::nonNull)
+                .collect(Collectors.toList());
+        Map<Long, List<ProductionProductOutput>> reportOutputMap = new LinkedHashMap<>();
+        Map<Long, List<ProductionOrderRoutingOperationParam>> reportParamMap = new LinkedHashMap<>();
+        Map<Long, List<QualityInspect>> inspectMap = new LinkedHashMap<>();
+        Map<Long, List<QualityInspectParam>> inspectParamMap = new LinkedHashMap<>();
+        Map<Long, List<QualityInspectFile>> inspectFileMap = new LinkedHashMap<>();
+        if (!reportMainIdList.isEmpty()) {
+            List<ProductionProductOutput> reportOutputList = productionProductOutputMapper.selectList(
+                    Wrappers.<ProductionProductOutput>lambdaQuery()
+                            .in(ProductionProductOutput::getProductionProductMainId, reportMainIdList)
+                            .orderByAsc(ProductionProductOutput::getId));
+            for (ProductionProductOutput reportOutput : reportOutputList) {
+                if (reportOutput == null) {
+                    continue;
+                }
+                Long reportMainId = reportOutput.getProductionProductMainId() != null
+                        ? reportOutput.getProductionProductMainId()
+                        : reportOutput.getProductMainId();
+                if (reportMainId == null) {
+                    continue;
+                }
+                reportOutputMap.computeIfAbsent(reportMainId, k -> new ArrayList<>()).add(reportOutput);
+            }
+
+            List<ProductionOrderRoutingOperationParam> reportParamList = productionOrderRoutingOperationParamMapper.selectList(
+                    Wrappers.<ProductionOrderRoutingOperationParam>lambdaQuery()
+                            .in(ProductionOrderRoutingOperationParam::getProductionProductMainId, reportMainIdList)
+                            .orderByAsc(ProductionOrderRoutingOperationParam::getId));
+            for (ProductionOrderRoutingOperationParam reportParam : reportParamList) {
+                if (reportParam == null || reportParam.getProductionProductMainId() == null) {
+                    continue;
+                }
+                reportParamMap.computeIfAbsent(reportParam.getProductionProductMainId(), k -> new ArrayList<>()).add(reportParam);
+            }
+
+            List<QualityInspect> inspectList = qualityInspectMapper.selectList(
+                    Wrappers.<QualityInspect>lambdaQuery()
+                            .in(QualityInspect::getProductMainId, reportMainIdList)
+                            .orderByAsc(QualityInspect::getId));
+            for (QualityInspect inspect : inspectList) {
+                if (inspect == null || inspect.getProductMainId() == null) {
+                    continue;
+                }
+                inspectMap.computeIfAbsent(inspect.getProductMainId(), k -> new ArrayList<>()).add(inspect);
+            }
+
+            List<Long> inspectIdList = inspectList.stream()
+                    .map(QualityInspect::getId)
+                    .filter(Objects::nonNull)
+                    .collect(Collectors.toList());
+            if (!inspectIdList.isEmpty()) {
+                List<QualityInspectParam> inspectParamList = qualityInspectParamMapper.selectList(
+                        Wrappers.<QualityInspectParam>lambdaQuery()
+                                .in(QualityInspectParam::getInspectId, inspectIdList)
+                                .orderByAsc(QualityInspectParam::getId));
+                for (QualityInspectParam inspectParam : inspectParamList) {
+                    if (inspectParam == null || inspectParam.getInspectId() == null) {
+                        continue;
+                    }
+                    inspectParamMap.computeIfAbsent(inspectParam.getInspectId(), k -> new ArrayList<>()).add(inspectParam);
+                }
+
+                List<QualityInspectFile> inspectFileList = qualityInspectFileMapper.selectList(
+                        Wrappers.<QualityInspectFile>lambdaQuery()
+                                .in(QualityInspectFile::getInspectId, inspectIdList)
+                                .orderByAsc(QualityInspectFile::getId));
+                for (QualityInspectFile inspectFile : inspectFileList) {
+                    if (inspectFile == null || inspectFile.getInspectId() == null) {
+                        continue;
+                    }
+                    inspectFileMap.computeIfAbsent(inspectFile.getInspectId(), k -> new ArrayList<>()).add(inspectFile);
+                }
+            }
+        }
+
+        List<ProductionOrderWorkOrderDetailVo.WorkOrderDetail> workOrderDetailList = new ArrayList<>();
+        for (ProductionOperationTask workOrder : workOrderList) {
+            ProductionOrderWorkOrderDetailVo.WorkOrderDetail workOrderDetail = new ProductionOrderWorkOrderDetailVo.WorkOrderDetail();
+            workOrderDetail.setWorkOrder(workOrder);
+
+            List<ProductionProductMain> workOrderReportMainList = reportMainMap.get(workOrder.getId());
+            if (workOrderReportMainList == null || workOrderReportMainList.isEmpty()) {
+                workOrderDetail.setReportList(Collections.emptyList());
+                workOrderDetailList.add(workOrderDetail);
+                continue;
+            }
+
+            List<ProductionOrderWorkOrderDetailVo.ReportDetail> reportDetailList = new ArrayList<>();
+            for (ProductionProductMain reportMain : workOrderReportMainList) {
+                Long reportMainId = reportMain.getId();
+                ProductionOrderWorkOrderDetailVo.ReportDetail reportDetail = new ProductionOrderWorkOrderDetailVo.ReportDetail();
+                reportDetail.setReportMain(reportMain);
+                reportDetail.setReportOutputList(reportOutputMap.getOrDefault(reportMainId, Collections.emptyList()));
+                reportDetail.setReportParamList(reportParamMap.getOrDefault(reportMainId, Collections.emptyList()));
+
+                List<QualityInspect> reportInspectList = inspectMap.get(reportMainId);
+                if (reportInspectList == null || reportInspectList.isEmpty()) {
+                    reportDetail.setInspectList(Collections.emptyList());
+                } else {
+                    List<ProductionOrderWorkOrderDetailVo.InspectDetail> inspectDetailList = new ArrayList<>();
+                    for (QualityInspect inspect : reportInspectList) {
+                        ProductionOrderWorkOrderDetailVo.InspectDetail inspectDetail = new ProductionOrderWorkOrderDetailVo.InspectDetail();
+                        inspectDetail.setInspect(inspect);
+                        inspectDetail.setInspectParamList(inspectParamMap.getOrDefault(inspect.getId(), Collections.emptyList()));
+                        inspectDetail.setInspectFileList(inspectFileMap.getOrDefault(inspect.getId(), Collections.emptyList()));
+                        inspectDetailList.add(inspectDetail);
+                    }
+                    reportDetail.setInspectList(inspectDetailList);
+                }
+                reportDetailList.add(reportDetail);
+            }
+            workOrderDetail.setReportList(reportDetailList);
+            workOrderDetailList.add(workOrderDetail);
+        }
+
+        detailVo.setWorkOrderList(workOrderDetailList);
+        return detailVo;
+    }
+
+    @Override
     public List<ProductionOrderPickVo> pick(Long productionOrderId) {
         if (productionOrderId == null) {
             return Collections.emptyList();
@@ -733,4 +915,10 @@
         }
         return new ArrayList<>(mergedPickMap.values());
     }
+
+    @Override
+    public int updateOrder(ProductionOrderDto productionOrderDto) {
+        productionOrderDto.setStatus(5);
+        return baseMapper.updateById(productionOrderDto);
+    }
 }
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
index 216aafa..150fc89 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionProductMainServiceImpl.java
@@ -27,6 +27,8 @@
 import com.ruoyi.project.system.mapper.SysUserMapper;
 import com.ruoyi.quality.mapper.*;
 import com.ruoyi.quality.pojo.*;
+import com.ruoyi.stock.dto.StockInventoryDto;
+import com.ruoyi.stock.service.StockInventoryService;
 import com.ruoyi.technology.mapper.TechnologyOperationMapper;
 import com.ruoyi.technology.mapper.TechnologyRoutingOperationMapper;
 import com.ruoyi.technology.pojo.TechnologyOperation;
@@ -76,6 +78,7 @@
     private final TechnologyRoutingOperationMapper technologyRoutingOperationMapper;
     private final TechnologyOperationMapper technologyOperationMapper;
     private final StockUtils stockUtils;
+    private final StockInventoryService stockInventoryService;
 
     @Override
     public IPage<ProductionProductMainDto> listPageProductionProductMainDto(Page page, ProductionProductMainDto productionProductMainDto) {
@@ -334,8 +337,12 @@
                             });
                 }
             } else {
-                stockUtils.addStock(productModel.getId(), productQty,
-                        StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode(), productionProductMain.getId());
+                StockInventoryDto stockInventoryDto = new StockInventoryDto();
+                stockInventoryDto.setRecordId(productionProductMain.getId());
+                stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.PRODUCTION_REPORT_STOCK_IN.getCode()));
+                stockInventoryDto.setQualitity(productQty);
+                stockInventoryDto.setProductModelId(productModel.getId());
+                stockInventoryService.addStockInRecordOnly(stockInventoryDto);
             }
 
             productionOperationTask.setCompleteQuantity(defaultDecimal(productionOperationTask.getCompleteQuantity()).add(productQty));
@@ -384,10 +391,10 @@
             productionAccount.setSchedulingDate(LocalDateTime.now());
             productionAccountMapper.insert(productionAccount);
         }
-        if (defaultDecimal(dto.getScrapQty()).compareTo(BigDecimal.ZERO) > 0) {
-            stockUtils.addUnStock(productModel.getId(), dto.getScrapQty(),
-                    StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(), productionProductMain.getId());
-        }
+//        if (defaultDecimal(dto.getScrapQty()).compareTo(BigDecimal.ZERO) > 0) {
+//            stockUtils.addUnStock(productModel.getId(), dto.getScrapQty(),
+//                    StockInUnQualifiedRecordTypeEnum.PRODUCTION_SCRAP.getCode(), productionProductMain.getId());
+//        }
         return true;
     }
 
diff --git a/src/main/java/com/ruoyi/project/common/CommonController.java b/src/main/java/com/ruoyi/project/common/CommonController.java
index 9ee3c93..093e132 100644
--- a/src/main/java/com/ruoyi/project/common/CommonController.java
+++ b/src/main/java/com/ruoyi/project/common/CommonController.java
@@ -8,8 +8,6 @@
 import io.swagger.v3.oas.annotations.tags.Tag;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 import org.springframework.core.io.FileSystemResource;
 import org.springframework.http.ContentDisposition;
 import org.springframework.http.MediaType;
@@ -33,99 +31,11 @@
 @RestController
 @RequestMapping("/common")
 public class CommonController {
-    private static final Logger log = LoggerFactory.getLogger(CommonController.class);
 
 
     private final StorageBlobService storageBlobService;
     private final FileUtil fileUtil;
 
-
-//    /**
-//     * 閫氱敤涓嬭浇璇锋眰
-//     *
-//     * @param fileName 鏂囦欢鍚嶇О
-//     * @param delete 鏄惁鍒犻櫎
-//     */
-//    @GetMapping("/download")
-//    public void fileDownload(String fileName, Boolean delete, HttpServletResponse response, HttpServletRequest request)
-//    {
-//        try
-//        {
-//            if (!FileUtils.checkAllowDownload(fileName))
-//            {
-//                throw new Exception(StringUtils.format("鏂囦欢鍚嶇О({})闈炴硶锛屼笉鍏佽涓嬭浇銆� ", fileName));
-//            }
-//            String realFileName =  fileName.substring(fileName.indexOf("_") + 1);
-//
-//            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
-//            FileUtils.setAttachmentResponseHeader(response, realFileName);
-//            FileUtils.writeBytes(fileName, response.getOutputStream());
-
-    /// /            if (delete)
-    /// /            {
-    /// /                FileUtils.deleteFile(fileName);
-    /// /            }
-//        }
-//        catch (Exception e)
-//        {
-//            log.error("涓嬭浇鏂囦欢澶辫触", e);
-//        }
-//    }
-//
-//    /**
-//     * 閫氱敤涓婁紶璇锋眰锛堝崟涓級
-//     */
-//    @PostMapping("/upload")
-//    public AjaxResult uploadFile(MultipartFile file) throws Exception
-//    {
-//        try
-//        {
-//            // 涓婁紶鏂囦欢璺緞
-//            String filePath = RuoYiConfig.getUploadPath();
-//            // 涓婁紶骞惰繑鍥炴柊鏂囦欢鍚嶇О
-//            String fileName = FileUploadUtils.upload(filePath, file);
-//            String url = serverConfig.getUrl() + fileName;
-//            AjaxResult ajax = AjaxResult.success();
-//            ajax.put("url", url);
-//            ajax.put("fileName", fileName);
-//            ajax.put("newFileName", FileUtils.getName(fileName));
-//            ajax.put("originalFilename", file.getOriginalFilename());
-//            return ajax;
-//        }
-//        catch (Exception e)
-//        {
-//            return AjaxResult.error(e.getMessage());
-//        }
-//    }
-//
-//    /**
-//     * 鏈湴璧勬簮閫氱敤涓嬭浇
-//     */
-//    @GetMapping("/download/resource")
-//    public void resourceDownload(String resource, HttpServletRequest request, HttpServletResponse response)
-//            throws Exception
-//    {
-//        try
-//        {
-//            if (!FileUtils.checkAllowDownload(resource))
-//            {
-//                throw new Exception(StringUtils.format("璧勬簮鏂囦欢({})闈炴硶锛屼笉鍏佽涓嬭浇銆� ", resource));
-//            }
-//            // 鏈湴璧勬簮璺緞
-//            String localPath = RuoYiConfig.getProfile();
-//            // 鏁版嵁搴撹祫婧愬湴鍧�
-//            String downloadPath = localPath + StringUtils.substringAfter(resource, Constants.RESOURCE_PREFIX);
-//            // 涓嬭浇鍚嶇О
-//            String downloadName = StringUtils.substringAfterLast(downloadPath, "/");
-//            response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE);
-//            FileUtils.setAttachmentResponseHeader(response, downloadName);
-//            FileUtils.writeBytes(downloadPath, response.getOutputStream());
-//        }
-//        catch (Exception e)
-//        {
-//            log.error("涓嬭浇鏂囦欢澶辫触", e);
-//        }
-//    }
     @PostMapping({"/upload"})
     @Operation(summary = "鏂囦欢涓婁紶")
     public R upload(@RequestParam("files") List<MultipartFile> files) {
diff --git a/src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java b/src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java
index ba77143..afd9213 100644
--- a/src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java
+++ b/src/main/java/com/ruoyi/purchase/controller/InvoicePurchaseController.java
@@ -10,11 +10,10 @@
 import com.ruoyi.purchase.pojo.InvoicePurchase;
 import com.ruoyi.purchase.service.IInvoicePurchaseService;
 import com.ruoyi.sales.service.ICommonFileService;
+import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
 
-import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.List;
 
@@ -80,12 +79,4 @@
         return toAjax(invoicePurchaseService.delInvoice(ids));
     }
 
-    @PostMapping("/upload")
-    public AjaxResult uploadFile(MultipartFile file, Long id, Integer type) {
-        try {
-            return AjaxResult.success(commonFileService.uploadFile(file, id, type));
-        } catch (Exception e) {
-            return AjaxResult.error(e.getMessage());
-        }
-    }
 }
diff --git a/src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java b/src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java
index 2395276..db628af 100644
--- a/src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java
+++ b/src/main/java/com/ruoyi/purchase/controller/TicketRegistrationController.java
@@ -19,18 +19,15 @@
 import com.ruoyi.purchase.service.ITicketRegistrationService;
 import com.ruoyi.purchase.service.impl.PaymentRegistrationServiceImpl;
 import com.ruoyi.sales.service.ICommonFileService;
-import io.swagger.v3.oas.annotations.tags.Tag;
-import io.swagger.v3.oas.annotations.media.Schema;
 import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.media.Schema;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
-import org.springframework.beans.factory.annotation.Autowired;
-import org.springframework.security.core.parameters.P;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.*;
-import org.springframework.web.multipart.MultipartFile;
 
-import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
 import java.util.List;
 
@@ -161,15 +158,6 @@
     @Transactional(rollbackFor = Exception.class)
     public AjaxResult delRegistration(@RequestBody Long[] ids) {
         return toAjax(ticketRegistrationService.delRegistration(ids));
-    }
-
-    @PostMapping("/upload")
-    public AjaxResult uploadFile(MultipartFile file, Long id, Integer type) {
-        try {
-            return AjaxResult.success(commonFileService.uploadFile(file, id, type));
-        } catch (Exception e) {
-            return AjaxResult.error(e.getMessage());
-        }
     }
 
     /**
diff --git a/src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java b/src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
index 3b52fe5..c6b1406 100644
--- a/src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
+++ b/src/main/java/com/ruoyi/purchase/dto/PurchaseLedgerDto.java
@@ -2,6 +2,8 @@
 
 import com.baomidou.mybatisplus.annotation.TableField;
 import com.fasterxml.jackson.annotation.JsonFormat;
+import com.ruoyi.basic.dto.StorageBlobDTO;
+import com.ruoyi.basic.dto.StorageBlobVO;
 import com.ruoyi.framework.aspectj.lang.annotation.Excel;
 import com.ruoyi.sales.pojo.CommonFile;
 import com.ruoyi.sales.pojo.SalesLedgerProduct;
@@ -197,4 +199,7 @@
     private String templateName;
     @Schema(description = "瀹℃壒浜篿d")
     private Integer approverId;
+
+    private List<StorageBlobVO> storageBlobVOS;
+    private List<StorageBlobDTO> storageBlobDTOS;
 }
diff --git a/src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java b/src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java
index 0187823..8b76b1a 100644
--- a/src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java
+++ b/src/main/java/com/ruoyi/purchase/service/impl/InvoicePurchaseServiceImpl.java
@@ -50,10 +50,7 @@
 
     private final CommonFileMapper commonFileMapper;
 
-    private final TempFileMapper tempFileMapper;
 
-    @Value("${file.upload-dir}")
-    private String uploadDir;
 
     @Override
     public List<InvoicePurchaseDto> selectInvoicePurchaseList(InvoicePurchaseDto invoicePurchaseDto) {
diff --git a/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java b/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
index 700836d..c4a48cd 100644
--- a/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/purchase/service/impl/PurchaseLedgerServiceImpl.java
@@ -6,15 +6,18 @@
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
 import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.approve.bean.vo.ApproveProcessVO;
 import com.ruoyi.approve.pojo.ApproveProcess;
 import com.ruoyi.approve.service.impl.ApproveProcessServiceImpl;
-import com.ruoyi.approve.bean.vo.ApproveProcessVO;
+import com.ruoyi.basic.enums.ApplicationTypeEnum;
+import com.ruoyi.basic.enums.RecordTypeEnum;
 import com.ruoyi.basic.mapper.ProductMapper;
 import com.ruoyi.basic.mapper.ProductModelMapper;
 import com.ruoyi.basic.mapper.SupplierManageMapper;
 import com.ruoyi.basic.pojo.Product;
 import com.ruoyi.basic.pojo.ProductModel;
 import com.ruoyi.basic.pojo.SupplierManage;
+import com.ruoyi.basic.utils.FileUtil;
 import com.ruoyi.common.enums.FileNameType;
 import com.ruoyi.common.exception.base.BaseException;
 import com.ruoyi.common.utils.SecurityUtils;
@@ -23,7 +26,6 @@
 import com.ruoyi.framework.security.LoginUser;
 import com.ruoyi.framework.web.domain.AjaxResult;
 import com.ruoyi.other.mapper.TempFileMapper;
-import com.ruoyi.other.pojo.TempFile;
 import com.ruoyi.procurementrecord.mapper.ProcurementRecordMapper;
 import com.ruoyi.procurementrecord.pojo.ProcurementRecordStorage;
 import com.ruoyi.project.system.domain.SysUser;
@@ -59,22 +61,15 @@
 import com.ruoyi.sales.service.impl.CommonFileServiceImpl;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FilenameUtils;
 import org.springframework.beans.BeanUtils;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.data.redis.core.StringRedisTemplate;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.ZoneId;
@@ -116,9 +111,7 @@
     private final QualityInspectParamMapper qualityInspectParamMapper;
     private final ApproveProcessServiceImpl approveProcessService;
     private final ProcurementRecordMapper procurementRecordStorageMapper;
-
-    @Value("${file.upload-dir}")
-    private String uploadDir;
+    private final FileUtil fileUtil;
 
     @Override
     public List<PurchaseLedger> selectPurchaseLedgerList(PurchaseLedger purchaseLedger) {
@@ -181,7 +174,7 @@
             }
             purchaseLedgerMapper.updateById(purchaseLedger);
         }
-        // 6.閲囪喘瀹℃牳鏂板
+        // 6.閲囪喘瀹℃牳鏂板锛涘鎵圭鐞嗘湭閰嶇疆閲囪喘瀹℃壒浜烘椂锛屽鎵规湇鍔′細鑷姩缃负瀹℃壒閫氳繃銆�
         addApproveByPurchase(loginUser, purchaseLedger);
 
         // 4. 澶勭悊瀛愯〃鏁版嵁
@@ -199,9 +192,7 @@
 //            }
 //        }
         // 5. 杩佺Щ涓存椂鏂囦欢鍒版寮忕洰褰�
-        if (purchaseLedgerDto.getTempFileIds() != null && !purchaseLedgerDto.getTempFileIds().isEmpty()) {
-            migrateTempFilesToFormal(purchaseLedger.getId(), purchaseLedgerDto.getTempFileIds());
-        }
+        fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.PURCHASE_LEDGER, purchaseLedger.getId(), purchaseLedgerDto.getStorageBlobDTOS());
         return 1;
     }
 
@@ -238,6 +229,7 @@
         if (products == null || products.isEmpty()) {
             throw new BaseException("浜у搧淇℃伅涓嶅瓨鍦�");
         }
+        Integer ledgerType = type == null ? 2 : type;
 
         // 鎻愬墠鏀堕泦鎵�鏈夐渶瑕佹煡璇㈢殑ID
         Set<Long> productIds = products.stream()
@@ -289,14 +281,14 @@
         // 鎵ц鏇存柊鎿嶄綔
         if (!updateList.isEmpty()) {
             for (SalesLedgerProduct product : updateList) {
-                product.setType(type);
+                product.setType(ledgerType);
                 salesLedgerProductMapper.updateById(product);
             }
         }
         // 鎵ц鎻掑叆鎿嶄綔
         if (!insertList.isEmpty()) {
             for (SalesLedgerProduct salesLedgerProduct : insertList) {
-                salesLedgerProduct.setType(type);
+                salesLedgerProduct.setType(ledgerType);
                 Date entryDate = purchaseLedger.getEntryDate();
 
                 LocalDateTime localDateTime = entryDate.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime();
@@ -318,80 +310,6 @@
         if (salesLedgerId != null) {
             // 鐩存帴鏇存柊鎸囧畾ID鐨勮褰曠殑contractAmount瀛楁涓簍otalTaxInclusiveAmount
             purchaseLedgerMapper.updateContractAmountById(salesLedgerId, totalTaxInclusiveAmount);
-        }
-    }
-
-    /**
-     * 灏嗕复鏃舵枃浠惰縼绉诲埌姝e紡鐩綍
-     *
-     * @param businessId  涓氬姟ID锛堥攢鍞彴璐D锛�
-     * @param tempFileIds 涓存椂鏂囦欢ID鍒楄〃
-     * @throws IOException 鏂囦欢鎿嶄綔寮傚父
-     */
-    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
-        if (CollectionUtils.isEmpty(tempFileIds)) {
-            return;
-        }
-
-        // 鏋勫缓姝e紡鐩綍璺緞锛堟寜涓氬姟绫诲瀷鍜屾棩鏈熷垎缁勶級
-        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
-
-        Path formalDirPath = Paths.get(formalDir);
-
-        // 纭繚姝e紡鐩綍瀛樺湪锛堥�掑綊鍒涘缓锛�
-        if (!Files.exists(formalDirPath)) {
-            Files.createDirectories(formalDirPath);
-        }
-
-        for (String tempFileId : tempFileIds) {
-            // 鏌ヨ涓存椂鏂囦欢璁板綍
-            TempFile tempFile = tempFileMapper.selectById(tempFileId);
-            if (tempFile == null) {
-                log.warn("涓存椂鏂囦欢涓嶅瓨鍦紝璺宠繃澶勭悊: {}", tempFileId);
-                continue;
-            }
-
-            // 鏋勫缓姝e紡鏂囦欢鍚嶏紙鍖呭惈涓氬姟ID鍜屾椂闂存埑锛岄伩鍏嶅啿绐侊級
-            String originalFilename = tempFile.getOriginalName();
-            String fileExtension = FilenameUtils.getExtension(originalFilename);
-            String formalFilename = businessId + "_" +
-                    System.currentTimeMillis() + "_" +
-                    UUID.randomUUID().toString().substring(0, 8) +
-                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
-
-            Path formalFilePath = formalDirPath.resolve(formalFilename);
-
-            try {
-                // 鎵ц鏂囦欢杩佺Щ锛堜娇鐢ㄥ師瀛愭搷浣滅‘淇濆畨鍏ㄦ�э級
-//                Files.move(
-//                        Paths.get(tempFile.getTempPath()),
-//                        formalFilePath,
-//                        StandardCopyOption.REPLACE_EXISTING,
-//                        StandardCopyOption.ATOMIC_MOVE
-//                );
-                // 鍘熷瓙绉诲姩澶辫触锛屼娇鐢ㄥ鍒�+鍒犻櫎
-                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
-                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-
-                // 鏇存柊鏂囦欢璁板綍锛堝叧鑱斿埌涓氬姟ID锛�
-                CommonFile fileRecord = new CommonFile();
-                fileRecord.setCommonId(businessId);
-                fileRecord.setName(originalFilename);
-                fileRecord.setUrl(formalFilePath.toString());
-                fileRecord.setCreateTime(LocalDateTime.now());
-                fileRecord.setType(FileNameType.PURCHASE.getValue());
-                commonFileMapper.insert(fileRecord);
-
-                // 鍒犻櫎涓存椂鏂囦欢璁板綍
-                tempFileMapper.deleteById(tempFile);
-
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-            } catch (IOException e) {
-                log.error("鏂囦欢杩佺Щ澶辫触: {}", tempFile.getTempPath(), e);
-                // 鍙�夋嫨鍥炴粴浜嬪姟鎴栬褰曞け璐ユ枃浠�
-                throw new IOException("鏂囦欢杩佺Щ寮傚父", e);
-            }
         }
     }
 
@@ -492,11 +410,6 @@
                 .eq(SalesLedgerProduct::getType, purchaseLedgerDto.getType());
         List<SalesLedgerProduct> products = salesLedgerProductMapper.selectList(productWrapper);
 
-        // 3.鏌ヨ涓婁紶鏂囦欢
-        LambdaQueryWrapper<CommonFile> salesLedgerFileWrapper = new LambdaQueryWrapper<>();
-        salesLedgerFileWrapper.eq(CommonFile::getCommonId, purchaseLedger.getId())
-                .eq(CommonFile::getType,FileNameType.PURCHASE.getValue());
-        List<CommonFile> salesLedgerFiles = commonFileMapper.selectList(salesLedgerFileWrapper);
 
         // 4. 杞崲 DTO
         PurchaseLedgerDto resultDto = new PurchaseLedgerDto();
@@ -504,7 +417,7 @@
         if (!products.isEmpty()) {
             resultDto.setHasChildren(true);
             resultDto.setProductData(products);
-            resultDto.setSalesLedgerFiles(salesLedgerFiles);
+            resultDto.setStorageBlobVOS(fileUtil.getStorageBlobVOsByApplicationAndRecordTypeAndRecordId(ApplicationTypeEnum.FILE, RecordTypeEnum.PURCHASE_LEDGER, purchaseLedger.getId()));
         }
         return resultDto;
     }
@@ -689,20 +602,21 @@
                 if(salesLedger1 != null){
                     salesLedger.setSalesLedgerId(salesLedger1.getId());
                 }
-                // 閲囪喘瀹℃牳
-                // 閫氳繃鏄电О鑾峰彇鐢ㄦ埛ID
-                String[] split = salesLedger.getApproveUserIds().split("锛�");
-                List<Long> ids = new ArrayList<>();
-                for (int i = 0; i < split.length; i++) {
-                    SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getNickName, split[i])
-                            .last("LIMIT 1"));
-                    if (sysUser != null) {
-                        ids.add(sysUser.getUserId());
+                if (StringUtils.hasText(salesLedger.getApproveUserIds())) {
+                    // 閲囪喘瀹℃牳锛氬巻鍙插鍏ユā鏉夸紶瀹℃壒浜哄鍚嶆椂锛岀户缁吋瀹硅浆鎹负鐢ㄦ埛ID銆�
+                    String[] split = salesLedger.getApproveUserIds().split("锛�");
+                    List<Long> ids = new ArrayList<>();
+                    for (int i = 0; i < split.length; i++) {
+                        SysUser sysUser = sysUserMapper.selectOne(new LambdaQueryWrapper<SysUser>().eq(SysUser::getNickName, split[i])
+                                .last("LIMIT 1"));
+                        if (sysUser != null) {
+                            ids.add(sysUser.getUserId());
+                        }
                     }
+                    // 灏嗛泦鍚堣浆涓哄瓧绗︿覆锛岄殧寮�
+                    String collect = ids.stream().map(Object::toString).collect(Collectors.joining(","));
+                    salesLedger.setApproveUserIds(collect);
                 }
-                // 灏嗛泦鍚堣浆涓哄瓧绗︿覆锛岄殧寮�
-                String collect = ids.stream().map(Object::toString).collect(Collectors.joining(","));
-                salesLedger.setApproveUserIds(collect);
                 purchaseLedgerMapper.insert(salesLedger);
 
                 for (PurchaseLedgerProductImportDto salesLedgerProductImportDto : salesLedgerProductImportDtos) {
@@ -770,6 +684,9 @@
     }
 
     public void addApproveByPurchase(LoginUser loginUser,PurchaseLedger purchaseLedger) throws Exception {
+        if (loginUser == null) {
+            return;
+        }
         ApproveProcessVO approveProcessVO = new ApproveProcessVO();
         approveProcessVO.setApproveType(5);
         approveProcessVO.setApproveDeptId(loginUser.getCurrentDeptId());
diff --git a/src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java b/src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java
index b0b2a59..a98d924 100644
--- a/src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java
+++ b/src/main/java/com/ruoyi/purchase/service/impl/TicketRegistrationServiceImpl.java
@@ -14,7 +14,6 @@
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.bean.BeanUtils;
 import com.ruoyi.other.mapper.TempFileMapper;
-import com.ruoyi.other.pojo.TempFile;
 import com.ruoyi.purchase.dto.PaymentRegistrationDto;
 import com.ruoyi.purchase.dto.PurchaseLedgerDto;
 import com.ruoyi.purchase.dto.TicketRegistrationDto;
@@ -34,21 +33,17 @@
 import com.ruoyi.sales.service.ISalesLedgerProductService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FilenameUtils;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
 import java.io.IOException;
 import java.math.BigDecimal;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.time.LocalDate;
-import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
-import java.util.*;
+import java.util.Arrays;
+import java.util.List;
+import java.util.Map;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -77,9 +72,6 @@
     private final ISalesLedgerProductService salesLedgerProductService;
 
     private final PaymentRegistrationMapper paymentRegistrationMapper;
-
-    @Value("${file.upload-dir}")
-    private String uploadDir;
 
 
     @Override
@@ -175,78 +167,7 @@
                 throw new RuntimeException("浜у搧寮�绁ㄦ暟閮戒负0锛岃妫�鏌�");
             }
         }
-        // 杩佺Щ涓存椂鏂囦欢鍒版寮忕洰褰�
-        if (ticketRegistrationDto.getTempFileIds() != null && !ticketRegistrationDto.getTempFileIds().isEmpty()) {
-            migrateTempFilesToFormal(ticketRegistration.getId(), ticketRegistrationDto.getTempFileIds());
-        }
         return rowsAffected;
-    }
-
-
-    /**
-     * 灏嗕复鏃舵枃浠惰縼绉诲埌姝e紡鐩綍
-     *
-     * @param businessId  涓氬姟ID锛堥攢鍞彴璐D锛�
-     * @param tempFileIds 涓存椂鏂囦欢ID鍒楄〃
-     * @throws IOException 鏂囦欢鎿嶄綔寮傚父
-     */
-    public void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
-        if (CollectionUtils.isEmpty(tempFileIds)) {
-            return;
-        }
-
-        // 鏋勫缓姝e紡鐩綍璺緞锛堟寜涓氬姟绫诲瀷鍜屾棩鏈熷垎缁勶級
-        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
-
-        Path formalDirPath = Paths.get(formalDir);
-
-        // 纭繚姝e紡鐩綍瀛樺湪锛堥�掑綊鍒涘缓锛�
-        if (!Files.exists(formalDirPath)) {
-            Files.createDirectories(formalDirPath);
-        }
-
-        for (String tempFileId : tempFileIds) {
-            // 鏌ヨ涓存椂鏂囦欢璁板綍
-            TempFile tempFile = tempFileMapper.selectById(tempFileId);
-            if (tempFile == null) {
-                log.warn("涓存椂鏂囦欢涓嶅瓨鍦紝璺宠繃澶勭悊: {}", tempFileId);
-                continue;
-            }
-
-            // 鏋勫缓姝e紡鏂囦欢鍚嶏紙鍖呭惈涓氬姟ID鍜屾椂闂存埑锛岄伩鍏嶅啿绐侊級
-            String originalFilename = tempFile.getOriginalName();
-            String fileExtension = FilenameUtils.getExtension(originalFilename);
-            String baseName = FilenameUtils.getBaseName(originalFilename);
-            String formalFilename = businessId + "_" +
-                    System.currentTimeMillis() + "_" +
-                    UUID.randomUUID().toString().substring(0, 8) +baseName+
-                    (com.ruoyi.common.utils.StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
-
-            Path formalFilePath = formalDirPath.resolve(formalFilename);
-
-            try {
-
-                // 鍘熷瓙绉诲姩澶辫触锛屼娇鐢ㄥ鍒�+鍒犻櫎
-                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
-                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-
-                // 鏇存柊鏂囦欢璁板綍锛堝叧鑱斿埌涓氬姟ID锛�
-                CommonFile fileRecord = new CommonFile();
-                fileRecord.setCommonId(businessId);
-                fileRecord.setName(originalFilename);
-                fileRecord.setUrl(formalFilePath.toString());
-                fileRecord.setCreateTime(LocalDateTime.now());
-                fileRecord.setType(4);
-                commonFileMapper.insert(fileRecord);
-
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-            } catch (IOException e) {
-                log.error("鏂囦欢杩佺Щ澶辫触: {}", tempFile.getTempPath(), e);
-                // 鍙�夋嫨鍥炴粴浜嬪姟鎴栬褰曞け璐ユ枃浠�
-                throw new IOException("鏂囦欢杩佺Щ寮傚父", e);
-            }
-        }
     }
 
 
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 1bedb6c..8c53500 100644
--- a/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
+++ b/src/main/java/com/ruoyi/quality/service/impl/QualityInspectServiceImpl.java
@@ -9,7 +9,6 @@
 import com.deepoove.poi.XWPFTemplate;
 import com.deepoove.poi.config.Configure;
 import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
-import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
 import com.ruoyi.common.utils.HackLoopTableRenderPolicy;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.procurementrecord.service.ProcurementRecordService;
@@ -24,12 +23,14 @@
 import com.ruoyi.quality.service.IQualityInspectParamService;
 import com.ruoyi.quality.service.IQualityInspectService;
 import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
+import com.ruoyi.stock.dto.StockInventoryDto;
+import com.ruoyi.stock.service.StockInventoryService;
+import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
 import org.springframework.beans.BeanUtils;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 
-import jakarta.servlet.http.HttpServletResponse;
 import java.io.InputStream;
 import java.io.OutputStream;
 import java.net.URLEncoder;
@@ -43,6 +44,7 @@
 public class QualityInspectServiceImpl extends ServiceImpl<QualityInspectMapper, QualityInspect> implements IQualityInspectService {
 
     private final StockUtils stockUtils;
+    private final StockInventoryService stockInventoryService;
     private QualityInspectMapper qualityInspectMapper;
 
     private IQualityInspectParamService qualityInspectParamService;
@@ -98,7 +100,14 @@
             qualityUnqualifiedMapper.insert(qualityUnqualified);
         } else {
             //鍚堟牸鐩存帴鍏ュ簱
-            stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
+            // stockUtils.addStock(qualityInspect.getProductModelId(), qualityInspect.getQuantity(), StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode(), qualityInspect.getId());
+            //浠呮坊鍔犲叆搴撹褰�
+            StockInventoryDto stockInventoryDto = new StockInventoryDto();
+            stockInventoryDto.setRecordType(String.valueOf(StockInQualifiedRecordTypeEnum.QUALITYINSPECT_STOCK_IN.getCode()));
+            stockInventoryDto.setRecordId(qualityInspect.getId());
+            stockInventoryDto.setProductModelId(qualityInspect.getProductModelId());
+            stockInventoryDto.setQualitity(qualityInspect.getQuantity());
+            stockInventoryService.addStockInRecordOnly(stockInventoryDto);
         }
         qualityInspect.setInspectState(1);//宸叉彁浜�
         return qualityInspectMapper.updateById(qualityInspect);
diff --git a/src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java b/src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java
index ae8fafa..dd8b6f5 100644
--- a/src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java
+++ b/src/main/java/com/ruoyi/safe/controller/SafeTrainingController.java
@@ -29,8 +29,8 @@
 @RequiredArgsConstructor
 public class SafeTrainingController {
 
-    private SafeTrainingService safeTrainingService;
-    private SafeTrainingDetailsService safeTrainingDetailsService;
+    private final SafeTrainingService safeTrainingService;
+    private final SafeTrainingDetailsService safeTrainingDetailsService;
 
     @GetMapping("/page")
     @Operation(summary = "鍒嗛〉鏌ヨ")
diff --git a/src/main/java/com/ruoyi/sales/service/ICommonFileService.java b/src/main/java/com/ruoyi/sales/service/ICommonFileService.java
index 398fc5d..6cba7dc 100644
--- a/src/main/java/com/ruoyi/sales/service/ICommonFileService.java
+++ b/src/main/java/com/ruoyi/sales/service/ICommonFileService.java
@@ -1,15 +1,9 @@
 package com.ruoyi.sales.service;
 
-import com.ruoyi.sales.pojo.CommonFile;
-import org.springframework.web.multipart.MultipartFile;
-
-import java.io.IOException;
-
 public interface ICommonFileService {
 
     int deleteSalesLedgerByIds(Long[] ids);
 
-    CommonFile uploadFile(MultipartFile file, Long id, Integer type) throws IOException;
 
     int delCommonFileByIds(Long[] ids);
 }
diff --git a/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java
index f3aac49..eb36693 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/CommonFileServiceImpl.java
@@ -1,32 +1,17 @@
 package com.ruoyi.sales.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
-import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ruoyi.other.mapper.TempFileMapper;
-import com.ruoyi.other.pojo.TempFile;
 import com.ruoyi.sales.mapper.CommonFileMapper;
 import com.ruoyi.sales.pojo.CommonFile;
 import com.ruoyi.sales.service.ICommonFileService;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FilenameUtils;
-import org.springframework.beans.factory.annotation.Value;
 import org.springframework.stereotype.Service;
-import org.springframework.transaction.annotation.Transactional;
-import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
-import java.time.LocalDate;
-import java.time.LocalDateTime;
-import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
 import java.util.List;
-import java.util.UUID;
 
 @Service
 @RequiredArgsConstructor
@@ -36,9 +21,6 @@
     private final CommonFileMapper commonFileMapper;
 
     private final TempFileMapper tempFileMapper;
-
-    @Value("${file.upload-dir}")
-    private String uploadDir;
 
     public List<CommonFile> getFileListByBusinessId(Long businessId,Integer type) {
         return commonFileMapper.selectList(new LambdaQueryWrapper<CommonFile>().eq(CommonFile::getCommonId, businessId)
@@ -66,105 +48,9 @@
         return commonFileMapper.deleteBatchIds(Arrays.asList(ids));
     }
 
-    @Override
-    public CommonFile uploadFile(MultipartFile file, Long id, Integer type) throws IOException {
-        // 1. 鐢熸垚姝e紡鏂囦欢ID鍜岃矾寰�
-        String tempId = UUID.randomUUID().toString();
-        Path tempFilePath = Paths.get(uploadDir, tempId + "_" + file.getOriginalFilename());
-
-        // 2. 纭繚鐩綍瀛樺湪
-        Path parentDir = tempFilePath.getParent();
-        if (parentDir != null) {
-            Files.createDirectories(parentDir); // 閫掑綊鍒涘缓鐩綍
-        }
-
-        // 3. 淇濆瓨鏂囦欢鍒扮洰褰�
-        file.transferTo(tempFilePath.toFile());
-
-        // 4. 淇濆瓨鏂囦欢璁板綍
-        CommonFile commonFile = new CommonFile();
-        commonFile.setCommonId(id);
-        commonFile.setName(file.getOriginalFilename());
-        commonFile.setUrl(tempFilePath.toString());
-        commonFile.setType(type);
-        commonFileMapper.insert(commonFile);
-        return commonFile;
-    }
 
     @Override
     public int delCommonFileByIds(Long[] ids) {
         return commonFileMapper.deleteBatchIds(Arrays.asList(ids));
-    }
-
-    /**
-     * 灏嗕复鏃舵枃浠惰縼绉诲埌姝e紡鐩綍
-     *
-     * @param businessId  涓氬姟ID锛堥攢鍞彴璐D锛�
-     * @param tempFileIds 涓存椂鏂囦欢ID鍒楄〃
-     * @throws IOException 鏂囦欢鎿嶄綔寮傚父
-     */
-    @Transactional(rollbackFor = Exception.class)
-    public void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
-        if (CollectionUtils.isEmpty(tempFileIds)) {
-            return;
-        }
-
-        // 鏋勫缓姝e紡鐩綍璺緞锛堟寜涓氬姟绫诲瀷鍜屾棩鏈熷垎缁勶級
-        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
-
-        Path formalDirPath = Paths.get(formalDir);
-
-        // 纭繚姝e紡鐩綍瀛樺湪锛堥�掑綊鍒涘缓锛�
-        if (!Files.exists(formalDirPath)) {
-            Files.createDirectories(formalDirPath);
-        }
-
-        for (String tempFileId : tempFileIds) {
-            // 鏌ヨ涓存椂鏂囦欢璁板綍
-            TempFile tempFile = tempFileMapper.selectById(tempFileId);
-            if (tempFile == null) {
-                log.warn("涓存椂鏂囦欢涓嶅瓨鍦紝璺宠繃澶勭悊: {}", tempFileId);
-                continue;
-            }
-
-            // 鏋勫缓姝e紡鏂囦欢鍚嶏紙鍖呭惈涓氬姟ID鍜屾椂闂存埑锛岄伩鍏嶅啿绐侊級
-            String originalFilename = tempFile.getOriginalName();
-            String fileExtension = FilenameUtils.getExtension(originalFilename);
-            String formalFilename = businessId + "_" +
-                    System.currentTimeMillis() + "_" +
-                    UUID.randomUUID().toString().substring(0, 8) +
-                    (com.ruoyi.common.utils.StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
-
-            Path formalFilePath = formalDirPath.resolve(formalFilename);
-
-            try {
-                // 鎵ц鏂囦欢杩佺Щ锛堜娇鐢ㄥ師瀛愭搷浣滅‘淇濆畨鍏ㄦ�э級
-//                Files.move(
-//                        Paths.get(tempFile.getTempPath()),
-//                        formalFilePath,
-//                        StandardCopyOption.REPLACE_EXISTING,
-//                        StandardCopyOption.ATOMIC_MOVE
-//                );
-                // 鍘熷瓙绉诲姩澶辫触锛屼娇鐢ㄥ鍒�+鍒犻櫎
-                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
-                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-
-                // 鏇存柊鏂囦欢璁板綍锛堝叧鑱斿埌涓氬姟ID锛�
-                CommonFile fileRecord = new CommonFile();
-                fileRecord.setCommonId(businessId);
-                fileRecord.setName(originalFilename);
-                fileRecord.setUrl(formalFilePath.toString());
-                fileRecord.setCreateTime(LocalDateTime.now());
-                fileRecord.setType(tempFile.getType());
-                commonFileMapper.insert(fileRecord);
-
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-            } catch (IOException e) {
-                log.error("鏂囦欢杩佺Щ澶辫触: {}", tempFile.getTempPath(), e);
-                // 鍙�夋嫨鍥炴粴浜嬪姟鎴栬褰曞け璐ユ枃浠�
-                throw new IOException("鏂囦欢杩佺Щ寮傚父", e);
-            }
-        }
     }
 }
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 eb8fcce..ccbe035 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -29,7 +29,6 @@
 import com.ruoyi.framework.security.LoginUser;
 import com.ruoyi.framework.web.domain.AjaxResult;
 import com.ruoyi.other.mapper.TempFileMapper;
-import com.ruoyi.other.pojo.TempFile;
 import com.ruoyi.production.mapper.ProductionProductInputMapper;
 import com.ruoyi.production.mapper.ProductionProductMainMapper;
 import com.ruoyi.production.mapper.ProductionProductOutputMapper;
@@ -48,7 +47,6 @@
 import com.ruoyi.sales.vo.SalesLedgerVo;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
-import org.apache.commons.io.FilenameUtils;
 import org.jetbrains.annotations.Nullable;
 import org.springframework.beans.BeanUtils;
 import org.springframework.beans.factory.annotation.Value;
@@ -58,15 +56,10 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
-import java.io.IOException;
 import java.io.InputStream;
 import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
-import java.nio.file.Files;
-import java.nio.file.Path;
-import java.nio.file.Paths;
-import java.nio.file.StandardCopyOption;
 import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.YearMonth;
@@ -89,13 +82,11 @@
     private static final String LOCK_PREFIX = "contract_no_lock:";
     private static final long LOCK_WAIT_TIMEOUT = 10; // 閿佺瓑寰呰秴鏃舵椂闂达紙绉掞級
     private static final long LOCK_EXPIRE_TIME = 30;  // 閿佽嚜鍔ㄨ繃鏈熸椂闂达紙绉掞級
-    private final AccountIncomeService accountIncomeService;
     private final SalesLedgerMapper salesLedgerMapper;
     private final CustomerMapper customerMapper;
     private final SalesLedgerProductMapper salesLedgerProductMapper;
     private final SalesLedgerProductServiceImpl salesLedgerProductServiceImpl;
     private final CommonFileMapper commonFileMapper;
-    private final TempFileMapper tempFileMapper;
     private final ReceiptPaymentMapper receiptPaymentMapper;
     private final ShippingInfoServiceImpl shippingInfoServiceImpl;
     private final CommonFileServiceImpl commonFileService;
@@ -103,15 +94,9 @@
     private final InvoiceLedgerMapper invoiceLedgerMapper;
     private final InvoiceRegistrationProductMapper invoiceRegistrationProductMapper;
     private final InvoiceRegistrationMapper invoiceRegistrationMapper;
-    private final ProductionProductMainMapper productionProductMainMapper;
-    private final ProductionProductOutputMapper productionProductOutputMapper;
-    private final ProductionProductInputMapper productionProductInputMapper;
-    private final QualityInspectMapper qualityInspectMapper;
     private final ProductModelMapper productModelMapper;
     private final RedisTemplate<String, String> redisTemplate;
     private final SysDeptMapper sysDeptMapper;
-    @Value("${file.upload-dir}")
-    private String uploadDir;
     private final ProductionProductMainService productionProductMainService;
     private final PurchaseReturnOrderProductsMapper purchaseReturnOrderProductsMapper;
     private final SysUserMapper sysUserMapper;
@@ -621,81 +606,6 @@
             fileUtil.saveStorageAttachment(ApplicationTypeEnum.FILE, RecordTypeEnum.SALES_LEDGER, salesLedger.getId(), salesLedgerDto.getStorageBlobDTOs());
         }
         return 1;
-    }
-
-    /**
-     * 灏嗕复鏃舵枃浠惰縼绉诲埌姝e紡鐩綍
-     *
-     * @param businessId  涓氬姟ID锛堥攢鍞彴璐D锛�
-     * @param tempFileIds 涓存椂鏂囦欢ID鍒楄〃
-     * @throws IOException 鏂囦欢鎿嶄綔寮傚父
-     */
-    private void migrateTempFilesToFormal(Long businessId, List<String> tempFileIds) throws IOException {
-        if (CollectionUtils.isEmpty(tempFileIds)) {
-            return;
-        }
-
-        // 鏋勫缓姝e紡鐩綍璺緞锛堟寜涓氬姟绫诲瀷鍜屾棩鏈熷垎缁勶級
-        String formalDir = uploadDir + LocalDate.now().format(DateTimeFormatter.ISO_LOCAL_DATE);
-
-        Path formalDirPath = Paths.get(formalDir);
-
-        // 纭繚姝e紡鐩綍瀛樺湪锛堥�掑綊鍒涘缓锛�
-        if (!Files.exists(formalDirPath)) {
-            Files.createDirectories(formalDirPath);
-        }
-
-        for (String tempFileId : tempFileIds) {
-            // 鏌ヨ涓存椂鏂囦欢璁板綍
-            TempFile tempFile = tempFileMapper.selectById(tempFileId);
-            if (tempFile == null) {
-                log.warn("涓存椂鏂囦欢涓嶅瓨鍦紝璺宠繃澶勭悊: {}", tempFileId);
-                continue;
-            }
-
-            // 鏋勫缓姝e紡鏂囦欢鍚嶏紙鍖呭惈涓氬姟ID鍜屾椂闂存埑锛岄伩鍏嶅啿绐侊級
-            String originalFilename = tempFile.getOriginalName();
-            String fileExtension = FilenameUtils.getExtension(originalFilename);
-            String formalFilename = businessId + "_" +
-                    System.currentTimeMillis() + "_" +
-                    UUID.randomUUID().toString().substring(0, 8) +
-                    (StringUtils.hasText(fileExtension) ? "." + fileExtension : "");
-
-            Path formalFilePath = formalDirPath.resolve(formalFilename);
-
-            try {
-                // 鎵ц鏂囦欢杩佺Щ锛堜娇鐢ㄥ師瀛愭搷浣滅‘淇濆畨鍏ㄦ�э級
-//                Files.move(
-//                        Paths.get(tempFile.getTempPath()),
-//                        formalFilePath,
-//                        StandardCopyOption.REPLACE_EXISTING,
-//                        StandardCopyOption.ATOMIC_MOVE
-//                );
-                // 鍘熷瓙绉诲姩澶辫触锛屼娇鐢ㄥ鍒�+鍒犻櫎
-                Files.copy(Paths.get(tempFile.getTempPath()), formalFilePath, StandardCopyOption.REPLACE_EXISTING);
-                Files.deleteIfExists(Paths.get(tempFile.getTempPath()));
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-
-                // 鏇存柊鏂囦欢璁板綍锛堝叧鑱斿埌涓氬姟ID锛�
-                CommonFile fileRecord = new CommonFile();
-                fileRecord.setCommonId(businessId);
-                fileRecord.setName(originalFilename);
-                fileRecord.setUrl(formalFilePath.toString());
-                fileRecord.setCreateTime(LocalDateTime.now());
-                //閿�鍞�
-                fileRecord.setType(FileNameType.SALE.getValue());
-                commonFileMapper.insert(fileRecord);
-
-                // 鍒犻櫎涓存椂鏂囦欢璁板綍
-                tempFileMapper.deleteById(tempFile);
-
-                log.info("鏂囦欢杩佺Щ鎴愬姛: {} -> {}", tempFile.getTempPath(), formalFilePath);
-            } catch (IOException e) {
-                log.error("鏂囦欢杩佺Щ澶辫触: {}", tempFile.getTempPath(), e);
-                // 鍙�夋嫨鍥炴粴浜嬪姟鎴栬褰曞け璐ユ枃浠�
-                throw new IOException("鏂囦欢杩佺Щ寮傚父", e);
-            }
-        }
     }
 
     // 鏂囦欢杩佺Щ鏂规硶
diff --git a/src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
index 3f44734..08322a2 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -11,7 +11,6 @@
 import com.ruoyi.basic.utils.FileUtil;
 import com.ruoyi.common.enums.FileNameType;
 import com.ruoyi.common.enums.StockOutQualifiedRecordTypeEnum;
-import com.ruoyi.other.service.impl.TempFileServiceImpl;
 import com.ruoyi.procurementrecord.utils.StockUtils;
 import com.ruoyi.sales.dto.SalesLedgerProductDto;
 import com.ruoyi.sales.dto.ShippingInfoDto;
@@ -23,7 +22,6 @@
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
 import org.apache.commons.collections4.CollectionUtils;
-import org.springframework.beans.factory.annotation.Autowired;
 import org.springframework.stereotype.Service;
 
 import java.util.Collections;
@@ -41,7 +39,6 @@
 
     private final ShippingInfoMapper shippingInfoMapper;
 
-    private final TempFileServiceImpl tempFileService;
 
     private final SalesLedgerProductMapper salesLedgerProductMapper;
 
diff --git a/src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java b/src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
index 4d525c3..c6b32b4 100644
--- a/src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
+++ b/src/main/java/com/ruoyi/stock/service/impl/StockInventoryServiceImpl.java
@@ -4,12 +4,14 @@
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.ObjectUtils;
+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.basic.mapper.ProductModelMapper;
+import com.ruoyi.basic.pojo.ProductModel;
 import com.ruoyi.common.enums.StockInQualifiedRecordTypeEnum;
 import com.ruoyi.common.enums.StockInUnQualifiedRecordTypeEnum;
 import com.ruoyi.common.exception.ServiceException;
-import com.ruoyi.common.exception.base.BaseException;
 import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.framework.web.domain.R;
@@ -21,18 +23,21 @@
 import com.ruoyi.stock.dto.StockUninventoryDto;
 import com.ruoyi.stock.execl.StockInventoryExportData;
 import com.ruoyi.stock.mapper.StockInventoryMapper;
+import com.ruoyi.stock.pojo.StockInRecord;
 import com.ruoyi.stock.pojo.StockInventory;
 import com.ruoyi.stock.service.StockInRecordService;
 import com.ruoyi.stock.service.StockInventoryService;
 import com.ruoyi.stock.service.StockOutRecordService;
 import com.ruoyi.stock.service.StockUninventoryService;
+import jakarta.servlet.http.HttpServletResponse;
 import lombok.AllArgsConstructor;
 import org.springframework.stereotype.Service;
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.web.multipart.MultipartFile;
 
-import jakarta.servlet.http.HttpServletResponse;
 import java.math.BigDecimal;
+import java.time.LocalDate;
+import java.time.format.DateTimeFormatter;
 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
@@ -55,6 +60,7 @@
     private StockOutRecordService stockOutRecordService;
     private StockUninventoryService stockUninventoryService;
     private SalesLedgerProductMapper salesLedgerProductMapper;
+    private ProductModelMapper productModelMapper;
     @Override
     public IPage<StockInventoryDto> pagestockInventory(Page page, StockInventoryDto stockInventoryDto) {
         return stockInventoryMapper.pagestockInventory(page, stockInventoryDto);
@@ -69,14 +75,15 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Boolean addstockInventory(StockInventoryDto stockInventoryDto) {
+        String batchNo = StringUtils.trim(stockInventoryDto.getBatchNo());
+        if (StringUtils.isEmpty(batchNo)) {
+            batchNo = generateAutoBatchNo(stockInventoryDto.getProductModelId());
+        }
+        stockInventoryDto.setBatchNo(batchNo);
+
         LambdaQueryWrapper<StockInventory> eq = new QueryWrapper<StockInventory>().lambda()
                 .eq(StockInventory::getProductModelId, stockInventoryDto.getProductModelId());
-        if (StringUtils.isEmpty(stockInventoryDto.getBatchNo())) {
-            eq.isNull(StockInventory::getBatchNo);
-            stockInventoryDto.setBatchNo(null);
-        } else {
-            eq.eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo());
-        }
+        eq.eq(StockInventory::getBatchNo, stockInventoryDto.getBatchNo());
         //鏂板鍏ュ簱璁板綍鍐嶆坊鍔犲簱瀛�
         StockInRecordDto stockInRecordDto = new StockInRecordDto();
         stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
@@ -147,11 +154,17 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Boolean addStockInRecordOnly(StockInventoryDto stockInventoryDto) {
+        String batchNo = StringUtils.trim(stockInventoryDto.getBatchNo());
+        if (StringUtils.isEmpty(batchNo)) {
+            batchNo = generateAutoBatchNo(stockInventoryDto.getProductModelId());
+        }
+        stockInventoryDto.setBatchNo(batchNo);
+
         StockInRecordDto stockInRecordDto = new StockInRecordDto();
         stockInRecordDto.setRecordId(stockInventoryDto.getRecordId());
         stockInRecordDto.setRecordType(stockInventoryDto.getRecordType());
         stockInRecordDto.setStockInNum(stockInventoryDto.getQualitity());
-        stockInRecordDto.setBatchNo(stockInventoryDto.getBatchNo());
+        stockInRecordDto.setBatchNo(batchNo);
         stockInRecordDto.setProductModelId(stockInventoryDto.getProductModelId());
         stockInRecordDto.setType("0");
         stockInRecordDto.setRemark(stockInventoryDto.getRemark());
@@ -159,6 +172,82 @@
         return true;
     }
 
+    //瑙勫垯鐢熸垚锛�20260424-浜у搧缂栧彿-001
+    private String generateAutoBatchNo(Long productModelId) {
+        if (productModelId == null) {
+            throw new ServiceException("浜у搧瑙勬牸ID涓嶈兘涓虹┖");
+        }
+        ProductModel productModel = productModelMapper.selectById(productModelId);
+        if (productModel == null) {
+            throw new ServiceException("浜у搧瑙勬牸涓嶅瓨鍦紝ID=" + productModelId);
+        }
+        String productCode = StringUtils.trim(productModel.getProductCode());
+        if (StringUtils.isEmpty(productCode)) {
+            throw new ServiceException("浜у搧瑙勬牸鏈淮鎶や骇鍝佺紪鐮侊紝ID=" + productModelId);
+        }
+
+        String dateText = LocalDate.now().format(DateTimeFormatter.BASIC_ISO_DATE);
+        String prefix = dateText + "-" + productCode + "-";
+        int maxSequence = resolveMaxSequence(prefix);
+        int sequence = maxSequence + 1;
+        while (sequence < 1_000_000) {
+            String batchNo = prefix + String.format("%03d", sequence);
+            if (!isBatchNoExists(batchNo)) {
+                return batchNo;
+            }
+            sequence++;
+        }
+        throw new ServiceException("鎵瑰彿搴忓彿瓒呭嚭鑼冨洿锛岃妫�鏌ユ壒鍙锋暟鎹�");
+    }
+
+    private int resolveMaxSequence(String prefix) {
+        int maxSequence = 0;
+        List<StockInventory> stockInventoryList = stockInventoryMapper.selectList(
+                Wrappers.<StockInventory>lambdaQuery()
+                        .select(StockInventory::getBatchNo)
+                        .likeRight(StockInventory::getBatchNo, prefix));
+        for (StockInventory stockInventory : stockInventoryList) {
+            maxSequence = Math.max(maxSequence, parseSequence(stockInventory.getBatchNo(), prefix));
+        }
+
+        List<StockInRecord> stockInRecordList = stockInRecordService.list(
+                Wrappers.<StockInRecord>lambdaQuery()
+                        .select(StockInRecord::getBatchNo)
+                        .likeRight(StockInRecord::getBatchNo, prefix));
+        for (StockInRecord stockInRecord : stockInRecordList) {
+            maxSequence = Math.max(maxSequence, parseSequence(stockInRecord.getBatchNo(), prefix));
+        }
+        return maxSequence;
+    }
+
+    private int parseSequence(String batchNo, String prefix) {
+        if (StringUtils.isEmpty(batchNo) || StringUtils.isEmpty(prefix) || !batchNo.startsWith(prefix)) {
+            return 0;
+        }
+        String sequenceText = batchNo.substring(prefix.length());
+        if (StringUtils.isEmpty(sequenceText) || !sequenceText.matches("\\d+")) {
+            return 0;
+        }
+        try {
+            return Integer.parseInt(sequenceText);
+        } catch (NumberFormatException ignored) {
+            return 0;
+        }
+    }
+
+    private boolean isBatchNoExists(String batchNo) {
+        if (StringUtils.isEmpty(batchNo)) {
+            return false;
+        }
+        Long inventoryCount = stockInventoryMapper.selectCount(
+                Wrappers.<StockInventory>lambdaQuery().eq(StockInventory::getBatchNo, batchNo));
+        if (inventoryCount != null && inventoryCount > 0) {
+            return true;
+        }
+        return stockInRecordService.count(
+                Wrappers.<StockInRecord>lambdaQuery().eq(StockInRecord::getBatchNo, batchNo)) > 0;
+    }
+
     @Override
     @Transactional(rollbackFor = Exception.class)
     public Boolean addStockOutRecordOnly(StockInventoryDto stockInventoryDto) {
diff --git a/src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java b/src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java
index e433efa..9ab62e1 100644
--- a/src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java
+++ b/src/main/java/com/ruoyi/technology/controller/TechnologyRoutingController.java
@@ -52,7 +52,7 @@
     /**
      * 淇敼宸ヨ壓璺嚎銆�
      */
-    @PutMapping("editTechRoute")
+    @PutMapping("/editTechRoute")
     @Operation(summary = "淇敼宸ヨ壓璺嚎")
     public R edit(@RequestBody TechnologyRouting technologyRouting) {
         return R.ok(technologyRoutingService.updateTechnologyRouting(technologyRouting));
@@ -67,5 +67,4 @@
         return R.ok(technologyRoutingService.removeTechnologyRouting(ids));
     }
 
-    //TODO 澧炲姞宸ヨ壓璺嚎闄勪欢涓婁紶 @闄堟捣鏉�
 }
diff --git a/src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java b/src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java
index 3130951..4079f84 100644
--- a/src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java
+++ b/src/main/java/com/ruoyi/technology/service/impl/TechnologyRoutingServiceImpl.java
@@ -116,7 +116,7 @@
                 Wrappers.<TechnologyBomStructure>lambdaQuery()
                         .eq(TechnologyBomStructure::getBomId, technologyRouting.getBomId())
                         .isNotNull(TechnologyBomStructure::getOperationId)
-                        .orderByAsc(TechnologyBomStructure::getId)
+                        .orderByDesc(TechnologyBomStructure::getId)
         );
         if (bomStructures.isEmpty()) {
             throw new ServiceException("bom浜у搧缁撴瀯涓虹┖锛�");
diff --git a/src/main/resources/application-dev-pro.yml b/src/main/resources/application-dev-pro.yml
index 1552370..abb1950 100644
--- a/src/main/resources/application-dev-pro.yml
+++ b/src/main/resources/application-dev-pro.yml
@@ -254,8 +254,8 @@
 
 # 鏂囦欢涓婁紶閰嶇疆
 file:
-  temp-dir: D:/ruoyi/temp/uploads   # 涓存椂鐩綍
-  upload-dir: D:/ruoyi/prod/uploads # 姝e紡鐩綍
+  temp-dir: D:/ruoyi/temp/uploads   # 涓存椂鐩綍 鍚庢湡鍒犻櫎
+  upload-dir: D:/ruoyi/prod/uploads # 姝e紡鐩綍 鍚庢湡鍒犻櫎
   path: C:/Users/12631/Desktop/download/uploads # 涓婁紶鐩綍
   urlPrefix: /common # 閾炬帴鍓嶇紑
   domain: http://127.0.0.1:7003 # 鍩熷悕鍓嶇紑
diff --git a/src/main/resources/approve-todo-agent-prompt.txt b/src/main/resources/approve-todo-agent-prompt.txt
index 404b25a..c309da8 100644
--- a/src/main/resources/approve-todo-agent-prompt.txt
+++ b/src/main/resources/approve-todo-agent-prompt.txt
@@ -1,4 +1,4 @@
-浣犳槸涓�涓鎵瑰緟鍔炲姪鎵嬶紝璐熻矗瀹℃壒寰呭姙鐨勬煡璇€�佸鏍搞�佸彇娑堝鏍搞�佷慨鏀广�佸垹闄ゅ拰缁熻鍒嗘瀽銆�
+浣犳槸涓�涓鎵瑰緟鍔炲姪鎵嬶紝璐熻矗鍗忓悓鍔炲叕瀹℃壒寰呭姙鐨勬煡璇€�佸鏍搞�佸彇娑堝鏍搞�佷慨鏀广�佸垹闄ゅ拰缁熻鍒嗘瀽銆�
 
 宸ヤ綔瑕佹眰锛�
 1. 鐢ㄦ埛闂緟鍔炲垪琛ㄣ�佸鎵硅繘搴︺�佸鎵硅鎯呫�佺粺璁℃暟鎹椂锛屼紭鍏堣皟鐢ㄥ伐鍏凤紝涓嶈鑷嗛�犳暟鎹��
@@ -6,8 +6,13 @@
 3. 瀹℃牳鍔ㄤ綔閲岋紝`approve` 琛ㄧず閫氳繃锛宍reject` 琛ㄧず椹冲洖銆�
 4. 淇敼瀹℃壒鍗曟椂锛屽鏋滅敤鎴锋病鏈夋槑纭淇敼鍝簺瀛楁锛岃鍏堣拷闂己澶卞瓧娈碉紝涓嶈鐚溿��
 5. 鍒犻櫎銆佸鏍搞�佸彇娑堝鏍歌繖绫诲姩浣滃睘浜庣姸鎬佸彉鏇达紝鎵ц鍚庤鏄庣‘鍙嶉缁撴灉銆�
-6. 闄も�滄煡璇㈠鎵瑰緟鍔炶鎯呪�濆锛屽叾浠栧伐鍏烽粯璁よ繑鍥� JSON銆�
-7. 瀵逛簬杩欎簺 JSON 宸ュ叿锛屼綘蹇呴』鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓叉湰韬紝涓嶈鏀瑰啓锛屼笉瑕侀澶栬В閲婏紝涓嶈鍖呰9 Markdown 浠g爜鍧楋紝涓嶈鍦� JSON 鍓嶅悗鍔犱换浣曟枃瀛椼��
-8. 鍙湁鈥滄煡璇㈠鎵瑰緟鍔炶鎯呪�濊繖涓伐鍏峰厑璁歌緭鍑鸿嚜鐒惰瑷�鏂囨湰銆�
-9. 濡傛灉宸ュ叿杩斿洖鐨勬槸缁熻 JSON锛屼篃鍚屾牱鐩存帴杈撳嚭鍘熷 JSON锛涘叾涓� `description`銆乣summary`銆乣charts` 宸茬粡渚涘墠绔娇鐢ㄣ��
-10. 鍥炵瓟浣跨敤涓枃锛涗絾鍦� JSON 鍦烘櫙涓嬶紝鏈�缁堣緭鍑哄繀椤绘槸鍚堟硶 JSON 鏈綋銆�
+6. 鐢ㄦ埛璇粹�滃崟鎹�濃�滄祦绋嬧�濃�滃鎵规壒鈥濃�滃緟鍔炩�濓紝閮芥寜瀹℃壒寰呭姙鐞嗚В锛涚敤鎴疯鈥滃崱鍦ㄥ摢涓妭鐐光�濃�滃綋鍓嶅鎵逛汉鈥濃�滄祦杞褰曗�濓紝璋冪敤鈥滄煡璇㈠鎵规祦杞褰曗�濄��
+7. 鐢ㄦ埛璇粹�滄垜鍙戣捣鐨勨�濃�滄垜鎻愪氦鐨勨�濃�滄垜鐢宠鐨勨�濓紝鏌ヨ鑼冨洿浣跨敤 `applicant`锛涚敤鎴疯鈥滃緟鎴戝鎵光�濃�滃綋鍓嶅緟鎴戝鐞嗏�濃�滈渶瑕佹垜澶勭悊鈥濓紝鏌ヨ鑼冨洿浣跨敤 `approver`锛涙病鏈夋槑纭寖鍥存椂浣跨敤 `related`銆�
+8. 鐢ㄦ埛璇粹�滃鐞嗕腑鈥濃�滃姙鐞嗕腑鈥濓紝鐘舵�佷娇鐢� `processing`锛涜鈥滃緟瀹℃壒鈥濃�滃緟瀹℃牳鈥濓紝鐘舵�佷娇鐢� `pending`锛涜鈥滈�氳繃鈥濃�滃凡閫氳繃鈥濓紝鐘舵�佷娇鐢� `approved`锛涜鈥滈┏鍥炩�濃�滄嫆缁濃�濃�滄湭閫氳繃鈥濓紝鐘舵�佷娇鐢� `rejected`銆�
+9. 鐢ㄦ埛瑕佹眰鈥滆繎7澶┾�濃�滄湰鏈堚�濃�滆繎30澶┾�濃�滃悇绫诲瀷鍒嗗竷鈥濃�滈�氳繃/椹冲洖/澶勭悊涓悇鏈夊灏戔�濈瓑缁熻鍙e緞鏃讹紝璋冪敤缁熻宸ュ叿銆�
+10. 鐢ㄦ埛璇粹�滃娉ㄥ悓鎰忊�濃�滃娉ㄨ姹傝ˉ鍏呰鏄庘�濇椂锛屾妸澶囨敞鍐呭浼犵粰瀹℃牳宸ュ叿鐨� remark锛涢┏鍥炴椂濡傛灉娌℃湁鈥滃師鍥犫�濅絾鏈夆�滃娉ㄢ�濓紝涔熶娇鐢ㄥ娉ㄣ��
+11. 闄も�滄煡璇㈠鎵瑰緟鍔炶鎯呪�濆锛屽叾浠栧伐鍏烽粯璁よ繑鍥� JSON銆�
+12. 瀵逛簬杩欎簺 JSON 宸ュ叿锛屼綘蹇呴』鐩存帴杈撳嚭鍘熷 JSON 瀛楃涓叉湰韬紝涓嶈鏀瑰啓锛屼笉瑕侀澶栬В閲婏紝涓嶈鍖呰9 Markdown 浠g爜鍧楋紝涓嶈鍦� JSON 鍓嶅悗鍔犱换浣曟枃瀛椼��
+13. 鍙湁鈥滄煡璇㈠鎵瑰緟鍔炶鎯呪�濊繖涓伐鍏峰厑璁歌緭鍑鸿嚜鐒惰瑷�鏂囨湰銆�
+14. 濡傛灉宸ュ叿杩斿洖鐨勬槸缁熻 JSON锛屼篃鍚屾牱鐩存帴杈撳嚭鍘熷 JSON锛涘叾涓� `description`銆乣summary`銆乣charts` 宸茬粡渚涘墠绔娇鐢ㄣ��
+15. 鍥炵瓟浣跨敤涓枃锛涗絾鍦� JSON 鍦烘櫙涓嬶紝鏈�缁堣緭鍑哄繀椤绘槸鍚堟硶 JSON 鏈綋銆�
diff --git a/src/main/resources/mapper/basic/StorageAttachmentMapper.xml b/src/main/resources/mapper/basic/StorageAttachmentMapper.xml
index a2cc6cf..d2b7b92 100644
--- a/src/main/resources/mapper/basic/StorageAttachmentMapper.xml
+++ b/src/main/resources/mapper/basic/StorageAttachmentMapper.xml
@@ -10,13 +10,9 @@
                     <result column="deleted" property="deleted" />
                     <result column="record_type" property="recordType" />
                     <result column="record_id" property="recordId" />
-                    <result column="name" property="name" />
+                    <result column="application" property="application" />
                     <result column="storage_blob_id" property="storageBlobId" />
         </resultMap>
 
-        <!-- 閫氱敤鏌ヨ缁撴灉鍒� -->
-        <sql id="Base_Column_List">
-            id, create_time, update_time, deleted, record_type, record_id, name, storage_blob_id
-        </sql>
 
-</mapper>
\ No newline at end of file
+</mapper>
diff --git a/src/main/resources/mapper/basic/StorageBlobMapper.xml b/src/main/resources/mapper/basic/StorageBlobMapper.xml
index 84e3b00..d8a03fa 100644
--- a/src/main/resources/mapper/basic/StorageBlobMapper.xml
+++ b/src/main/resources/mapper/basic/StorageBlobMapper.xml
@@ -2,21 +2,50 @@
 <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
 <mapper namespace="com.ruoyi.basic.mapper.StorageBlobMapper">
 
-        <!-- 閫氱敤鏌ヨ鏄犲皠缁撴灉 -->
-        <resultMap id="BaseResultMap" type="com.ruoyi.basic.pojo.StorageBlob">
-                    <id column="id" property="id" />
-                    <result column="create_time" property="createTime" />
-                    <result column="key" property="key" />
-                    <result column="content_type" property="contentType" />
-                    <result column="original_filename" property="originalFilename" />
-                    <result column="bucket_filename" property="bucketFilename" />
-                    <result column="bucket_name" property="bucketName" />
-                    <result column="byte_size" property="byteSize" />
-        </resultMap>
+    <!-- 閫氱敤鏌ヨ鏄犲皠缁撴灉 -->
+    <resultMap id="BaseResultMap" type="com.ruoyi.basic.pojo.StorageBlob">
+        <id column="id" property="id"/>
+        <result column="resource_key" property="resourceKey"/>
+        <result column="content_type" property="contentType"/>
+        <result column="original_filename" property="originalFilename"/>
+        <result column="uid_filename" property="uidFilename"/>
+        <result column="byte_size" property="byteSize"/>
+        <result column="path" property="path"/>
+    </resultMap>
 
-        <!-- 閫氱敤鏌ヨ缁撴灉鍒� -->
-        <sql id="Base_Column_List">
-            id, create_time, key, content_type, original_filename,bucket_filename,bucket_name,  byte_size
-        </sql>
+    <!-- 閫氱敤鏌ヨ缁撴灉鍒� -->
+    <sql id="Base_Column_List">
+        id, resource_key, content_type, original_filename, uid_filename, byte_size, path
+    </sql>
 
-</mapper>
\ No newline at end of file
+    <select id="selectOrphanBlobsByIdRange" resultMap="BaseResultMap">
+        SELECT
+        <include refid="Base_Column_List"/>
+        FROM storage_blob sb
+        LEFT JOIN storage_attachment sa
+        ON sa.storage_blob_id = sb.id
+        AND sa.deleted = 0
+        WHERE sb.id <![CDATA[>]]> #{lastId}
+        AND sa.id IS NULL
+        ORDER BY sb.id ASC
+        LIMIT #{limit}
+    </select>
+
+    <delete id="deleteByIdList">
+        DELETE FROM storage_blob
+        WHERE id IN
+        <foreach collection="ids" item="id" open="(" separator="," close=")">
+            #{id}
+        </foreach>
+    </delete>
+
+    <select id="selectExistingUidFilenames" resultType="java.lang.String">
+        SELECT uid_filename
+        FROM storage_blob
+        WHERE uid_filename IN
+        <foreach collection="fileNames" item="fileName" open="(" separator="," close=")">
+            #{fileName}
+        </foreach>
+    </select>
+
+</mapper>
diff --git a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
index 31bf61b..0ccb06f 100644
--- a/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOperationTaskMapper.xml
@@ -24,6 +24,7 @@
     <select id="pageProductionOperationTask" resultType="com.ruoyi.production.bean.vo.ProductionOperationTaskVo">
         select pot.*,
                po.nps_no as npsNo,
+               po.is_end_order as endOrder,
                p.product_name as productName,
                pm.model as model,
                pm.unit as unit,
diff --git a/src/main/resources/mapper/production/ProductionOrderMapper.xml b/src/main/resources/mapper/production/ProductionOrderMapper.xml
index b279001..2ea7d33 100644
--- a/src/main/resources/mapper/production/ProductionOrderMapper.xml
+++ b/src/main/resources/mapper/production/ProductionOrderMapper.xml
@@ -49,7 +49,9 @@
         po_sales.customerName,
         p.product_name as productName,
         pm.model as model,
+        po.is_end_order as endOrder,
         tr.process_route_code as processRouteCode,
+        ROUND(po.complete_quantity / po.quantity * 100, 2) AS completionStatus,
         tb.bom_no as bomNo
     </sql>
 
diff --git a/src/main/resources/mapper/system/SysUserMapper.xml b/src/main/resources/mapper/system/SysUserMapper.xml
index e4f818a..e27a224 100644
--- a/src/main/resources/mapper/system/SysUserMapper.xml
+++ b/src/main/resources/mapper/system/SysUserMapper.xml
@@ -153,13 +153,18 @@
 	<select id="checkEmailUnique" parameterType="String" resultMap="SysUserResult">
 		select user_id, email from sys_user where email = #{email} and del_flag = '0' limit 1
 	</select>
-	<select id="selectUserByIds" resultType="com.ruoyi.project.system.domain.SysUser">
-		<include refid="selectUserVo"/>
-		where u.user_id in <foreach collection="userIds" item="item" open="(" separator="," close=")">
- 			#{item}
-        </foreach>
-		and u.del_flag = '0'
-	</select>
+    <select id="selectUserByIds" resultType="com.ruoyi.project.system.domain.SysUser">
+        <include refid="selectUserVo"/>
+        <where>
+            <if test="userIds != null and userIds.size > 0">
+                and u.user_id in
+                <foreach collection="userIds" item="item" open="(" separator="," close=")">
+                    #{item}
+                </foreach>
+            </if>
+            and u.del_flag = '0'
+        </where>
+    </select>
 	<select id="selectRegistrantIds" resultType="com.ruoyi.project.system.domain.SysUser">
 		SELECT user_id, nick_name FROM sys_user
 		<where>
diff --git a/src/main/resources/purchase-agent-prompt.txt b/src/main/resources/purchase-agent-prompt.txt
index 891df45..97f7eb2 100644
--- a/src/main/resources/purchase-agent-prompt.txt
+++ b/src/main/resources/purchase-agent-prompt.txt
@@ -4,6 +4,11 @@
 宸ヤ綔瑙勫垯锛�
 1. 浼樺厛璋冪敤宸ュ叿鍑芥暟鑾峰彇閲囪喘鍙拌处銆佷粯娆俱�佸彂绁ㄣ�侀��璐х瓑缁撴瀯鍖栨暟鎹��
 2. 閬囧埌鈥滅粺璁�/鍒嗘瀽/鎶ヨ〃/浠婂勾/鏈湀/杩慩X澶┾�濈瓑闇�姹傦紝浼樺厛缁欏嚭缁熻缁撴灉鍜屽叧閿粨璁恒��
-3. 鏃犳硶鐩存帴寰楀嚭缁撹鏃讹紝鏄庣‘璇存槑缂哄皯鍝簺瀛楁鎴栫瓫閫夋潯浠躲��
-4. 缁撴灉鐢ㄧ畝娲佷腑鏂囧洖绛旓紝鍏堢粰缁撹锛屽啀缁欏叧閿暟鎹偣銆�
-5. 涓嶈缂栭�犻噰璐暟鎹紝鎵�鏈夌粨璁哄繀椤诲熀浜庡伐鍏疯繑鍥炪��
+3. 鐢ㄦ埛闂�滄湰鏈堥噰璐噾棰濇帓鍚嶉潬鍓嶇殑鐗╂枡鈥濃�滈噰璐噾棰濇帓琛屸�濃�滅墿鏂欐帓琛屸�濇椂锛岃皟鐢ㄢ�滈噰璐墿鏂欓噾棰濇帓琛屸�濄��
+4. 鐢ㄦ埛闂�滃摢浜涢噰璐鍗曡繕鏈叆搴撯�濃�滄湭鍏ュ簱閲囪喘鍗曗�濃�滃緟鍏ュ簱璁㈠崟鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈡湭鍏ュ簱閲囪喘璁㈠崟鈥濄��
+5. 鐢ㄦ埛闂�滄渶杩�7澶╀緵搴斿晢鍒拌揣寮傚父鈥濃�滃埌璐ч棶棰樷�濃�滃埌璐у紓甯糕�濇椂锛岃皟鐢ㄢ�滄煡璇㈤噰璐埌璐у紓甯糕�濄��
+6. 鐢ㄦ埛闂�滃緟浠樻閲囪喘鍗曗�濃�滄湭浠樻閲囪喘鍗曗�濃�滄湭浠樻竻閲囪喘璁㈠崟鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈠緟浠樻閲囪喘鍗曗�濄��
+7. 鐢ㄦ埛闂�滄湰鏈堥噰璐��璐ф儏鍐碘�濃�滈噰璐��璐у垪琛ㄢ�濃�滈��鏂�/鎷掓敹鎯呭喌鈥濇椂锛岃皟鐢ㄢ�滄煡璇㈤噰璐��璐ф儏鍐碘�濄��
+8. 缁撴灉鐢ㄧ畝娲佷腑鏂囧洖绛旓紝鍏堢粰缁撹锛屽啀缁欏叧閿暟鎹偣銆�
+9. 涓嶈缂栭�犻噰璐暟鎹紝鎵�鏈夌粨璁哄繀椤诲熀浜庡伐鍏疯繑鍥炪��
+10. 鏃犳硶鐩存帴寰楀嚭缁撹鏃讹紝鏄庣‘璇存槑缂哄皯鍝簺瀛楁鎴栫瓫閫夋潯浠躲��

--
Gitblit v1.9.3