From 55d694aaae4e421b5e55cd941500e08f85cb5d4d Mon Sep 17 00:00:00 2001
From: 云 <2163098428@qq.com>
Date: 星期四, 27 八月 2026 18:21:33 +0800
Subject: [PATCH] feat(sales): 添加框架合同订单功能并优化销售台账管理

---
 src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java                                                    |    2 
 src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml                  |   10 
 src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java                                   |  194 +++++
 src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java                       |   15 
 src/main/resources/mapper/sales/SalesLedgerMapper.xml                                                    |   24 
 src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java                                         |    6 
 src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java                                               |   11 
 src/main/java/com/ruoyi/sales/pojo/SalesLedger.java                                                      |   10 
 src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java                                      |   41 
 src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java                                    |    8 
 src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java                            |   49 
 src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java                                      |   18 
 src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java                                  |   23 
 src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java                           |   34 
 src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java                                              |   10 
 src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java                                   |  149 +++
 src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java                                      |   19 
 src/main/resources/mapper/device/DeviceLedgerMapper.xml                                                  |   49 +
 src/main/java/com/ruoyi/device/pojo/DeviceArea.java                                                      |   88 ++
 src/main/java/com/ruoyi/measuringinstrumentledger/mapper/MeasuringInstrumentLedgerMapper.java            |    3 
 src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java                                                      |    5 
 src/main/java/com/ruoyi/sales/vo/FrameworkOrderVo.java                                                   |   17 
 doc/20260827_设备区域前端联调文档.md                                                                               |  277 +++++++
 src/main/java/com/ruoyi/device/service/IDeviceAreaService.java                                           |   35 
 src/main/java/com/ruoyi/device/pojo/DeviceLedger.java                                                    |   11 
 sql/migration_framework_contract.sql                                                                     |   51 +
 src/main/java/com/ruoyi/device/mapper/DeviceLedgerMapper.java                                            |    6 
 src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java |   96 ++
 src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java                           |   44 +
 doc/计量器具台账批量导入前端对接文档.md                                                                                  |  242 ++++++
 src/main/java/com/ruoyi/measuringinstrumentledger/controller/MeasuringInstrumentLedgerController.java    |   14 
 src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java                                           |    7 
 sql/migration_device_area.sql                                                                            |   25 
 src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java                                |   10 
 src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java                                 |    5 
 src/main/java/com/ruoyi/measuringinstrumentledger/dto/MeasuringInstrumentLedgerImportDto.java            |   40 +
 src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java                                                  |   11 
 src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerService.java          |    6 
 src/main/java/com/ruoyi/device/vo/DeviceAreaTreeDto.java                                                 |   29 
 doc/设备保养年度保养前端联调说明.md                                                                                    |  171 ++++
 src/main/java/com/ruoyi/device/controller/DeviceAreaController.java                                      |   44 +
 doc/20260827_框架合同与框架订单前端联调文档.md                                                                          |  290 +++++++
 src/main/java/com/ruoyi/device/dto/DeviceAreaDto.java                                                    |   35 
 src/main/java/com/ruoyi/device/mapper/DeviceAreaMapper.java                                              |   12 
 44 files changed, 2,175 insertions(+), 71 deletions(-)

