huminmin
8 天以前 b48ed5159987e8cee81bc765010cfd9393ad7003
Merge branch 'dev_business' of http://114.132.189.42:9002/r/mom-pro2-after into dev_business
已修改3个文件
已添加24个文件
1996 ■■■■■ 文件已修改
docs/mes_dv_metering_frontend_integration.md 116 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_dv_metering_init.sql 119 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/MesDvMeteringController.java 145 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringPageReqVO.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringRespVO.java 103 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/metering/vo/MesDvMeteringSaveReqVO.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/MesDvMeteringCheckController.java 130 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckPageReqVO.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckRespVO.java 74 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/dv/meteringcheck/vo/MesDvMeteringCheckSaveReqVO.java 48 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/metering/MesDvMeteringDO.java 100 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/dv/meteringcheck/MesDvMeteringCheckDO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/metering/MesDvMeteringMapper.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/dv/meteringcheck/MesDvMeteringCheckMapper.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/RedisKeyConstants.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/redis/dv/metering/MesDvMeteringRemindRedisDAO.java 54 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java 3 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 9 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringCheckResultEnum.java 37 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringExpireStatusEnum.java 61 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/dv/MesDvMeteringStatusEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringRemindServiceImpl.java 104 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringService.java 109 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/metering/MesDvMeteringServiceImpl.java 181 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckService.java 69 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/dv/meteringcheck/MesDvMeteringCheckServiceImpl.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
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 | å¦ | è´£ä»»äºº |
| status | Integer | å¦ | çŠ¶æ€ï¼ˆå­—å…¸ï¼‰ |
| expireStatus | Integer | å¦ | åˆ°æœŸçŠ¶æ€ï¼š1 å·²è¿‡æœŸ / 2 å³å°†åˆ°æœŸ / 3 æ­£å¸¸ |
### è®¡é‡æ£€æµ‹è®°å½•
| æ–¹æ³• | è·¯å¾„ | è¯´æ˜Ž | æƒé™ |
|------|------|------|------|
| 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 | è´£ä»»äººæ˜µç§° |
| expireStatus | Integer | åˆ°æœŸçŠ¶æ€ï¼š1 å·²è¿‡æœŸ / 2 å³å°†åˆ°æœŸ / 3 æ­£å¸¸ |
| expireDays | Long | è·åˆ°æœŸå¤©æ•°ï¼ˆæ­£æ•°æœªåˆ°ã€è´Ÿæ•°å·²è¿‡æœŸï¼‰ |
计量检测记录列表/详情响应额外返回:
| å­—段 | ç±»åž‹ | è¯´æ˜Ž |
|------|------|------|
| meteringCode | String | è®¡é‡å™¨å…·ç¼–码 |
| meteringName | String | è®¡é‡å™¨å…·åç§° |
| checkUserNickname | String | æ£€æµ‹äººæ˜µç§° |
## å­—段展示规则
| å­—段 | å±•示位置 | è¯´æ˜Ž |
|------|----------|------|
| expireStatus | è®¡é‡å™¨å…·åˆ—表 | 1 å·²è¿‡æœŸ=危险(danger);2 å³å°†åˆ°æœŸ=警告(warning);3 æ­£å¸¸=成功(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`,改后立即生效。
- ä¸‰ä¸ªæ–°å­—典可在「系统管理 -> å­—典管理」中维护:mes_dv_metering_category、mes_dv_metering_status、mes_dv_metering_check_result。
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 '责任人编号(站内信发送对象)',
    `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 å¤©æé†’(站内信发送给责任人)', '1', NOW(), '1', NOW(), b'0');
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()));
        });
    }
}
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 = "责任人编号", example = "200")
    private Long chargeUserId;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "到期状态(1 å·²è¿‡æœŸ/2 å³å°†åˆ°æœŸ/3 æ­£å¸¸ï¼‰", example = "2")
    private Integer expireStatus;
    @Schema(description = "下次检测日期开始(由后端根据到期状态自动填充)", hidden = true)
    private LocalDate nextCheckDateBegin;
    @Schema(description = "下次检测日期结束(由后端根据到期状态自动填充)", hidden = true)
    private LocalDate nextCheckDateEnd;
}
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 = "责任人编号", example = "200")
    private Long chargeUserId;
    @Schema(description = "责任人名称", example = "张三")
    @ExcelProperty("责任人")
    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 æ­£å¸¸ï¼‰", 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;
}
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 = "责任人编号", 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;
}
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()));
        });
    }
}
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;
}
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;
}
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;
}
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、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 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;
    /**
     * è´£ä»»äººç¼–号
     */
    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;
}
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、PostgreSQL、Kingbase、DB2、H2 æ•°æ®åº“的主键自增。如果是 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;
}
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));
    }
}
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);
    }
}
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 æ ¼å¼ï¼šmes:dv:metering:remind:{meteringId}:{nextCheckDate}
     * VALUE æ•°æ®æ ¼å¼ï¼š1
     */
    String METERING_EXPIRE_REMIND = "mes:dv:metering:remind:";
    /**
     * è®¡é‡å™¨å…·åˆ°æœŸæé†’扫描门闩
     *
     * KEY æ ¼å¼ï¼šmes:dv:metering:remind:scan:{today}
     * VALUE æ•°æ®æ ¼å¼ï¼š1
     */
    String METERING_EXPIRE_REMIND_SCAN = "mes:dv:metering:remind:scan:";
}
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));
    }
}
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 å‡æœŸç±»åž‹
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, "工具类型编码已存在");
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;
    }
}
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, "正常");
    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;
    }
}
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;
    }
}
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();
}
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(infra_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);
    }
}
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);
}
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(infra_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 -> { // æ­£å¸¸ï¼šåˆ°æœŸæ—¥ > ä»Šå¤©+提醒天数
                    pageReqVO.setNextCheckDateBegin(today.plusDays(remindDays + 1));
                }
                default -> {
                }
            }
        }
        // 2. æŸ¥è¯¢
        PageResult<MesDvMeteringDO> pageResult = meteringMapper.selectPage(pageReqVO);
        // 3. æ‡’触发到期提醒站内信(best-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);
    }
}
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);
}
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());
        }
    }
}