From b48ed5159987e8cee81bc765010cfd9393ad7003 Mon Sep 17 00:00:00 2001
From: huminmin <mac@MacBook-Pro.local>
Date: 星期四, 13 八月 2026 13:28:09 +0800
Subject: [PATCH] Merge branch 'dev_business' of http://114.132.189.42:9002/r/mom-pro2-after into dev_business
---
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckService.java | 69 ++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java | 3
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckRespVO.java | 74 ++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckSaveReqVO.java | 48 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/meteringcheck/MesDvMeteringCheckDO.java | 73 ++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java | 109 +++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringExpireStatusEnum.java | 61 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java | 45 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindService.java | 17
docs/mes_dv_metering_frontend_integration.md | 116 +++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/MesDvMeteringCheckController.java | 130 +++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java | 104 +++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/meteringcheck/MesDvMeteringCheckMapper.java | 43 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringPageReqVO.java | 47 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java | 54 +
sql/mysql/mes_dv_metering_init.sql | 119 +++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java | 181 +++++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringRespVO.java | 103 +++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java | 145 ++++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringStatusEnum.java | 38 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckServiceImpl.java | 165 ++++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java | 9
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java | 16
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringCheckResultEnum.java | 37 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringSaveReqVO.java | 61 +
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/metering/MesDvMeteringDO.java | 100 ++
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckPageReqVO.java | 29
27 files changed, 1,996 insertions(+), 0 deletions(-)
diff --git a/docs/mes_dv_metering_frontend_integration.md b/docs/mes_dv_metering_frontend_integration.md
new file mode 100644
index 0000000..75dddc8
--- /dev/null
+++ b/docs/mes_dv_metering_frontend_integration.md
@@ -0,0 +1,116 @@
+# MES 璁¢噺鍣ㄥ叿涓庤閲忔娴嬭褰� - 鍓嶇鑱旇皟鏂规
+
+## 娑夊強椤甸潰
+
+| 椤甸潰 | 鑿滃崟璺緞 | 璺敱璺緞 | component_name |
+|------|----------|----------|----------------|
+| 璁¢噺鍣ㄥ叿 | 璁惧绠$悊 -> 璁¢噺鍣ㄥ叿 | `mes/dv/metering/index` | MesDvMetering |
+| 璁¢噺妫�娴嬭褰� | 璁惧绠$悊 -> 璁¢噺妫�娴嬭褰� | `mes/dv/meteringcheck/index` | MesDvMeteringCheck |
+
+> 涓や釜椤甸潰涓哄钩绾ц彍鍗曘�傛娴嬭褰曢〉鍙�氳繃銆岃閲忓櫒鍏枫�嶄笅鎷夌瓫閫夛紝浠呮煡鐪嬫煇鍙板櫒鍏风殑妫�娴嬪巻鍙层��
+
+## 涓氬姟娴佺▼涓庢暟鎹甫鍏�
+
+1. 璁¢噺鍣ㄥ叿鍒楄〃 -> 鏂板/缂栬緫琛ㄥ崟锛氬綍鍏ュ櫒鍏风紪鐮併�佸悕绉般�佺被鍒�佹牎鍑嗗懆鏈熴�佷娇鐢ㄩ儴闂ㄣ�佽矗浠讳汉绛夈��
+2. 璁¢噺妫�娴嬭褰� -> 鏂板/缂栬緫琛ㄥ崟锛氶�夋嫨銆岃閲忓櫒鍏枫�嶏紙浠� `/mes/dv/metering/simple-list` 甯﹀叆 `meteringId`銆乣code`銆乣name`锛夈��
+3. 妫�娴嬭褰曚繚瀛樺悗锛屽悗绔嚜鍔ㄨ绠� `nextCheckDate` 骞�**鍥炲啓**鍒拌閲忓櫒鍏风殑 `lastCheckDate`銆乣nextCheckDate`锛�
+ - `nextCheckDate` 浼犵┖鏃� = `checkDate + 鏍″噯鍛ㄦ湡(鏈�)`锛堣嚜鍔ㄨ绠楋級锛�
+ - `nextCheckDate` 浼犲�兼椂 = 浠ヤ紶鍏ュ�间负鍑嗭紙鎵嬪姩鎸囧畾锛夈��
+4. 璁¢噺鍣ㄥ叿鍒楄〃灞曠ず `expireStatus` 鍒版湡鐘舵�佹爣璁帮紝鐢卞悗绔寜 `nextCheckDate` 涓庡叏灞�閰嶇疆鐨勬彁閱掑ぉ鏁拌绠椼��
+
+## API
+
+### 璁¢噺鍣ㄥ叿
+
+| 鏂规硶 | 璺緞 | 璇存槑 | 鏉冮檺 |
+|------|------|------|------|
+| POST | /mes/dv/metering/create | 鍒涘缓璁¢噺鍣ㄥ叿 | mes:dv-metering:create |
+| PUT | /mes/dv/metering/update | 鏇存柊璁¢噺鍣ㄥ叿 | mes:dv-metering:update |
+| DELETE | /mes/dv/metering/delete?id= | 鍒犻櫎璁¢噺鍣ㄥ叿锛堟湁鍏宠仈妫�娴嬭褰曟椂鎷掔粷锛� | mes:dv-metering:delete |
+| GET | /mes/dv/metering/get?id= | 鑾峰緱璁¢噺鍣ㄥ叿璇︽儏 | mes:dv-metering:query |
+| GET | /mes/dv/metering/page | 璁¢噺鍣ㄥ叿鍒嗛〉 | mes:dv-metering:query |
+| GET | /mes/dv/metering/export-excel | 瀵煎嚭 Excel | mes:dv-metering:export |
+| GET | /mes/dv/metering/simple-list | 绮剧畝鍒楄〃锛堜笅鎷夐�夐」锛� | mes:dv-metering:query |
+
+**鍒嗛〉绛涢�夊弬鏁帮紙page 鎺ュ彛锛夛細**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| code | String | 鍚� | 鍣ㄥ叿缂栫爜妯$硦 |
+| name | String | 鍚� | 鍣ㄥ叿鍚嶇О妯$硦 |
+| brand | String | 鍚� | 鍝佺墝/鐢熶骇鍘傚妯$硦 |
+| category | Integer | 鍚� | 鍣ㄥ叿绫诲埆锛堝瓧鍏革級 |
+| deptId | Long | 鍚� | 浣跨敤閮ㄩ棬 |
+| chargeUserId | Long | 鍚� | 璐d换浜� |
+| status | Integer | 鍚� | 鐘舵�侊紙瀛楀吀锛� |
+| expireStatus | Integer | 鍚� | 鍒版湡鐘舵�侊細1 宸茶繃鏈� / 2 鍗冲皢鍒版湡 / 3 姝e父 |
+
+### 璁¢噺妫�娴嬭褰�
+
+| 鏂规硶 | 璺緞 | 璇存槑 | 鏉冮檺 |
+|------|------|------|------|
+| POST | /mes/dv/metering-check/create | 鍒涘缓妫�娴嬭褰� | mes:dv-metering-check:create |
+| PUT | /mes/dv/metering-check/update | 鏇存柊妫�娴嬭褰� | mes:dv-metering-check:update |
+| DELETE | /mes/dv/metering-check/delete?id= | 鍒犻櫎妫�娴嬭褰� | mes:dv-metering-check:delete |
+| GET | /mes/dv/metering-check/get?id= | 鑾峰緱妫�娴嬭褰曡鎯� | mes:dv-metering-check:query |
+| GET | /mes/dv/metering-check/page | 妫�娴嬭褰曞垎椤� | mes:dv-metering-check:query |
+| GET | /mes/dv/metering-check/export-excel | 瀵煎嚭 Excel | mes:dv-metering-check:export |
+
+**鍒嗛〉绛涢�夊弬鏁帮紙page 鎺ュ彛锛夛細**
+
+| 鍙傛暟 | 绫诲瀷 | 蹇呭~ | 璇存槑 |
+|------|------|------|------|
+| meteringId | Long | 鍚� | 璁¢噺鍣ㄥ叿锛堜粠鍣ㄥ叿鍒楄〃甯﹀叆锛� |
+| checkCode | String | 鍚� | 妫�娴嬬紪鍙锋ā绯� |
+| checkResult | Integer | 鍚� | 妫�娴嬬粨璁猴紙瀛楀吀锛� |
+| checkDate | LocalDate[] | 鍚� | 妫�娴嬫棩鏈熷尯闂� |
+
+### 鍝嶅簲瀛楁鏂板璇存槑
+
+璁¢噺鍣ㄥ叿鍒楄〃/璇︽儏鍝嶅簲棰濆杩斿洖锛�
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| deptName | String | 浣跨敤閮ㄩ棬鍚嶇О |
+| chargeUserNickname | String | 璐d换浜烘樀绉� |
+| expireStatus | Integer | 鍒版湡鐘舵�侊細1 宸茶繃鏈� / 2 鍗冲皢鍒版湡 / 3 姝e父 |
+| expireDays | Long | 璺濆埌鏈熷ぉ鏁帮紙姝f暟鏈埌銆佽礋鏁板凡杩囨湡锛� |
+
+璁¢噺妫�娴嬭褰曞垪琛�/璇︽儏鍝嶅簲棰濆杩斿洖锛�
+
+| 瀛楁 | 绫诲瀷 | 璇存槑 |
+|------|------|------|
+| meteringCode | String | 璁¢噺鍣ㄥ叿缂栫爜 |
+| meteringName | String | 璁¢噺鍣ㄥ叿鍚嶇О |
+| checkUserNickname | String | 妫�娴嬩汉鏄电О |
+
+## 瀛楁灞曠ず瑙勫垯
+
+| 瀛楁 | 灞曠ず浣嶇疆 | 璇存槑 |
+|------|----------|------|
+| expireStatus | 璁¢噺鍣ㄥ叿鍒楄〃 | 1 宸茶繃鏈�=鍗遍櫓(danger)锛�2 鍗冲皢鍒版湡=璀﹀憡(warning)锛�3 姝e父=鎴愬姛(success) |
+| status | 璁¢噺鍣ㄥ叿鍒楄〃銆佽〃鍗� | 瀛楀吀 mes_dv_metering_status锛堝湪鐢�=success/鍋滅敤=info/鎶ュ簾=danger锛� |
+| category | 璁¢噺鍣ㄥ叿鍒楄〃銆佽〃鍗� | 瀛楀吀 mes_dv_metering_category |
+| nextCheckDate | 涓や釜椤甸潰 | 璁¢噺鍣ㄥ叿灞曠ず涓恒�屼笅娆℃娴嬫棩鏈熴�嶅垪锛涙娴嬭褰曚繚瀛樻椂鍥炲啓 |
+| checkResult | 妫�娴嬭褰曞垪琛ㄣ�佽〃鍗� | 瀛楀吀 mes_dv_metering_check_result锛堝悎鏍�=success/涓嶅悎鏍�=danger锛� |
+| meteringCode/meteringName | 妫�娴嬭褰曞垪琛� | 浠庡櫒鍏峰甫鍏ュ睍绀� |
+| deptName/chargeUserNickname | 璁¢噺鍣ㄥ叿鍒楄〃 | 閮ㄩ棬銆佽矗浠讳汉鍚嶇О |
+
+## 涓氬姟瑙勫垯璇存槑
+
+| 鍦烘櫙 | 瑙勫垯 |
+|------|------|
+| 妫�娴嬭褰曟柊澧� | 鏈~ nextCheckDate 鏃舵寜 `checkDate + 鏍″噯鍛ㄦ湡` 鑷姩璁$畻骞朵繚瀛� |
+| 妫�娴嬭褰曟洿鏂� | 閲嶆柊璁$畻 nextCheckDate锛屽苟鍥炲啓鍣ㄥ叿 |
+| 妫�娴嬭褰曞垹闄� | 鍣ㄥ叿鎸夊墿浣欍�屾渶鏂颁竴鏉¤褰曘�嶆暣浣撻噸绠楀洖鍐欙紙鏃犺褰曞垯 lastCheckDate/nextCheckDate 缃┖锛� |
+| 璁¢噺鍣ㄥ叿鍒犻櫎 | 宸插叧鑱旀娴嬭褰曟椂鎷掔粷鍒犻櫎锛屾彁绀恒�岃閲忓櫒鍏峰凡鍏宠仈妫�娴嬭褰曪紝鏃犳硶鍒犻櫎銆� |
+| 鍒版湡鎻愰啋 | 鍒版湡鍓� N 澶╋紙鍏ㄥ眬閰嶇疆 `mes.dv-metering.remind-days`锛岄粯璁� 30锛夊彂閫佺珯鍐呬俊缁欒矗浠讳汉锛涘悓涓�鍣ㄥ叿鍚屼竴鍒版湡鏃� 60 澶╁唴鍙彁閱掍竴娆� |
+| 缂栫爜/缂栧彿鍞竴 | 鍣ㄥ叿 code銆佹娴� checkCode 鍧囧敮涓�锛岄噸澶嶆椂鍚庣鎶ラ敊 |
+
+## 娉ㄦ剰浜嬮」
+
+- expireStatus 鐢卞悗绔绠楀苟杩斿洖锛屽墠绔彧闇�鎸変笁鎬佹覆鏌� tag 棰滆壊锛屾棤闇�鑷璁$畻澶╂暟銆�
+- 鍒犻櫎鍣ㄥ叿鍓嶉渶纭鏃犲叧鑱旀娴嬭褰曪紱鍒犻櫎妫�娴嬭褰曞悗鍣ㄥ叿鐨勫埌鏈熸棩浼氶殢涔嬪彉鍖栵紝鍓嶇搴斿埛鏂板櫒鍏峰垪琛ㄣ��
+- 妫�娴嬭褰曢〉渚濊禆璁¢噺鍣ㄥ叿鏁版嵁锛屽簲鍏堝瓨鍦ㄥ櫒鍏峰啀鐧昏妫�娴嬭褰曘��
+- 鎻愰啋澶╂暟鍙湪銆屽熀纭�璁炬柦 -> 閰嶇疆绠$悊銆嶄腑淇敼 `mes.dv-metering.remind-days`锛屾敼鍚庣珛鍗崇敓鏁堛��
+- 涓変釜鏂板瓧鍏稿彲鍦ㄣ�岀郴缁熺鐞� -> 瀛楀吀绠$悊銆嶄腑缁存姢锛歮es_dv_metering_category銆乵es_dv_metering_status銆乵es_dv_metering_check_result銆�
diff --git a/sql/mysql/mes_dv_metering_init.sql b/sql/mysql/mes_dv_metering_init.sql
new file mode 100644
index 0000000..0504144
--- /dev/null
+++ b/sql/mysql/mes_dv_metering_init.sql
@@ -0,0 +1,119 @@
+-- ============================================
+-- MES 璁惧绠$悊-璁¢噺鍣ㄥ叿 & 璁¢噺妫�娴嬭褰� 妯″潡鍒濆鍖栬剼鏈�
+-- 1. 寤鸿〃 2. 鑿滃崟 3. 瀛楀吀 4. 绔欏唴淇℃ā鏉� 5. 鍏ㄥ眬閰嶇疆
+-- ============================================
+
+-- 1. 寤鸿〃
+DROP TABLE IF EXISTS `mes_dv_metering_check`;
+DROP TABLE IF EXISTS `mes_dv_metering`;
+CREATE TABLE `mes_dv_metering` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+ `code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '璁¢噺鍣ㄥ叿缂栫爜',
+ `name` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '璁¢噺鍣ㄥ叿鍚嶇О',
+ `specification` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '瑙勬牸鍨嬪彿',
+ `brand` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '鍝佺墝/鐢熶骇鍘傚',
+ `category` int NULL DEFAULT NULL COMMENT '鍣ㄥ叿绫诲埆锛堝崱灏�/鍗冨垎灏�/鍘嬪姏琛ㄧ瓑锛夛紝瀛楀吀 mes_dv_metering_category',
+ `precision` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '绮惧害',
+ `range` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '閲忕▼',
+ `dept_id` bigint NULL DEFAULT NULL COMMENT '浣跨敤閮ㄩ棬缂栧彿',
+ `charge_user_id` bigint NULL DEFAULT NULL COMMENT '璐d换浜虹紪鍙凤紙绔欏唴淇″彂閫佸璞★級',
+ `check_cycle_count` int NOT NULL COMMENT '鏍″噯鍛ㄦ湡锛堟湀锛�',
+ `use_date` date NULL DEFAULT NULL COMMENT '鎶曞叆浣跨敤鏃ユ湡',
+ `last_check_date` date NULL DEFAULT NULL COMMENT '涓婃妫�娴嬫棩鏈�',
+ `next_check_date` date NULL DEFAULT NULL COMMENT '涓嬫妫�娴嬫棩鏈燂紙鍒版湡鏃ワ級',
+ `status` int NOT NULL COMMENT '鐘舵�侊紙鍦ㄧ敤/鍋滅敤/鎶ュ簾锛夛紝瀛楀吀 mes_dv_metering_status',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '澶囨敞',
+
+ -- 鏍囧噯瀛楁
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_code` (`code`),
+ INDEX `idx_dept_id` (`dept_id`),
+ INDEX `idx_charge_user_id` (`charge_user_id`),
+ INDEX `idx_next_check_date` (`next_check_date`)
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES 璁¢噺鍣ㄥ叿' ROW_FORMAT = Dynamic;
+
+CREATE TABLE `mes_dv_metering_check` (
+ `id` bigint NOT NULL AUTO_INCREMENT COMMENT '涓婚敭ID',
+ `metering_id` bigint NOT NULL COMMENT '璁¢噺鍣ㄥ叿ID',
+ `check_code` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '妫�娴嬬紪鍙�',
+ `check_date` date NULL DEFAULT NULL COMMENT '妫�娴嬫棩鏈�',
+ `check_org` varchar(200) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '妫�娴嬫満鏋�',
+ `check_result` int NULL DEFAULT NULL COMMENT '妫�娴嬬粨璁猴紙鍚堟牸/涓嶅悎鏍硷級锛屽瓧鍏� mes_dv_metering_check_result',
+ `cert_no` varchar(100) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '璇佷功缂栧彿',
+ `check_user_id` bigint NULL DEFAULT NULL COMMENT '妫�娴嬩汉缂栧彿',
+ `next_check_date` date NULL DEFAULT NULL COMMENT '涓嬫妫�娴嬫棩鏈燂紙鑷姩璁$畻鎴栨墜鍔ㄦ寚瀹氾級',
+ `remark` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '澶囨敞',
+
+ -- 鏍囧噯瀛楁
+ `creator` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '鍒涘缓鑰�',
+ `create_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '鍒涘缓鏃堕棿',
+ `updater` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT '' COMMENT '鏇存柊鑰�',
+ `update_time` datetime NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '鏇存柊鏃堕棿',
+ `deleted` bit(1) NOT NULL DEFAULT b'0' COMMENT '鏄惁鍒犻櫎',
+ PRIMARY KEY (`id`) USING BTREE,
+ UNIQUE INDEX `uk_check_code` (`check_code`),
+ INDEX `idx_metering_id` (`metering_id`),
+ INDEX `idx_check_date` (`check_date`)
+) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES 璁¢噺妫�娴嬭褰�' ROW_FORMAT = Dynamic;
+
+-- 2. 鑿滃崟锛堢埗鑿滃崟 5300 璁惧绠$悊锛�
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES (5380, '璁¢噺鍣ㄥ叿', '', 2, 8, 5300, 'metering', 'ep:scale-to-original', 'mes/dv/metering/index', 'MesDvMetering', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
+
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+(5381, '璁¢噺鍣ㄥ叿鏌ヨ', 'mes:dv-metering:query', 3, 1, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5382, '璁¢噺鍣ㄥ叿鍒涘缓', 'mes:dv-metering:create', 3, 2, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5383, '璁¢噺鍣ㄥ叿鏇存柊', 'mes:dv-metering:update', 3, 3, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5384, '璁¢噺鍣ㄥ叿鍒犻櫎', 'mes:dv-metering:delete', 3, 4, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5385, '璁¢噺鍣ㄥ叿瀵煎嚭', 'mes:dv-metering:export', 3, 5, 5380, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
+
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES (5386, '璁¢噺妫�娴嬭褰�', '', 2, 9, 5300, 'metering-check', 'ep:checked', 'mes/dv/meteringcheck/index', 'MesDvMeteringCheck', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
+
+INSERT INTO `system_menu` (`id`, `name`, `permission`, `type`, `sort`, `parent_id`, `path`, `icon`, `component`, `component_name`, `status`, `visible`, `keep_alive`, `always_show`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+(5387, '妫�娴嬭褰曟煡璇�', 'mes:dv-metering-check:query', 3, 1, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5388, '妫�娴嬭褰曞垱寤�', 'mes:dv-metering-check:create', 3, 2, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5389, '妫�娴嬭褰曟洿鏂�', 'mes:dv-metering-check:update', 3, 3, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5390, '妫�娴嬭褰曞垹闄�', 'mes:dv-metering-check:delete', 3, 4, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
+(5391, '妫�娴嬭褰曞鍑�', 'mes:dv-metering-check:export', 3, 5, 5386, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
+
+-- 3. 瀛楀吀绫诲瀷
+INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+('MES 璁¢噺鍣ㄥ叿绫诲埆', 'mes_dv_metering_category', 0, '璁¢噺鍣ㄥ叿绫诲埆', '1', NOW(), '1', NOW(), b'0'),
+('MES 璁¢噺鍣ㄥ叿鐘舵��', 'mes_dv_metering_status', 0, '璁¢噺鍣ㄥ叿鐘舵��', '1', NOW(), '1', NOW(), b'0'),
+('MES 璁¢噺妫�娴嬬粨璁�', 'mes_dv_metering_check_result', 0, '璁¢噺妫�娴嬬粨璁�', '1', NOW(), '1', NOW(), b'0');
+
+-- 4. 瀛楀吀鏁版嵁
+INSERT INTO `system_dict_data` (`sort`, `label`, `value`, `dict_type`, `status`, `color_type`, `css_class`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES
+-- 鍣ㄥ叿绫诲埆
+(1, '鍗″昂', '1', 'mes_dv_metering_category', 0, '', '', '鍗″昂', '1', NOW(), '1', NOW(), b'0'),
+(2, '鍗冨垎灏�', '2', 'mes_dv_metering_category', 0, '', '', '鍗冨垎灏�', '1', NOW(), '1', NOW(), b'0'),
+(3, '鍘嬪姏琛�', '3', 'mes_dv_metering_category', 0, '', '', '鍘嬪姏琛�', '1', NOW(), '1', NOW(), b'0'),
+(4, '娓╁害璁�', '4', 'mes_dv_metering_category', 0, '', '', '娓╁害璁�', '1', NOW(), '1', NOW(), b'0'),
+(5, '涓囩敤琛�', '5', 'mes_dv_metering_category', 0, '', '', '涓囩敤琛�', '1', NOW(), '1', NOW(), b'0'),
+(6, '澶╁钩鐮濈爜', '6', 'mes_dv_metering_category', 0, '', '', '澶╁钩鐮濈爜', '1', NOW(), '1', NOW(), b'0'),
+(7, '鍏朵粬', '7', 'mes_dv_metering_category', 0, '', '', '鍏朵粬', '1', NOW(), '1', NOW(), b'0'),
+-- 鍣ㄥ叿鐘舵��
+(1, '鍦ㄧ敤', '1', 'mes_dv_metering_status', 0, 'success', '', '鍦ㄧ敤', '1', NOW(), '1', NOW(), b'0'),
+(2, '鍋滅敤', '2', 'mes_dv_metering_status', 0, 'info', '', '鍋滅敤', '1', NOW(), '1', NOW(), b'0'),
+(3, '鎶ュ簾', '3', 'mes_dv_metering_status', 0, 'danger', '', '鎶ュ簾', '1', NOW(), '1', NOW(), b'0'),
+-- 妫�娴嬬粨璁�
+(1, '鍚堟牸', '1', 'mes_dv_metering_check_result', 0, 'success', '', '鍚堟牸', '1', NOW(), '1', NOW(), b'0'),
+(2, '涓嶅悎鏍�', '2', 'mes_dv_metering_check_result', 0, 'danger', '', '涓嶅悎鏍�', '1', NOW(), '1', NOW(), b'0');
+
+-- 5. 绔欏唴淇℃ā鏉�
+INSERT INTO `system_notify_template` (`name`, `code`, `nickname`, `content`, `type`, `params`, `status`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES ('璁¢噺鍣ㄥ叿鍒版湡鎻愰啋', 'mes_metering_expire_remind', 'MES 绯荤粺', '璁¢噺鍣ㄥ叿銆恵meteringCode}銆憑meteringName} 灏嗕簬 {nextCheckDate} 鍒版湡锛岃鍙婃椂瀹夋帓鏍″噯/妫�瀹氥��', 2, '["meteringCode","meteringName","nextCheckDate"]', 0, '1', NOW(), '1', NOW(), b'0');
+
+-- 6. 鍏ㄥ眬閰嶇疆锛堟彁閱掑ぉ鏁帮紝榛樿 30 澶╋級
+INSERT INTO `infra_config` (`category`, `type`, `name`, `config_key`, `value`, `visible`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
+VALUES ('biz', 2, 'MES 璁¢噺鍣ㄥ叿鍒版湡鎻愰啋澶╂暟', 'mes.dv-metering.remind-days', '30', 0, '璁¢噺鍣ㄥ叿涓嬫妫�娴嬫棩鏈熷埌鏈熷墠 N 澶╂彁閱掞紙绔欏唴淇″彂閫佺粰璐d换浜猴級', '1', NOW(), '1', NOW(), b'0');
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java
new file mode 100644
index 0000000..531ccb3
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java
@@ -0,0 +1,145 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.metering;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringRespVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
+import cn.iocoder.yudao.module.mes.enums.dv.MesDvMeteringExpireStatusEnum;
+import cn.iocoder.yudao.module.mes.service.dv.metering.MesDvMeteringService;
+import cn.iocoder.yudao.module.system.api.dept.DeptApi;
+import cn.iocoder.yudao.module.system.api.dept.dto.DeptRespDTO;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "绠$悊鍚庡彴 - MES 璁¢噺鍣ㄥ叿")
+@RestController
+@RequestMapping("/mes/dv/metering")
+@Validated
+public class MesDvMeteringController {
+
+ @Resource
+ private MesDvMeteringService meteringService;
+ @Resource
+ private DeptApi deptApi;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓璁¢噺鍣ㄥ叿")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering:create')")
+ public CommonResult<Long> createMetering(@Valid @RequestBody MesDvMeteringSaveReqVO createReqVO) {
+ return success(meteringService.createMetering(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "鏇存柊璁¢噺鍣ㄥ叿")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering:update')")
+ public CommonResult<Boolean> updateMetering(@Valid @RequestBody MesDvMeteringSaveReqVO updateReqVO) {
+ meteringService.updateMetering(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎璁¢噺鍣ㄥ叿")
+ @Parameter(name = "id", description = "缂栧彿", required = true)
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering:delete')")
+ public CommonResult<Boolean> deleteMetering(@RequestParam("id") Long id) {
+ meteringService.deleteMetering(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱璁¢噺鍣ㄥ叿")
+ @Parameter(name = "id", description = "缂栧彿", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering:query')")
+ public CommonResult<MesDvMeteringRespVO> getMetering(@RequestParam("id") Long id) {
+ MesDvMeteringDO metering = meteringService.getMetering(id);
+ if (metering == null) {
+ return success(null);
+ }
+ return success(buildMeteringRespVOList(Collections.singletonList(metering)).get(0));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱璁¢噺鍣ㄥ叿鍒嗛〉")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering:query')")
+ public CommonResult<PageResult<MesDvMeteringRespVO>> getMeteringPage(@Valid MesDvMeteringPageReqVO pageReqVO) {
+ PageResult<MesDvMeteringDO> pageResult = meteringService.getMeteringPage(pageReqVO);
+ return success(new PageResult<>(buildMeteringRespVOList(pageResult.getList()), pageResult.getTotal()));
+ }
+
+ @GetMapping("/export-excel")
+ @Operation(summary = "瀵煎嚭璁¢噺鍣ㄥ叿 Excel")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering:export')")
+ @ApiAccessLog(operateType = EXPORT)
+ public void exportMeteringExcel(@Valid MesDvMeteringPageReqVO pageReqVO,
+ HttpServletResponse response) throws IOException {
+ pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+ List<MesDvMeteringDO> list = meteringService.getMeteringPage(pageReqVO).getList();
+ ExcelUtils.write(response, "璁¢噺鍣ㄥ叿.xls", "鏁版嵁", MesDvMeteringRespVO.class,
+ buildMeteringRespVOList(list));
+ }
+
+ @GetMapping("/simple-list")
+ @Operation(summary = "鑾峰緱璁¢噺鍣ㄥ叿绮剧畝鍒楄〃", description = "涓昏鐢ㄤ簬鍓嶇鐨勪笅鎷夐�夐」")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering:query')")
+ public CommonResult<List<MesDvMeteringRespVO>> getMeteringSimpleList() {
+ List<MesDvMeteringDO> list = meteringService.getMeteringList();
+ return success(buildMeteringRespVOList(list));
+ }
+
+ // ==================== 鎷兼帴 VO ====================
+
+ @SuppressWarnings("DuplicatedCode")
+ private List<MesDvMeteringRespVO> buildMeteringRespVOList(List<MesDvMeteringDO> list) {
+ if (CollUtil.isEmpty(list)) {
+ return Collections.emptyList();
+ }
+ // 1. 鎵归噺鑾峰彇鍏宠仈鏁版嵁
+ int remindDays = meteringService.getRemindDays();
+ Map<Long, DeptRespDTO> deptMap = CollUtil.isNotEmpty(convertSet(list, MesDvMeteringDO::getDeptId))
+ ? deptApi.getDeptMap(convertSet(list, MesDvMeteringDO::getDeptId)) : Collections.emptyMap();
+ Map<Long, AdminUserRespDTO> userMap = CollUtil.isNotEmpty(convertSet(list, MesDvMeteringDO::getChargeUserId))
+ ? adminUserApi.getUserMap(convertSet(list, MesDvMeteringDO::getChargeUserId)) : Collections.emptyMap();
+ // 2. 鎷兼帴 VO
+ return BeanUtils.toBean(list, MesDvMeteringRespVO.class, vo -> {
+ // 璁$畻鍒版湡鐘舵�佷笌鍓╀綑澶╂暟
+ vo.setExpireStatus(MesDvMeteringExpireStatusEnum.compute(vo.getNextCheckDate(), remindDays));
+ if (vo.getNextCheckDate() != null) {
+ vo.setExpireDays(ChronoUnit.DAYS.between(LocalDate.now(), vo.getNextCheckDate()));
+ }
+ MapUtils.findAndThen(deptMap, vo.getDeptId(), dept -> vo.setDeptName(dept.getName()));
+ MapUtils.findAndThen(userMap, vo.getChargeUserId(),
+ user -> vo.setChargeUserNickname(user.getNickname()));
+ });
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringPageReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringPageReqVO.java
new file mode 100644
index 0000000..be2cd57
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringPageReqVO.java
@@ -0,0 +1,47 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDate;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 璁¢噺鍣ㄥ叿鍒嗛〉 Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MesDvMeteringPageReqVO extends PageParam {
+
+ @Schema(description = "璁¢噺鍣ㄥ叿缂栫爜", example = "MD-001")
+ private String code;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿鍚嶇О", example = "鏁版樉鍗″昂")
+ private String name;
+
+ @Schema(description = "鍝佺墝/鐢熶骇鍘傚", example = "涓変赴")
+ private String brand;
+
+ @Schema(description = "鍣ㄥ叿绫诲埆", example = "1")
+ private Integer category;
+
+ @Schema(description = "浣跨敤閮ㄩ棬缂栧彿", example = "100")
+ private Long deptId;
+
+ @Schema(description = "璐d换浜虹紪鍙�", example = "200")
+ private Long chargeUserId;
+
+ @Schema(description = "鐘舵��", example = "1")
+ private Integer status;
+
+ @Schema(description = "鍒版湡鐘舵�侊紙1 宸茶繃鏈�/2 鍗冲皢鍒版湡/3 姝e父锛�", example = "2")
+ private Integer expireStatus;
+
+ @Schema(description = "涓嬫妫�娴嬫棩鏈熷紑濮嬶紙鐢卞悗绔牴鎹埌鏈熺姸鎬佽嚜鍔ㄥ~鍏咃級", hidden = true)
+ private LocalDate nextCheckDateBegin;
+
+ @Schema(description = "涓嬫妫�娴嬫棩鏈熺粨鏉燂紙鐢卞悗绔牴鎹埌鏈熺姸鎬佽嚜鍔ㄥ~鍏咃級", hidden = true)
+ private LocalDate nextCheckDateEnd;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringRespVO.java
new file mode 100644
index 0000000..f7ca46d
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringRespVO.java
@@ -0,0 +1,103 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 璁¢噺鍣ㄥ叿 Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class MesDvMeteringRespVO {
+
+ @Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @ExcelProperty("缂栧彿")
+ private Long id;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿缂栫爜", requiredMode = Schema.RequiredMode.REQUIRED, example = "MD-001")
+ @ExcelProperty("璁¢噺鍣ㄥ叿缂栫爜")
+ private String code;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "鏁版樉鍗″昂")
+ @ExcelProperty("璁¢噺鍣ㄥ叿鍚嶇О")
+ private String name;
+
+ @Schema(description = "瑙勬牸鍨嬪彿", example = "0-150mm")
+ @ExcelProperty("瑙勬牸鍨嬪彿")
+ private String specification;
+
+ @Schema(description = "鍝佺墝/鐢熶骇鍘傚", example = "涓変赴")
+ @ExcelProperty("鍝佺墝/鐢熶骇鍘傚")
+ private String brand;
+
+ @Schema(description = "鍣ㄥ叿绫诲埆", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @ExcelProperty(value = "鍣ㄥ叿绫诲埆", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.MES_DV_METERING_CATEGORY)
+ private Integer category;
+
+ @Schema(description = "绮惧害", example = "0.01mm")
+ @ExcelProperty("绮惧害")
+ private String precision;
+
+ @Schema(description = "閲忕▼", example = "0-150mm")
+ @ExcelProperty("閲忕▼")
+ private String range;
+
+ @Schema(description = "浣跨敤閮ㄩ棬缂栧彿", example = "100")
+ private Long deptId;
+
+ @Schema(description = "浣跨敤閮ㄩ棬鍚嶇О", example = "璐ㄦ閮�")
+ @ExcelProperty("浣跨敤閮ㄩ棬")
+ private String deptName;
+
+ @Schema(description = "璐d换浜虹紪鍙�", example = "200")
+ private Long chargeUserId;
+
+ @Schema(description = "璐d换浜哄悕绉�", example = "寮犱笁")
+ @ExcelProperty("璐d换浜�")
+ private String chargeUserNickname;
+
+ @Schema(description = "鏍″噯鍛ㄦ湡锛堟湀锛�", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
+ @ExcelProperty("鏍″噯鍛ㄦ湡(鏈�)")
+ private Integer checkCycleCount;
+
+ @Schema(description = "鎶曞叆浣跨敤鏃ユ湡")
+ @ExcelProperty("鎶曞叆浣跨敤鏃ユ湡")
+ private LocalDate useDate;
+
+ @Schema(description = "涓婃妫�娴嬫棩鏈�")
+ @ExcelProperty("涓婃妫�娴嬫棩鏈�")
+ private LocalDate lastCheckDate;
+
+ @Schema(description = "涓嬫妫�娴嬫棩鏈燂紙鍒版湡鏃ワ級")
+ @ExcelProperty("涓嬫妫�娴嬫棩鏈�")
+ private LocalDate nextCheckDate;
+
+ @Schema(description = "鍒版湡鐘舵�侊紙1 宸茶繃鏈�/2 鍗冲皢鍒版湡/3 姝e父锛�", example = "2")
+ @ExcelProperty("鍒版湡鐘舵��")
+ private Integer expireStatus;
+
+ @Schema(description = "璺濈鍒版湡澶╂暟锛堟鏁版湭鍒帮紝璐熸暟宸茶繃鏈燂級", example = "10")
+ @ExcelProperty("鍓╀綑澶╂暟")
+ private Long expireDays;
+
+ @Schema(description = "鐘舵��", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @ExcelProperty(value = "鐘舵��", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.MES_DV_METERING_STATUS)
+ private Integer status;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ @ExcelProperty("澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍒涘缓鏃堕棿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringSaveReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringSaveReqVO.java
new file mode 100644
index 0000000..d461ec1
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringSaveReqVO.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 璁¢噺鍣ㄥ叿鏂板/淇敼 Request VO")
+@Data
+public class MesDvMeteringSaveReqVO {
+
+ @Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿缂栫爜", requiredMode = Schema.RequiredMode.REQUIRED, example = "MD-001")
+ @NotEmpty(message = "璁¢噺鍣ㄥ叿缂栫爜涓嶈兘涓虹┖")
+ private String code;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿鍚嶇О", requiredMode = Schema.RequiredMode.REQUIRED, example = "鏁版樉鍗″昂")
+ @NotEmpty(message = "璁¢噺鍣ㄥ叿鍚嶇О涓嶈兘涓虹┖")
+ private String name;
+
+ @Schema(description = "瑙勬牸鍨嬪彿", example = "0-150mm")
+ private String specification;
+
+ @Schema(description = "鍝佺墝/鐢熶骇鍘傚", example = "涓変赴")
+ private String brand;
+
+ @Schema(description = "鍣ㄥ叿绫诲埆", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鍣ㄥ叿绫诲埆涓嶈兘涓虹┖")
+ private Integer category;
+
+ @Schema(description = "绮惧害", example = "0.01mm")
+ private String precision;
+
+ @Schema(description = "閲忕▼", example = "0-150mm")
+ private String range;
+
+ @Schema(description = "浣跨敤閮ㄩ棬缂栧彿", example = "100")
+ private Long deptId;
+
+ @Schema(description = "璐d换浜虹紪鍙�", example = "200")
+ private Long chargeUserId;
+
+ @Schema(description = "鏍″噯鍛ㄦ湡锛堟湀锛�", requiredMode = Schema.RequiredMode.REQUIRED, example = "12")
+ @NotNull(message = "鏍″噯鍛ㄦ湡涓嶈兘涓虹┖")
+ private Integer checkCycleCount;
+
+ @Schema(description = "鎶曞叆浣跨敤鏃ユ湡")
+ private LocalDate useDate;
+
+ @Schema(description = "鐘舵��", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "鐘舵�佷笉鑳戒负绌�")
+ private Integer status;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ private String remark;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/MesDvMeteringCheckController.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/MesDvMeteringCheckController.java
new file mode 100644
index 0000000..332d12d
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/MesDvMeteringCheckController.java
@@ -0,0 +1,130 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.iocoder.yudao.framework.apilog.core.annotation.ApiAccessLog;
+import cn.iocoder.yudao.framework.common.pojo.CommonResult;
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.collection.MapUtils;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.framework.excel.core.util.ExcelUtils;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckRespVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
+import cn.iocoder.yudao.module.mes.service.dv.metering.MesDvMeteringService;
+import cn.iocoder.yudao.module.mes.service.dv.meteringcheck.MesDvMeteringCheckService;
+import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
+import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
+import io.swagger.v3.oas.annotations.Operation;
+import io.swagger.v3.oas.annotations.Parameter;
+import io.swagger.v3.oas.annotations.tags.Tag;
+import jakarta.annotation.Resource;
+import jakarta.servlet.http.HttpServletResponse;
+import jakarta.validation.Valid;
+import org.springframework.security.access.prepost.PreAuthorize;
+import org.springframework.validation.annotation.Validated;
+import org.springframework.web.bind.annotation.*;
+
+import java.io.IOException;
+import java.util.Collections;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.apilog.core.enums.OperateTypeEnum.EXPORT;
+import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
+
+@Tag(name = "绠$悊鍚庡彴 - MES 璁¢噺妫�娴嬭褰�")
+@RestController
+@RequestMapping("/mes/dv/metering-check")
+@Validated
+public class MesDvMeteringCheckController {
+
+ @Resource
+ private MesDvMeteringCheckService meteringCheckService;
+ @Resource
+ private MesDvMeteringService meteringService;
+ @Resource
+ private AdminUserApi adminUserApi;
+
+ @PostMapping("/create")
+ @Operation(summary = "鍒涘缓璁¢噺妫�娴嬭褰�")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:create')")
+ public CommonResult<Long> createMeteringCheck(@Valid @RequestBody MesDvMeteringCheckSaveReqVO createReqVO) {
+ return success(meteringCheckService.createMeteringCheck(createReqVO));
+ }
+
+ @PutMapping("/update")
+ @Operation(summary = "鏇存柊璁¢噺妫�娴嬭褰�")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:update')")
+ public CommonResult<Boolean> updateMeteringCheck(@Valid @RequestBody MesDvMeteringCheckSaveReqVO updateReqVO) {
+ meteringCheckService.updateMeteringCheck(updateReqVO);
+ return success(true);
+ }
+
+ @DeleteMapping("/delete")
+ @Operation(summary = "鍒犻櫎璁¢噺妫�娴嬭褰�")
+ @Parameter(name = "id", description = "缂栧彿", required = true)
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:delete')")
+ public CommonResult<Boolean> deleteMeteringCheck(@RequestParam("id") Long id) {
+ meteringCheckService.deleteMeteringCheck(id);
+ return success(true);
+ }
+
+ @GetMapping("/get")
+ @Operation(summary = "鑾峰緱璁¢噺妫�娴嬭褰�")
+ @Parameter(name = "id", description = "缂栧彿", required = true, example = "1024")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:query')")
+ public CommonResult<MesDvMeteringCheckRespVO> getMeteringCheck(@RequestParam("id") Long id) {
+ MesDvMeteringCheckDO check = meteringCheckService.getMeteringCheck(id);
+ if (check == null) {
+ return success(null);
+ }
+ return success(buildMeteringCheckRespVOList(Collections.singletonList(check)).get(0));
+ }
+
+ @GetMapping("/page")
+ @Operation(summary = "鑾峰緱璁¢噺妫�娴嬭褰曞垎椤�")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:query')")
+ public CommonResult<PageResult<MesDvMeteringCheckRespVO>> getMeteringCheckPage(
+ @Valid MesDvMeteringCheckPageReqVO pageReqVO) {
+ PageResult<MesDvMeteringCheckDO> pageResult = meteringCheckService.getMeteringCheckPage(pageReqVO);
+ return success(new PageResult<>(buildMeteringCheckRespVOList(pageResult.getList()), pageResult.getTotal()));
+ }
+
+ @GetMapping("/export-excel")
+ @Operation(summary = "瀵煎嚭璁¢噺妫�娴嬭褰� Excel")
+ @PreAuthorize("@ss.hasPermission('mes:dv-metering-check:export')")
+ @ApiAccessLog(operateType = EXPORT)
+ public void exportMeteringCheckExcel(@Valid MesDvMeteringCheckPageReqVO pageReqVO,
+ HttpServletResponse response) throws IOException {
+ pageReqVO.setPageSize(PageParam.PAGE_SIZE_NONE);
+ List<MesDvMeteringCheckDO> list = meteringCheckService.getMeteringCheckPage(pageReqVO).getList();
+ ExcelUtils.write(response, "璁¢噺妫�娴嬭褰�.xls", "鏁版嵁", MesDvMeteringCheckRespVO.class,
+ buildMeteringCheckRespVOList(list));
+ }
+
+ // ==================== 鎷兼帴 VO ====================
+
+ @SuppressWarnings("DuplicatedCode")
+ private List<MesDvMeteringCheckRespVO> buildMeteringCheckRespVOList(List<MesDvMeteringCheckDO> list) {
+ if (CollUtil.isEmpty(list)) {
+ return Collections.emptyList();
+ }
+ // 1. 鎵归噺鑾峰彇鍏宠仈鏁版嵁
+ Map<Long, MesDvMeteringDO> meteringMap = meteringService.getMeteringMap(
+ convertSet(list, MesDvMeteringCheckDO::getMeteringId));
+ Map<Long, AdminUserRespDTO> userMap = CollUtil.isNotEmpty(convertSet(list, MesDvMeteringCheckDO::getCheckUserId))
+ ? adminUserApi.getUserMap(convertSet(list, MesDvMeteringCheckDO::getCheckUserId)) : Collections.emptyMap();
+ // 2. 鎷兼帴 VO
+ return BeanUtils.toBean(list, MesDvMeteringCheckRespVO.class, vo -> {
+ MapUtils.findAndThen(meteringMap, vo.getMeteringId(),
+ metering -> vo.setMeteringCode(metering.getCode()).setMeteringName(metering.getName()));
+ MapUtils.findAndThen(userMap, vo.getCheckUserId(),
+ user -> vo.setCheckUserNickname(user.getNickname()));
+ });
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckPageReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckPageReqVO.java
new file mode 100644
index 0000000..8176fda
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckPageReqVO.java
@@ -0,0 +1,29 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo;
+
+import cn.iocoder.yudao.framework.common.pojo.PageParam;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import lombok.EqualsAndHashCode;
+import lombok.ToString;
+
+import java.time.LocalDate;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 璁¢噺妫�娴嬭褰曞垎椤� Request VO")
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+public class MesDvMeteringCheckPageReqVO extends PageParam {
+
+ @Schema(description = "璁¢噺鍣ㄥ叿缂栧彿", example = "100")
+ private Long meteringId;
+
+ @Schema(description = "妫�娴嬬紪鍙�", example = "CK-001")
+ private String checkCode;
+
+ @Schema(description = "妫�娴嬬粨璁�", example = "1")
+ private Integer checkResult;
+
+ @Schema(description = "妫�娴嬫棩鏈�")
+ private LocalDate[] checkDate;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckRespVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckRespVO.java
new file mode 100644
index 0000000..c3b21c6
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckRespVO.java
@@ -0,0 +1,74 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo;
+
+import cn.iocoder.yudao.framework.excel.core.annotations.DictFormat;
+import cn.iocoder.yudao.framework.excel.core.convert.DictConvert;
+import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
+import cn.idev.excel.annotation.ExcelIgnoreUnannotated;
+import cn.idev.excel.annotation.ExcelProperty;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+
+import java.time.LocalDate;
+import java.time.LocalDateTime;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 璁¢噺妫�娴嬭褰� Response VO")
+@Data
+@ExcelIgnoreUnannotated
+public class MesDvMeteringCheckRespVO {
+
+ @Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ @ExcelProperty("缂栧彿")
+ private Long id;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+ private Long meteringId;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿缂栫爜", example = "MD-001")
+ @ExcelProperty("璁¢噺鍣ㄥ叿缂栫爜")
+ private String meteringCode;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿鍚嶇О", example = "鏁版樉鍗″昂")
+ @ExcelProperty("璁¢噺鍣ㄥ叿鍚嶇О")
+ private String meteringName;
+
+ @Schema(description = "妫�娴嬬紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "CK-001")
+ @ExcelProperty("妫�娴嬬紪鍙�")
+ private String checkCode;
+
+ @Schema(description = "妫�娴嬫棩鏈�", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("妫�娴嬫棩鏈�")
+ private LocalDate checkDate;
+
+ @Schema(description = "妫�娴嬫満鏋�", example = "甯傝閲忛櫌")
+ @ExcelProperty("妫�娴嬫満鏋�")
+ private String checkOrg;
+
+ @Schema(description = "妫�娴嬬粨璁�", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @ExcelProperty(value = "妫�娴嬬粨璁�", converter = DictConvert.class)
+ @DictFormat(DictTypeConstants.MES_DV_METERING_CHECK_RESULT)
+ private Integer checkResult;
+
+ @Schema(description = "璇佷功缂栧彿", example = "CN-2026-001")
+ @ExcelProperty("璇佷功缂栧彿")
+ private String certNo;
+
+ @Schema(description = "妫�娴嬩汉缂栧彿", example = "200")
+ private Long checkUserId;
+
+ @Schema(description = "妫�娴嬩汉鍚嶇О", example = "鏉庡洓")
+ @ExcelProperty("妫�娴嬩汉")
+ private String checkUserNickname;
+
+ @Schema(description = "涓嬫妫�娴嬫棩鏈�")
+ @ExcelProperty("涓嬫妫�娴嬫棩鏈�")
+ private LocalDate nextCheckDate;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ @ExcelProperty("澶囨敞")
+ private String remark;
+
+ @Schema(description = "鍒涘缓鏃堕棿", requiredMode = Schema.RequiredMode.REQUIRED)
+ @ExcelProperty("鍒涘缓鏃堕棿")
+ private LocalDateTime createTime;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckSaveReqVO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckSaveReqVO.java
new file mode 100644
index 0000000..cb2ebc1
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckSaveReqVO.java
@@ -0,0 +1,48 @@
+package cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.NotEmpty;
+import jakarta.validation.constraints.NotNull;
+import lombok.Data;
+
+import java.time.LocalDate;
+
+@Schema(description = "绠$悊鍚庡彴 - MES 璁¢噺妫�娴嬭褰曟柊澧�/淇敼 Request VO")
+@Data
+public class MesDvMeteringCheckSaveReqVO {
+
+ @Schema(description = "缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
+ private Long id;
+
+ @Schema(description = "璁¢噺鍣ㄥ叿缂栧彿", requiredMode = Schema.RequiredMode.REQUIRED, example = "100")
+ @NotNull(message = "璁¢噺鍣ㄥ叿涓嶈兘涓虹┖")
+ private Long meteringId;
+
+ @Schema(description = "妫�娴嬬紪鍙�", requiredMode = Schema.RequiredMode.REQUIRED, example = "CK-001")
+ @NotEmpty(message = "妫�娴嬬紪鍙蜂笉鑳戒负绌�")
+ private String checkCode;
+
+ @Schema(description = "妫�娴嬫棩鏈�", requiredMode = Schema.RequiredMode.REQUIRED)
+ @NotNull(message = "妫�娴嬫棩鏈熶笉鑳戒负绌�")
+ private LocalDate checkDate;
+
+ @Schema(description = "妫�娴嬫満鏋�", example = "甯傝閲忛櫌")
+ private String checkOrg;
+
+ @Schema(description = "妫�娴嬬粨璁�", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
+ @NotNull(message = "妫�娴嬬粨璁轰笉鑳戒负绌�")
+ private Integer checkResult;
+
+ @Schema(description = "璇佷功缂栧彿", example = "CN-2026-001")
+ private String certNo;
+
+ @Schema(description = "妫�娴嬩汉缂栧彿", example = "200")
+ private Long checkUserId;
+
+ @Schema(description = "涓嬫妫�娴嬫棩鏈燂紙涓虹┖鍒欐寜 妫�娴嬫棩鏈� + 鏍″噯鍛ㄦ湡 鑷姩璁$畻锛�")
+ private LocalDate nextCheckDate;
+
+ @Schema(description = "澶囨敞", example = "澶囨敞")
+ private String remark;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/metering/MesDvMeteringDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/metering/MesDvMeteringDO.java
new file mode 100644
index 0000000..7b6770b
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/metering/MesDvMeteringDO.java
@@ -0,0 +1,100 @@
+package cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableField;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDate;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿 DO
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("mes_dv_metering")
+@KeySequence("mes_dv_metering_seq") // 鐢ㄤ簬 Oracle銆丳ostgreSQL銆並ingbase銆丏B2銆丠2 鏁版嵁搴撶殑涓婚敭鑷銆傚鏋滄槸 MySQL 绛夋暟鎹簱锛屽彲涓嶅啓銆�
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MesDvMeteringDO extends BaseDO {
+
+ /**
+ * 缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 璁¢噺鍣ㄥ叿缂栫爜
+ */
+ private String code;
+ /**
+ * 璁¢噺鍣ㄥ叿鍚嶇О
+ */
+ private String name;
+ /**
+ * 瑙勬牸鍨嬪彿
+ */
+ private String specification;
+ /**
+ * 鍝佺墝/鐢熶骇鍘傚
+ */
+ private String brand;
+ /**
+ * 鍣ㄥ叿绫诲埆
+ *
+ * 瀛楀吀 {@link DictTypeConstants#MES_DV_METERING_CATEGORY}
+ */
+ private Integer category;
+ /**
+ * 绮惧害
+ */
+ @TableField("`precision`")
+ private String precision;
+ /**
+ * 閲忕▼
+ */
+ @TableField("`range`")
+ private String range;
+ /**
+ * 浣跨敤閮ㄩ棬缂栧彿
+ */
+ private Long deptId;
+ /**
+ * 璐d换浜虹紪鍙�
+ */
+ private Long chargeUserId;
+ /**
+ * 鏍″噯鍛ㄦ湡锛堟湀锛�
+ */
+ private Integer checkCycleCount;
+ /**
+ * 鎶曞叆浣跨敤鏃ユ湡
+ */
+ private LocalDate useDate;
+ /**
+ * 涓婃妫�娴嬫棩鏈�
+ */
+ private LocalDate lastCheckDate;
+ /**
+ * 涓嬫妫�娴嬫棩鏈燂紙鍒版湡鏃ワ級
+ */
+ private LocalDate nextCheckDate;
+ /**
+ * 鐘舵��
+ *
+ * 瀛楀吀 {@link DictTypeConstants#MES_DV_METERING_STATUS}
+ */
+ private Integer status;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/meteringcheck/MesDvMeteringCheckDO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/meteringcheck/MesDvMeteringCheckDO.java
new file mode 100644
index 0000000..4a6847b
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/meteringcheck/MesDvMeteringCheckDO.java
@@ -0,0 +1,73 @@
+package cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck;
+
+import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
+import cn.iocoder.yudao.module.mes.enums.DictTypeConstants;
+import com.baomidou.mybatisplus.annotation.KeySequence;
+import com.baomidou.mybatisplus.annotation.TableId;
+import com.baomidou.mybatisplus.annotation.TableName;
+import lombok.*;
+
+import java.time.LocalDate;
+
+/**
+ * MES 璁¢噺妫�娴嬭褰� DO
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@TableName("mes_dv_metering_check")
+@KeySequence("mes_dv_metering_check_seq") // 鐢ㄤ簬 Oracle銆丳ostgreSQL銆並ingbase銆丏B2銆丠2 鏁版嵁搴撶殑涓婚敭鑷銆傚鏋滄槸 MySQL 绛夋暟鎹簱锛屽彲涓嶅啓銆�
+@Data
+@EqualsAndHashCode(callSuper = true)
+@ToString(callSuper = true)
+@Builder
+@NoArgsConstructor
+@AllArgsConstructor
+public class MesDvMeteringCheckDO extends BaseDO {
+
+ /**
+ * 缂栧彿
+ */
+ @TableId
+ private Long id;
+ /**
+ * 璁¢噺鍣ㄥ叿缂栧彿
+ *
+ * 鍏宠仈 {@link cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO#getId()}
+ */
+ private Long meteringId;
+ /**
+ * 妫�娴嬬紪鍙�
+ */
+ private String checkCode;
+ /**
+ * 妫�娴嬫棩鏈�
+ */
+ private LocalDate checkDate;
+ /**
+ * 妫�娴嬫満鏋�
+ */
+ private String checkOrg;
+ /**
+ * 妫�娴嬬粨璁�
+ *
+ * 瀛楀吀 {@link DictTypeConstants#MES_DV_METERING_CHECK_RESULT}
+ */
+ private Integer checkResult;
+ /**
+ * 璇佷功缂栧彿
+ */
+ private String certNo;
+ /**
+ * 妫�娴嬩汉缂栧彿
+ */
+ private Long checkUserId;
+ /**
+ * 涓嬫妫�娴嬫棩鏈燂紙鑷姩璁$畻鎴栨墜鍔ㄦ寚瀹氾級
+ */
+ private LocalDate nextCheckDate;
+ /**
+ * 澶囨敞
+ */
+ private String remark;
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java
new file mode 100644
index 0000000..a4078de
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java
@@ -0,0 +1,45 @@
+package cn.iocoder.yudao.module.mes.dal.mysql.dv.metering;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
+import org.apache.ibatis.annotations.Mapper;
+
+import java.time.LocalDate;
+import java.util.List;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿 Mapper
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface MesDvMeteringMapper extends BaseMapperX<MesDvMeteringDO> {
+
+ default PageResult<MesDvMeteringDO> selectPage(MesDvMeteringPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX<MesDvMeteringDO>()
+ .likeIfPresent(MesDvMeteringDO::getCode, reqVO.getCode())
+ .likeIfPresent(MesDvMeteringDO::getName, reqVO.getName())
+ .likeIfPresent(MesDvMeteringDO::getBrand, reqVO.getBrand())
+ .eqIfPresent(MesDvMeteringDO::getCategory, reqVO.getCategory())
+ .eqIfPresent(MesDvMeteringDO::getDeptId, reqVO.getDeptId())
+ .eqIfPresent(MesDvMeteringDO::getChargeUserId, reqVO.getChargeUserId())
+ .eqIfPresent(MesDvMeteringDO::getStatus, reqVO.getStatus())
+ .geIfPresent(MesDvMeteringDO::getNextCheckDate, reqVO.getNextCheckDateBegin())
+ .leIfPresent(MesDvMeteringDO::getNextCheckDate, reqVO.getNextCheckDateEnd())
+ .orderByDesc(MesDvMeteringDO::getId));
+ }
+
+ default MesDvMeteringDO selectByCode(String code) {
+ return selectOne(MesDvMeteringDO::getCode, code);
+ }
+
+ default List<MesDvMeteringDO> selectListByNextCheckDateBetween(LocalDate begin, LocalDate end) {
+ return selectList(new LambdaQueryWrapperX<MesDvMeteringDO>()
+ .ge(MesDvMeteringDO::getNextCheckDate, begin)
+ .le(MesDvMeteringDO::getNextCheckDate, end));
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/meteringcheck/MesDvMeteringCheckMapper.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/meteringcheck/MesDvMeteringCheckMapper.java
new file mode 100644
index 0000000..5ccf30e
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/meteringcheck/MesDvMeteringCheckMapper.java
@@ -0,0 +1,43 @@
+package cn.iocoder.yudao.module.mes.dal.mysql.dv.meteringcheck;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
+import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
+import org.apache.ibatis.annotations.Mapper;
+
+/**
+ * MES 璁¢噺妫�娴嬭褰� Mapper
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Mapper
+public interface MesDvMeteringCheckMapper extends BaseMapperX<MesDvMeteringCheckDO> {
+
+ default PageResult<MesDvMeteringCheckDO> selectPage(MesDvMeteringCheckPageReqVO reqVO) {
+ return selectPage(reqVO, new LambdaQueryWrapperX<MesDvMeteringCheckDO>()
+ .eqIfPresent(MesDvMeteringCheckDO::getMeteringId, reqVO.getMeteringId())
+ .likeIfPresent(MesDvMeteringCheckDO::getCheckCode, reqVO.getCheckCode())
+ .eqIfPresent(MesDvMeteringCheckDO::getCheckResult, reqVO.getCheckResult())
+ .betweenIfPresent(MesDvMeteringCheckDO::getCheckDate, reqVO.getCheckDate())
+ .orderByDesc(MesDvMeteringCheckDO::getId));
+ }
+
+ default MesDvMeteringCheckDO selectByCheckCode(String checkCode) {
+ return selectOne(MesDvMeteringCheckDO::getCheckCode, checkCode);
+ }
+
+ default MesDvMeteringCheckDO selectLatestByMeteringId(Long meteringId) {
+ return selectOne(new LambdaQueryWrapperX<MesDvMeteringCheckDO>()
+ .eq(MesDvMeteringCheckDO::getMeteringId, meteringId)
+ .orderByDesc(MesDvMeteringCheckDO::getCheckDate)
+ .orderByDesc(MesDvMeteringCheckDO::getId)
+ .last("LIMIT 1"));
+ }
+
+ default Long selectCountByMeteringId(Long meteringId) {
+ return selectCount(MesDvMeteringCheckDO::getMeteringId, meteringId);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java
index ca2bc14..ca7c0fe 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java
@@ -15,4 +15,20 @@
*/
String AUTO_CODE = "mes:md:auto_code:";
+ /**
+ * 璁¢噺鍣ㄥ叿鍒版湡鎻愰啋鍘婚噸
+ *
+ * KEY 鏍煎紡锛歮es:dv:metering:remind:{meteringId}:{nextCheckDate}
+ * VALUE 鏁版嵁鏍煎紡锛�1
+ */
+ String METERING_EXPIRE_REMIND = "mes:dv:metering:remind:";
+
+ /**
+ * 璁¢噺鍣ㄥ叿鍒版湡鎻愰啋鎵弿闂ㄩ棭
+ *
+ * KEY 鏍煎紡锛歮es:dv:metering:remind:scan:{today}
+ * VALUE 鏁版嵁鏍煎紡锛�1
+ */
+ String METERING_EXPIRE_REMIND_SCAN = "mes:dv:metering:remind:scan:";
+
}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java
new file mode 100644
index 0000000..a12e6f7
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java
@@ -0,0 +1,54 @@
+package cn.iocoder.yudao.module.mes.dal.redis.dv.metering;
+
+import cn.iocoder.yudao.module.mes.dal.redis.RedisKeyConstants;
+import jakarta.annotation.Resource;
+import org.springframework.data.redis.core.StringRedisTemplate;
+import org.springframework.stereotype.Repository;
+
+import java.time.Duration;
+import java.time.LocalDate;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿鍒版湡鎻愰啋 Redis DAO
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Repository
+public class MesDvMeteringRemindRedisDAO {
+
+ /**
+ * 鎻愰啋鍘婚噸 KEY 鐨勮繃鏈熸椂闂达紙60 澶╋級锛屽悓涓�鍣ㄥ叿鍚屼竴鍒版湡鏃� 60 澶╁唴鍙彁閱掍竴娆�
+ */
+ private static final Duration REMIND_TIMEOUT = Duration.ofDays(60);
+ /**
+ * 鎵弿闂ㄩ棭鐨勮繃鏈熸椂闂达紙30 鍒嗛挓锛夛紝閬垮厤澶氬疄渚�/澶氭骞跺彂閲嶅鎵弿
+ */
+ private static final Duration SCAN_LOCK_TIMEOUT = Duration.ofMinutes(30);
+
+ @Resource
+ private StringRedisTemplate stringRedisTemplate;
+
+ /**
+ * 灏濊瘯鏍囪宸叉彁閱掞紙鍚屼竴鍣ㄥ叿鍚屼竴鍒版湡鏃� 60 澶╁唴鍙彁閱掍竴娆★級
+ *
+ * @param meteringId 璁¢噺鍣ㄥ叿缂栧彿
+ * @param nextCheckDate 鍒版湡鏃�
+ * @return true 琛ㄧず棣栨鏍囪鎴愬姛锛岄渶瑕佸彂閫佹彁閱�
+ */
+ public boolean tryMarkReminded(Long meteringId, LocalDate nextCheckDate) {
+ String key = RedisKeyConstants.METERING_EXPIRE_REMIND + meteringId + ":" + nextCheckDate;
+ return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", REMIND_TIMEOUT));
+ }
+
+ /**
+ * 灏濊瘯鑾峰彇褰撳ぉ鐨勬壂鎻忛棬闂╋紝閬垮厤澶氬疄渚嬪苟鍙戦噸澶嶆壂鎻�
+ *
+ * @param today 褰撳ぉ鏃ユ湡
+ * @return true 琛ㄧず鑾峰彇鎴愬姛锛堝彲浠ユ墽琛屾壂鎻忥級
+ */
+ public boolean tryAcquireScanLock(LocalDate today) {
+ String key = RedisKeyConstants.METERING_EXPIRE_REMIND_SCAN + today;
+ return Boolean.TRUE.equals(stringRedisTemplate.opsForValue().setIfAbsent(key, "1", SCAN_LOCK_TIMEOUT));
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
index 6cf7ede..0fd3d74 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
@@ -93,6 +93,9 @@
String MES_DV_CHECK_RESULT = "mes_dv_check_result"; // MES 鐐规缁撴灉
String MES_DV_REPAIR_STATUS = "mes_dv_repair_status"; // MES 缁翠慨宸ュ崟鐘舵��
String MES_DV_REPAIR_RESULT = "mes_dv_repair_result"; // MES 缁翠慨缁撴灉
+ String MES_DV_METERING_CATEGORY = "mes_dv_metering_category"; // MES 璁¢噺鍣ㄥ叿绫诲埆
+ String MES_DV_METERING_STATUS = "mes_dv_metering_status"; // MES 璁¢噺鍣ㄥ叿鐘舵��
+ String MES_DV_METERING_CHECK_RESULT = "mes_dv_metering_check_result"; // MES 璁¢噺妫�娴嬬粨璁�
// ========== 鎺掔彮鏃ュ巻 (CAL) ==========
String MES_CAL_HOLIDAY_TYPE = "mes_cal_holiday_type"; // MES 鍋囨湡绫诲瀷
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
index 1e5d6ff..ca229f4 100644
--- a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -215,6 +215,15 @@
// ========== MES 璁惧绠$悊-鐐规璁板綍鏄庣粏锛�1-040-306-100锛� ==========
ErrorCode DV_CHECK_RECORD_LINE_NOT_EXISTS = new ErrorCode(1_040_306_100, "璁惧鐐规璁板綍鏄庣粏涓嶅瓨鍦�");
+ // ========== MES 璁惧绠$悊-璁¢噺鍣ㄥ叿锛�1-040-307-000锛� ==========
+ ErrorCode DV_METERING_NOT_EXISTS = new ErrorCode(1_040_307_000, "璁¢噺鍣ㄥ叿涓嶅瓨鍦�");
+ ErrorCode DV_METERING_CODE_DUPLICATE = new ErrorCode(1_040_307_001, "璁¢噺鍣ㄥ叿缂栫爜宸插瓨鍦�");
+ ErrorCode DV_METERING_HAS_CHECK_RECORD = new ErrorCode(1_040_307_002, "璁¢噺鍣ㄥ叿宸插叧鑱旀娴嬭褰曪紝鏃犳硶鍒犻櫎");
+
+ // ========== MES 璁惧绠$悊-璁¢噺妫�娴嬭褰曪紙1-040-307-100锛� ==========
+ ErrorCode DV_METERING_CHECK_NOT_EXISTS = new ErrorCode(1_040_307_100, "璁¢噺妫�娴嬭褰曚笉瀛樺湪");
+ ErrorCode DV_METERING_CHECK_CODE_DUPLICATE = new ErrorCode(1_040_307_101, "璁¢噺妫�娴嬬紪鍙峰凡瀛樺湪");
+
// ========== MES 宸ュ叿绠$悊-宸ュ叿绫诲瀷锛�1-040-400-000锛� ==========
ErrorCode TM_TOOL_TYPE_NOT_EXISTS = new ErrorCode(1_040_400_000, "宸ュ叿绫诲瀷涓嶅瓨鍦�");
ErrorCode TM_TOOL_TYPE_CODE_DUPLICATE = new ErrorCode(1_040_400_001, "宸ュ叿绫诲瀷缂栫爜宸插瓨鍦�");
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringCheckResultEnum.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringCheckResultEnum.java
new file mode 100644
index 0000000..cdc4d3d
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringCheckResultEnum.java
@@ -0,0 +1,37 @@
+package cn.iocoder.yudao.module.mes.enums.dv;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * MES 璁¢噺妫�娴嬬粨璁烘灇涓�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Getter
+@AllArgsConstructor
+public enum MesDvMeteringCheckResultEnum implements ArrayValuable<Integer> {
+
+ QUALIFIED(1, "鍚堟牸"),
+ UNQUALIFIED(2, "涓嶅悎鏍�");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesDvMeteringCheckResultEnum::getResult).toArray(Integer[]::new);
+
+ /**
+ * 缁撹鍊�
+ */
+ private final Integer result;
+ /**
+ * 缁撹鍚�
+ */
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringExpireStatusEnum.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringExpireStatusEnum.java
new file mode 100644
index 0000000..76d1040
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringExpireStatusEnum.java
@@ -0,0 +1,61 @@
+package cn.iocoder.yudao.module.mes.enums.dv;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.time.LocalDate;
+import java.time.temporal.ChronoUnit;
+import java.util.Arrays;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿鍒版湡鐘舵�佹灇涓�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Getter
+@AllArgsConstructor
+public enum MesDvMeteringExpireStatusEnum implements ArrayValuable<Integer> {
+
+ OVERDUE(1, "宸茶繃鏈�"),
+ WITHIN(2, "鍗冲皢鍒版湡"),
+ NORMAL(3, "姝e父");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesDvMeteringExpireStatusEnum::getStatus).toArray(Integer[]::new);
+
+ /**
+ * 鐘舵�佸��
+ */
+ private final Integer status;
+ /**
+ * 鐘舵�佸悕
+ */
+ private final String name;
+
+ /**
+ * 璁$畻鍒版湡鐘舵��
+ *
+ * @param nextCheckDate 涓嬫妫�娴嬫棩鏈燂紙鍒版湡鏃ワ級
+ * @param remindDays 鎻愬墠鎻愰啋澶╂暟
+ * @return 鍒版湡鐘舵��
+ */
+ public static Integer compute(LocalDate nextCheckDate, int remindDays) {
+ if (nextCheckDate == null) {
+ return NORMAL.getStatus();
+ }
+ long days = ChronoUnit.DAYS.between(LocalDate.now(), nextCheckDate);
+ if (days < 0) {
+ return OVERDUE.getStatus();
+ }
+ if (days <= remindDays) {
+ return WITHIN.getStatus();
+ }
+ return NORMAL.getStatus();
+ }
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringStatusEnum.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringStatusEnum.java
new file mode 100644
index 0000000..4b26581
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringStatusEnum.java
@@ -0,0 +1,38 @@
+package cn.iocoder.yudao.module.mes.enums.dv;
+
+import cn.iocoder.yudao.framework.common.core.ArrayValuable;
+import lombok.AllArgsConstructor;
+import lombok.Getter;
+
+import java.util.Arrays;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿鐘舵�佹灇涓�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Getter
+@AllArgsConstructor
+public enum MesDvMeteringStatusEnum implements ArrayValuable<Integer> {
+
+ IN_USE(1, "鍦ㄧ敤"),
+ STOPPED(2, "鍋滅敤"),
+ SCRAPPED(3, "鎶ュ簾");
+
+ public static final Integer[] ARRAYS = Arrays.stream(values()).map(MesDvMeteringStatusEnum::getStatus).toArray(Integer[]::new);
+
+ /**
+ * 鐘舵�佸��
+ */
+ private final Integer status;
+ /**
+ * 鐘舵�佸悕
+ */
+ private final String name;
+
+ @Override
+ public Integer[] array() {
+ return ARRAYS;
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindService.java
new file mode 100644
index 0000000..ea98a8d
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindService.java
@@ -0,0 +1,17 @@
+package cn.iocoder.yudao.module.mes.service.dv.metering;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿鍒版湡鎻愰啋 Service 鎺ュ彛
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface MesDvMeteringRemindService {
+
+ /**
+ * 鍙戦�佸埌鏈熸彁閱掔珯鍐呬俊锛堟噿瑙﹀彂锛屽箓绛夛級
+ *
+ * 鍦ㄨ閲忓櫒鍏峰垎椤垫煡璇㈡椂瑙﹀彂锛氬厛鎶㈠綋澶╂壂鎻忛棬闂╋紝鍐嶆煡鍒版湡鍣ㄥ叿锛屾寜鍣ㄥ叿+鍒版湡鏃ュ幓閲嶅悗閫愬彴鍙戠珯鍐呬俊銆�
+ */
+ void sendExpireRemind();
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java
new file mode 100644
index 0000000..2c8416c
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java
@@ -0,0 +1,104 @@
+package cn.iocoder.yudao.module.mes.service.dv.metering;
+
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.dv.metering.MesDvMeteringMapper;
+import cn.iocoder.yudao.module.mes.dal.redis.dv.metering.MesDvMeteringRemindRedisDAO;
+import cn.iocoder.yudao.module.system.api.notify.NotifyMessageSendApi;
+import cn.iocoder.yudao.module.system.api.notify.dto.NotifySendSingleToUserReqDTO;
+import jakarta.annotation.Resource;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Service;
+
+import java.time.LocalDate;
+import java.util.HashMap;
+import java.util.List;
+import java.util.Map;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿鍒版湡鎻愰啋 Service 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Slf4j
+@Service
+public class MesDvMeteringRemindServiceImpl implements MesDvMeteringRemindService {
+
+ /**
+ * 鎻愰啋澶╂暟閰嶇疆 KEY锛坕nfra_config锛�
+ */
+ private static final String REMIND_DAYS_CONFIG_KEY = "mes.dv-metering.remind-days";
+ /**
+ * 鎻愰啋澶╂暟榛樿鍊�
+ */
+ private static final int DEFAULT_REMIND_DAYS = 30;
+ /**
+ * 鍒版湡鎻愰啋绔欏唴淇℃ā鏉跨紪鍙�
+ */
+ private static final String REMIND_TEMPLATE_CODE = "mes_metering_expire_remind";
+
+ @Resource
+ private MesDvMeteringMapper meteringMapper;
+ @Resource
+ private MesDvMeteringRemindRedisDAO meteringRemindRedisDAO;
+ @Resource
+ private ConfigApi configApi;
+ @Resource
+ private NotifyMessageSendApi notifyMessageSendApi;
+
+ @Override
+ public void sendExpireRemind() {
+ try {
+ // 1. 鎶㈠綋澶╂壂鎻忛棬闂╋紝閬垮厤澶氬疄渚�/澶氭骞跺彂閲嶅鎵弿
+ LocalDate today = LocalDate.now();
+ if (!meteringRemindRedisDAO.tryAcquireScanLock(today)) {
+ return;
+ }
+ // 2. 璇诲彇鎻愰啋澶╂暟锛堥粯璁� 30锛�
+ int remindDays = getRemindDays();
+ // 3. 鏌ヨ [today, today + remindDays] 鍒版湡鐨勫櫒鍏�
+ List<MesDvMeteringDO> list = meteringMapper.selectListByNextCheckDateBetween(
+ today, today.plusDays(remindDays));
+ // 4. 閫愬彴鍙戦�佺珯鍐呬俊锛堟寜鍣ㄥ叿 + 鍒版湡鏃ュ幓閲嶏級
+ for (MesDvMeteringDO metering : list) {
+ if (metering.getChargeUserId() == null || metering.getNextCheckDate() == null) {
+ continue;
+ }
+ if (!meteringRemindRedisDAO.tryMarkReminded(metering.getId(), metering.getNextCheckDate())) {
+ continue;
+ }
+ sendRemindMessage(metering);
+ }
+ } catch (Exception ex) {
+ // best-effort锛氭彁閱掑け璐ヤ笉褰卞搷鍒楄〃鏌ヨ
+ log.error("[sendExpireRemind][鍙戦�佽閲忓櫒鍏峰埌鏈熸彁閱掑け璐", ex);
+ }
+ }
+
+ private int getRemindDays() {
+ String value = configApi.getConfigValueByKey(REMIND_DAYS_CONFIG_KEY);
+ if (StrUtil.isBlank(value)) {
+ return DEFAULT_REMIND_DAYS;
+ }
+ try {
+ return Integer.parseInt(value.trim());
+ } catch (NumberFormatException ex) {
+ log.warn("[getRemindDays][閰嶇疆 {} 鐨勫�� {} 闈炴硶锛屼娇鐢ㄩ粯璁ゅ�� {}]", REMIND_DAYS_CONFIG_KEY, value, DEFAULT_REMIND_DAYS);
+ return DEFAULT_REMIND_DAYS;
+ }
+ }
+
+ private void sendRemindMessage(MesDvMeteringDO metering) {
+ Map<String, Object> params = new HashMap<>();
+ params.put("meteringCode", metering.getCode());
+ params.put("meteringName", metering.getName());
+ params.put("nextCheckDate", metering.getNextCheckDate().toString());
+ NotifySendSingleToUserReqDTO reqDTO = new NotifySendSingleToUserReqDTO()
+ .setUserId(metering.getChargeUserId())
+ .setTemplateCode(REMIND_TEMPLATE_CODE)
+ .setTemplateParams(params);
+ notifyMessageSendApi.sendSingleMessageToAdmin(reqDTO);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java
new file mode 100644
index 0000000..6104c6e
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java
@@ -0,0 +1,109 @@
+package cn.iocoder.yudao.module.mes.service.dv.metering;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
+import jakarta.validation.Valid;
+
+import java.time.LocalDate;
+import java.util.Collection;
+import java.util.List;
+import java.util.Map;
+
+import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertMap;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿 Service 鎺ュ彛
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface MesDvMeteringService {
+
+ /**
+ * 鍒涘缓璁¢噺鍣ㄥ叿
+ *
+ * @param createReqVO 鍒涘缓淇℃伅
+ * @return 缂栧彿
+ */
+ Long createMetering(@Valid MesDvMeteringSaveReqVO createReqVO);
+
+ /**
+ * 鏇存柊璁¢噺鍣ㄥ叿
+ *
+ * @param updateReqVO 鏇存柊淇℃伅
+ */
+ void updateMetering(@Valid MesDvMeteringSaveReqVO updateReqVO);
+
+ /**
+ * 鍒犻櫎璁¢噺鍣ㄥ叿
+ *
+ * @param id 缂栧彿
+ */
+ void deleteMetering(Long id);
+
+ /**
+ * 鑾峰緱璁¢噺鍣ㄥ叿
+ *
+ * @param id 缂栧彿
+ * @return 璁¢噺鍣ㄥ叿
+ */
+ MesDvMeteringDO getMetering(Long id);
+
+ /**
+ * 鑾峰緱璁¢噺鍣ㄥ叿鍒嗛〉
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @return 璁¢噺鍣ㄥ叿鍒嗛〉
+ */
+ PageResult<MesDvMeteringDO> getMeteringPage(MesDvMeteringPageReqVO pageReqVO);
+
+ /**
+ * 鑾峰緱璁¢噺鍣ㄥ叿鍒楄〃
+ *
+ * @return 璁¢噺鍣ㄥ叿鍒楄〃
+ */
+ List<MesDvMeteringDO> getMeteringList();
+
+ /**
+ * 鑾峰緱璁¢噺鍣ㄥ叿鍒楄〃
+ *
+ * @param ids 缂栧彿鏁扮粍
+ * @return 璁¢噺鍣ㄥ叿鍒楄〃
+ */
+ List<MesDvMeteringDO> getMeteringList(Collection<Long> ids);
+
+ /**
+ * 鑾峰緱璁¢噺鍣ㄥ叿 Map
+ *
+ * @param ids 缂栧彿鏁扮粍
+ * @return 璁¢噺鍣ㄥ叿 Map
+ */
+ default Map<Long, MesDvMeteringDO> getMeteringMap(Collection<Long> ids) {
+ return convertMap(getMeteringList(ids), MesDvMeteringDO::getId);
+ }
+
+ /**
+ * 鏍¢獙璁¢噺鍣ㄥ叿瀛樺湪
+ *
+ * @param id 缂栧彿
+ */
+ void validateMeteringExists(Long id);
+
+ /**
+ * 鑾峰緱鍒版湡鎻愰啋澶╂暟锛堥粯璁� 30锛�
+ *
+ * @return 鎻愰啋澶╂暟
+ */
+ int getRemindDays();
+
+ /**
+ * 鍥炲啓璁¢噺鍣ㄥ叿鐨勬渶杩戞娴嬩俊鎭紙妫�娴嬭褰曟柊澧�/淇敼/鍒犻櫎鍚庤皟鐢級
+ *
+ * @param meteringId 璁¢噺鍣ㄥ叿缂栧彿
+ * @param lastCheckDate 鏈�杩戞娴嬫棩鏈燂紙鏃犲垯缃┖锛�
+ * @param nextCheckDate 涓嬫妫�娴嬫棩鏈燂紙鏃犲垯缃┖锛�
+ */
+ void updateMeteringCheckInfo(Long meteringId, LocalDate lastCheckDate, LocalDate nextCheckDate);
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java
new file mode 100644
index 0000000..6c5ccbe
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java
@@ -0,0 +1,181 @@
+package cn.iocoder.yudao.module.mes.service.dv.metering;
+
+import cn.hutool.core.collection.CollUtil;
+import cn.hutool.core.util.ObjUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.infra.api.config.ConfigApi;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.metering.vo.MesDvMeteringSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.dv.metering.MesDvMeteringMapper;
+import cn.iocoder.yudao.module.mes.service.dv.meteringcheck.MesDvMeteringCheckService;
+import cn.hutool.core.util.StrUtil;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDate;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.List;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_CODE_DUPLICATE;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_HAS_CHECK_RECORD;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_NOT_EXISTS;
+
+/**
+ * MES 璁¢噺鍣ㄥ叿 Service 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Service
+@Validated
+public class MesDvMeteringServiceImpl implements MesDvMeteringService {
+
+ /**
+ * 鎻愰啋澶╂暟閰嶇疆 KEY锛坕nfra_config锛�
+ */
+ private static final String REMIND_DAYS_CONFIG_KEY = "mes.dv-metering.remind-days";
+ /**
+ * 鎻愰啋澶╂暟榛樿鍊�
+ */
+ private static final int DEFAULT_REMIND_DAYS = 30;
+
+ @Resource
+ private MesDvMeteringMapper meteringMapper;
+ @Resource
+ @Lazy // 寤惰繜鍔犺浇锛岄伩鍏嶄笌璁¢噺妫�娴嬭褰� Service 寰幆渚濊禆
+ private MesDvMeteringCheckService meteringCheckService;
+ @Resource
+ private MesDvMeteringRemindService meteringRemindService;
+ @Resource
+ private ConfigApi configApi;
+
+ @Override
+ public Long createMetering(MesDvMeteringSaveReqVO createReqVO) {
+ // 鏍¢獙缂栫爜鍞竴
+ validateMeteringCodeUnique(null, createReqVO.getCode());
+
+ // 鎻掑叆
+ MesDvMeteringDO metering = BeanUtils.toBean(createReqVO, MesDvMeteringDO.class);
+ meteringMapper.insert(metering);
+ return metering.getId();
+ }
+
+ @Override
+ public void updateMetering(MesDvMeteringSaveReqVO updateReqVO) {
+ // 鏍¢獙瀛樺湪
+ validateMeteringExists(updateReqVO.getId());
+ // 鏍¢獙缂栫爜鍞竴
+ validateMeteringCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
+
+ // 鏇存柊
+ MesDvMeteringDO updateObj = BeanUtils.toBean(updateReqVO, MesDvMeteringDO.class);
+ meteringMapper.updateById(updateObj);
+ }
+
+ @Override
+ public void deleteMetering(Long id) {
+ // 鏍¢獙瀛樺湪
+ validateMeteringExists(id);
+ // 鏍¢獙鏃犲叧鑱旀娴嬭褰�
+ if (meteringCheckService.getMeteringCheckCountByMeteringId(id) > 0) {
+ throw exception(DV_METERING_HAS_CHECK_RECORD);
+ }
+
+ // 鍒犻櫎
+ meteringMapper.deleteById(id);
+ }
+
+ @Override
+ public void validateMeteringExists(Long id) {
+ if (meteringMapper.selectById(id) == null) {
+ throw exception(DV_METERING_NOT_EXISTS);
+ }
+ }
+
+ private void validateMeteringCodeUnique(Long id, String code) {
+ if (StrUtil.isBlank(code)) {
+ return;
+ }
+ MesDvMeteringDO metering = meteringMapper.selectByCode(code);
+ if (metering == null) {
+ return;
+ }
+ if (ObjUtil.notEqual(metering.getId(), id)) {
+ throw exception(DV_METERING_CODE_DUPLICATE);
+ }
+ }
+
+ @Override
+ public MesDvMeteringDO getMetering(Long id) {
+ return meteringMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult<MesDvMeteringDO> getMeteringPage(MesDvMeteringPageReqVO pageReqVO) {
+ // 1. 鍒版湡鐘舵�佽浆鎹负鏃ユ湡鑼冨洿锛岃蛋 DB 杩囨护
+ Integer expireStatus = pageReqVO.getExpireStatus();
+ if (expireStatus != null) {
+ LocalDate today = LocalDate.now();
+ int remindDays = getRemindDays();
+ switch (expireStatus) {
+ case 1 -> { // 宸茶繃鏈燂細鍒版湡鏃� < 浠婂ぉ
+ pageReqVO.setNextCheckDateEnd(today.minusDays(1));
+ }
+ case 2 -> { // 鍗冲皢鍒版湡锛氬埌鏈熸棩鍦� [浠婂ぉ, 浠婂ぉ+鎻愰啋澶╂暟]
+ pageReqVO.setNextCheckDateBegin(today);
+ pageReqVO.setNextCheckDateEnd(today.plusDays(remindDays));
+ }
+ case 3 -> { // 姝e父锛氬埌鏈熸棩 > 浠婂ぉ+鎻愰啋澶╂暟
+ pageReqVO.setNextCheckDateBegin(today.plusDays(remindDays + 1));
+ }
+ default -> {
+ }
+ }
+ }
+ // 2. 鏌ヨ
+ PageResult<MesDvMeteringDO> pageResult = meteringMapper.selectPage(pageReqVO);
+ // 3. 鎳掕Е鍙戝埌鏈熸彁閱掔珯鍐呬俊锛坆est-effort锛屼笉褰卞搷鏌ヨ锛�
+ meteringRemindService.sendExpireRemind();
+ return pageResult;
+ }
+
+ @Override
+ public int getRemindDays() {
+ String value = configApi.getConfigValueByKey(REMIND_DAYS_CONFIG_KEY);
+ if (StrUtil.isBlank(value)) {
+ return DEFAULT_REMIND_DAYS;
+ }
+ try {
+ return Integer.parseInt(value.trim());
+ } catch (NumberFormatException ex) {
+ return DEFAULT_REMIND_DAYS;
+ }
+ }
+
+ @Override
+ public void updateMeteringCheckInfo(Long meteringId, LocalDate lastCheckDate, LocalDate nextCheckDate) {
+ meteringMapper.updateById(new MesDvMeteringDO()
+ .setId(meteringId)
+ .setLastCheckDate(lastCheckDate)
+ .setNextCheckDate(nextCheckDate));
+ }
+
+ @Override
+ public List<MesDvMeteringDO> getMeteringList() {
+ return meteringMapper.selectList();
+ }
+
+ @Override
+ public List<MesDvMeteringDO> getMeteringList(Collection<Long> ids) {
+ if (CollUtil.isEmpty(ids)) {
+ return Collections.emptyList();
+ }
+ return meteringMapper.selectByIds(ids);
+ }
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckService.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckService.java
new file mode 100644
index 0000000..33f52c3
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckService.java
@@ -0,0 +1,69 @@
+package cn.iocoder.yudao.module.mes.service.dv.meteringcheck;
+
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
+import jakarta.validation.Valid;
+
+/**
+ * MES 璁¢噺妫�娴嬭褰� Service 鎺ュ彛
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+public interface MesDvMeteringCheckService {
+
+ /**
+ * 鍒涘缓璁¢噺妫�娴嬭褰�
+ *
+ * @param createReqVO 鍒涘缓淇℃伅
+ * @return 缂栧彿
+ */
+ Long createMeteringCheck(@Valid MesDvMeteringCheckSaveReqVO createReqVO);
+
+ /**
+ * 鏇存柊璁¢噺妫�娴嬭褰�
+ *
+ * @param updateReqVO 鏇存柊淇℃伅
+ */
+ void updateMeteringCheck(@Valid MesDvMeteringCheckSaveReqVO updateReqVO);
+
+ /**
+ * 鍒犻櫎璁¢噺妫�娴嬭褰�
+ *
+ * @param id 缂栧彿
+ */
+ void deleteMeteringCheck(Long id);
+
+ /**
+ * 鑾峰緱璁¢噺妫�娴嬭褰�
+ *
+ * @param id 缂栧彿
+ * @return 璁¢噺妫�娴嬭褰�
+ */
+ MesDvMeteringCheckDO getMeteringCheck(Long id);
+
+ /**
+ * 鑾峰緱璁¢噺妫�娴嬭褰曞垎椤�
+ *
+ * @param pageReqVO 鍒嗛〉鏌ヨ
+ * @return 璁¢噺妫�娴嬭褰曞垎椤�
+ */
+ PageResult<MesDvMeteringCheckDO> getMeteringCheckPage(MesDvMeteringCheckPageReqVO pageReqVO);
+
+ /**
+ * 鑾峰緱鎸囧畾璁¢噺鍣ㄥ叿涓嬬殑妫�娴嬭褰曟暟閲�
+ *
+ * @param meteringId 璁¢噺鍣ㄥ叿缂栧彿
+ * @return 妫�娴嬭褰曟暟閲�
+ */
+ Long getMeteringCheckCountByMeteringId(Long meteringId);
+
+ /**
+ * 鏍¢獙璁¢噺妫�娴嬭褰曞瓨鍦�
+ *
+ * @param id 缂栧彿
+ */
+ void validateMeteringCheckExists(Long id);
+
+}
diff --git a/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckServiceImpl.java b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckServiceImpl.java
new file mode 100644
index 0000000..b1e0f3c
--- /dev/null
+++ b/yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckServiceImpl.java
@@ -0,0 +1,165 @@
+package cn.iocoder.yudao.module.mes.service.dv.meteringcheck;
+
+import cn.hutool.core.util.ObjUtil;
+import cn.hutool.core.util.StrUtil;
+import cn.iocoder.yudao.framework.common.pojo.PageResult;
+import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckPageReqVO;
+import cn.iocoder.yudao.module.mes.controller.admin.dv.meteringcheck.vo.MesDvMeteringCheckSaveReqVO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.metering.MesDvMeteringDO;
+import cn.iocoder.yudao.module.mes.dal.dataobject.dv.meteringcheck.MesDvMeteringCheckDO;
+import cn.iocoder.yudao.module.mes.dal.mysql.dv.meteringcheck.MesDvMeteringCheckMapper;
+import cn.iocoder.yudao.module.mes.service.dv.metering.MesDvMeteringService;
+import jakarta.annotation.Resource;
+import org.springframework.context.annotation.Lazy;
+import org.springframework.stereotype.Service;
+import org.springframework.transaction.annotation.Transactional;
+import org.springframework.validation.annotation.Validated;
+
+import java.time.LocalDate;
+
+import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_CHECK_CODE_DUPLICATE;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_CHECK_NOT_EXISTS;
+import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.DV_METERING_NOT_EXISTS;
+
+/**
+ * MES 璁¢噺妫�娴嬭褰� Service 瀹炵幇绫�
+ *
+ * @author 瓒呯骇绠$悊鍛�
+ */
+@Service
+@Validated
+public class MesDvMeteringCheckServiceImpl implements MesDvMeteringCheckService {
+
+ @Resource
+ private MesDvMeteringCheckMapper meteringCheckMapper;
+ @Resource
+ @Lazy // 寤惰繜鍔犺浇锛岄伩鍏嶄笌璁¢噺鍣ㄥ叿 Service 寰幆渚濊禆
+ private MesDvMeteringService meteringService;
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public Long createMeteringCheck(MesDvMeteringCheckSaveReqVO createReqVO) {
+ // 鏍¢獙璁¢噺鍣ㄥ叿瀛樺湪
+ MesDvMeteringDO metering = validateMeteringExists(createReqVO.getMeteringId());
+ // 鏍¢獙妫�娴嬬紪鍙峰敮涓�
+ validateMeteringCheckCodeUnique(null, createReqVO.getCheckCode());
+
+ // 璁$畻涓嬫妫�娴嬫棩鏈燂紙涓虹┖鍒欐寜鏍″噯鍛ㄦ湡鑷姩璁$畻锛�
+ LocalDate nextCheckDate = computeNextCheckDate(createReqVO.getCheckDate(),
+ createReqVO.getNextCheckDate(), metering);
+ MesDvMeteringCheckDO check = BeanUtils.toBean(createReqVO, MesDvMeteringCheckDO.class);
+ check.setNextCheckDate(nextCheckDate);
+ meteringCheckMapper.insert(check);
+
+ // 鍥炲啓璁¢噺鍣ㄥ叿鐨勬渶杩戞娴嬩俊鎭�
+ refreshMeteringCheckInfo(createReqVO.getMeteringId());
+ return check.getId();
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void updateMeteringCheck(MesDvMeteringCheckSaveReqVO updateReqVO) {
+ // 鏍¢獙瀛樺湪
+ validateMeteringCheckExists(updateReqVO.getId());
+ // 鏍¢獙璁¢噺鍣ㄥ叿瀛樺湪
+ MesDvMeteringDO metering = validateMeteringExists(updateReqVO.getMeteringId());
+ // 鏍¢獙妫�娴嬬紪鍙峰敮涓�
+ validateMeteringCheckCodeUnique(updateReqVO.getId(), updateReqVO.getCheckCode());
+
+ // 璁$畻涓嬫妫�娴嬫棩鏈燂紙涓虹┖鍒欐寜鏍″噯鍛ㄦ湡鑷姩璁$畻锛�
+ LocalDate nextCheckDate = computeNextCheckDate(updateReqVO.getCheckDate(),
+ updateReqVO.getNextCheckDate(), metering);
+ MesDvMeteringCheckDO updateObj = BeanUtils.toBean(updateReqVO, MesDvMeteringCheckDO.class);
+ updateObj.setNextCheckDate(nextCheckDate);
+ meteringCheckMapper.updateById(updateObj);
+
+ // 鍥炲啓璁¢噺鍣ㄥ叿鐨勬渶杩戞娴嬩俊鎭�
+ refreshMeteringCheckInfo(updateReqVO.getMeteringId());
+ }
+
+ @Override
+ @Transactional(rollbackFor = Exception.class)
+ public void deleteMeteringCheck(Long id) {
+ // 鏍¢獙瀛樺湪
+ validateMeteringCheckExists(id);
+ MesDvMeteringCheckDO check = meteringCheckMapper.selectById(id);
+
+ // 鍒犻櫎
+ meteringCheckMapper.deleteById(id);
+
+ // 鍥炲啓璁¢噺鍣ㄥ叿鐨勬渶杩戞娴嬩俊鎭�
+ refreshMeteringCheckInfo(check.getMeteringId());
+ }
+
+ @Override
+ public MesDvMeteringCheckDO getMeteringCheck(Long id) {
+ return meteringCheckMapper.selectById(id);
+ }
+
+ @Override
+ public PageResult<MesDvMeteringCheckDO> getMeteringCheckPage(MesDvMeteringCheckPageReqVO pageReqVO) {
+ return meteringCheckMapper.selectPage(pageReqVO);
+ }
+
+ @Override
+ public Long getMeteringCheckCountByMeteringId(Long meteringId) {
+ return meteringCheckMapper.selectCountByMeteringId(meteringId);
+ }
+
+ @Override
+ public void validateMeteringCheckExists(Long id) {
+ if (meteringCheckMapper.selectById(id) == null) {
+ throw exception(DV_METERING_CHECK_NOT_EXISTS);
+ }
+ }
+
+ private MesDvMeteringDO validateMeteringExists(Long meteringId) {
+ MesDvMeteringDO metering = meteringService.getMetering(meteringId);
+ if (metering == null) {
+ throw exception(DV_METERING_NOT_EXISTS);
+ }
+ return metering;
+ }
+
+ private void validateMeteringCheckCodeUnique(Long id, String checkCode) {
+ if (StrUtil.isBlank(checkCode)) {
+ return;
+ }
+ MesDvMeteringCheckDO check = meteringCheckMapper.selectByCheckCode(checkCode);
+ if (check == null) {
+ return;
+ }
+ if (ObjUtil.notEqual(check.getId(), id)) {
+ throw exception(DV_METERING_CHECK_CODE_DUPLICATE);
+ }
+ }
+
+ /**
+ * 璁$畻涓嬫妫�娴嬫棩鏈燂細鎵嬪姩鎸囧畾浼樺厛锛屽惁鍒欐寜 妫�娴嬫棩鏈� + 鏍″噯鍛ㄦ湡锛堟湀锛夎嚜鍔ㄨ绠�
+ */
+ private LocalDate computeNextCheckDate(LocalDate checkDate, LocalDate nextCheckDate, MesDvMeteringDO metering) {
+ if (nextCheckDate != null) {
+ return nextCheckDate;
+ }
+ if (checkDate != null && metering.getCheckCycleCount() != null) {
+ return checkDate.plusMonths(metering.getCheckCycleCount());
+ }
+ return nextCheckDate;
+ }
+
+ /**
+ * 鎸�"鏈�鏂颁竴鏉℃娴嬭褰�"鏁翠綋閲嶇畻鍥炲啓璁¢噺鍣ㄥ叿锛屾纭鐩栫紪杈戦潪鏈�鏂拌褰�/鍒犻櫎涓棿璁板綍绛夎竟鐣�
+ */
+ private void refreshMeteringCheckInfo(Long meteringId) {
+ MesDvMeteringCheckDO latest = meteringCheckMapper.selectLatestByMeteringId(meteringId);
+ if (latest == null) {
+ meteringService.updateMeteringCheckInfo(meteringId, null, null);
+ } else {
+ meteringService.updateMeteringCheckInfo(meteringId,
+ latest.getCheckDate(), latest.getNextCheckDate());
+ }
+ }
+
+}
--
Gitblit v1.9.3