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