diff --git "a/doc/20260827_\346\241\206\346\236\266\345\220\210\345\220\214\344\270\216\346\241\206\346\236\266\350\256\242\345\215\225\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260827_\346\241\206\346\236\266\345\220\210\345\220\214\344\270\216\346\241\206\346\236\266\350\256\242\345\215\225\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..e9057e2
--- /dev/null
+++ "b/doc/20260827_\346\241\206\346\236\266\345\220\210\345\220\214\344\270\216\346\241\206\346\236\266\350\256\242\345\215\225\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,290 @@
+# 妗嗘灦鍚堝悓涓庢鏋惰鍗曞墠绔仈璋冩枃妗�
+
+## 涓�銆佷笟鍔¤鏄�
+
+閿�鍞彴璐︿腑鐨勯攢鍞悎鍚屾寜妗嗘灦鍚堝悓澶勭悊锛�
+
+- 妗嗘灦鍚堝悓鍙櫥璁板悎鍚屽熀纭�淇℃伅鍜屼骇鍝佽寖鍥淬��
+- 妗嗘灦鍚堝悓涓嶅~鍐欐暟閲忓拰浜よ揣鏃ユ湡銆�
+- 涓�涓鏋跺悎鍚屼笅鍙互鍒涘缓澶氫釜妗嗘灦璁㈠崟銆�
+- 妗嗘灦璁㈠崟鍙櫥璁颁骇鍝佸拰浜よ揣鏃ユ湡锛屼笉濉啓鏁伴噺銆�
+- 瀹為檯鏁伴噺鍙湪姣忔鍙戣揣鏃跺~鍐欙紝淇濆瓨浜庡彂璐ф槑缁嗐��
+- 涓�涓鏋惰鍗曚骇鍝佸彲浠ュ娆″垎鎵瑰彂璐с��
+- 妗嗘灦鍚堝悓浜у搧涓嶈兘鐩存帴鍙戣揣锛屽繀椤婚�夋嫨妗嗘灦璁㈠崟浜у搧銆�
+- 妗嗘灦鍚堝悓鍒涘缓鏃朵笉鐢熸垚鐢熶骇璁″垝锛涙鏋惰鍗曞垱寤烘椂鑷姩鐢熸垚鐢熶骇璁″垝锛堥渶姹傛暟閲忕暀绌猴級銆�
+- 鐢熶骇鏁伴噺鍦ㄧ敓浜ц鍒掍笅鍙戞椂濉啓锛岀敓浜ц鍒掍笅鍙戞椂涓嶅彈閿�鍞彴璐︽暟閲忛檺鍒躲��
+
+## 浜屻�佸悎鍚岀被鍨�
+
+| contractType | 鍚箟 | 鏁伴噺 | 浜よ揣鏃ユ湡 | 鏄惁鑷姩鐢熸垚鐢熶骇璁″垝 |
+| --- | --- | --- | --- | --- |
+| 1 | 鏅�氶攢鍞悎鍚� | 鎸夊師鏈変笟鍔� | 鎸夊師鏈変笟鍔� | 鎸夊師鏈変笟鍔� |
+| 2 | 妗嗘灦鍚堝悓 | 涓嶅~鍐� | 涓嶅~鍐� | 鍚� |
+| 3 | 妗嗘灦璁㈠崟 | 涓嶅~鍐� | 浜у搧鏄庣粏濉啓 | 鏄紙闇�姹傛暟閲忕暀绌猴紝涓嬪彂鏃跺~鍐欙級 |
+
+## 涓夈�佸垱寤烘垨淇濆瓨妗嗘灦鍚堝悓
+
+鎺ュ彛锛�
+
+```text
+POST /sales/ledger/addOrUpdateSalesLedger
+```
+
+璇锋眰绀轰緥锛�
+
+```json
+{
+  "contractType": 2,
+  "customerId": 1001,
+  "customerContractNo": "瀹㈡埛鍚堝悓鍙�",
+  "projectName": "椤圭洰鍚嶇О",
+  "salesman": "涓氬姟鍛�",
+  "entryDate": "2026-08-27",
+  "executionDate": "2026-08-27",
+  "paymentMethod": "鎸夊悎鍚岀害瀹�",
+  "remarks": "妗嗘灦鍚堝悓",
+  "productData": [
+    {
+      "productId": 10,
+      "productModelId": 101,
+      "productCategory": "浜у搧鍚嶇О",
+      "specificationModel": "瑙勬牸鍨嬪彿",
+      "unit": "浠�",
+      "quantity": null,
+      "deliveryDate": null,
+      "isProduction": false,
+      "type": 1
+    }
+  ]
+}
+```
+
+鍓嶇瑕佹眰锛�
+
+- `contractType` 浼� `2`銆�
+- 浜у搧鏁伴噺浼� `null` 鎴栦笉浼狅紝涓嶄紶绌哄瓧绗︿覆銆�
+- 浜у搧浜よ揣鏃ユ湡浼� `null` 鎴栦笉浼犮��
+- 涓嶈鏍规嵁妗嗘灦鍚堝悓浜у搧鏁伴噺鎺у埗鍙戣揣鏁伴噺銆�
+- 涓嶈鍦ㄤ繚瀛樻垚鍔熷悗鑷姩璺宠浆鐢熶骇璁″垝鍒涘缓銆�
+
+## 鍥涖�佸垱寤烘鏋惰鍗�
+
+鎺ュ彛锛�
+
+```text
+POST /sales/ledger/createFrameworkOrder
+```
+
+璇锋眰绀轰緥锛�
+
+```json
+{
+  "id": 10001,
+  "remarks": "绗竴鎵规鏋惰鍗�",
+  "productData": [
+    {
+      "parentProductId": 20001,
+      "deliveryDate": "2026-09-15"
+    },
+    {
+      "parentProductId": 20002,
+      "deliveryDate": "2026-09-20"
+    }
+  ]
+}
+```
+
+瀛楁璇存槑锛�
+
+- `id`锛氱埗妗嗘灦鍚堝悓 ID銆�
+- `remarks`锛氭鏋惰鍗曞娉ㄣ��
+- `productData[].parentProductId`锛氱埗妗嗘灦鍚堝悓浜у搧 ID銆�
+- `productData[].deliveryDate`锛氬綋鍓嶆鏋惰鍗曚骇鍝佷氦璐ф棩鏈燂紝蹇呭~銆�
+- 涓嶈浼犳暟閲忥紝妗嗘灦璁㈠崟浜у搧鏁伴噺鍥哄畾涓虹┖銆�
+- 鍚屼竴妗嗘灦璁㈠崟涓笉鑳介噸澶嶉�夋嫨鍚屼竴涓埗鍚堝悓浜у搧銆�
+
+鎴愬姛杩斿洖锛氭部鐢ㄧ郴缁� `toAjax` 杩斿洖鏍煎紡锛屾垚鍔熸椂琛ㄧず妗嗘灦璁㈠崟宸插垱寤恒��
+
+澶辫触鍦烘櫙锛�
+
+- 鏈紶鐖跺悎鍚� ID锛氳閫夋嫨妗嗘灦鍚堝悓銆�
+- 鐖跺崟涓嶆槸妗嗘灦鍚堝悓锛氱埗鍙拌处涓嶆槸妗嗘灦鍚堝悓銆�
+- 鏈�夋嫨浜у搧锛氳濉啓妗嗘灦璁㈠崟浜у搧銆�
+- 鏈~鍐欑埗浜у搧鎴栦氦璐ф棩鏈燂細妗嗘灦璁㈠崟浜у搧蹇呴』鍏宠仈鐖朵骇鍝佸苟濉啓浜よ揣鏃ユ湡銆�
+- 鐖朵骇鍝佷笉灞炰簬璇ユ鏋跺悎鍚岋細妗嗘灦璁㈠崟浜у搧涓嶅睘浜庤妗嗘灦鍚堝悓銆�
+
+## 浜斻�佹煡璇㈡鏋惰鍗曞垪琛�
+
+鎺ュ彛锛�
+
+```text
+GET /sales/ledger/listFrameworkOrders?parentContractId=10001
+```
+
+杩斿洖绀轰緥锛�
+
+```json
+{
+  "code": 200,
+  "data": [
+    {
+      "id": 30001,
+      "salesContractNo": "閿�鍞悎鍚屽彿",
+      "contractType": 3,
+      "parentContractId": 10001,
+      "customerId": 1001,
+      "customerName": "瀹㈡埛鍚嶇О",
+      "projectName": "椤圭洰鍚嶇О",
+      "remarks": "绗竴鎵规鏋惰鍗�",
+      "shippedQuantity": 120
+    }
+  ]
+}
+```
+
+璇存槑锛�
+
+- `contractType` 鍥哄畾涓� `3`銆�
+- `orderedQuantity` 涓嶄綔涓烘暟閲忔潵婧愶紝妗嗘灦璁㈠崟涓嶈褰曞悎鍚屾暟閲忋��
+- `shippedQuantity` 鏄妗嗘灦璁㈠崟涓嬫墍鏈夊凡鍙戣揣鍙戣揣鏄庣粏鏁伴噺涔嬪拰銆�
+- `shippedQuantity` 浠呯粺璁$姸鎬佷负鈥滃凡鍙戣揣鈥濈殑鍙戣揣鍗曘��
+- 鏃犲彂璐ц褰曟椂 `shippedQuantity` 杩斿洖 `0`銆�
+
+## 鍏�佹煡璇㈡鏋跺悎鍚岀疮璁″彂璐ф暟閲�
+
+鎺ュ彛锛�
+
+```text
+GET /sales/ledger/frameworkContractShippedQuantity?parentContractId=10001
+```
+
+杩斿洖绀轰緥锛�
+
+```json
+{
+  "code": 200,
+  "data": 320
+}
+```
+
+璇ユ暟閲忎负鐖舵鏋跺悎鍚屼笅鎵�鏈夋鏋惰鍗曘�佹墍鏈夊凡鍙戣揣鍙戣揣鍗曟槑缁嗙殑鏁伴噺鍚堣銆傝鎺ュ彛鍙敤浜庡睍绀虹粺璁★紝涓嶇敤浜庨檺鍒跺悗缁彂璐ф暟閲忋��
+
+## 涓冦�佸垱寤哄彂璐х敵璇�
+
+鎺ュ彛锛�
+
+```text
+POST /shippingInfo/addReq
+```
+
+鐜版湁鍙戣揣璇锋眰涓户缁娇鐢細
+
+```json
+{
+  "salesLedgerId": 30001,
+  "salesLedgerProductId": 30101,
+  "batchNoDetailList": [
+    {
+      "stockInventoryId": 5001,
+      "batchNo": "RK20260827001",
+      "productModelId": 101,
+      "quantity": 80
+    }
+  ],
+  "shippingAddress": "鏀惰揣鍦板潃",
+  "type": "鐗╂祦"
+}
+```
+
+鑱旇皟瑙勫垯锛�
+
+- `salesLedgerId` 蹇呴』涓� `salesLedgerProductId` 鎵�灞炲彴璐︿竴鑷淬��
+- `salesLedgerProductId` 蹇呴』鍏宠仈鏅�氶攢鍞悎鍚屼骇鍝佹垨妗嗘灦璁㈠崟浜у搧銆�
+- 涓嶅厑璁镐娇鐢ㄦ鏋跺悎鍚屼骇鍝� ID 鍙戣揣銆�
+- `batchNoDetailList` 涓殑鏁伴噺鏄湰娆″疄闄呭彂璐ф暟閲忥紝蹇呭~涓斿ぇ浜庨浂銆�
+- 鍚屼竴涓鏋惰鍗曚骇鍝佸彲浠ュ娆℃彁浜ゅ彂璐х敵璇枫��
+- 涓嶈兘鍚屾椂瀛樺湪澶氫釜鈥滃緟瀹℃牳鈥濇垨鈥滃鏍镐腑鈥濈殑鍚屼骇鍝佸彂璐х敵璇枫��
+- 宸插彂璐у巻鍙蹭笉浼氶樆姝笅涓�娆″垎鎵瑰彂璐с��
+- 鍙戣揣鏁伴噺涓嶄笌閿�鍞彴璐︽暟閲忔瘮杈冿紝鏈�缁堜互搴撳瓨鎵规鍙敤閲忓拰搴撳瓨鎵e噺缁撴灉涓哄噯銆�
+
+## 鍏�佺敓浜ц鍒掕仈璋�
+
+鍒涘缓妗嗘灦璁㈠崟鏃讹紝鍚庣浼氫负姣忎釜妗嗘灦璁㈠崟浜у搧**鑷姩鐢熸垚涓�鏉$敓浜ц鍒�**锛�
+
+- `salesLedgerId`銆乣salesLedgerProductId` 鑷姩缁戝畾褰撳墠妗嗘灦璁㈠崟銆�
+- `productModelId` 鍙栨鏋惰鍗曚骇鍝佸瀷鍙枫��
+- `qtyRequired`锛堥渶姹傛暟閲忥級**鐣欑┖**锛岀敓浜ф暟閲忓湪鐢熶骇璁″垝涓嬪彂鏃跺~鍐欍��
+- `requiredDate`銆乣promisedDeliveryDate` 鍙栨鏋惰鍗曚骇鍝佷氦璐ф棩鏈熴��
+- `source = 閿�鍞甡锛岀姸鎬佷负寰呬笅鍙戙��
+- 妗嗘灦鍚堝悓鍒涘缓鏃朵笉鐢熸垚鐢熶骇璁″垝銆�
+
+涓嬪彂鐢熶骇璁㈠崟鏃朵娇鐢ㄤ笅鍙戣姹備腑鐨勬暟閲忥紝涓嶄緷璧栭攢鍞彴璐︿骇鍝� `quantity`锛屼篃涓嶆牎楠岃鍒掗渶姹傛暟閲忥紝鍙户缁垎鎵逛笅鍙戙��
+
+`validateSalesSource` 鏍¢獙瑙勫垯涓嶅彉锛�
+
+- `salesLedgerId` 鍜� `salesLedgerProductId` 蹇呴』鍚屾椂濉啓锛屾垨鍚屾椂涓嶅~鍐欍��
+- 鍏宠仈閿�鍞潵婧愭椂锛岄攢鍞骇鍝佸繀椤诲睘浜庤閿�鍞彴璐︺��
+- 閿�鍞彴璐︾被鍨嬪彧鑳芥槸鏅�氶攢鍞悎鍚� `1` 鎴栨鏋惰鍗� `3`銆�
+- 涓嶈兘鐩存帴鍏宠仈妗嗘灦鍚堝悓 `2`銆�
+- `productModelId` 蹇呴』涓庨攢鍞骇鍝佸瀷鍙蜂竴鑷淬��
+
+## 涔濄�侀〉闈氦浜掑缓璁�
+
+### 妗嗘灦鍚堝悓椤甸潰
+
+- 鍚堝悓绫诲瀷閫夋嫨鈥滄鏋跺悎鍚屸�濆悗闅愯棌鎴栫鐢ㄥ悎鍚屾暟閲忚緭鍏ャ��
+- 闅愯棌鎴栫鐢ㄥ悎鍚岀骇浜よ揣鏃ユ湡銆�
+- 浜у搧鏄庣粏闅愯棌鏁伴噺鍜屼氦璐ф棩鏈熻緭鍏ャ��
+- 淇濆瓨鍚庢彁渚涒�滃垱寤烘鏋惰鍗曗�濆叆鍙c��
+
+### 妗嗘灦璁㈠崟椤甸潰
+
+- 灞曠ず鐖舵鏋跺悎鍚屼俊鎭紝鍙銆�
+- 浜у搧閫夋嫨鑼冨洿涓虹埗妗嗘灦鍚堝悓浜у搧銆�
+- 姣忎釜浜у搧鍙~鍐欎氦璐ф棩鏈熴��
+- 闅愯棌鏁伴噺杈撳叆銆�
+- 淇濆瓨鎴愬姛鍚庡睍绀鸿鍗曠紪鍙峰拰璁㈠崟浜у搧鍒楄〃銆�
+
+### 鍙戣揣椤甸潰
+
+- 閫夋嫨閿�鍞悎鍚屾椂锛屾鏋跺悎鍚屼笉鑳戒綔涓虹洿鎺ュ彂璐у彴璐︺��
+- 妗嗘灦鍚堝悓搴斿睍寮�閫夋嫨鍏朵笅妗嗘灦璁㈠崟锛屽啀閫夋嫨妗嗘灦璁㈠崟浜у搧銆�
+- 姣忔鍙戣揣濉啓瀹為檯鍙戣揣鏁伴噺鍜屽簱瀛樻壒娆°��
+- 宸插彂璐у悗浠嶅厑璁稿啀娆″垱寤烘柊鐨勫彂璐ф壒娆°��
+
+### 鐢熶骇璁″垝椤甸潰
+
+- 妗嗘灦璁㈠崟鍒涘缓鍚庤嚜鍔ㄧ敓鎴愮敓浜ц鍒掕褰曪紙闇�姹傛暟閲忎负绌猴級銆�
+- 鐢熶骇璁″垝鏁伴噺鍦ㄧ敓浜ц鍒掍笅鍙戞椂鍗曠嫭濉啓銆�
+- 涓嶄粠閿�鍞彴璐︿骇鍝佹暟閲忓洖濉敓浜ц鍒掓暟閲忋��
+
+## 鍗併�佽仈璋冮獙鏀舵祦绋�
+
+1. 鍒涘缓涓�鏉℃鏋跺悎鍚岋紝浜у搧涓嶅~鍐欐暟閲忓拰浜よ揣鏃ユ湡銆�
+2. 鏌ヨ妗嗘灦鍚堝悓璇︽儏锛岀‘璁ゅ悎鍚岀被鍨嬩负 `2`銆�
+3. 鍩轰簬鍚屼竴妗嗘灦鍚堝悓鍒涘缓涓や釜妗嗘灦璁㈠崟銆�
+4. 纭涓や釜妗嗘灦璁㈠崟鍧囦负 `contractType = 3`锛屼骇鍝佹暟閲忎负绌恒��
+5. 鍒涘缓妗嗘灦璁㈠崟鍚庯紝鐢熶骇璁″垝鍒楄〃搴斿嚭鐜板搴旇鍒掞紝`qtyRequired` 涓虹┖锛宍source = 閿�鍞甡锛岄渶姹傛棩鏈熶负浜у搧浜よ揣鏃ユ湡銆�
+6. 涓虹涓�涓鏋惰鍗曚骇鍝佸垱寤虹涓�绗斿彂璐х敵璇枫��
+7. 瀹℃牳骞跺畬鎴愬彂璐с��
+8. 涓哄悓涓�涓鏋惰鍗曚骇鍝佸垱寤虹浜岀瑪鍙戣揣鐢宠銆�
+9. 鏌ヨ妗嗘灦璁㈠崟鍒楄〃锛岀‘璁� `shippedQuantity` 涓轰袱娆″彂璐ф暟閲忎箣鍜屻��
+10. 鏌ヨ妗嗘灦鍚堝悓绱鍙戣揣鏁伴噺锛岀‘璁ゅ寘鍚墍鏈夋鏋惰鍗曠殑宸插彂璐ф暟閲忋��
+11. 瀵硅嚜鍔ㄧ敓鎴愮殑鐢熶骇璁″垝濉啓涓嬪彂鏁伴噺骞朵笅鍙戯紝纭涓嶄緷璧栭攢鍞骇鍝佹暟閲忋��
+12. 灏濊瘯鐩存帴浣跨敤妗嗘灦鍚堝悓浜у搧鍙戣揣锛屾帴鍙e簲杩斿洖閿欒銆�
+13. 灏濊瘯涓哄悓涓�浜у搧鍚屾椂鎻愪氦涓ょ瑪寰呭鏍稿彂璐х敵璇凤紝绗簩绗斿簲琚嫆缁濄��
+
+## 鍗佷竴銆佸綋鍓嶅悗绔獙璇佺姸鎬�
+
+宸插畬鎴愶細
+
+- 妗嗘灦鍚堝悓銆佹鏋惰鍗曟暟鎹瓧娈点��
+- 妗嗘灦璁㈠崟鍒涘缓鎺ュ彛銆�
+- 妗嗘灦璁㈠崟鍒涘缓鏃惰嚜鍔ㄧ敓鎴愮敓浜ц鍒掞紙闇�姹傛暟閲忕暀绌猴級銆�
+- 妗嗘灦璁㈠崟鍒楄〃鎺ュ彛銆�
+- 妗嗘灦鍚堝悓绱宸插彂璐ф暟閲忔帴鍙c��
+- 妗嗘灦鍚堝悓浜у搧鐩存帴鍙戣揣鎷︽埅銆�
+- 鐢熶骇璁″垝閿�鍞潵婧愬綊灞炴牎楠屻��
+- 鏁版嵁搴撹縼绉诲強鐩爣鏁版嵁搴撳瓧娈甸獙璇併��
+
+璇存槑锛氬悗绔凡閫氳繃 `mvn compile` 缂栬瘧楠岃瘉銆�
diff --git "a/doc/20260827_\350\256\276\345\244\207\345\214\272\345\237\237\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md" "b/doc/20260827_\350\256\276\345\244\207\345\214\272\345\237\237\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..0eb2715
--- /dev/null
+++ "b/doc/20260827_\350\256\276\345\244\207\345\214\272\345\237\237\345\211\215\347\253\257\350\201\224\350\260\203\346\226\207\346\241\243.md"
@@ -0,0 +1,277 @@
+# 璁惧鍖哄煙锛堣溅闂�/浣嶇疆锛夊墠绔仈璋冩枃妗�
+
+## 涓�銆佷笟鍔¤鏄�
+
+璁惧鍙拌处闇�鎸夎溅闂�/鍖哄煙姹囨�昏澶囷紝杞﹂棿/浣嶇疆鍙嚜琛屾柊澧烇紝涓旀敮鎸佸瓙鐖剁骇閫掑綊锛�
+
+- 鏂板鐙珛鈥滆澶囧尯鍩熲�濇蹇碉紝杞﹂棿銆佸尯鍩熴�佷綅缃粺涓�浣滀负鍖哄煙鑺傜偣銆�
+- 鍖哄煙鏀寔浠绘剰灞傜骇鐖跺瓙閫掑綊銆�
+- 璁惧鍙拌处鍏宠仈涓�涓尯鍩燂紙`area_id`锛夈��
+- 鎸夊尯鍩熸眹鎬昏澶囨椂锛屾眹鎬昏鍖哄煙鍙婂叾鎵�鏈夊瓙绾у尯鍩熺殑璁惧銆�
+- 璁惧鍙拌处椤甸潰寤鸿閲囩敤鈥滃乏渚у尯鍩熸爲 + 鍙充晶璁惧鍒楄〃鈥濆舰寮忋��
+
+## 浜屻�佹暟鎹簱杩佺Щ
+
+鎵ц `sql/migration_device_area.sql`锛堝凡鍋氬箓绛夊鐞嗭紝`device_area` 琛ㄥ凡瀛樺湪鏃惰烦杩囧缓琛ㄣ�乣area_id` 鍒楀凡瀛樺湪鏃惰烦杩囧姞鍒楋紝鍙畨鍏ㄩ噸澶嶆墽琛岋級锛屽唴瀹癸細
+
+1. 鏂板琛� `device_area`锛堜富閿棤鍏跺畠绾︽潫锛夛細
+
+   | 瀛楁 | 绫诲瀷 | 璇存槑 |
+   | --- | --- | --- |
+   | `id` | bigint | 涓婚敭鑷 |
+   | `name` | varchar(255) | 鍖哄煙/杞﹂棿/浣嶇疆鍚嶇О |
+   | `parent_id` | bigint | 鐖剁骇鍖哄煙ID锛屾牴鑺傜偣涓� NULL |
+   | `ancestors` | varchar(1000) | 绁栫骇璺緞锛屽 `1,2,3` |
+   | `sort` | int | 鎺掑簭 |
+   | `dept_id` | bigint | 閮ㄩ棬ID锛堝彲閫夛級 |
+   | `tenant_id` | bigint | 绉熸埛ID |
+   | `create_user` | int | 鍒涘缓浜� |
+   | `create_time` | datetime | 鍒涘缓鏃堕棿 |
+   | `update_user` | int | 鏇存柊浜� |
+   | `update_time` | datetime | 鏇存柊鏃堕棿 |
+
+2. `device_ledger` 澧炲姞 `area_id bigint NULL`锛堣澶囧尯鍩烮D锛夈��
+
+## 涓夈�佹帴鍙e垪琛�
+
+| 鎺ュ彛 | 鏂规硶 | 璺緞 | 璇存槑 |
+| --- | --- | --- | --- |
+| 鏌ヨ鍖哄煙鏍� | GET | `/device/area/tree` | 杩斿洖鏍戝舰鍖哄煙鍙婇�掑綊璁惧鏁伴噺 |
+| 鏂板鍖哄煙 | POST | `/device/area` | 鏂板鍖哄煙/杞﹂棿/浣嶇疆 |
+| 淇敼鍖哄煙 | PUT | `/device/area` | 淇敼鍖哄煙鍚嶇О/鐖剁骇/鎺掑簭 |
+| 鍒犻櫎鍖哄煙 | DELETE | `/device/area/{ids}` | 鍒犻櫎鍖哄煙 |
+| 璁惧鍒楄〃 | GET | `/device/ledger/page` | 鏀寔鎸夊尯鍩熼�掑綊绛涢�夛紙鏂板 `areaId` 鍙傛暟锛� |
+| 璁惧閫夋嫨鍒楄〃 | GET | `/device/ledger/getDeviceLedger` | 宸℃閫夎澶囩敤锛屾敮鎸� `areaId` 閫掑綊绛涢�夛紝杩斿洖 `areaName` |
+
+## 鍥涖�佸尯鍩熸爲鎺ュ彛
+
+### 鏌ヨ鍖哄煙鏍�
+
+```text
+GET /device/area/tree
+```
+
+杩斿洖绀轰緥锛�
+
+```json
+{
+  "code": 200,
+  "msg": "鎿嶄綔鎴愬姛",
+  "data": [
+    {
+      "id": 1,
+      "parentId": null,
+      "label": "涓�鍙疯溅闂�",
+      "deviceCount": 8,
+      "children": [
+        {
+          "id": 2,
+          "parentId": 1,
+          "label": "鍐插帇鍖�",
+          "deviceCount": 5,
+          "children": []
+        },
+        {
+          "id": 3,
+          "parentId": 1,
+          "label": "瑁呴厤鍖�",
+          "deviceCount": 3,
+          "children": []
+        }
+      ]
+    }
+  ]
+}
+```
+
+瀛楁璇存槑锛�
+
+- `id`锛氬尯鍩烮D銆�
+- `parentId`锛氱埗绾у尯鍩烮D锛屾牴鑺傜偣涓� `null`銆�
+- `label`锛氬尯鍩熸樉绀哄悕绉帮紝鍓嶇 el-tree 鐩存帴鐢� `label` 娓叉煋銆�
+- `deviceCount`锛�**璇ュ尯鍩熷強鎵�鏈夊瓙绾у尯鍩�**鐨勮澶囨暟閲忥紙閫掑綊姹囨�伙級銆�
+
+鍓嶇寤鸿鐩存帴鐢ㄤ簬 `el-tree`锛�
+
+```html
+<el-tree
+  :data="areaTree"
+  node-key="id"
+  :props="{ label: 'label', children: 'children' }"
+  @node-click="onAreaClick"
+/>
+```
+
+鐐瑰嚮鑺傜偣鏃讹紝鎶� `id` 浣滀负 `areaId` 浼犵粰璁惧鍒楄〃鏌ヨ銆�
+
+## 浜斻�佹柊澧炲尯鍩�
+
+```text
+POST /device/area
+Content-Type: application/json
+```
+
+璇锋眰浣擄細
+
+```json
+{
+  "name": "浜屽彿杞﹂棿",
+  "parentId": null,
+  "sort": 1
+}
+```
+
+- `name`锛氬繀濉紝鍖哄煙鍚嶇О銆�
+- `parentId`锛氱埗绾у尯鍩烮D锛屽垱寤烘牴鑺傜偣鏃朵紶 `null` 鎴栦笉浼犮��
+- `sort`锛氭帓搴忥紝鍙┖銆�
+
+鎴愬姛杩斿洖 `AjaxResult.success()`銆�
+
+澶辫触鍦烘櫙锛�
+
+- 鍚嶇О涓虹┖锛歚鍖哄煙鍚嶇О涓嶈兘涓虹┖`銆�
+- 鐖剁骇鍖哄煙涓嶅瓨鍦細`鐖剁骇鍖哄煙涓嶅瓨鍦╜銆�
+
+## 鍏�佷慨鏀瑰尯鍩�
+
+```text
+PUT /device/area
+Content-Type: application/json
+```
+
+璇锋眰浣擄細
+
+```json
+{
+  "id": 2,
+  "name": "鍐插帇杞﹂棿",
+  "parentId": 1,
+  "sort": 1
+}
+```
+
+鏀寔淇敼鍚嶇О銆佺埗绾э紙鎷栧姩璋冩暣灞傜骇锛夈�佹帓搴忋�傚悗绔嚜鍔ㄧ淮鎶� `ancestors` 绁栫骇璺緞锛涗慨鏀圭埗绾ф椂鍓嶇鏃犻渶浼� `ancestors`銆�
+
+澶辫触鍦烘櫙锛�
+
+- 鏈紶ID锛歚鍖哄煙ID涓嶈兘涓虹┖`銆�
+- 鍖哄煙涓嶅瓨鍦細`鍖哄煙涓嶅瓨鍦╜銆�
+- 鎸傝浇鍒拌嚜韬細`涓嶈兘灏嗗尯鍩熸寕杞藉埌鑷韩`銆�
+- 鎸傝浇鍒拌嚜宸辩殑瀛愮骇锛歚涓嶈兘灏嗗尯鍩熸寕杞藉埌鑷繁鐨勫瓙绾т笅`锛堥槻姝㈠惊鐜級銆�
+
+## 涓冦�佸垹闄ゅ尯鍩�
+
+```text
+DELETE /device/area/{ids}
+```
+
+绀轰緥锛�
+
+```text
+DELETE /device/area/2,3
+```
+
+鎴愬姛杩斿洖 `AjaxResult.success()`銆�
+
+澶辫触鍦烘櫙锛�
+
+- 璇ュ尯鍩熷瓨鍦ㄥ瓙绾у尯鍩燂紝璇峰厛鍒犻櫎瀛愮骇銆�
+- 璇ュ尯鍩熶笅瀛樺湪璁惧锛岃鍏堢Щ闄よ澶囧叧鑱斻��
+
+鍓嶇寤鸿锛氬垹闄ゅ墠寮圭獥纭锛涜嫢鍚庣杩斿洖涓婅堪閿欒鎻愮ず锛岀洿鎺ュ睍绀虹粰鐢ㄦ埛銆�
+
+## 鍏�佽澶囧垪琛ㄦ寜鍖哄煙绛涢��
+
+鍦ㄥ師鏈夎澶囧彴璐﹀垎椤垫帴鍙e鍔� `areaId` 鍙傛暟锛�
+
+```text
+GET /device/ledger/page?areaId=1&pageNum=1&pageSize=10
+```
+
+鏂板琛屼负锛�
+
+- 杩斿洖瀛楁鏂板 `areaId`銆乣areaName`锛堝尯鍩熷悕绉帮級锛屼緵鍒楄〃灞曠ず銆�
+- 褰� `areaId` 鏈夊�兼椂锛屾煡璇㈣鍖哄煙**鍙婂叾鎵�鏈夊瓙绾у尯鍩�**涓嬬殑璁惧锛堥�氳繃 `ancestors` 绁栫骇鍖归厤锛屽墠绔棤闇�鍏冲績锛夈��
+
+绀轰緥杩斿洖璁板綍锛�
+
+```json
+{
+  "records": [
+    {
+      "id": 100,
+      "deviceName": "娑插帇鏈�",
+      "deviceModel": "YQ-100",
+      "areaId": 2,
+      "areaName": "鍐插帇鍖�",
+      "...": "..."
+    }
+  ]
+}
+```
+
+## 涔濄�侀〉闈氦浜掑缓璁�
+
+### 璁惧鍙拌处鍒楄〃椤�
+
+- 宸︿晶灞曠ず鍖哄煙鏍戯紙`/device/area/tree`锛夈��
+- 鏍戦《鍥哄畾涓�涓�滃叏閮ㄨ澶団�濆叆鍙o紝鐐瑰嚮鏃� `areaId` 浼犵┖锛屾煡璇㈡墍鏈夎澶囥��
+- 鐐瑰嚮浠绘剰鍖哄煙鑺傜偣锛屽彸渚ц澶囧垪琛ㄦ寜璇ュ尯鍩熷強鍏跺瓙绾ч�掑綊绛涢�夈��
+- 鍒楄〃澧炲姞鈥滄墍灞炲尯鍩熲�濆垪锛屾樉绀� `areaName`銆�
+- 鏂板/缂栬緫璁惧鏃讹紝鐢ㄥ尯鍩熸爲閫夋嫨鍣紙鍙骇鑱斿睍寮�锛夐�夋嫨鎵�灞炲尯鍩燂紝鎻愪氦鏃朵紶 `areaId`銆�
+
+### 鍖哄煙绠$悊
+
+- 鍙湪鍖哄煙鏍戜笂鐩存帴鈥滄柊澧炲悓绾�/鏂板瀛愮骇鈥濊妭鐐广��
+- 鏀寔鎷栨嫿璋冩暣鐖跺瓙鍏崇郴锛堣皟鐢� PUT 浼犳柊 `parentId`锛夈��
+- 鍒犻櫎鎸夐挳鍦ㄦ棤瀛愮骇涓旀棤璁惧鍏宠仈鏃舵墠鍙敤銆�
+
+### 宸℃閫夋嫨璁惧
+
+宸℃浠诲姟锛坄/inspectionTask/addOrEditInspectionTask`锛変笌瀹氭椂宸℃锛坄/timingTask/addOrEditTimingTask`锛夊垱寤�/缂栬緫鏃堕�夋嫨宸℃璁惧锛屽缓璁敼涓烘寜杞﹂棿/浣嶇疆绛涢�夛細
+
+- 寮圭獥宸︿晶灞曠ず鍖哄煙鏍戯紙`GET /device/area/tree`锛夛紝椤堕儴鍥哄畾鈥滃叏閮ㄨ澶団�濄��
+- 鐐瑰嚮鍖哄煙鑺傜偣鍚庯紝鍙充晶璋� `GET /device/ledger/getDeviceLedger?areaId=x` 鍔犺浇璇ュ尯鍩燂紙鍚瓙绾э級璁惧鍒楄〃銆�
+- 閫変腑璁惧鍚庡洖濉� `taskId`锛堣澶嘔D锛変笌 `taskName`锛堣澶囧悕绉帮級锛岄殢宸℃浠诲姟涓�璧锋彁浜ゃ��
+- `getDeviceLedger` 杩斿洖绀轰緥锛�
+
+  ```json
+  [
+    {
+      "id": 100,
+      "deviceName": "娑插帇鏈�",
+      "deviceModel": "YQ-100",
+      "deviceBrand": "XX",
+      "type": "鍘嬪姏鏈�",
+      "areaId": 2,
+      "areaName": "鍐插帇鍖�"
+    }
+  ]
+  ```
+
+- `areaId` 涓嶄紶鏃惰繑鍥炲叏閮ㄨ澶囷紙鍏煎鏃ц皟鐢級銆�
+
+鍚庣鏂板鏍¢獙锛氬贰妫�浠诲姟/瀹氭椂宸℃淇濆瓨鏃讹紝鑻� `taskId` 瀵瑰簲璁惧涓嶅瓨鍦紝杩斿洖 `鎵�閫夎澶囦笉瀛樺湪锛岃閲嶆柊閫夋嫨`銆�
+
+## 鍗併�佽仈璋冮獙鏀舵祦绋�
+
+1. 鍒涘缓涓�涓牴鍖哄煙鈥滀竴鍙疯溅闂粹�濄��
+2. 鍦ㄢ�滀竴鍙疯溅闂粹�濅笅鍒涘缓鈥滃啿鍘嬪尯鈥濃�滆閰嶅尯鈥濄��
+3. 鏌ヨ鍖哄煙鏍戯紝纭灞傜骇姝g‘锛宍deviceCount` 鍏ㄩ儴涓� 0銆�
+4. 鏂板 2 鍙拌澶囨寕鍒扳�滃啿鍘嬪尯鈥濓紝1 鍙版寕鍒扳�滆閰嶅尯鈥濄��
+5. 鏌ヨ鍖哄煙鏍戯紝纭鈥滃啿鍘嬪尯鈥漙deviceCount` 涓� 2锛屸�滆閰嶅尯鈥濅负 1锛屸�滀竴鍙疯溅闂粹�濅负 3锛堥�掑綊姹囨�伙級銆�
+6. 璁惧鍒楄〃浼� `areaId=涓�鍙疯溅闂碻锛岀‘璁よ繑鍥� 3 鍙拌澶囷紙鍚瓙绾э級銆�
+7. 璁惧鍒楄〃浼� `areaId=鍐插帇鍖篳锛岀‘璁ゅ彧杩斿洖 2 鍙般��
+8. 灏濊瘯鍒犻櫎鈥滀竴鍙疯溅闂粹�濓紝搴旀姤鈥滃瓨鍦ㄥ瓙绾у尯鍩熲�濄��
+9. 灏濊瘯鍒犻櫎鈥滃啿鍘嬪尯鈥濓紝搴旀姤鈥滃尯鍩熶笅瀛樺湪璁惧鈥濄��
+10. 灏嗏�滆閰嶅尯鈥濈埗绾ф敼涓衡�滃啿鍘嬪尯鈥濓紝鍐嶆煡鍖哄煙鏍戯紝纭灞傜骇鍙樺寲鍛戒腑寰幆鏍¢獙涓嶅け璐ャ��
+11. 鏂板/缂栬緫璁惧鏃堕�夋嫨鍖哄煙锛岀‘璁� `areaId` 姝g‘淇濆瓨涓斿垪琛ㄦ樉绀� `areaName`銆�
+
+### 宸℃閫夎澶囪仈璋�
+
+1. 宸℃浠诲姟鏂板缓锛岀偣閫夎澶囧脊绐楋紝纭宸︿晶鍑虹幇鍖哄煙鏍戙��
+2. 鐐瑰嚮鈥滀竴鍙疯溅闂粹�濓紝鍙充晶搴旀樉绀鸿杞﹂棿锛堝惈瀛愮骇锛夊叏閮ㄨ澶囥��
+3. 鐐瑰嚮鈥滃啿鍘嬪尯鈥濓紝鍙充晶鍙樉绀哄啿鍘嬪尯璁惧銆�
+4. 閫変腑璁惧鍚庢彁浜わ紝宸℃浠诲姟淇濆瓨鎴愬姛锛宍taskId`/`taskName` 姝g‘鍥炴樉銆�
+5. 鎵嬪伐鏋勯�犱竴涓笉瀛樺湪鐨勮澶嘔D鎻愪氦锛屽簲杩斿洖 `鎵�閫夎澶囦笉瀛樺湪锛岃閲嶆柊閫夋嫨`銆�
+6. 瀹氭椂宸℃椤甸潰鎵ц鍚屾牱鎿嶄綔锛岃涓轰竴鑷淬��
\ No newline at end of file
diff --git "a/doc/\350\256\241\351\207\217\345\231\250\345\205\267\345\217\260\350\264\246\346\211\271\351\207\217\345\257\274\345\205\245\345\211\215\347\253\257\345\257\271\346\216\245\346\226\207\346\241\243.md" "b/doc/\350\256\241\351\207\217\345\231\250\345\205\267\345\217\260\350\264\246\346\211\271\351\207\217\345\257\274\345\205\245\345\211\215\347\253\257\345\257\271\346\216\245\346\226\207\346\241\243.md"
new file mode 100644
index 0000000..38f4032
--- /dev/null
+++ "b/doc/\350\256\241\351\207\217\345\231\250\345\205\267\345\217\260\350\264\246\346\211\271\351\207\217\345\257\274\345\205\245\345\211\215\347\253\257\345\257\271\346\216\245\346\226\207\346\241\243.md"
@@ -0,0 +1,242 @@
+# 璁¢噺鍣ㄥ叿鍙拌处鎵归噺瀵煎叆鍓嶇瀵规帴鏂囨。
+
+## 涓�銆佸姛鑳借鏄�
+
+璁¢噺鍣ㄥ叿鍙拌处鏀寔閫氳繃 Excel 鏂囦欢鎵归噺鏂板鏁版嵁銆�
+
+鏈姛鑳藉寘鍚細
+
+- 涓嬭浇 Excel 瀵煎叆妯℃澘
+- 涓婁紶 Excel 鏂囦欢鎵归噺瀵煎叆
+- 瀵煎叆鍓嶈繘琛屾暟鎹牎楠�
+- 鏍¢獙澶辫触鏃舵暣鎵规暟鎹笉鍐欏叆鏁版嵁搴�
+- 鑷姩浣跨敤褰撳墠鐧诲綍鐢ㄦ埛鍜屽綋鍓嶇鎴蜂俊鎭�
+- 鑷姩璁$畻棰勮涓嬫妫�瀹氭棩鏈�
+
+鎺ュ彛鍩虹璺緞锛�
+
+```text
+/measuringInstrumentLedger
+```
+
+鎺ュ彛璇锋眰闇�瑕佹惡甯﹂」鐩幇鏈夌櫥褰曡璇佷俊鎭��
+
+## 浜屻�佷笅杞藉鍏ユā鏉�
+
+### 璇锋眰淇℃伅
+
+```text
+POST /measuringInstrumentLedger/downloadTemplate
+```
+
+### 璇锋眰鍙傛暟
+
+鏃犮��
+
+### 鍓嶇绀轰緥
+
+```javascript
+const response = await request({
+  url: '/measuringInstrumentLedger/downloadTemplate',
+  method: 'post',
+  responseType: 'blob'
+})
+
+const blob = new Blob([response], {
+  type: 'application/vnd.openxmlformats-officedocument.spreadsheetml.sheet'
+})
+const link = document.createElement('a')
+link.href = URL.createObjectURL(blob)
+link.download = '璁¢噺鍣ㄥ叿鍙拌处瀵煎叆妯℃澘.xlsx'
+link.click()
+URL.revokeObjectURL(link.href)
+```
+
+椤圭洰宸叉湁璇锋眰灏佽鐨勶紝鎸夐」鐩姹傚皝瑁呮柟寮忚缃� `responseType: 'blob'` 鍗冲彲銆�
+
+## 涓夈�佹壒閲忓鍏�
+
+### 璇锋眰淇℃伅
+
+```text
+POST /measuringInstrumentLedger/import
+```
+
+### 璇锋眰绫诲瀷
+
+```text
+multipart/form-data
+```
+
+### 璇锋眰鍙傛暟
+
+| 鍙傛暟鍚� | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|---|---|---|---|
+| file | File | 鏄� | Excel 鏂囦欢锛屼粎鏀寔 `.xls` 鍜� `.xlsx` |
+
+### 鍓嶇绀轰緥
+
+```javascript
+const formData = new FormData()
+formData.append('file', file)
+
+const result = await request({
+  url: '/measuringInstrumentLedger/import',
+  method: 'post',
+  data: formData,
+  headers: {
+    'Content-Type': 'multipart/form-data'
+  }
+})
+```
+
+娉ㄦ剰锛氫笉瑕佸皢鏂囦欢杞垚 JSON锛屼篃涓嶈浣跨敤 `@RequestBody` 鏂瑰紡鎻愪氦銆�
+
+## 鍥涖�丒xcel 妯℃澘鍒�
+
+妯℃澘绗竴琛屼负琛ㄥご锛屾暟鎹粠绗簩琛屽紑濮嬨��
+
+| Excel 琛ㄥご | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|---|---|---|---|
+| 璁¢噺鍣ㄥ叿缂栧彿 | 鏂囨湰 | 鏄� | 褰撳墠绉熸埛鍐呬笉鑳介噸澶� |
+| 璁¢噺鍣ㄥ叿鍚嶇О | 鏂囨湰 | 鏄� | 涓嶈兘濉啓绌哄�� |
+| 瑙勬牸鍨嬪彿 | 鏂囨湰 | 鍚� | 璁¢噺鍣ㄥ叿瑙勬牸鎴栧瀷鍙� |
+| 鏈�杩戜竴娆℃瀹氭棩鏈� | 鏃ユ湡 | 鍚� | 寤鸿鏍煎紡锛歚yyyy-MM-dd` |
+| 妫�瀹氭湁鏁堟湡 | 鏁存暟 | 鍚� | 鍗曚綅锛氬ぉ锛涘~鍐欐椂蹇呴』澶т簬 0 |
+| 褰曞叆鏃ユ湡 | 鏃ユ湡 | 鍚� | 寤鸿鏍煎紡锛歚yyyy-MM-dd`锛涗笉濉啓鏃朵娇鐢ㄥ綋鍓嶆棩鏈� |
+| 鍗曚綅 | 鏂囨湰 | 鍚� | 浣跨敤鍗曚綅 |
+| 閮ㄩ棬ID | 鏁存暟 | 鍚� | 閮ㄩ棬 ID |
+| 瀹夎浣嶇疆 | 鏂囨湰 | 鍚� | 璁¢噺鍣ㄥ叿瀹夎浣嶇疆 |
+| 妫�瀹氬懆鏈� | 鏂囨湰 | 鍚� | 妫�瀹氬懆鏈� |
+
+### 鏃ユ湡鍜屾湁鏁堟湡瑙勫垯
+
+褰撳悓鏃跺~鍐欎互涓嬩袱鍒楁椂锛屽悗绔嚜鍔ㄨ绠楅璁′笅娆℃瀹氭棩鏈燂細
+
+```text
+棰勮涓嬫妫�瀹氭棩鏈� = 鏈�杩戜竴娆℃瀹氭棩鏈� + 妫�瀹氭湁鏁堟湡锛堝ぉ锛�
+```
+
+Excel 涓嶉渶瑕佸~鍐欌�滈璁′笅娆℃瀹氭棩鏈熲�濄��
+
+浠ヤ笅瀛楁涓嶉渶瑕併�佷篃涓嶅厑璁搁�氳繃 Excel 缁存姢锛�
+
+- ID
+- 褰曞叆浜� ID
+- 褰曞叆浜哄悕绉�
+- 绉熸埛 ID
+- 鐘舵��
+- 鍒涘缓浜恒�佸垱寤烘椂闂�
+- 淇敼浜恒�佷慨鏀规椂闂�
+
+杩欎簺瀛楁鐢卞悗绔牴鎹綋鍓嶇櫥褰曚俊鎭垨绯荤粺瑙勫垯鑷姩澶勭悊銆�
+
+## 浜斻�佹垚鍔熻繑鍥�
+
+鎴愬姛鏃惰繑鍥為」鐩粺涓�鎴愬姛缁撴灉锛屾彁绀轰俊鎭被浼硷細
+
+```json
+{
+  "code": 200,
+  "msg": "鎴愬姛瀵煎叆 10 鏉℃暟鎹�"
+}
+```
+
+鍓嶇寤鸿锛�
+
+1. 鍏抽棴涓婁紶寮圭獥銆�
+2. 鎻愮ず `msg` 鍐呭銆�
+3. 鍒锋柊鍙拌处鍒嗛〉鍒楄〃銆�
+
+## 鍏�佸け璐ヨ繑鍥�
+
+### 1. 鏈�夋嫨鏂囦欢
+
+```json
+{
+  "code": 500,
+  "msg": "璇蜂笂浼� Excel 鏂囦欢"
+}
+```
+
+### 2. 鏂囦欢鏍煎紡涓嶆纭�
+
+```json
+{
+  "code": 500,
+  "msg": "鍙敮鎸佸鍏� xls 鎴� xlsx 鏂囦欢"
+}
+```
+
+### 3. 鏂囦欢娌℃湁鏁版嵁
+
+```json
+{
+  "code": 500,
+  "msg": "Excel 涓病鏈夊彲瀵煎叆鐨勬暟鎹�"
+}
+```
+
+### 4. 鏁版嵁鏍¢獙澶辫触
+
+```json
+{
+  "code": 500,
+  "msg": "瀵煎叆鏍¢獙澶辫触锛氳閲忓櫒鍏风紪鍙蜂笉鑳戒负绌猴紱璁¢噺鍣ㄥ叿缂栧彿鍦� Excel 涓噸澶嶏紱妫�瀹氭湁鏁堟湡蹇呴』澶т簬 0"
+}
+```
+
+甯歌鏍¢獙鎻愮ず锛�
+
+| 鎻愮ず | 澶勭悊寤鸿 |
+|---|---|
+| 璁¢噺鍣ㄥ叿缂栧彿涓嶈兘涓虹┖ | 琛ュ厖鈥滆閲忓櫒鍏风紪鍙封�� |
+| 璁¢噺鍣ㄥ叿鍚嶇О涓嶈兘涓虹┖ | 琛ュ厖鈥滆閲忓櫒鍏峰悕绉扳�� |
+| 璁¢噺鍣ㄥ叿缂栧彿鍦� Excel 涓噸澶� | 鍒犻櫎鎴栦慨鏀归噸澶嶇紪鍙� |
+| 璁¢噺鍣ㄥ叿缂栧彿宸插瓨鍦細xxx | 淇敼璇ョ紪鍙凤紝閬垮厤涓庡綋鍓嶇鎴峰凡鏈夊彴璐﹂噸澶� |
+| 妫�瀹氭湁鏁堟湡蹇呴』澶т簬 0 | 濉啓澶т簬 0 鐨勬暣鏁� |
+| 瀵煎叆澶辫触锛岃妫�鏌� Excel 鍐呭 | 妫�鏌ユ枃浠舵牸寮忋�佽〃澶村拰鍗曞厓鏍肩被鍨� |
+
+## 涓冦�侀〉闈氦浜掑缓璁�
+
+寤鸿鍦ㄥ彴璐﹀垪琛ㄩ〉闈㈠鍔犱袱涓寜閽細
+
+- 涓嬭浇妯℃澘
+- 鎵归噺瀵煎叆
+
+鎺ㄨ崘浜や簰娴佺▼锛�
+
+1. 鐢ㄦ埛鐐瑰嚮鈥滀笅杞芥ā鏉库�濄��
+2. 鍓嶇涓嬭浇骞朵繚瀛� Excel 妯℃澘銆�
+3. 鐢ㄦ埛鎸夌収妯℃澘濉啓鏁版嵁銆�
+4. 鐢ㄦ埛鐐瑰嚮鈥滄壒閲忓鍏モ�濄��
+5. 寮瑰嚭鏂囦欢閫夋嫨妗嗭紝浠呭厑璁搁�夋嫨 `.xls`銆乣.xlsx` 鏂囦欢銆�
+6. 閫夋嫨鏂囦欢鍚庝笂浼犮��
+7. 涓婁紶鏈熼棿鎸夐挳鏄剧ず鍔犺浇鐘舵�侊紝骞剁姝㈤噸澶嶆彁浜ゃ��
+8. 鏍规嵁鎺ュ彛杩斿洖鐨� `code` 鍜� `msg` 鏄剧ず缁撴灉銆�
+9. 瀵煎叆鎴愬姛鍚庡埛鏂板垪琛ㄣ��
+10. 瀵煎叆澶辫触鏃朵繚鐣欏脊绐楋紝鏂逛究鐢ㄦ埛閲嶆柊閫夋嫨鏂囦欢銆�
+
+## 鍏�佸墠绔枃浠堕�夋嫨闄愬埗
+
+```javascript
+const accept = '.xls,.xlsx'
+```
+
+鍓嶇鍙互鍏堟牎楠屾枃浠舵墿灞曞悕锛屼絾鍚庣鏍¢獙浠嶇劧蹇呴』淇濈暀銆�
+
+寤鸿鍚屾椂鏍¢獙锛�
+
+- 鏂囦欢涓嶈兘涓虹┖
+- 鏂囦欢鎵╁睍鍚嶄负 `.xls` 鎴� `.xlsx`
+- 鏂囦欢澶у皬涓嶈秴杩囬」鐩粺涓�涓婁紶闄愬埗
+
+## 涔濄�侀噸瑕佹敞鎰忎簨椤�
+
+1. 涓�娆″鍏ヤ腑鍙鏈変竴鏉℃暟鎹牎楠屽け璐ワ紝鏁存壒鏁版嵁閮戒笉浼氬啓鍏ャ��
+2. 璁¢噺鍣ㄥ叿缂栧彿鎸夊綋鍓嶇鎴锋牎楠岋紝涓嶅悓绉熸埛涔嬮棿鍙互浣跨敤鐩稿悓缂栧彿銆�
+3. Excel 涓殑褰曞叆浜恒�佺鎴枫�佺姸鎬佺瓑瀛楁涓嶄細浣滀负鏈夋晥瀵煎叆鏁版嵁浣跨敤銆�
+4. 鈥滄渶杩戜竴娆℃瀹氭棩鏈熲�濆拰鈥滄瀹氭湁鏁堟湡鈥濆彲浠ヤ笉濉啓锛涘鏋滈兘濉啓锛屽悗绔嚜鍔ㄨ绠楅璁′笅娆℃瀹氭棩鏈熴��
+5. Excel 琛ㄥご蹇呴』浣跨敤妯℃澘涓殑鍚嶇О锛屼笉瑕佽嚜琛屼慨鏀硅〃澶存枃瀛椼��
+6. 瀵煎叆鎺ュ彛涓嶆槸 JSON 鎺ュ彛锛屽繀椤讳娇鐢� `FormData` 涓婁紶鏂囦欢銆�
+7. 褰撳墠浠撳簱涓嶅寘鍚墠绔唬鐮侊紝浠ヤ笂浠g爜涓哄墠绔鎺ョず渚嬶紝鍏蜂綋璇锋眰鍑芥暟鍚嶇О鎸夊墠绔」鐩疄闄呭皝瑁呰皟鏁淬��
diff --git "a/doc/\350\256\276\345\244\207\344\277\235\345\205\273\345\271\264\345\272\246\344\277\235\345\205\273\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md" "b/doc/\350\256\276\345\244\207\344\277\235\345\205\273\345\271\264\345\272\246\344\277\235\345\205\273\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..558106e
--- /dev/null
+++ "b/doc/\350\256\276\345\244\207\344\277\235\345\205\273\345\271\264\345\272\246\344\277\235\345\205\273\345\211\215\347\253\257\350\201\224\350\260\203\350\257\264\346\230\216.md"
@@ -0,0 +1,171 @@
+# 璁惧淇濆吇骞村害淇濆吇鍓嶇鑱旇皟璇存槑
+
+## 1. 鍙樻洿璇存槑
+
+璁惧淇濆吇瀹氭椂浠诲姟鏂板鈥滃勾搴︿繚鍏烩�濅换鍔¢鐜囥��
+
+- 棰戠巼缂栫爜锛歚YEARLY`
+- 鍓嶇鏄剧ず鍚嶇О锛歚骞村害淇濆吇`
+- 鍩虹璺緞锛歚/deviceMaintenanceTask`
+- 閫傜敤鎺ュ彛锛氭柊澧炪�佷慨鏀广�佹煡璇㈣澶囦繚鍏诲畾鏃朵换鍔�
+
+## 2. 鎺ュ彛娓呭崟
+
+| 璇锋眰鏂瑰紡 | 鎺ュ彛 | 璇存槑 |
+|---|---|---|
+| `GET` | `/deviceMaintenanceTask/listPage` | 鏌ヨ璁惧淇濆吇瀹氭椂浠诲姟 |
+| `POST` | `/deviceMaintenanceTask/add` | 鏂板璁惧淇濆吇瀹氭椂浠诲姟 |
+| `POST` | `/deviceMaintenanceTask/update` | 淇敼璁惧淇濆吇瀹氭椂浠诲姟 |
+| `DELETE` | `/deviceMaintenanceTask/delete` | 鍒犻櫎璁惧淇濆吇瀹氭椂浠诲姟 |
+
+## 3. 棰戠巼閫夐」
+
+鍓嶇浠诲姟棰戠巼涓嬫媺妗嗗鍔犱互涓嬮�夐」锛�
+
+| 鏄剧ず鍚嶇О | 鎻愪氦鍊� |
+|---|---|
+| 姣忔棩淇濆吇 | `DAILY` |
+| 姣忓懆淇濆吇 | `WEEKLY` |
+| 姣忔湀淇濆吇 | `MONTHLY` |
+| 姣忓搴︿繚鍏� | `QUARTERLY` |
+| 骞村害淇濆吇 | `YEARLY` |
+
+`frequencyType` 蹇呴』鎻愪氦缂栫爜鍊硷紝涓嶈鎻愪氦涓枃鍚嶇О銆�
+
+## 4. 骞村害淇濆吇鍙傛暟鏍煎紡
+
+骞村害淇濆吇鐨� `frequencyDetail` 鏍煎紡涓猴細
+
+```text
+鏈堜唤,鏃ユ湡,鏃堕棿
+```
+
+绀轰緥锛�
+
+```text
+12,31,09:00
+```
+
+鍙傛暟璇存槑锛�
+
+| 浣嶇疆 | 鍐呭 | 鏍煎紡 | 绀轰緥 |
+|---|---|---|---|
+| 绗� 1 娈� | 鏈堜唤 | `1-12` | `12` |
+| 绗� 2 娈� | 鏃ユ湡 | `1-31` | `31` |
+| 绗� 3 娈� | 鎵ц鏃堕棿 | `HH:mm` | `09:00` |
+
+鍓嶇寤鸿浣跨敤鈥滄湀浠介�夋嫨鍣� + 鏃ユ湡閫夋嫨鍣� + 鏃堕棿閫夋嫨鍣ㄢ�濈敓鎴愯瀛楃涓诧紝涓嶈璁╃敤鎴锋墜宸ヨ緭鍏ラ�楀彿鍒嗛殧鏍煎紡銆�
+
+## 5. 鏂板璇锋眰绀轰緥
+
+鎺ュ彛锛歚POST /deviceMaintenanceTask/add`
+
+```json
+{
+  "taskName": "绌哄帇鏈哄勾搴︿繚鍏讳换鍔�",
+  "taskId": 1001,
+  "deviceModel": "C-01",
+  "machineryCategory": "鐢熶骇璁惧",
+  "maintenancePerson": "寮犱笁",
+  "frequencyType": "YEARLY",
+  "frequencyDetail": "12,31,09:00",
+  "remarks": "姣忓勾骞存湯淇濆吇"
+}
+```
+
+鏂板鎴愬姛鍚庯紝鍚庣浼氭牴鎹鐜囪鎯呰绠� `nextExecutionTime`锛屽苟鍒涘缓骞村害 Quartz 瀹氭椂浠诲姟銆�
+
+## 6. 淇敼璇锋眰绀轰緥
+
+鎺ュ彛锛歚POST /deviceMaintenanceTask/update`
+
+```json
+{
+  "id": 1,
+  "taskName": "绌哄帇鏈哄勾搴︿繚鍏讳换鍔�",
+  "taskId": 1001,
+  "maintenancePerson": "寮犱笁",
+  "frequencyType": "YEARLY",
+  "frequencyDetail": "6,15,14:30",
+  "remarks": "姣忓勾鍏湀淇濆吇"
+}
+```
+
+淇敼棰戠巼鎴栭鐜囪鎯呭悗锛屽悗绔細閲嶆柊鐢熸垚瀹氭椂浠诲姟瑙﹀彂鍣ㄣ��
+
+## 7. 鏌ヨ杩斿洖绀轰緥
+
+鎺ュ彛锛歚GET /deviceMaintenanceTask/listPage`
+
+骞村害浠诲姟杩斿洖绀轰緥锛�
+
+```json
+{
+  "code": 200,
+  "msg": "鎿嶄綔鎴愬姛",
+  "data": {
+    "records": [
+      {
+        "id": 1,
+        "taskName": "绌哄帇鏈哄勾搴︿繚鍏讳换鍔�",
+        "taskId": 1001,
+        "maintenancePerson": "寮犱笁",
+        "frequencyType": "YEARLY",
+        "frequencyDetail": "12,31,09:00",
+        "nextExecutionTime": "2026-12-31 09:00:00",
+        "lastExecutionTime": null,
+        "isActive": true,
+        "remarks": "姣忓勾骞存湯淇濆吇"
+      }
+    ],
+    "total": 1
+  }
+}
+```
+
+鍒楄〃灞曠ず鏃跺缓璁皢锛�
+
+- `YEARLY` 鏄剧ず涓衡�滃勾搴︿繚鍏烩��
+- `12,31,09:00` 鏄剧ず涓衡�滄瘡骞�12鏈�31鏃� 09:00鈥�
+
+## 8. 瀹氭椂鎵ц瑙勫垯
+
+1. 骞村害浠诲姟棣栨鎵ц鏃堕棿鍙栧綋鍓嶅勾浠戒腑閰嶇疆鐨勬湀浠姐�佹棩鏈熷拰鏃堕棿銆�
+2. 濡傛灉褰撳墠骞翠唤鐨勬墽琛屾椂闂村凡缁忚繃鍘伙紝鍒欓娆℃墽琛屽畨鎺掑埌涓嬩竴骞村害銆�
+3. 浠诲姟鎵ц鍚庯紝鍚庣鑷姩璁$畻涓嬩竴骞村害鎵ц鏃堕棿銆�
+4. 閰嶇疆鏃ユ湡瓒呰繃鐩爣鏈堜唤瀹為檯澶╂暟鏃讹紝鎸夌洰鏍囨湀浠芥渶鍚庝竴澶╂墽琛屻�備緥濡傦細
+   - `2,31,09:00` 鎸� 2 鏈堟渶鍚庝竴澶╂墽琛屻��
+5. 淇濆吇浠诲姟鎵ц鍚庝細鑷姩鐢熸垚璁惧淇濆吇璁板綍锛屽苟鍚屾浠ヤ笅瀛楁锛�
+   - `frequencyType = YEARLY`
+   - `frequencyDetail = 骞村害浠诲姟閰嶇疆鍊糮
+   - `maintenanceActuallyName = maintenancePerson`
+
+## 9. 鍓嶇鏍¢獙瑕佹眰
+
+鎻愪氦骞村害淇濆吇浠诲姟鍓嶅簲鏍¢獙锛�
+
+- 鏈堜唤鑼冨洿涓� `1-12`
+- 鏃ユ湡鑼冨洿涓� `1-31`
+- 鏃堕棿鏍煎紡涓� `HH:mm`
+- `frequencyDetail` 蹇呴』涓轰笁娈甸�楀彿鍒嗛殧鍊�
+- `frequencyType` 鎻愪氦 `YEARLY`
+
+鎺ㄨ崘閿欒鎻愮ず锛�
+
+- 鈥滃勾搴︿繚鍏绘牸寮忓簲涓猴細鏈堜唤,鏃ユ湡,鏃堕棿锛屼緥濡� 12,31,09:00鈥�
+- 鈥滄湀浠藉繀椤诲湪1-12涔嬮棿鈥�
+- 鈥滄棩鏈熷繀椤诲湪1-31涔嬮棿鈥�
+- 鈥滄椂闂存牸寮忓繀椤讳负 HH:mm鈥�
+
+## 10. 鑱旇皟娴嬭瘯娓呭崟
+
+- [ ] 鏂板棰戠巼涓嬫媺妗嗘樉绀衡�滃勾搴︿繚鍏烩�濄��
+- [ ] 鏂板浠诲姟鎻愪氦 `frequencyType=YEARLY`銆�
+- [ ] 鏂板浠诲姟鎻愪氦 `frequencyDetail=12,31,09:00` 鎴愬姛銆�
+- [ ] 鍒楄〃涓纭樉绀衡�滃勾搴︿繚鍏烩�濄��
+- [ ] 缂栬緫骞村害浠诲姟骞朵慨鏀规湀浠姐�佹棩鏈熴�佹椂闂存垚鍔熴��
+- [ ] 缂栬緫鍚� `nextExecutionTime` 鎸夋柊閰嶇疆閲嶆柊璁$畻銆�
+- [ ] 鏍¢獙鏈堜唤涓� 0 鎴� 13 鏃朵笉鑳芥彁浜ゃ��
+- [ ] 鏍¢獙鏃ユ湡涓� 0 鎴� 32 鏃朵笉鑳芥彁浜ゃ��
+- [ ] 鏍¢獙鏃堕棿鏍煎紡閿欒鏃朵笉鑳芥彁浜ゃ��
+- [ ] 浣跨敤 `2,31,09:00` 楠岃瘉鏈堟湯鏃ユ湡澶勭悊绗﹀悎浜у搧棰勬湡銆�
diff --git a/sql/migration_device_area.sql b/sql/migration_device_area.sql
new file mode 100644
index 0000000..354297e
--- /dev/null
+++ b/sql/migration_device_area.sql
@@ -0,0 +1,25 @@
+-- 璁惧鍖哄煙锛堣溅闂�/浣嶇疆锛夌埗瀛愰�掑綊缁撴瀯
+CREATE TABLE IF NOT EXISTS `device_area` (
+    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+    `name` varchar(255) NOT NULL COMMENT '鍖哄煙/杞﹂棿/浣嶇疆鍚嶇О',
+    `parent_id` bigint NULL COMMENT '鐖剁骇鍖哄煙ID锛屾牴鑺傜偣涓篘ULL',
+    `ancestors` varchar(1000) NULL COMMENT '绁栫骇璺緞锛屽 1,2,3',
+    `sort` int NULL COMMENT '鎺掑簭',
+    `dept_id` bigint NULL COMMENT '閮ㄩ棬ID',
+    `tenant_id` bigint NULL COMMENT '绉熸埛ID',
+    `create_user` int NULL COMMENT '鍒涘缓浜�',
+    `create_time` datetime NULL COMMENT '鍒涘缓鏃堕棿',
+    `update_user` int NULL COMMENT '鏇存柊浜�',
+    `update_time` datetime NULL COMMENT '鏇存柊鏃堕棿',
+    PRIMARY KEY (`id`)
+) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='璁惧鍖哄煙琛紙杞﹂棿/浣嶇疆锛�';
+
+-- 璁惧鍙拌处鍏宠仈鍖哄煙锛堝箓绛夛細宸插瓨鍦ㄥ垪鏃惰烦杩囷級
+SET @col_exists = (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'device_ledger' AND COLUMN_NAME = 'area_id');
+SET @ddl = IF(@col_exists = 0,
+    'ALTER TABLE `device_ledger` ADD COLUMN `area_id` bigint NULL COMMENT ''璁惧鍖哄煙ID'' AFTER `dept_id`',
+    'SELECT 1');
+PREPARE stmt FROM @ddl;
+EXECUTE stmt;
+DEALLOCATE PREPARE stmt;
diff --git a/sql/migration_framework_contract.sql b/sql/migration_framework_contract.sql
new file mode 100644
index 0000000..0a7a328
--- /dev/null
+++ b/sql/migration_framework_contract.sql
@@ -0,0 +1,51 @@
+-- 妗嗘灦鍚堝悓涓庢鏋惰鍗曞叧绯伙紙骞傜瓑锛氬凡瀛樺湪鐨勫垪/瀛楁璺宠繃锛屽彲瀹夊叏閲嶅鎵ц锛�
+
+-- sales_ledger.contract_type / parent_contract_id
+SET @c1 = (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sales_ledger' AND COLUMN_NAME = 'contract_type');
+SET @ddl1 = IF(@c1 = 0,
+    'ALTER TABLE `sales_ledger` ADD COLUMN `contract_type` tinyint NOT NULL DEFAULT 1 COMMENT ''鍚堝悓绫诲瀷锛�1鏅�氶攢鍞悎鍚岋紝2妗嗘灦鍚堝悓锛�3妗嗘灦璁㈠崟'' AFTER `sales_contract_no`',
+    'SELECT 1');
+PREPARE stmt1 FROM @ddl1;
+EXECUTE stmt1;
+DEALLOCATE PREPARE stmt1;
+
+SET @c2 = (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sales_ledger' AND COLUMN_NAME = 'parent_contract_id');
+SET @ddl2 = IF(@c2 = 0,
+    'ALTER TABLE `sales_ledger` ADD COLUMN `parent_contract_id` bigint NULL COMMENT ''鐖舵鏋跺悎鍚孖D'' AFTER `contract_type`',
+    'SELECT 1');
+PREPARE stmt2 FROM @ddl2;
+EXECUTE stmt2;
+DEALLOCATE PREPARE stmt2;
+
+-- sales_ledger_product.quantity 鍏佽涓虹┖
+SET @c3 = (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sales_ledger_product' AND COLUMN_NAME = 'quantity'
+      AND IS_NULLABLE = 'NO');
+SET @ddl3 = IF(@c3 = 1,
+    'ALTER TABLE `sales_ledger_product` MODIFY COLUMN `quantity` decimal(18, 2) NULL COMMENT ''鏁伴噺锛屾鏋跺悎鍚屽拰妗嗘灦璁㈠崟涓嶈褰曟暟閲忥紝瀹為檯鍙戣揣鏁伴噺璁板綍鍦ㄥ彂璐ф槑缁�''',
+    'SELECT 1');
+PREPARE stmt3 FROM @ddl3;
+EXECUTE stmt3;
+DEALLOCATE PREPARE stmt3;
+
+-- sales_ledger_product.parent_product_id
+SET @c4 = (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sales_ledger_product' AND COLUMN_NAME = 'parent_product_id');
+SET @ddl4 = IF(@c4 = 0,
+    'ALTER TABLE `sales_ledger_product` ADD COLUMN `parent_product_id` bigint NULL COMMENT ''鐖舵鏋跺悎鍚屼骇鍝両D'' AFTER `sales_ledger_id`',
+    'SELECT 1');
+PREPARE stmt4 FROM @ddl4;
+EXECUTE stmt4;
+DEALLOCATE PREPARE stmt4;
+
+-- sales_ledger_product.delivery_date
+SET @c5 = (SELECT COUNT(*) FROM information_schema.COLUMNS
+    WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'sales_ledger_product' AND COLUMN_NAME = 'delivery_date');
+SET @ddl5 = IF(@c5 = 0,
+    'ALTER TABLE `sales_ledger_product` ADD COLUMN `delivery_date` date NULL COMMENT ''妗嗘灦璁㈠崟浜よ揣鏃ユ湡'' AFTER `parent_product_id`',
+    'SELECT 1');
+PREPARE stmt5 FROM @ddl5;
+EXECUTE stmt5;
+DEALLOCATE PREPARE stmt5;
diff --git a/src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java b/src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
index 4404387..7e307b5 100644
--- a/src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
+++ b/src/main/java/com/ruoyi/basic/service/impl/CustomerServiceImpl.java
@@ -3,6 +3,7 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+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;
@@ -365,20 +366,34 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public void recycleCustomer(CustomerDto customerDto) {
-        Customer customer = customerMapper.selectById(customerDto.getId());
-        if (customer.getType() == 1 && customer.getIsAssigned() == 1) {  // 鍏捣涓斿凡鍒嗛厤
-            validatePublicSeaRecyclePermission(customer.getId());
-            customer.setIsAssigned(0);
-            customer.setUsageStatus(0L);
-            customer.setUsageUser(0L);
-            customerMapper.updateById(customer);
-
-            // 鍒犻櫎璇ュ鎴风殑鎵�鏈夊叡浜叧绯�
-            customerUserService.remove(
-                new QueryWrapper<CustomerUser>().lambda()
-                    .eq(CustomerUser::getCustomerId, customerDto.getId())
-            );
+        if (customerDto == null || customerDto.getId() == null) {
+            throw new RuntimeException("瀹㈡埛ID涓嶈兘涓虹┖");
         }
+        Customer customer = customerMapper.selectById(customerDto.getId());
+        if (customer == null) {
+            throw new RuntimeException("瀹㈡埛妗f涓嶅瓨鍦�");
+        }
+        boolean assigned = Objects.equals(customer.getIsAssigned(), 1)
+                || Objects.equals(customer.getUsageStatus(), 1L)
+                || (customer.getUsageUser() != null && customer.getUsageUser() > 0);
+        if (!assigned) {
+            throw new RuntimeException("璇ュ鎴峰綋鍓嶆湭澶勪簬棰嗙敤鐘舵�侊紝鏃犳硶鍥炴敹");
+        }
+        validatePublicSeaRecyclePermission(customer.getId());
+        int updated = customerMapper.update(null, new UpdateWrapper<Customer>()
+                .eq("id", customer.getId())
+                .set("is_assigned", 0)
+                .set("usage_status", 0)
+                .set("usage_user", null));
+        if (updated == 0) {
+            throw new RuntimeException("瀹㈡埛鍥炴敹澶辫触锛岃鍒锋柊鍚庨噸璇�");
+        }
+
+        // 鍒犻櫎璇ュ鎴风殑鎵�鏈夊叡浜叧绯�
+        customerUserService.remove(
+            new QueryWrapper<CustomerUser>().lambda()
+                .eq(CustomerUser::getCustomerId, customerDto.getId())
+        );
     }
 
     // 瀹㈡埛鍏变韩
diff --git a/src/main/java/com/ruoyi/device/controller/DeviceAreaController.java b/src/main/java/com/ruoyi/device/controller/DeviceAreaController.java
new file mode 100644
index 0000000..96fab77
--- /dev/null
+++ b/src/main/java/com/ruoyi/device/controller/DeviceAreaController.java
@@ -0,0 +1,44 @@
+package com.ruoyi.device.controller;
+
+import com.ruoyi.device.dto.DeviceAreaDto;
+import com.ruoyi.device.service.IDeviceAreaService;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import lombok.AllArgsConstructor;
+import org.springframework.web.bind.annotation.*;
+
+import java.util.List;
+
+@Tag(name = "璁惧鍖哄煙绠$悊")
+@RequestMapping("/device/area")
+@RestController
+@AllArgsConstructor
+public class DeviceAreaController {
+
+    private final IDeviceAreaService deviceAreaService;
+
+    @Operation(summary = "鏌ヨ璁惧鍖哄煙鏍戯紙鍚�掑綊璁惧鏁伴噺锛�")
+    @GetMapping("/tree")
+    public AjaxResult tree() {
+        return AjaxResult.success(deviceAreaService.selectAreaTree());
+    }
+
+    @Operation(summary = "鏂板璁惧鍖哄煙")
+    @PostMapping()
+    public AjaxResult add(@RequestBody DeviceAreaDto dto) {
+        return deviceAreaService.addArea(dto);
+    }
+
+    @Operation(summary = "淇敼璁惧鍖哄煙")
+    @PutMapping()
+    public AjaxResult update(@RequestBody DeviceAreaDto dto) {
+        return deviceAreaService.updateArea(dto);
+    }
+
+    @Operation(summary = "鍒犻櫎璁惧鍖哄煙")
+    @DeleteMapping("/{ids}")
+    public AjaxResult delete(@PathVariable("ids") List<Long> ids) {
+        return deviceAreaService.deleteArea(ids);
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java b/src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java
index 298980e..0f403a1 100644
--- a/src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java
+++ b/src/main/java/com/ruoyi/device/controller/DeviceLedgerController.java
@@ -1,7 +1,6 @@
 package com.ruoyi.device.controller;
 
 
-import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.ruoyi.common.utils.poi.ExcelUtil;
 import com.ruoyi.device.dto.DeviceLedgerDto;
@@ -95,10 +94,9 @@
 
 
     @GetMapping("getDeviceLedger")
-    @Operation(summary = "鑾峰彇璁惧鍙拌处")
-    public AjaxResult getDeviceLedger() {
-        return AjaxResult.success(deviceLedgerService.list(new QueryWrapper<DeviceLedger>().lambda()
-                .select(DeviceLedger::getId, DeviceLedger::getDeviceName, DeviceLedger::getDeviceModel)));
+    @Operation(summary = "鑾峰彇璁惧鍙拌处锛堝贰妫�閫夋嫨璁惧锛屾敮鎸佹寜鍖哄煙绛涢�夛級")
+    public AjaxResult getDeviceLedger(Long areaId) {
+        return AjaxResult.success(deviceLedgerService.selectDeviceOptions(areaId));
     }
 
     @GetMapping("scanDevice")
diff --git a/src/main/java/com/ruoyi/device/dto/DeviceAreaDto.java b/src/main/java/com/ruoyi/device/dto/DeviceAreaDto.java
new file mode 100644
index 0000000..c17909c
--- /dev/null
+++ b/src/main/java/com/ruoyi/device/dto/DeviceAreaDto.java
@@ -0,0 +1,35 @@
+package com.ruoyi.device.dto;
+
+import lombok.Data;
+
+/**
+ * 璁惧鍖哄煙 DTO
+ */
+@Data
+public class DeviceAreaDto {
+
+    /**
+     * 涓婚敭ID
+     */
+    private Long id;
+
+    /**
+     * 鍖哄煙/杞﹂棿/浣嶇疆鍚嶇О
+     */
+    private String name;
+
+    /**
+     * 鐖剁骇鍖哄煙ID
+     */
+    private Long parentId;
+
+    /**
+     * 鎺掑簭
+     */
+    private Integer sort;
+
+    /**
+     * 閮ㄩ棬ID
+     */
+    private Long deptId;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java b/src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java
index 8ceb915..7a08480 100644
--- a/src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java
+++ b/src/main/java/com/ruoyi/device/dto/DeviceLedgerDto.java
@@ -162,4 +162,15 @@
 
     @Schema(description = "璁惧绫诲瀷")
     private String type;
+
+    /**
+     * 璁惧鍖哄煙ID
+     */
+    private Long areaId;
+
+    /**
+     * 璁惧鍖哄煙鍚嶇О锛堥潪鏁版嵁搴撳瓧娈碉紝鍒楄〃灞曠ず锛�
+     */
+    @TableField(exist = false)
+    private String areaName;
 }
diff --git a/src/main/java/com/ruoyi/device/mapper/DeviceAreaMapper.java b/src/main/java/com/ruoyi/device/mapper/DeviceAreaMapper.java
new file mode 100644
index 0000000..29bbda0
--- /dev/null
+++ b/src/main/java/com/ruoyi/device/mapper/DeviceAreaMapper.java
@@ -0,0 +1,12 @@
+package com.ruoyi.device.mapper;
+
+import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.ruoyi.device.pojo.DeviceArea;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * 璁惧鍖哄煙 Mapper
+ */
+@Mapper
+public interface DeviceAreaMapper extends BaseMapper<DeviceArea> {
+}
diff --git a/src/main/java/com/ruoyi/device/mapper/DeviceLedgerMapper.java b/src/main/java/com/ruoyi/device/mapper/DeviceLedgerMapper.java
index dc43e78..48a8e0c 100644
--- a/src/main/java/com/ruoyi/device/mapper/DeviceLedgerMapper.java
+++ b/src/main/java/com/ruoyi/device/mapper/DeviceLedgerMapper.java
@@ -19,6 +19,12 @@
 
     IPage<DeviceLedgerDto> queryPage(Page page, @Param("deviceLedger") DeviceLedgerDto deviceLedgerDto);
 
+    /**
+     * 璁惧閫夋嫨鍒楄〃锛堝贰妫�绛夋ā鍧楅�夎澶囩敤锛夛紝鏀寔鎸夊尯鍩熼�掑綊绛涢��
+     * @param areaId 鍖哄煙ID锛屽彲涓虹┖琛ㄧず鍏ㄩ儴
+     */
+    List<DeviceLedgerDto> selectDeviceOptions(@Param("areaId") Long areaId);
+
     List<DeviceLedgerExeclDto> deviceLedgerExportList(DeviceLedger deviceLedger);
 
     @InterceptorIgnore(tenantLine = "true")
diff --git a/src/main/java/com/ruoyi/device/pojo/DeviceArea.java b/src/main/java/com/ruoyi/device/pojo/DeviceArea.java
new file mode 100644
index 0000000..62b46bd
--- /dev/null
+++ b/src/main/java/com/ruoyi/device/pojo/DeviceArea.java
@@ -0,0 +1,88 @@
+package com.ruoyi.device.pojo;
+
+import com.baomidou.mybatisplus.annotation.FieldFill;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableName;
+import com.fasterxml.jackson.annotation.JsonFormat;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import org.springframework.format.annotation.DateTimeFormat;
+
+import java.time.LocalDateTime;
+
+/**
+ * 璁惧鍖哄煙瀹炰綋绫伙紙杞﹂棿/浣嶇疆锛岀埗瀛愰�掑綊锛�
+ */
+@Data
+@TableName("device_area")
+@Schema
+public class DeviceArea {
+
+    /**
+     * 涓婚敭ID锛岃嚜澧�
+     */
+    private Long id;
+
+    /**
+     * 鍖哄煙/杞﹂棿/浣嶇疆鍚嶇О
+     */
+    @Schema(description = "鍖哄煙/杞﹂棿/浣嶇疆鍚嶇О")
+    private String name;
+
+    /**
+     * 鐖剁骇鍖哄煙ID锛屾牴鑺傜偣涓篘ULL
+     */
+    @Schema(description = "鐖剁骇鍖哄煙ID")
+    private Long parentId;
+
+    /**
+     * 绁栫骇璺緞锛屽 1,2,3
+     */
+    @Schema(description = "绁栫骇璺緞")
+    private String ancestors;
+
+    /**
+     * 鎺掑簭
+     */
+    @Schema(description = "鎺掑簭")
+    private Integer sort;
+
+    /**
+     * 閮ㄩ棬ID
+     */
+    private Long deptId;
+
+    /**
+     * 绉熸埛ID
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Long tenantId;
+
+    /**
+     * 鍒涘缓浜�
+     */
+    @TableField(fill = FieldFill.INSERT)
+    private Integer createUser;
+
+    /**
+     * 鍒涘缓鏃堕棿
+     */
+    @TableField(fill = FieldFill.INSERT)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime createTime;
+
+    /**
+     * 鏇存柊浜�
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    private Integer updateUser;
+
+    /**
+     * 鏇存柊鏃堕棿
+     */
+    @TableField(fill = FieldFill.INSERT_UPDATE)
+    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
+    private LocalDateTime updateTime;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/device/pojo/DeviceLedger.java b/src/main/java/com/ruoyi/device/pojo/DeviceLedger.java
index 61239ac..eb4ec57 100644
--- a/src/main/java/com/ruoyi/device/pojo/DeviceLedger.java
+++ b/src/main/java/com/ruoyi/device/pojo/DeviceLedger.java
@@ -161,4 +161,15 @@
 
     @TableField(fill = FieldFill.INSERT)
     private Long deptId;
+
+    /**
+     * 璁惧鍖哄煙ID
+     */
+    private Long areaId;
+
+    /**
+     * 璁惧鍖哄煙鍚嶇О锛堥潪鏁版嵁搴撳瓧娈碉紝鍒楄〃灞曠ず锛�
+     */
+    @TableField(exist = false)
+    private String areaName;
 }
diff --git a/src/main/java/com/ruoyi/device/service/IDeviceAreaService.java b/src/main/java/com/ruoyi/device/service/IDeviceAreaService.java
new file mode 100644
index 0000000..f7e5d77
--- /dev/null
+++ b/src/main/java/com/ruoyi/device/service/IDeviceAreaService.java
@@ -0,0 +1,35 @@
+package com.ruoyi.device.service;
+
+import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.device.dto.DeviceAreaDto;
+import com.ruoyi.device.pojo.DeviceArea;
+import com.ruoyi.device.vo.DeviceAreaTreeDto;
+import com.ruoyi.framework.web.domain.AjaxResult;
+
+import java.util.List;
+
+/**
+ * 璁惧鍖哄煙 Service
+ */
+public interface IDeviceAreaService extends IService<DeviceArea> {
+
+    /**
+     * 鏌ヨ鍖哄煙鏍戯紙鍚悇鑺傜偣閫掑綊璁惧鏁伴噺锛�
+     */
+    List<DeviceAreaTreeDto> selectAreaTree();
+
+    /**
+     * 鏂板鍖哄煙
+     */
+    AjaxResult addArea(DeviceAreaDto dto);
+
+    /**
+     * 淇敼鍖哄煙
+     */
+    AjaxResult updateArea(DeviceAreaDto dto);
+
+    /**
+     * 鍒犻櫎鍖哄煙锛堟湁瀛愮骇鎴栧叧鑱旇澶囨椂绂佹鍒犻櫎锛�
+     */
+    AjaxResult deleteArea(List<Long> ids);
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java b/src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java
index 78dfcac..1571fb7 100644
--- a/src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java
+++ b/src/main/java/com/ruoyi/device/service/IDeviceLedgerService.java
@@ -10,10 +10,16 @@
 
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
+import java.util.List;
 
 public interface IDeviceLedgerService  extends IService<DeviceLedger> {
     IPage<DeviceLedgerDto> queryPage(Page page, DeviceLedgerDto deviceLedger);
 
+    /**
+     * 璁惧閫夋嫨鍒楄〃锛堝贰妫�绛夋ā鍧楅�夎澶囩敤锛夛紝鏀寔鎸夊尯鍩熼�掑綊绛涢��
+     */
+    List<DeviceLedgerDto> selectDeviceOptions(Long areaId);
+
     AjaxResult saveDeviceLedger(DeviceLedgerDto deviceLedgerDto);
 
     AjaxResult updateDeviceLedger(DeviceLedgerDto deviceLedgerDto);
diff --git a/src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java b/src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java
new file mode 100644
index 0000000..06340f6
--- /dev/null
+++ b/src/main/java/com/ruoyi/device/service/impl/DeviceAreaServiceImpl.java
@@ -0,0 +1,194 @@
+package com.ruoyi.device.service.impl;
+
+import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
+import com.ruoyi.common.utils.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
+import com.ruoyi.device.dto.DeviceAreaDto;
+import com.ruoyi.device.mapper.DeviceAreaMapper;
+import com.ruoyi.device.mapper.DeviceLedgerMapper;
+import com.ruoyi.device.pojo.DeviceArea;
+import com.ruoyi.device.pojo.DeviceLedger;
+import com.ruoyi.device.service.IDeviceAreaService;
+import com.ruoyi.device.vo.DeviceAreaTreeDto;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import lombok.RequiredArgsConstructor;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+
+import java.util.ArrayList;
+import java.util.List;
+import java.util.Map;
+import java.util.stream.Collectors;
+
+/**
+ * 璁惧鍖哄煙 Service 瀹炵幇
+ */
+@Service
+@RequiredArgsConstructor
+public class DeviceAreaServiceImpl extends ServiceImpl<DeviceAreaMapper, DeviceArea> implements IDeviceAreaService {
+
+    private final DeviceAreaMapper deviceAreaMapper;
+    private final DeviceLedgerMapper deviceLedgerMapper;
+
+    @Override
+    public List<DeviceAreaTreeDto> selectAreaTree() {
+        List<DeviceArea> all = this.list(new LambdaQueryWrapper<DeviceArea>().orderByAsc(DeviceArea::getSort));
+        if (all == null || all.isEmpty()) {
+            return new ArrayList<>();
+        }
+
+        // 缁熻姣忎釜鍖哄煙鐩存帴鎸傝浇鐨勮澶囨暟閲�
+        Map<Long, Long> directCountMap = deviceLedgerMapper.selectList(
+                        new LambdaQueryWrapper<DeviceLedger>()
+                                .select(DeviceLedger::getAreaId)
+                                .isNotNull(DeviceLedger::getAreaId))
+                .stream()
+                .filter(d -> d.getAreaId() != null)
+                .collect(Collectors.groupingBy(DeviceLedger::getAreaId, Collectors.counting()));
+
+        Map<Long, DeviceArea> areaMap = all.stream()
+                .collect(Collectors.toMap(DeviceArea::getId, a -> a));
+
+        // 閫掑綊姹囨�绘瘡涓妭鐐圭殑璁惧鏁伴噺锛堝惈鎵�鏈夊瓙绾э級
+        Map<Long, Long> countMap = directCountMap.entrySet().stream()
+                .collect(Collectors.toMap(Map.Entry::getKey, Map.Entry::getValue));
+        for (DeviceArea area : all) {
+            accumulate(area.getId(), directCountMap.getOrDefault(area.getId(), 0L), countMap, areaMap);
+        }
+
+        List<Long> rootIds = all.stream()
+                .filter(a -> a.getParentId() == null || !areaMap.containsKey(a.getParentId()))
+                .map(DeviceArea::getId)
+                .collect(Collectors.toList());
+
+        List<DeviceAreaTreeDto> tree = new ArrayList<>();
+        for (Long rootId : rootIds) {
+            DeviceArea root = areaMap.get(rootId);
+            tree.add(buildNode(root, all, countMap));
+        }
+        return tree;
+    }
+
+    // 灏嗗綋鍓嶈妭鐐规暟閲忕疮鍔犲埌鎵�鏈夌绾ц妭鐐�
+    private void accumulate(Long areaId, Long count, Map<Long, Long> countMap, Map<Long, DeviceArea> areaMap) {
+        DeviceArea area = areaMap.get(areaId);
+        if (area == null) {
+            return;
+        }
+        countMap.merge(areaId, count, Long::sum);
+        // 娌跨鍏堥摼缁х画绱姞锛堥伩鍏嶇鍏堥摼寰幆渚濊禆锛�
+        if (area.getParentId() != null && areaMap.containsKey(area.getParentId())) {
+            accumulate(area.getParentId(), count, countMap, areaMap);
+        }
+    }
+
+    private DeviceAreaTreeDto buildNode(DeviceArea area, List<DeviceArea> all, Map<Long, Long> countMap) {
+        DeviceAreaTreeDto node = new DeviceAreaTreeDto();
+        node.setId(area.getId());
+        node.setParentId(area.getParentId());
+        node.setLabel(area.getName());
+        node.setDeviceCount(countMap.getOrDefault(area.getId(), 0L));
+        for (DeviceArea child : all) {
+            if (area.getId().equals(child.getParentId())) {
+                node.getChildren().add(buildNode(child, all, countMap));
+            }
+        }
+        return node;
+    }
+
+    @Override
+    public AjaxResult addArea(DeviceAreaDto dto) {
+        if (StringUtils.isEmpty(dto.getName())) {
+            return AjaxResult.error("鍖哄煙鍚嶇О涓嶈兘涓虹┖");
+        }
+        DeviceArea area = new DeviceArea();
+        area.setName(dto.getName());
+        area.setParentId(dto.getParentId());
+        area.setSort(dto.getSort());
+        area.setDeptId(dto.getDeptId());
+        area.setTenantId(SecurityUtils.getLoginUser().getTenantId());
+
+        if (dto.getParentId() != null) {
+            DeviceArea parent = this.getById(dto.getParentId());
+            if (parent == null) {
+                return AjaxResult.error("鐖剁骇鍖哄煙涓嶅瓨鍦�");
+            }
+            area.setAncestors(StringUtils.isEmpty(parent.getAncestors())
+                    ? String.valueOf(parent.getId())
+                    : parent.getAncestors() + "," + parent.getId());
+        }
+        this.save(area);
+        return AjaxResult.success();
+    }
+
+    @Override
+    public AjaxResult updateArea(DeviceAreaDto dto) {
+        if (dto.getId() == null) {
+            return AjaxResult.error("鍖哄煙ID涓嶈兘涓虹┖");
+        }
+        DeviceArea area = this.getById(dto.getId());
+        if (area == null) {
+            return AjaxResult.error("鍖哄煙涓嶅瓨鍦�");
+        }
+        // 涓嶅厑璁告妸鍖哄煙鎸傚埌鑷繁鐨勫瓙绾т笅锛岄伩鍏嶅惊鐜�
+        if (dto.getParentId() != null && dto.getParentId().equals(dto.getId())) {
+            return AjaxResult.error("涓嶈兘灏嗗尯鍩熸寕杞藉埌鑷韩");
+        }
+        if (dto.getParentId() != null && isDescendant(dto.getId(), dto.getParentId())) {
+            return AjaxResult.error("涓嶈兘灏嗗尯鍩熸寕杞藉埌鑷繁鐨勫瓙绾т笅");
+        }
+        area.setName(dto.getName());
+        area.setSort(dto.getSort());
+        area.setDeptId(dto.getDeptId());
+        if (!java.util.Objects.equals(dto.getParentId(), area.getParentId())) {
+            area.setParentId(dto.getParentId());
+            if (dto.getParentId() == null) {
+                area.setAncestors(null);
+            } else {
+                DeviceArea parent = this.getById(dto.getParentId());
+                if (parent == null) {
+                    return AjaxResult.error("鐖剁骇鍖哄煙涓嶅瓨鍦�");
+                }
+                area.setAncestors(StringUtils.isEmpty(parent.getAncestors())
+                        ? String.valueOf(parent.getId())
+                        : parent.getAncestors() + "," + parent.getId());
+            }
+        }
+        this.updateById(area);
+        return AjaxResult.success();
+    }
+
+    private boolean isDescendant(Long targetId, Long candidateParentId) {
+        DeviceArea cur = this.getById(candidateParentId);
+        while (cur != null && cur.getParentId() != null) {
+            if (cur.getParentId().equals(targetId)) {
+                return true;
+            }
+            cur = this.getById(cur.getParentId());
+        }
+        return false;
+    }
+
+    @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult deleteArea(List<Long> ids) {
+        for (Long id : ids) {
+            DeviceArea area = this.getById(id);
+            if (area == null) {
+                return AjaxResult.error("鍖哄煙涓嶅瓨鍦�");
+            }
+            long childCount = this.count(new LambdaQueryWrapper<DeviceArea>().eq(DeviceArea::getParentId, id));
+            if (childCount > 0) {
+                return AjaxResult.error("璇ュ尯鍩熷瓨鍦ㄥ瓙绾у尯鍩燂紝璇峰厛鍒犻櫎瀛愮骇");
+            }
+            long deviceCount = deviceLedgerMapper.selectCount(
+                    new LambdaQueryWrapper<DeviceLedger>().eq(DeviceLedger::getAreaId, id));
+            if (deviceCount > 0) {
+                return AjaxResult.error("璇ュ尯鍩熶笅瀛樺湪璁惧锛岃鍏堢Щ闄よ澶囧叧鑱�");
+            }
+        }
+        this.removeByIds(ids);
+        return AjaxResult.success();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java b/src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java
index 154923d..5315eff 100644
--- a/src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/device/service/impl/DeviceLedgerServiceImpl.java
@@ -48,6 +48,11 @@
     }
 
     @Override
+    public List<DeviceLedgerDto> selectDeviceOptions(Long areaId) {
+        return deviceLedgerMapper.selectDeviceOptions(areaId);
+    }
+
+    @Override
     public AjaxResult saveDeviceLedger(DeviceLedgerDto deviceLedgerDto) {
         LambdaQueryWrapper<DeviceLedger> deviceLedgerLambdaQueryWrapper = new LambdaQueryWrapper<>();
         deviceLedgerLambdaQueryWrapper.eq(DeviceLedger::getDeviceName,deviceLedgerDto.getDeviceName());
diff --git a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
index 204667b..42f3f36 100644
--- a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
+++ b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskJob.java
@@ -124,6 +124,8 @@
                     return calculateMonthlyNextTime(frequencyDetail, currentTime);
                 case "QUARTERLY":
                     return calculateQuarterlyNextTime(frequencyDetail, currentTime);
+                case "YEARLY":
+                    return calculateYearlyNextTime(frequencyDetail, currentTime);
                 default:
                     throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + frequencyType);
             }
@@ -221,6 +223,23 @@
         );
     }
 
+    private LocalDateTime calculateYearlyNextTime(String detail, LocalDateTime current) {
+        String[] parts = detail.split(",");
+        int month = Integer.parseInt(parts[0]);
+        int day = Integer.parseInt(parts[1]);
+        LocalTime time = LocalTime.parse(parts[2]);
+
+        int year = current.getYear();
+        LocalDateTime nextTime = LocalDateTime.of(java.time.LocalDate.of(year, month,
+                Math.min(day, YearMonth.of(year, month).lengthOfMonth())), time);
+        if (!nextTime.isAfter(current)) {
+            year++;
+            nextTime = LocalDateTime.of(java.time.LocalDate.of(year, month,
+                    Math.min(day, YearMonth.of(year, month).lengthOfMonth())), time);
+        }
+        return nextTime;
+    }
+
     /**
      * 瑙f瀽鏄熸湡鍑犲瓧绗︿覆
      */
diff --git a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
index ec7a8e3..18d426d 100644
--- a/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
+++ b/src/main/java/com/ruoyi/device/service/impl/MaintenanceTaskScheduler.java
@@ -160,6 +160,8 @@
                 return convertMonthlyToCron(task.getFrequencyDetail());
             case "QUARTERLY":
                 return convertQuarterlyToCron(task.getFrequencyDetail());
+            case "YEARLY":
+                return convertYearlyToCron(task.getFrequencyDetail());
             default:
                 throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + task.getFrequencyType());
         }
@@ -204,6 +206,14 @@
                 quarterStartMonth);
     }
 
+    private String convertYearlyToCron(String frequencyDetail) {
+        String[] parts = validateAndSplit(frequencyDetail, ",", 3);
+        int month = validateMonth(parts[0]);
+        int day = validateDayOfMonth(parts[1]);
+        LocalTime time = parseTime(parts[2]);
+        return String.format("0 %d %d %d %d ?", time.getMinute(), time.getHour(), day, month);
+    }
+
     // 鏂板楠岃瘉鏈堜唤鐨勬柟娉�(1-12)
     private int validateMonth(String monthStr) {
         try {
diff --git a/src/main/java/com/ruoyi/device/vo/DeviceAreaTreeDto.java b/src/main/java/com/ruoyi/device/vo/DeviceAreaTreeDto.java
new file mode 100644
index 0000000..bdfe4e8
--- /dev/null
+++ b/src/main/java/com/ruoyi/device/vo/DeviceAreaTreeDto.java
@@ -0,0 +1,29 @@
+package com.ruoyi.device.vo;
+
+import lombok.Data;
+
+import java.util.ArrayList;
+import java.util.List;
+
+/**
+ * 璁惧鍖哄煙鏍� VO锛堝墠绔� el-tree 缁撴瀯锛�
+ */
+@Data
+public class DeviceAreaTreeDto {
+
+    private Long id;
+
+    private Long parentId;
+
+    /**
+     * 鑺傜偣鍚嶇О
+     */
+    private String label;
+
+    /**
+     * 褰撳墠鑺傜偣鍙婃墍鏈夊瓙绾у尯鍩熶笅鐨勮澶囨暟閲�
+     */
+    private Long deviceCount;
+
+    private List<DeviceAreaTreeDto> children = new ArrayList<>();
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java b/src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
index 9729b40..890238d 100644
--- a/src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
+++ b/src/main/java/com/ruoyi/inspectiontask/service/impl/InspectionTaskServiceImpl.java
@@ -161,6 +161,7 @@
             }
         }
         validateInspectionInput(inspectionTaskDto, oldInspectionTask);
+        validateDeviceExists(inspectionTaskDto, oldInspectionTask);
 
         InspectionTask inspectionTask = new InspectionTask();
         BeanUtils.copyProperties(inspectionTaskDto, inspectionTask);
@@ -190,6 +191,20 @@
         return i;
     }
 
+    private void validateDeviceExists(InspectionTaskDto inspectionTaskDto, InspectionTask oldInspectionTask) {
+        Integer taskId = inspectionTaskDto.getTaskId();
+        if (taskId == null && oldInspectionTask != null) {
+            taskId = oldInspectionTask.getTaskId();
+        }
+        if (taskId == null || taskId <= 0) {
+            return;
+        }
+        DeviceLedger deviceLedger = deviceLedgerMapper.selectById(taskId.longValue());
+        if (deviceLedger == null) {
+            throw new IllegalArgumentException("鎵�閫夎澶囦笉瀛樺湪锛岃閲嶆柊閫夋嫨");
+        }
+    }
+
     private void validateInspectionInput(InspectionTaskDto inspectionTaskDto, InspectionTask oldInspectionTask) {
         String inspectionResult = inspectionTaskDto.getInspectionResult();
         if (StringUtils.isBlank(inspectionResult) && oldInspectionTask != null) {
diff --git a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
index ebe615d..02a444b 100644
--- a/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
+++ b/src/main/java/com/ruoyi/inspectiontask/service/impl/TimingTaskServiceImpl.java
@@ -35,6 +35,7 @@
     private final TimingTaskMapper timingTaskMapper;
     private final TimingTaskScheduler timingTaskScheduler;
     private final SysUserMapper sysUserMapper;
+    private final com.ruoyi.device.mapper.DeviceLedgerMapper deviceLedgerMapper;
     private static final int ENABLED = 1;
     private static final int DISABLED = 0;
 
@@ -131,6 +132,7 @@
                 throw new IllegalArgumentException("瀹氭椂浠诲姟涓嶅瓨鍦�");
             }
         }
+        validateDeviceExists(timingTaskDto, oldTimingTask);
         TimingTask timingTask = new TimingTask();
         BeanUtils.copyProperties(timingTaskDto, timingTask);
         timingTask.setIsEnabled(resolveEnabledValue(timingTask.getIsEnabled(), oldTimingTask));
@@ -181,6 +183,19 @@
         }
     }
 
+    private void validateDeviceExists(TimingTaskDto timingTaskDto, TimingTask oldTimingTask) {
+        Integer taskId = timingTaskDto.getTaskId();
+        if (taskId == null && oldTimingTask != null) {
+            taskId = oldTimingTask.getTaskId();
+        }
+        if (taskId == null || taskId <= 0) {
+            return;
+        }
+        if (deviceLedgerMapper.selectById(taskId.longValue()) == null) {
+            throw new IllegalArgumentException("鎵�閫夎澶囦笉瀛樺湪锛岃閲嶆柊閫夋嫨");
+        }
+    }
+
     public LocalDateTime calculateFirstExecutionTime(TimingTask task) {
         // 鏍规嵁棰戠巼绫诲瀷鍜岃鎯呰绠楅娆℃墽琛屾椂闂�
         String frequencyType = task.getFrequencyType();
@@ -194,8 +209,9 @@
             // 濡傛灉鏄瘡鏈堟墽琛岋紝璁$畻涓嬩釜鏈堢殑鍏蜂綋鏃ユ湡
             return calculateMonthlyFirstExecution(task.getFrequencyDetail());
         } else if ("QUARTERLY".equals(frequencyType)) {
-            // 鑷畾涔夐鐜囷紝濡傛瘡灏忔椂銆佹瘡30鍒嗛挓绛�
             return calculateCustomFirstExecution(task.getFrequencyDetail());
+        } else if ("YEARLY".equals(frequencyType)) {
+            return calculateAnnualFirstExecution(task.getFrequencyDetail());
         } else {
             throw new IllegalArgumentException("涓嶆敮鎸佺殑棰戠巼绫诲瀷: " + task.getFrequencyType());
         }
@@ -314,6 +330,32 @@
         return targetDateTime;
     }
 
+    private LocalDateTime calculateAnnualFirstExecution(String frequencyDetail) {
+        String[] parts = frequencyDetail.split(",");
+        if (parts.length != 3) {
+            throw new IllegalArgumentException("鍙傛暟鏍煎紡閿欒锛屽簲涓�'12,31,09:00'鏍煎紡");
+        }
+
+        int month;
+        int day;
+        try {
+            month = Integer.parseInt(parts[0].trim());
+            day = Integer.parseInt(parts[1].trim());
+        } catch (NumberFormatException e) {
+            throw new IllegalArgumentException("鏈堜唤鎴栨棩鏈熸牸寮忛敊璇�", e);
+        }
+        if (month < 1 || month > 12 || day < 1 || day > 31) {
+            throw new IllegalArgumentException("鏈堜唤蹇呴』鍦�1-12涔嬮棿锛屾棩鏈熷繀椤诲湪1-31涔嬮棿");
+        }
+
+        LocalTime time = LocalTime.parse(parts[2].trim(), DateTimeFormatter.ofPattern("HH:mm"));
+        LocalDateTime now = LocalDateTime.now();
+        int year = now.getYear();
+        LocalDateTime target = LocalDateTime.of(LocalDate.of(year, month, Math.min(day, YearMonth.of(year, month).lengthOfMonth())), time);
+        return target.isAfter(now) ? target : LocalDateTime.of(LocalDate.of(year + 1, month,
+                Math.min(day, YearMonth.of(year + 1, month).lengthOfMonth())), time);
+    }
+
     private LocalDateTime calculateCustomFirstExecution(String frequencyDetail) {
         return null;
     }
diff --git a/src/main/java/com/ruoyi/measuringinstrumentledger/controller/MeasuringInstrumentLedgerController.java b/src/main/java/com/ruoyi/measuringinstrumentledger/controller/MeasuringInstrumentLedgerController.java
index e092741..e359a9c 100644
--- a/src/main/java/com/ruoyi/measuringinstrumentledger/controller/MeasuringInstrumentLedgerController.java
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/controller/MeasuringInstrumentLedgerController.java
@@ -21,6 +21,7 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 import org.springframework.web.bind.annotation.*;
+import org.springframework.web.multipart.MultipartFile;
 
 import java.io.IOException;
 import java.util.List;
@@ -108,6 +109,19 @@
         return update ? AjaxResult.success("妫�瀹氭垚鍔�") : AjaxResult.error("妫�瀹氬け璐�");
     }
 
+    @PostMapping("/downloadTemplate")
+    @Operation(summary = "涓嬭浇璁¢噺鍣ㄥ叿鍙拌处瀵煎叆妯℃澘")
+    public void downloadTemplate(HttpServletResponse response) {
+        measuringInstrumentLedgerService.downloadTemplate(response);
+    }
+
+    @PostMapping("/import")
+    @Operation(summary = "鎵归噺瀵煎叆璁¢噺鍣ㄥ叿鍙拌处")
+    @Log(title = "鎵归噺瀵煎叆璁¢噺鍣ㄥ叿鍙拌处", businessType = BusinessType.IMPORT)
+    public AjaxResult importData(@RequestParam("file") MultipartFile file) {
+        return measuringInstrumentLedgerService.importData(file);
+    }
+
     /**
      * 瀵煎嚭璁¢噺鍣ㄥ叿鍙拌处
      */
diff --git a/src/main/java/com/ruoyi/measuringinstrumentledger/dto/MeasuringInstrumentLedgerImportDto.java b/src/main/java/com/ruoyi/measuringinstrumentledger/dto/MeasuringInstrumentLedgerImportDto.java
new file mode 100644
index 0000000..e1d6c95
--- /dev/null
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/dto/MeasuringInstrumentLedgerImportDto.java
@@ -0,0 +1,40 @@
+package com.ruoyi.measuringinstrumentledger.dto;
+
+import com.ruoyi.framework.aspectj.lang.annotation.Excel;
+import lombok.Data;
+
+import java.util.Date;
+
+@Data
+public class MeasuringInstrumentLedgerImportDto {
+
+    @Excel(name = "璁¢噺鍣ㄥ叿缂栧彿")
+    private String code;
+
+    @Excel(name = "璁¢噺鍣ㄥ叿鍚嶇О")
+    private String name;
+
+    @Excel(name = "瑙勬牸鍨嬪彿")
+    private String model;
+
+    @Excel(name = "鏈�杩戜竴娆℃瀹氭棩鏈�", dateFormat = "yyyy-MM-dd")
+    private Date mostDate;
+
+    @Excel(name = "妫�瀹氭湁鏁堟湡")
+    private Integer valid;
+
+    @Excel(name = "褰曞叆鏃ユ湡", dateFormat = "yyyy-MM-dd")
+    private Date recordDate;
+
+    @Excel(name = "鍗曚綅")
+    private String unit;
+
+    @Excel(name = "閮ㄩ棬ID")
+    private Long deptId;
+
+    @Excel(name = "瀹夎浣嶇疆")
+    private String instationLocation;
+
+    @Excel(name = "妫�瀹氬懆鏈�")
+    private String cycle;
+}
diff --git a/src/main/java/com/ruoyi/measuringinstrumentledger/mapper/MeasuringInstrumentLedgerMapper.java b/src/main/java/com/ruoyi/measuringinstrumentledger/mapper/MeasuringInstrumentLedgerMapper.java
index 961a2f1..36000a9 100644
--- a/src/main/java/com/ruoyi/measuringinstrumentledger/mapper/MeasuringInstrumentLedgerMapper.java
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/mapper/MeasuringInstrumentLedgerMapper.java
@@ -6,6 +6,7 @@
 import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedger;
 import org.apache.ibatis.annotations.Param;
 
+import java.util.Collection;
 import java.util.List;
 
 /**
@@ -24,4 +25,6 @@
     IPage<MeasuringInstrumentLedger> listPage(Page page,@Param("req") MeasuringInstrumentLedger measuringInstrumentLedger);
 
     List<MeasuringInstrumentLedger> listPage(@Param("req") MeasuringInstrumentLedger measuringInstrumentLedger);
+
+    List<String> selectExistingCodes(@Param("tenantId") Long tenantId, @Param("codes") Collection<String> codes);
 }
diff --git a/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerService.java b/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerService.java
index c28fe83..dc9f64a 100644
--- a/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerService.java
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/service/MeasuringInstrumentLedgerService.java
@@ -3,8 +3,10 @@
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
 import com.baomidou.mybatisplus.extension.service.IService;
+import com.ruoyi.framework.web.domain.AjaxResult;
 import com.ruoyi.measuringinstrumentledger.dto.MeasuringInstrumentLedgerDto;
 import com.ruoyi.measuringinstrumentledger.pojo.MeasuringInstrumentLedger;
+import org.springframework.web.multipart.MultipartFile;
 
 import jakarta.servlet.http.HttpServletResponse;
 import java.io.IOException;
@@ -34,6 +36,10 @@
 
     void export(HttpServletResponse response);
 
+    AjaxResult importData(MultipartFile file);
+
+    void downloadTemplate(HttpServletResponse response);
+
     /**
      * 鏂板
      *
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 4025d75..56d5694 100644
--- a/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/measuringinstrumentledger/service/impl/MeasuringInstrumentLedgerServiceImpl.java
@@ -2,11 +2,14 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
-import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
 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.SecurityUtils;
+import com.ruoyi.common.utils.StringUtils;
 import com.ruoyi.common.utils.poi.ExcelUtil;
+import com.ruoyi.framework.web.domain.AjaxResult;
+import com.ruoyi.measuringinstrumentledger.dto.MeasuringInstrumentLedgerImportDto;
 import com.ruoyi.measuringinstrumentledger.dto.MeasuringInstrumentLedgerDto;
 import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerMapper;
 import com.ruoyi.measuringinstrumentledger.mapper.MeasuringInstrumentLedgerRecordMapper;
@@ -25,7 +28,14 @@
 import java.io.IOException;
 import java.util.ArrayList;
 import java.util.Date;
+import java.util.HashSet;
+import java.util.LinkedHashMap;
+import java.util.Map;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.util.CollectionUtils;
+import org.springframework.web.multipart.MultipartFile;
 import java.util.List;
+import java.util.Set;
 import java.util.stream.Collectors;
 
 /**
@@ -89,6 +99,90 @@
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    public AjaxResult importData(MultipartFile file) {
+        if (file == null || file.isEmpty()) {
+            return AjaxResult.error("璇蜂笂浼� Excel 鏂囦欢");
+        }
+        String filename = file.getOriginalFilename();
+        if (filename == null || !(filename.endsWith(".xls") || filename.endsWith(".xlsx"))) {
+            return AjaxResult.error("鍙敮鎸佸鍏� xls 鎴� xlsx 鏂囦欢");
+        }
+        try {
+            ExcelUtil<MeasuringInstrumentLedgerImportDto> util = new ExcelUtil<>(MeasuringInstrumentLedgerImportDto.class);
+            List<MeasuringInstrumentLedgerImportDto> rows = util.importExcel(file.getInputStream());
+            if (CollectionUtils.isEmpty(rows)) {
+                return AjaxResult.error("Excel 涓病鏈夊彲瀵煎叆鐨勬暟鎹�");
+            }
+            Set<String> codes = new HashSet<>();
+            Map<Integer, String> errors = new LinkedHashMap<>();
+            List<MeasuringInstrumentLedger> ledgers = new ArrayList<>();
+            Long tenantId = SecurityUtils.getLoginUser().getTenantId();
+            Long userId = SecurityUtils.getUserId();
+            String userName = SecurityUtils.getLoginUser().getNickName();
+            for (int i = 0; i < rows.size(); i++) {
+                MeasuringInstrumentLedgerImportDto row = rows.get(i);
+                int excelRow = i + 2;
+                String code = StringUtils.trim(row.getCode());
+                String name = StringUtils.trim(row.getName());
+                if (!StringUtils.hasText(code)) {
+                    errors.put(excelRow, "璁¢噺鍣ㄥ叿缂栧彿涓嶈兘涓虹┖");
+                    continue;
+                }
+                if (!StringUtils.hasText(name)) {
+                    errors.put(excelRow, "璁¢噺鍣ㄥ叿鍚嶇О涓嶈兘涓虹┖");
+                    continue;
+                }
+                if (!codes.add(code)) {
+                    errors.put(excelRow, "璁¢噺鍣ㄥ叿缂栧彿鍦� Excel 涓噸澶�");
+                    continue;
+                }
+                if (row.getValid() != null && row.getValid() <= 0) {
+                    errors.put(excelRow, "妫�瀹氭湁鏁堟湡蹇呴』澶т簬 0");
+                    continue;
+                }
+                MeasuringInstrumentLedger ledger = new MeasuringInstrumentLedger();
+                ledger.setCode(code);
+                ledger.setName(name);
+                ledger.setModel(StringUtils.trim(row.getModel()));
+                ledger.setMostDate(row.getMostDate());
+                ledger.setValid(row.getValid());
+                ledger.setRecordDate(row.getRecordDate() == null ? new Date() : row.getRecordDate());
+                ledger.setUnit(StringUtils.trim(row.getUnit()));
+                ledger.setDeptId(row.getDeptId());
+                ledger.setInstationLocation(StringUtils.trim(row.getInstationLocation()));
+                ledger.setCycle(StringUtils.trim(row.getCycle()));
+                ledger.setUserId(userId);
+                ledger.setUserName(userName);
+                ledger.setTenantId(tenantId);
+                if (row.getMostDate() != null && row.getValid() != null) {
+                    ledger.setNextDate(new Date(row.getMostDate().getTime() + 86400000L * row.getValid()));
+                }
+                ledgers.add(ledger);
+            }
+            if (!codes.isEmpty()) {
+                for (String code : measuringInstrumentLedgerMapper.selectExistingCodes(tenantId, codes)) {
+                    errors.put(-1, "璁¢噺鍣ㄥ叿缂栧彿宸插瓨鍦細" + code);
+                }
+            }
+            if (!errors.isEmpty()) {
+                return AjaxResult.error("瀵煎叆鏍¢獙澶辫触锛�" + errors.values().stream().limit(100).collect(Collectors.joining("锛�")));
+            }
+            saveBatch(ledgers);
+            return AjaxResult.success("鎴愬姛瀵煎叆 " + ledgers.size() + " 鏉℃暟鎹�");
+        } catch (Exception e) {
+            log.error("璁¢噺鍣ㄥ叿鍙拌处瀵煎叆澶辫触", e);
+            return AjaxResult.error("瀵煎叆澶辫触锛岃妫�鏌� Excel 鍐呭");
+        }
+    }
+
+    @Override
+    public void downloadTemplate(HttpServletResponse response) {
+        ExcelUtil<MeasuringInstrumentLedgerImportDto> util = new ExcelUtil<>(MeasuringInstrumentLedgerImportDto.class);
+        util.importTemplateExcel(response, "璁¢噺鍣ㄥ叿鍙拌处瀵煎叆妯℃澘");
+    }
+
+    @Override
     public void export(HttpServletResponse response) {
         List<MeasuringInstrumentLedger> list = measuringInstrumentLedgerMapper.listPage(new MeasuringInstrumentLedger());
         ExcelUtil<MeasuringInstrumentLedger> util = new ExcelUtil<MeasuringInstrumentLedger>(MeasuringInstrumentLedger.class);
diff --git a/src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java b/src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
index b3f424d..b40885e 100644
--- a/src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
+++ b/src/main/java/com/ruoyi/production/service/impl/ProductionPlanServiceImpl.java
@@ -24,6 +24,10 @@
 import com.ruoyi.production.pojo.ProductionPlan;
 import com.ruoyi.production.service.ProductionOrderService;
 import com.ruoyi.production.service.ProductionPlanService;
+import com.ruoyi.sales.mapper.SalesLedgerMapper;
+import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
+import com.ruoyi.sales.pojo.SalesLedger;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
 import jakarta.servlet.http.HttpServletResponse;
 import lombok.RequiredArgsConstructor;
 import org.springframework.stereotype.Service;
@@ -50,6 +54,8 @@
     private final ProductionOrderService productionOrderService;
     private final ProductModelMapper productModelMapper;
     private final ProductMapper productMapper;
+    private final SalesLedgerMapper salesLedgerMapper;
+    private final SalesLedgerProductMapper salesLedgerProductMapper;
 
     @Override
     public IPage<ProductionPlanVo> listPage(Page<ProductionPlanDto> page, ProductionPlanDto productionPlanDto) {
@@ -151,6 +157,7 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public boolean add(ProductionPlanDto dto) {
+        validateSalesSource(dto);
         // 鏂板涓荤敓浜ц鍒�
         if (StringUtils.isBlank(dto.getMpsNo())) {
             String datePrefix = LocalDate.now().format(DateTimeFormatter.ofPattern("yyyyMMdd"));
@@ -173,6 +180,7 @@
             throw new ServiceException("缂栬緫澶辫触锛屾暟鎹笉鑳戒负绌�");
         }
 
+        validateSalesSource(dto);
         ProductionPlan old = getById(dto.getId());
         if (old == null) {
             throw new ServiceException("缂栬緫澶辫触锛岀敓浜ц鍒掍笉瀛樺湪");
@@ -321,6 +329,32 @@
         util.exportExcel(response, exportList, "涓荤敓浜ц鍒�");
     }
 
+    private void validateSalesSource(ProductionPlanDto dto) {
+        if (dto == null) {
+            throw new ServiceException("鐢熶骇璁″垝涓嶈兘涓虹┖");
+        }
+        if (dto.getSalesLedgerId() == null && dto.getSalesLedgerProductId() == null) {
+            return;
+        }
+        if (dto.getSalesLedgerId() == null || dto.getSalesLedgerProductId() == null) {
+            throw new ServiceException("閿�鍞彴璐﹀拰閿�鍞骇鍝佸繀椤诲悓鏃跺~鍐�");
+        }
+        SalesLedgerProduct product = salesLedgerProductMapper.selectById(dto.getSalesLedgerProductId());
+        if (product == null || !Objects.equals(product.getSalesLedgerId(), dto.getSalesLedgerId())) {
+            throw new ServiceException("閿�鍞骇鍝佷笌閿�鍞彴璐︿笉鍖归厤");
+        }
+        SalesLedger ledger = salesLedgerMapper.selectById(dto.getSalesLedgerId());
+        if (ledger == null || (!Objects.equals(ledger.getContractType(), 1)
+                && !Objects.equals(ledger.getContractType(), 3))) {
+            throw new ServiceException("鐢熶骇璁″垝鍙兘鍏宠仈鏅�氶攢鍞骇鍝佹垨妗嗘灦璁㈠崟浜у搧");
+        }
+        if (dto.getProductModelId() == null) {
+            dto.setProductModelId(product.getProductModelId());
+        } else if (!Objects.equals(dto.getProductModelId(), product.getProductModelId())) {
+            throw new ServiceException("鐢熶骇璁″垝浜у搧鍨嬪彿涓庨攢鍞骇鍝佷笉涓�鑷�");
+        }
+    }
+
     /**
      * 鏍¢獙涓荤敓浜ц鍒掑彿鍞竴鎬э紝鍙�氳繃 excludeId 鎺掗櫎褰撳墠璁板綍銆�
      */
diff --git a/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java b/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
index ea18246..af93f28 100644
--- a/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
+++ b/src/main/java/com/ruoyi/sales/controller/SalesLedgerController.java
@@ -166,6 +166,24 @@
         return toAjax(salesLedgerService.addOrUpdateSalesLedger(salesLedgerDto));
     }
 
+    @PostMapping("/createFrameworkOrder")
+    @Operation(summary = "鍒涘缓妗嗘灦璁㈠崟")
+    public AjaxResult createFrameworkOrder(@RequestBody SalesLedgerDto salesLedgerDto) {
+        return toAjax(salesLedgerService.createFrameworkOrder(salesLedgerDto));
+    }
+
+    @GetMapping("/listFrameworkOrders")
+    @Operation(summary = "鏌ヨ妗嗘灦鍚堝悓涓嬬殑妗嗘灦璁㈠崟")
+    public AjaxResult listFrameworkOrders(@RequestParam("parentContractId") Long parentContractId) {
+        return AjaxResult.success(salesLedgerService.listFrameworkOrders(parentContractId));
+    }
+
+    @GetMapping("/frameworkContractShippedQuantity")
+    @Operation(summary = "鏌ヨ妗嗘灦鍚堝悓绱宸插彂璐ф暟閲�")
+    public AjaxResult frameworkContractShippedQuantity(@RequestParam("parentContractId") Long parentContractId) {
+        return AjaxResult.success(salesLedgerService.getFrameworkContractShippedQuantity(parentContractId));
+    }
+
     /**
      * 鍒犻櫎閿�鍞彴璐�
      */
diff --git a/src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java b/src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java
index a4fc57c..aa2b3fe 100644
--- a/src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java
+++ b/src/main/java/com/ruoyi/sales/dto/SalesLedgerDto.java
@@ -17,6 +17,8 @@
 public class SalesLedgerDto {
     private Long id;
     private String salesContractNo;
+    private Integer contractType = 1;
+    private Long parentContractId;
     private String customerContractNo;
     private String projectName;
     @JsonFormat(pattern = "yyyy-MM-dd", timezone = "GMT+8")
diff --git a/src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java b/src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java
index 5518290..899a15d 100644
--- a/src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java
+++ b/src/main/java/com/ruoyi/sales/mapper/SalesLedgerMapper.java
@@ -90,4 +90,14 @@
 
     IPage<PurchaseReportVo> selectPurchaseReportVoPage(Page page, @Param("customerName") String customerName);
 
+    /**
+     * 鏌ヨ妗嗘灦鍚堝悓涓嬬殑妗嗘灦璁㈠崟鍒楄〃锛屽苟鍏宠仈姹囨�诲凡鍙戣揣鏁伴噺
+     */
+    List<com.ruoyi.sales.vo.FrameworkOrderVo> listFrameworkOrders(@Param("parentContractId") Long parentContractId);
+
+    /**
+     * 鎸夋鏋跺悎鍚岀粺璁$疮璁″彂璐ф暟閲�
+     */
+    BigDecimal sumShippedByParentContract(@Param("parentContractId") Long parentContractId);
+
 }
diff --git a/src/main/java/com/ruoyi/sales/pojo/SalesLedger.java b/src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
index c8a0443..9561391 100644
--- a/src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
+++ b/src/main/java/com/ruoyi/sales/pojo/SalesLedger.java
@@ -35,6 +35,16 @@
     private String salesContractNo;
 
     /**
+     * 鍚堝悓绫诲瀷锛�1鏅�氶攢鍞悎鍚岋紝2妗嗘灦鍚堝悓锛�3妗嗘灦璁㈠崟
+     */
+    private Integer contractType = 1;
+
+    /**
+     * 鐖舵鏋跺悎鍚孖D
+     */
+    private Long parentContractId;
+
+    /**
      * 瀹㈡埛鍚堝悓鍙�
      */
     @Excel(name = "瀹㈡埛鍚堝悓鍙�", type = Excel.Type.IMPORT)
diff --git a/src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java b/src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
index 9bcbd07..9f050df 100644
--- a/src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
+++ b/src/main/java/com/ruoyi/sales/pojo/SalesLedgerProduct.java
@@ -39,6 +39,17 @@
     private Long salesLedgerId;
 
     /**
+     * 鐖舵鏋跺悎鍚屼骇鍝両D
+     */
+    private Long parentProductId;
+
+    /**
+     * 妗嗘灦璁㈠崟浜よ揣鏃ユ湡
+     */
+    @JsonFormat(pattern = "yyyy-MM-dd")
+    private java.time.LocalDate deliveryDate;
+
+    /**
      * 棰勮鏁伴噺
      */
     private BigDecimal warnNum;
diff --git a/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java b/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
index 539e67c..374ea54 100644
--- a/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
+++ b/src/main/java/com/ruoyi/sales/service/ISalesLedgerService.java
@@ -11,6 +11,7 @@
 import com.ruoyi.sales.dto.SalesLedgerDto;
 import com.ruoyi.sales.pojo.SalesLedger;
 import com.ruoyi.sales.pojo.SalesLedgerProduct;
+import com.ruoyi.sales.vo.FrameworkOrderVo;
 import com.ruoyi.sales.vo.SalesLedgerVo;
 import org.springframework.web.multipart.MultipartFile;
 
@@ -33,6 +34,12 @@
 
     int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto);
 
+    int createFrameworkOrder(SalesLedgerDto salesLedgerDto);
+
+    List<FrameworkOrderVo> listFrameworkOrders(Long parentContractId);
+
+    BigDecimal getFrameworkContractShippedQuantity(Long parentContractId);
+
     List<SalesLedgerProduct> getSalesLedgerProductListByRelateId(@NotNull Long relateId,@NotNull SaleEnum type);
     List<SalesLedgerProduct> getSalesLedgerProductListByIds(@Nullable List<Long> relateIds, @NotNull SaleEnum type);
 
diff --git a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
index 171f5ae..55cb1d4 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerProductServiceImpl.java
@@ -2,7 +2,9 @@
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
+import com.baomidou.mybatisplus.core.toolkit.Wrappers;
 import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
 import com.ruoyi.framework.web.domain.R;
 import com.ruoyi.procurementrecord.utils.StockUtils;
@@ -31,9 +33,9 @@
 import org.springframework.transaction.annotation.Transactional;
 import org.springframework.util.CollectionUtils;
 
-import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
+import java.time.LocalDate;
 import java.time.LocalDateTime;
 import java.time.format.DateTimeFormatter;
 import java.util.Arrays;
@@ -227,22 +229,34 @@
      * 鏂板鐢熶骇鏁版嵁
      */
     public void addProductionData(SalesLedgerProduct salesLedgerProduct) {
-        //鍏堝垽鏂浜у搧鏄惁闇�瑕佺敓浜�
-        if (!BooleanUtils.isTrue(salesLedgerProduct.getIsProduction())) {
+        SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerProduct.getSalesLedgerId());
+        if (salesLedger == null) {
             return;
         }
-        SalesLedger salesLedger = salesLedgerMapper.selectById(salesLedgerProduct.getSalesLedgerId());
+        boolean isFrameworkOrder = Objects.equals(salesLedger.getContractType(), 3);
+        // 妗嗘灦鍚堝悓涓嶇敓鎴愮敓浜ц鍒掞紱妗嗘灦璁㈠崟鍥哄畾鐢熸垚锛堟暟閲忕暀绌猴紝涓嬪彂鏃跺~鍐欙級
+        if (Objects.equals(salesLedger.getContractType(), 2)) {
+            return;
+        }
+        if (!isFrameworkOrder && !BooleanUtils.isTrue(salesLedgerProduct.getIsProduction())) {
+            return;
+        }
         ProductionPlan productionPlan = new ProductionPlan();
         productionPlan.setSalesLedgerId(salesLedgerProduct.getSalesLedgerId());
         productionPlan.setSalesLedgerProductId(salesLedgerProduct.getId());
-        productionPlan.setMpsNo(generateNextPlanNo(com.ruoyi.common.utils.DateUtils.toLocalDate(salesLedger.getEntryDate())
-                .format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
+        // 璁″垝鍗曞彿鏃ユ湡鍓嶇紑锛氱櫥璁版棩鏈熶负绌烘椂鍙栧綋鍓嶆棩鏈�
+        java.time.LocalDate planDate = salesLedger.getEntryDate() != null
+                ? com.ruoyi.common.utils.DateUtils.toLocalDate(salesLedger.getEntryDate())
+                : java.time.LocalDate.now();
+        productionPlan.setMpsNo(generateNextPlanNo(planDate.format(DateTimeFormatter.ofPattern("yyyyMMdd"))));
         productionPlan.setProductModelId(salesLedgerProduct.getProductModelId());
         productionPlan.setQtyRequired(salesLedgerProduct.getQuantity());
         productionPlan.setSource("閿�鍞�");
         productionPlan.setStatus(0);
-        productionPlan.setRequiredDate(salesLedger.getDeliveryDate());//闇�姹傛棩鏈�=浜よ揣鏃ユ湡
-        productionPlan.setPromisedDeliveryDate(salesLedger.getDeliveryDate());//鎵胯鏃ユ湡=浜よ揣鏃ユ湡
+        // 闇�姹傛棩鏈�=浜よ揣鏃ユ湡锛屾鏋惰鍗曞彇浜у搧浜よ揣鏃ユ湡锛屽叾浣欏彇涓昏〃浜よ揣鏃ユ湡
+        LocalDate deliveryDate = isFrameworkOrder ? salesLedgerProduct.getDeliveryDate() : salesLedger.getDeliveryDate();
+        productionPlan.setRequiredDate(deliveryDate);//闇�姹傛棩鏈�=浜よ揣鏃ユ湡
+        productionPlan.setPromisedDeliveryDate(deliveryDate);//鎵胯鏃ユ湡=浜よ揣鏃ユ湡
         productionPlanMapper.insert(productionPlan);
 
     }
@@ -287,20 +301,11 @@
                 .filter(Objects::nonNull)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
 
-        try {
-            S entity = mainEntityClass.getDeclaredConstructor().newInstance();
-            Field idField = mainEntityClass.getDeclaredField("id");
-            idField.setAccessible(true);
-            idField.set(entity, mainId);
-
-            Field amountField = mainEntityClass.getDeclaredField("contractAmount");
-            amountField.setAccessible(true);
-            amountField.set(entity, totalAmount);
-
-            mainMapper.updateById(entity);
-        } catch (Exception e) {
-            throw new RuntimeException("鍔ㄦ�佹洿鏂颁富琛ㄩ噾棰濆け璐�", e);
-        }
+        // 浠呮洿鏂伴噾棰濆垪锛岄伩鍏嶆暣瀹炰綋 updateById 鎶婂叾瀹冮潪绌哄瓧娈碉紙濡� contract_type 榛樿鍊� 1锛変竴骞惰鐩�
+        UpdateWrapper<S> wrapper = Wrappers.update();
+        wrapper.eq("id", mainId)
+                .set("contract_amount", totalAmount);
+        mainMapper.update(null, wrapper);
     }
     @Override
     public R judgmentInventory(SalesLedgerProduct salesLedgerProduct) {
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 00e913f..3f223ca 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/SalesLedgerServiceImpl.java
@@ -1,6 +1,7 @@
 package com.ruoyi.sales.service.impl;
 
 import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
+import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
 import com.baomidou.mybatisplus.core.mapper.BaseMapper;
 import com.baomidou.mybatisplus.core.metadata.IPage;
 import com.baomidou.mybatisplus.core.toolkit.CollectionUtils;
@@ -49,6 +50,7 @@
 import com.ruoyi.sales.pojo.SalesLedgerProduct;
 import com.ruoyi.sales.pojo.ShippingInfo;
 import com.ruoyi.sales.service.ISalesLedgerService;
+import com.ruoyi.sales.vo.FrameworkOrderVo;
 import com.ruoyi.sales.vo.SalesLedgerVo;
 import lombok.RequiredArgsConstructor;
 import lombok.extern.slf4j.Slf4j;
@@ -64,7 +66,6 @@
 import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
 
 import java.io.InputStream;
-import java.lang.reflect.Field;
 import java.math.BigDecimal;
 import java.math.RoundingMode;
 import java.time.LocalDate;
@@ -421,8 +422,7 @@
                     salesLedgerProduct.setApproveStatus(0);
                     salesLedgerProduct.setIsProduction(salesLedgerProductImportDto.getIsProduction() == 1);
                     salesLedgerProductMapper.insert(salesLedgerProduct);
-                    // 娣诲姞鐢熶骇鏁版嵁
-                    salesLedgerProductServiceImpl.addProductionData(salesLedgerProduct);
+                    addProductionDataIfNeeded(salesLedgerProduct);
                 }
             }
 
@@ -508,6 +508,11 @@
         if (CollectionUtils.isEmpty(idList)) {
             return 0;
         }
+        long childCount = salesLedgerMapper.selectCount(new LambdaQueryWrapper<SalesLedger>()
+                .in(SalesLedger::getParentContractId, idList));
+        if (childCount > 0) {
+            throw new BaseException("妗嗘灦鍚堝悓瀛樺湪妗嗘灦璁㈠崟锛屼笉鑳藉垹闄�");
+        }
         // 鍒犻櫎閿�鍞鐞嗘暟鎹�
         LambdaQueryWrapper<SalesLedgerProduct> queryWrapper = new LambdaQueryWrapper<>();
         queryWrapper.in(SalesLedgerProduct::getSalesLedgerId, idList)
@@ -551,6 +556,10 @@
     @Override
     @Transactional(rollbackFor = Exception.class)
     public int addOrUpdateSalesLedger(SalesLedgerDto salesLedgerDto) {
+        log.info("addOrUpdateSalesLedger 鏀跺埌 contractType={}, parentContractId={}, id={}",
+                salesLedgerDto == null ? null : salesLedgerDto.getContractType(),
+                salesLedgerDto == null ? null : salesLedgerDto.getParentContractId(),
+                salesLedgerDto == null ? null : salesLedgerDto.getId());
         // 1. 鏍¢獙瀹㈡埛淇℃伅
         Customer customer = customerMapper.selectById(salesLedgerDto.getCustomerId());
         if (customer == null) {
@@ -559,6 +568,7 @@
 
         // 2. DTO杞珽ntity
         SalesLedger salesLedger = convertToEntity(salesLedgerDto);
+        log.info("convertToEntity 鍚� contractType={}", salesLedger.getContractType());
         salesLedger.setCustomerName(customer.getCustomerName());
         salesLedger.setTenantId(customer.getTenantId());
         // 褰曞叆浜猴細濡傛灉鏈紶鍒欎娇鐢ㄥ綋鍓嶇櫥褰曠敤鎴�
@@ -576,6 +586,9 @@
         } else {
             salesLedgerMapper.updateById(salesLedger);
         }
+        // 璇婃柇锛歩nsert/update 鍚庡洖鏌ユ暟鎹簱瀹為檯鍐欏叆鐨� contract_type
+        SalesLedger dbCheckLedger = salesLedgerMapper.selectById(salesLedger.getId());
+        log.info("DB 瀹為檯 contract_type={}, id={}", dbCheckLedger == null ? null : dbCheckLedger.getContractType(), salesLedger.getId());
 
 
         // 4. 澶勭悊瀛愯〃鏁版嵁
@@ -609,6 +622,86 @@
     }
 
     @Override
+    @Transactional(rollbackFor = Exception.class)
+    public int createFrameworkOrder(SalesLedgerDto dto) {
+        if (dto == null || dto.getId() == null) {
+            throw new BaseException("璇烽�夋嫨妗嗘灦鍚堝悓");
+        }
+        SalesLedger parent = salesLedgerMapper.selectById(dto.getId());
+        if (parent == null || !Objects.equals(parent.getContractType(), 2)) {
+            throw new BaseException("鐖跺彴璐︿笉鏄鏋跺悎鍚�");
+        }
+        if (CollectionUtils.isEmpty(dto.getProductData())) {
+            throw new BaseException("璇峰~鍐欐鏋惰鍗曚骇鍝�");
+        }
+        SalesLedger order = new SalesLedger();
+        order.setCustomerContractNo(parent.getCustomerContractNo());
+        order.setProjectName(parent.getProjectName());
+        order.setSalesman(parent.getSalesman());
+        order.setCustomerId(parent.getCustomerId());
+        order.setCustomerName(parent.getCustomerName());
+        order.setEntryPerson(String.valueOf(SecurityUtils.getUserId()));
+        order.setRemarks(dto.getRemarks());
+        order.setTenantId(parent.getTenantId());
+        order.setExecutionDate(parent.getExecutionDate());
+        order.setPaymentMethod(parent.getPaymentMethod());
+        order.setEntryDate(parent.getEntryDate());
+        order.setId(null);
+        order.setContractType(3);
+        order.setParentContractId(parent.getId());
+        order.setSalesContractNo(generateSalesContractNo(new Date()));
+        order.setDeliveryDate(null);
+        order.setContractAmount(BigDecimal.ZERO);
+        order.setNetContractAmount(BigDecimal.ZERO);
+        salesLedgerMapper.insert(order);
+
+        Set<Long> parentProductIds = dto.getProductData().stream()
+                .map(SalesLedgerProduct::getParentProductId)
+                .collect(Collectors.toSet());
+        if (parentProductIds.contains(null) || parentProductIds.size() != dto.getProductData().size()) {
+            throw new BaseException("妗嗘灦璁㈠崟浜у搧蹇呴』鍏宠仈涓嶅悓鐨勭埗浜у搧");
+        }
+        Map<Long, SalesLedgerProduct> parentProducts = salesLedgerProductMapper.selectBatchIds(parentProductIds).stream()
+                .collect(Collectors.toMap(SalesLedgerProduct::getId, Function.identity()));
+        for (SalesLedgerProduct requestProduct : dto.getProductData()) {
+            if (requestProduct.getDeliveryDate() == null) {
+                throw new BaseException("妗嗘灦璁㈠崟浜у搧蹇呴』鍏宠仈鐖朵骇鍝佸苟濉啓浜よ揣鏃ユ湡");
+            }
+            SalesLedgerProduct parentProduct = parentProducts.get(requestProduct.getParentProductId());
+            if (parentProduct == null || !Objects.equals(parentProduct.getSalesLedgerId(), parent.getId())
+                    || !Objects.equals(parentProduct.getType(), SaleEnum.SALE.getCode())) {
+                throw new BaseException("妗嗘灦璁㈠崟浜у搧涓嶅睘浜庤妗嗘灦鍚堝悓");
+            }
+            SalesLedgerProduct product = new SalesLedgerProduct();
+            BeanUtils.copyProperties(parentProduct, product);
+            product.setId(null);
+            product.setSalesLedgerId(order.getId());
+            product.setParentProductId(parentProduct.getId());
+            product.setQuantity(null);
+            product.setDeliveryDate(requestProduct.getDeliveryDate());
+            product.setIsProduction(false);
+            product.setType(SaleEnum.SALE.getCode());
+            if (product.getTaxRate() == null) {
+                product.setTaxRate(BigDecimal.ZERO);
+            }
+            salesLedgerProductMapper.insert(product);
+            // 妗嗘灦璁㈠崟鍒涘缓鏃剁敓鎴愮敓浜ц鍒掞紙鏁伴噺鐣欑┖锛屼笅鍙戞椂濉啓锛�
+            salesLedgerProductServiceImpl.addProductionData(product);
+        }
+        return 1;
+    }
+
+    @Override
+    public List<FrameworkOrderVo> listFrameworkOrders(Long parentContractId) {
+        return salesLedgerMapper.listFrameworkOrders(parentContractId);
+    }
+
+    @Override
+    public BigDecimal getFrameworkContractShippedQuantity(Long parentContractId) {
+        return salesLedgerMapper.sumShippedByParentContract(parentContractId);
+    }
+
+    @Override
     public void handleSalesLedgerProducts(Long salesLedgerId, List<SalesLedgerProduct> products, SaleEnum type) {
         // 鎸塈D鍒嗙粍锛屽尯鍒嗘柊澧炲拰鏇存柊鐨勮褰�
         Map<Boolean, List<SalesLedgerProduct>> partitionedProducts = products.stream()
@@ -629,10 +722,20 @@
         if (!insertList.isEmpty()) {
             for (SalesLedgerProduct salesLedgerProduct : insertList) {
                 salesLedgerProduct.setType(type.getCode());
+                if (salesLedgerProduct.getTaxRate() == null) {
+                    salesLedgerProduct.setTaxRate(BigDecimal.ZERO);
+                }
                 salesLedgerProductMapper.insert(salesLedgerProduct);
-                // 娣诲姞鐢熶骇鏁版嵁
-                salesLedgerProductServiceImpl.addProductionData(salesLedgerProduct);
+                addProductionDataIfNeeded(salesLedgerProduct);
             }
+        }
+    }
+
+    private void addProductionDataIfNeeded(SalesLedgerProduct product) {
+        SalesLedger ledger = salesLedgerMapper.selectById(product.getSalesLedgerId());
+        if (ledger != null && !Objects.equals(ledger.getContractType(), 2)
+                && !Objects.equals(ledger.getContractType(), 3)) {
+            salesLedgerProductServiceImpl.addProductionData(product);
         }
     }
 
@@ -729,30 +832,22 @@
                 .filter(Objects::nonNull)
                 .reduce(BigDecimal.ZERO, BigDecimal::add);
 
-        // 鏋勯�犱富琛ㄦ洿鏂板璞�
+        // 浠呮洿鏂伴噾棰濆垪锛岄伩鍏嶆暣瀹炰綋 updateById 鎶婂叾瀹冮潪绌哄瓧娈碉紙濡� contract_type 榛樿鍊� 1锛変竴骞惰鐩�
+        UpdateWrapper<S> wrapper = Wrappers.update();
+        wrapper.eq("id", mainId)
+                .set("contract_amount", totalAmount);
+        // 瀹為檯鍚堝悓閲戦鍒濆鍊肩瓑浜庡悎鍚岄噾棰�
+        boolean hasNetAmount;
         try {
-            S entity = mainEntityClass.getDeclaredConstructor().newInstance();
-            Field idField = mainEntityClass.getDeclaredField("id");
-            idField.setAccessible(true);
-            idField.set(entity, mainId);
-
-            Field amountField = mainEntityClass.getDeclaredField("contractAmount");
-            amountField.setAccessible(true);
-            amountField.set(entity, totalAmount);
-
-            // 瀹為檯鍚堝悓閲戦鍒濆鍊肩瓑浜庡悎鍚岄噾棰�
-            try {
-                Field netAmountField = mainEntityClass.getDeclaredField("netContractAmount");
-                netAmountField.setAccessible(true);
-                netAmountField.set(entity, totalAmount);
-            } catch (NoSuchFieldException ignored) {
-                // 瀛愮被鍙兘娌℃湁璇ュ瓧娈碉紝蹇界暐
-            }
-
-            mainMapper.updateById(entity);
-        } catch (Exception e) {
-            throw new RuntimeException("鍔ㄦ�佹洿鏂颁富琛ㄩ噾棰濆け璐�", e);
+            mainEntityClass.getDeclaredField("netContractAmount");
+            hasNetAmount = true;
+        } catch (NoSuchFieldException e) {
+            hasNetAmount = false;
         }
+        if (hasNetAmount) {
+            wrapper.set("net_contract_amount", totalAmount);
+        }
+        mainMapper.update(null, wrapper);
     }
 
     // 鍐呴儴绫荤敤浜庡瓨鍌ㄨ仛鍚堢粨鏋�
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 be34bc0..82049ab 100644
--- a/src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
+++ b/src/main/java/com/ruoyi/sales/service/impl/ShippingInfoServiceImpl.java
@@ -23,9 +23,12 @@
 import com.ruoyi.sales.dto.ShippingApproveDto;
 import com.ruoyi.sales.dto.ShippingInfoDto;
 import com.ruoyi.sales.dto.ShippingProductDetailDto;
+import com.ruoyi.sales.mapper.SalesLedgerMapper;
 import com.ruoyi.sales.mapper.SalesLedgerProductMapper;
 import com.ruoyi.sales.mapper.ShippingInfoMapper;
 import com.ruoyi.sales.mapper.ShippingProductDetailMapper;
+import com.ruoyi.sales.pojo.SalesLedger;
+import com.ruoyi.sales.pojo.SalesLedgerProduct;
 import com.ruoyi.sales.pojo.ShippingInfo;
 import com.ruoyi.sales.pojo.ShippingProductDetail;
 import com.ruoyi.sales.service.ShippingInfoService;
@@ -52,6 +55,8 @@
 
 
     private final SalesLedgerProductMapper salesLedgerProductMapper;
+
+    private final SalesLedgerMapper salesLedgerMapper;
 
     private final StockUtils stockUtils;
 
@@ -172,9 +177,25 @@
     @Override
     public boolean addReq(ShippingInfoDto req) {
 
-        // 鏍¢獙璇ヤ骇鍝佹槸鍚﹀凡鏈夊緟瀹℃牳/瀹℃牳涓殑鍙戣揣璁板綍锛岄槻姝㈤噸澶嶆彁浜�
+        // 妗嗘灦鍚堝悓鍙敤浜庣櫥璁颁骇鍝佽寖鍥达紝瀹為檯鍙戣揣蹇呴』鍏宠仈妗嗘灦璁㈠崟浜у搧銆�
         Long salesLedgerProductId = req.getSalesLedgerProductId();
         if (salesLedgerProductId != null) {
+            SalesLedgerProduct product = salesLedgerProductMapper.selectById(salesLedgerProductId);
+            if (product == null) {
+                throw new RuntimeException("閿�鍞骇鍝佷笉瀛樺湪");
+            }
+            if (req.getSalesLedgerId() != null
+                    && !java.util.Objects.equals(req.getSalesLedgerId(), product.getSalesLedgerId())) {
+                throw new RuntimeException("鍙戣揣鍙拌处涓庨攢鍞骇鍝佷笉鍖归厤");
+            }
+            SalesLedger ledger = salesLedgerMapper.selectById(product.getSalesLedgerId());
+            if (ledger == null || (!java.util.Objects.equals(ledger.getContractType(), 1)
+                    && !java.util.Objects.equals(ledger.getContractType(), 3))) {
+                throw new RuntimeException("璇ラ攢鍞骇鍝佷笉鍏佽鍙戣揣");
+            }
+        }
+        // 鏍¢獙璇ヤ骇鍝佹槸鍚﹀凡鏈夊緟瀹℃牳/瀹℃牳涓殑鍙戣揣璁板綍锛岄槻姝㈤噸澶嶆彁浜�
+        if (salesLedgerProductId != null) {
             long pendingCount = this.count(new LambdaQueryWrapper<ShippingInfo>()
                     .eq(ShippingInfo::getSalesLedgerProductId, salesLedgerProductId)
                     .in(ShippingInfo::getStatus, "寰呭鏍�", "瀹℃牳涓�"));
diff --git a/src/main/java/com/ruoyi/sales/vo/FrameworkOrderVo.java b/src/main/java/com/ruoyi/sales/vo/FrameworkOrderVo.java
new file mode 100644
index 0000000..ea629c8
--- /dev/null
+++ b/src/main/java/com/ruoyi/sales/vo/FrameworkOrderVo.java
@@ -0,0 +1,17 @@
+package com.ruoyi.sales.vo;
+
+import com.ruoyi.sales.pojo.SalesLedger;
+import lombok.Data;
+
+import java.math.BigDecimal;
+
+/**
+ * 妗嗘灦鍚堝悓涓嬬殑妗嗘灦璁㈠崟鍒楄〃鍙婃眹鎬�
+ */
+@Data
+public class FrameworkOrderVo extends SalesLedger {
+
+    private BigDecimal shippedQuantity;
+
+    private BigDecimal orderedQuantity;
+}
\ No newline at end of file
diff --git a/src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java b/src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java
index fb932f8..d22d35b 100644
--- a/src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java
+++ b/src/main/java/com/ruoyi/sales/vo/SalesLedgerVo.java
@@ -4,10 +4,15 @@
 import com.ruoyi.sales.pojo.SalesLedger;
 import lombok.Data;
 
+import java.math.BigDecimal;
 import java.util.List;
 
 @Data
 public class SalesLedgerVo extends SalesLedger {
 
     private List<StorageBlobVO> storageBlobVOs;
+
+    private BigDecimal shippedQuantity;
+
+    private Integer frameworkOrderCount;
 }
diff --git a/src/main/resources/mapper/device/DeviceLedgerMapper.xml b/src/main/resources/mapper/device/DeviceLedgerMapper.xml
index f3b674e..b4075df 100644
--- a/src/main/resources/mapper/device/DeviceLedgerMapper.xml
+++ b/src/main/resources/mapper/device/DeviceLedgerMapper.xml
@@ -31,9 +31,12 @@
         dl.tenant_id,
         dl.is_depr,
         dl.annual_depreciation_amount,
-        dl.type
+        dl.type,
+        dl.area_id,
+        da.name AS area_name
         FROM device_ledger dl
         left join sys_user su on dl.create_user = su.user_id
+        left join device_area da on dl.area_id = da.id
         <where>
             <!-- 璁惧鍚嶇О -->
             <if test="deviceLedger.deviceName != null and deviceLedger.deviceName != ''">
@@ -76,9 +79,53 @@
             <if test="deviceLedger.tenantId != null">
                 AND tenant_id = #{deviceLedger.tenantId}
             </if>
+
+            <!-- 鍖哄煙ID锛氬惈璇ュ尯鍩熷強鍏舵墍鏈夊瓙绾э紙閫氳繃绁栫骇璺緞鍖归厤锛� -->
+            <if test="deviceLedger.areaId != null">
+                AND (
+                    dl.area_id = #{deviceLedger.areaId}
+                    OR EXISTS (
+                        SELECT 1 FROM device_area da2
+                        WHERE da2.id = dl.area_id
+                          AND (da2.ancestors = #{deviceLedger.areaId}
+                               OR da2.ancestors LIKE CONCAT(#{deviceLedger.areaId}, ',%')
+                               OR da2.ancestors LIKE CONCAT('%,', #{deviceLedger.areaId}, ',%')
+                               OR da2.ancestors LIKE CONCAT('%,', #{deviceLedger.areaId}))
+                    )
+                )
+            </if>
         </where>
         ORDER BY create_time DESC
     </select>
+    <select id="selectDeviceOptions" resultType="com.ruoyi.device.dto.DeviceLedgerDto">
+        SELECT
+        dl.id,
+        dl.device_name,
+        dl.device_model,
+        dl.device_brand,
+        dl.type,
+        dl.area_id,
+        da.name AS area_name
+        FROM device_ledger dl
+        left join device_area da on dl.area_id = da.id
+        <where>
+            <if test="areaId != null">
+                AND (
+                    dl.area_id = #{areaId}
+                    OR EXISTS (
+                        SELECT 1 FROM device_area da2
+                        WHERE da2.id = dl.area_id
+                          AND (da2.ancestors = #{areaId}
+                               OR da2.ancestors LIKE CONCAT(#{areaId}, ',%')
+                               OR da2.ancestors LIKE CONCAT('%,', #{areaId}, ',%')
+                               OR da2.ancestors LIKE CONCAT('%,', #{areaId}))
+                    )
+                )
+            </if>
+        </where>
+        ORDER BY dl.create_time DESC
+    </select>
+
     <select id="deviceLedgerExportList" resultType="com.ruoyi.device.execl.DeviceLedgerExeclDto">
 
     </select>
diff --git a/src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml b/src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml
index 7de15bd..88db470 100644
--- a/src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml
+++ b/src/main/resources/mapper/measuringinstrumentledger/MeasuringInstrumentLedgerMapper.xml
@@ -64,4 +64,14 @@
         </where>
         ORDER BY create_time DESC
     </select>
+
+    <select id="selectExistingCodes" resultType="java.lang.String">
+        SELECT code
+        FROM measuring_instrument_ledger
+        WHERE tenant_id = #{tenantId}
+        AND code IN
+        <foreach collection="codes" item="code" open="(" separator="," close=")">
+            #{code}
+        </foreach>
+    </select>
 </mapper>
diff --git a/src/main/resources/mapper/sales/SalesLedgerMapper.xml b/src/main/resources/mapper/sales/SalesLedgerMapper.xml
index 2d6b7f8..48632ed 100644
--- a/src/main/resources/mapper/sales/SalesLedgerMapper.xml
+++ b/src/main/resources/mapper/sales/SalesLedgerMapper.xml
@@ -141,4 +141,28 @@
         order by sl.entry_date desc
     </select>
 
+    <select id="listFrameworkOrders" resultType="com.ruoyi.sales.vo.FrameworkOrderVo">
+        select sl.*, IFNULL(shipped.shipped_quantity, 0) AS shippedQuantity
+        from sales_ledger sl
+        left join (
+            select slp.sales_ledger_id, SUM(spd.quantity) AS shipped_quantity
+            from sales_ledger_product slp
+            inner join shipping_info si on slp.id = si.sales_ledger_product_id and si.status = '宸插彂璐�'
+            inner join shipping_product_detail spd on si.id = spd.shipping_info_id
+            group by slp.sales_ledger_id
+        ) shipped on sl.id = shipped.sales_ledger_id
+        where sl.parent_contract_id = #{parentContractId}
+          and sl.contract_type = 3
+        order by sl.execution_date desc
+    </select>
+
+    <select id="sumShippedByParentContract" resultType="java.math.BigDecimal">
+        select IFNULL(SUM(spd.quantity), 0)
+        from sales_ledger slo
+        inner join sales_ledger_product slp on slo.id = slp.sales_ledger_id
+        inner join shipping_info si on slp.id = si.sales_ledger_product_id and si.status = '宸插彂璐�'
+        inner join shipping_product_detail spd on si.id = spd.shipping_info_id
+        where slo.parent_contract_id = #{parentContractId}
+    </select>
+
 </mapper>

--
Gitblit v1.9.3