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