gaoluyang
2026-10-18 f0d1f1ad146e284c0adcc851a9501f468ca1aa5d
feat(mes): 新增能耗管理模块(计量点/抄表/定额/预警/统计)

- 新增能耗计量点、抄表记录、考核指标、预警的数据模型与 CRUD 接口
- 抄表自动带出期初读数并计算本期用量、费用,冗余车间/产线/设备快照
- 预警检测覆盖超定额、环比、同比、跑冒滴漏四类规则
- 新增能耗总览与月度报表统计接口,提供同比、环比分析
- 新增 8 个能耗字典类型与 16 个按钮权限
- 新增建表、字典、菜单及角色授权初始化脚本

Co-Authored-By: Claude Code <noreply@anthropic.com>
已添加48个文件
已修改2个文件
4238 ■■■■■ 文件已修改
sql/mysql/mes_em_energy_init.sql 255 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/MesEmAlertController.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/vo/MesEmAlertHandleReqVO.java 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/vo/MesEmAlertPageReqVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/vo/MesEmAlertRespVO.java 95 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/MesEmMeterController.java 107 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/vo/MesEmMeterPageReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/vo/MesEmMeterRespVO.java 86 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/vo/MesEmMeterSaveReqVO.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/vo/MesEmOptionRespVO.java 44 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/MesEmQuotaController.java 77 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/vo/MesEmQuotaPageReqVO.java 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/vo/MesEmQuotaRespVO.java 73 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/vo/MesEmQuotaSaveReqVO.java 64 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/MesEmRecordController.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmLastReadingRespVO.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmRecordPageReqVO.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmRecordRespVO.java 89 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmRecordSaveReqVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/statistics/MesEmStatisticsController.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/statistics/vo/MesEmOverviewRespVO.java 161 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/statistics/vo/MesEmReportRespVO.java 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/alert/MesEmAlertDO.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/meter/MesEmMeterDO.java 115 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/quota/MesEmQuotaDO.java 98 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/record/MesEmRecordDO.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/alert/MesEmAlertMapper.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/meter/MesEmMeterMapper.java 56 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/quota/MesEmQuotaMapper.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/record/MesEmRecordMapper.java 165 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java 10 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java 13 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmAlertLevelEnum.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmAlertStatusEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmAlertTypeEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmEnergyTypeEnum.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmMeterStatusEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmQuotaObjectTypeEnum.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmQuotaPeriodEnum.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmRecordSourceEnum.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/alert/MesEmAlertService.java 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/alert/MesEmAlertServiceImpl.java 339 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/meter/MesEmMeterService.java 46 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/meter/MesEmMeterServiceImpl.java 249 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/quota/MesEmQuotaService.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/quota/MesEmQuotaServiceImpl.java 157 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/record/MesEmRecordService.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/record/MesEmRecordServiceImpl.java 178 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/statistics/MesEmStatisticsService.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/statistics/MesEmStatisticsServiceImpl.java 331 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
sql/mysql/mes_em_energy_init.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,255 @@
-- ============================================
-- MES èƒ½è€—管理 æ¨¡å—初始化脚本
-- 1. å»ºè¡¨  2. å­—å…¸  3. èœå•与按钮权限  4. è¶…级管理员菜单授权
--
-- è¯´æ˜Žï¼š
--   * ä¸šåŠ¡è¡¨ç»Ÿä¸€é€»è¾‘åˆ é™¤ï¼ˆdeleted ä½ï¼‰ï¼Œå› æ­¤ code / (meter_id, record_date) ç­‰
--     ä¸šåŠ¡å”¯ä¸€æ€§ç”±æœåŠ¡å±‚æ ¡éªŒï¼Œæ•°æ®åº“ä¾§åªå»ºæ™®é€šç´¢å¼•ï¼Œé¿å…é€»è¾‘åˆ é™¤åŽæ— æ³•å¤ç”¨ç¼–ç ã€‚
--   * èœå•挂在已存在的「能耗管理」目录下(id = 1065864,/energyManagement)。
-- ============================================
-- 1. å»ºè¡¨
CREATE TABLE IF NOT EXISTS `mes_em_meter` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '计量点编码',
    `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '计量点名称',
    `energy_type` int NOT NULL COMMENT '能源类型,字典 mes_em_energy_type',
    `unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量单位',
    `meter_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '现场表计编号',
    `workshop_id` bigint NULL DEFAULT NULL COMMENT '所属车间编号',
    `workshop_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属车间名称',
    `line_id` bigint NULL DEFAULT NULL COMMENT '所属产线编号',
    `line_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属产线名称',
    `machinery_id` bigint NULL DEFAULT NULL COMMENT '关联设备编号',
    `machinery_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联设备名称',
    `ratio` decimal(18, 4) NULL DEFAULT 1.0000 COMMENT '倍率',
    `initial_reading` decimal(18, 2) NULL DEFAULT 0.00 COMMENT '初始读数',
    `coal_factor` decimal(18, 4) NULL DEFAULT NULL COMMENT '折标煤系数(kgce/计量单位)',
    `leak_threshold` decimal(18, 2) NULL DEFAULT NULL COMMENT '日用量异常上限(超过触发疑似跑冒滴漏)',
    `charge_user_id` bigint NULL DEFAULT NULL COMMENT '责任人编号',
    `install_date` date NULL DEFAULT NULL COMMENT '安装日期',
    `status` int NOT NULL DEFAULT 1 COMMENT '状态,字典 mes_em_meter_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,
    INDEX `idx_code` (`code`),
    INDEX `idx_energy_type` (`energy_type`),
    INDEX `idx_workshop_id` (`workshop_id`),
    INDEX `idx_machinery_id` (`machinery_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES èƒ½è€—计量点' ROW_FORMAT = Dynamic;
CREATE TABLE IF NOT EXISTS `mes_em_record` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `meter_id` bigint NOT NULL COMMENT '计量点编号',
    `meter_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量点编码快照',
    `meter_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量点名称快照',
    `energy_type` int NULL DEFAULT NULL COMMENT '能源类型快照,字典 mes_em_energy_type',
    `unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量单位快照',
    `ratio` decimal(18, 4) NULL DEFAULT NULL COMMENT '倍率快照',
    `coal_factor` decimal(18, 4) NULL DEFAULT NULL COMMENT '折标煤系数快照',
    `workshop_id` bigint NULL DEFAULT NULL COMMENT '所属车间编号快照',
    `workshop_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属车间名称快照',
    `line_id` bigint NULL DEFAULT NULL COMMENT '所属产线编号快照',
    `line_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '所属产线名称快照',
    `machinery_id` bigint NULL DEFAULT NULL COMMENT '关联设备编号快照',
    `machinery_name` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '关联设备名称快照',
    `record_date` date NOT NULL COMMENT '抄表日期',
    `prev_reading` decimal(18, 2) NULL DEFAULT NULL COMMENT '期初读数',
    `reading` decimal(18, 2) NULL DEFAULT NULL COMMENT '期末读数',
    `usage_amount` decimal(18, 4) NULL DEFAULT NULL COMMENT '本期用量',
    `price` decimal(18, 4) NULL DEFAULT NULL COMMENT '单价(元/计量单位)',
    `cost` decimal(18, 2) NULL DEFAULT NULL COMMENT '本期费用(元)',
    `source` int NULL DEFAULT 1 COMMENT '数据来源,字典 mes_em_record_source',
    `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,
    INDEX `idx_meter_date` (`meter_id`, `record_date`),
    INDEX `idx_record_date` (`record_date`),
    INDEX `idx_energy_type` (`energy_type`),
    INDEX `idx_workshop_id` (`workshop_id`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES èƒ½è€—抄表记录' ROW_FORMAT = Dynamic;
CREATE TABLE IF NOT EXISTS `mes_em_quota` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NOT NULL COMMENT '指标名称',
    `energy_type` int NOT NULL COMMENT '能源类型,字典 mes_em_energy_type',
    `object_type` int NOT NULL COMMENT '考核对象类型,字典 mes_em_quota_object_type',
    `object_id` bigint NOT NULL DEFAULT 0 COMMENT '考核对象编号(全厂为 0)',
    `object_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '考核对象名称',
    `period_type` int NOT NULL COMMENT '考核周期,字典 mes_em_quota_period',
    `quota_value` decimal(18, 4) NOT NULL COMMENT '定额值',
    `unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量单位',
    `warn_ratio` decimal(8, 2) NULL DEFAULT 10.00 COMMENT '预警比例(%)',
    `alarm_ratio` decimal(8, 2) NULL DEFAULT 20.00 COMMENT '严重预警比例(%)',
    `mom_warn_ratio` decimal(8, 2) NULL DEFAULT 30.00 COMMENT '环比异常阈值(%)',
    `yoy_warn_ratio` decimal(8, 2) NULL DEFAULT 30.00 COMMENT '同比异常阈值(%)',
    `status` int NOT NULL DEFAULT 1 COMMENT '状态,字典 mes_em_meter_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,
    INDEX `idx_object` (`object_type`, `object_id`),
    INDEX `idx_energy_type` (`energy_type`),
    INDEX `idx_period_type` (`period_type`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES èƒ½è€—考核指标' ROW_FORMAT = Dynamic;
CREATE TABLE IF NOT EXISTS `mes_em_alert` (
    `id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
    `alert_no` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '预警编号',
    `meter_id` bigint NOT NULL DEFAULT 0 COMMENT '计量点编号(定额预警为 0)',
    `meter_code` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量点编码快照',
    `meter_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量点/考核对象名称',
    `energy_type` int NULL DEFAULT NULL COMMENT '能源类型,字典 mes_em_energy_type',
    `alert_type` int NOT NULL COMMENT '预警类型,字典 mes_em_alert_type',
    `alert_level` int NOT NULL COMMENT '预警级别,字典 mes_em_alert_level',
    `alert_date` date NOT NULL COMMENT '预警日期',
    `object_type` int NULL DEFAULT NULL COMMENT '考核对象类型,字典 mes_em_quota_object_type',
    `object_id` bigint NOT NULL DEFAULT 0 COMMENT '考核对象编号(计量点级预警为 0)',
    `object_name` varchar(128) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '考核对象名称',
    `actual_value` decimal(18, 4) NULL DEFAULT NULL COMMENT '实际值',
    `threshold_value` decimal(18, 4) NULL DEFAULT NULL COMMENT '阈值',
    `unit` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '计量单位',
    `content` varchar(500) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '预警内容',
    `status` int NOT NULL DEFAULT 0 COMMENT '处理状态,字典 mes_em_alert_status',
    `handle_user_id` bigint NULL DEFAULT NULL COMMENT '处理人编号',
    `handle_user_name` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '处理人名称',
    `handle_time` datetime NULL DEFAULT NULL COMMENT '处理时间',
    `handle_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,
    INDEX `idx_lookup` (`meter_id`, `alert_type`, `alert_date`, `object_id`),
    INDEX `idx_alert_date` (`alert_date`),
    INDEX `idx_status` (`status`)
) ENGINE = InnoDB AUTO_INCREMENT = 1 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci COMMENT = 'MES èƒ½è€—预警' ROW_FORMAT = Dynamic;
-- 2. å­—典类型
INSERT INTO `system_dict_type` (`name`, `type`, `status`, `remark`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
('MES èƒ½æºç±»åž‹', 'mes_em_energy_type', 0, '电、水、燃油、天然气', '1', NOW(), '1', NOW(), b'0'),
('MES èƒ½è€—计量点状态', 'mes_em_meter_status', 0, '能耗计量点/考核指标状态', '1', NOW(), '1', NOW(), b'0'),
('MES èƒ½è€—数据来源', 'mes_em_record_source', 0, '抄表记录来源', '1', NOW(), '1', NOW(), b'0'),
('MES èƒ½è€—考核对象类型', 'mes_em_quota_object_type', 0, '全厂/车间/产线/设备', '1', NOW(), '1', NOW(), b'0'),
('MES èƒ½è€—考核周期', 'mes_em_quota_period', 0, '日定额/月定额', '1', NOW(), '1', NOW(), b'0'),
('MES èƒ½è€—预警类型', 'mes_em_alert_type', 0, '超定额/环比异常/同比异常/跑冒滴漏', '1', NOW(), '1', NOW(), b'0'),
('MES èƒ½è€—预警级别', 'mes_em_alert_level', 0, '提示/警告/严重', '1', NOW(), '1', NOW(), b'0'),
('MES èƒ½è€—预警处理状态', 'mes_em_alert_status', 0, '未处理/已处理', '1', NOW(), '1', NOW(), b'0');
-- 3. å­—典数据
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_em_energy_type', 0, 'primary', '', '用电量,单位 kWh', '1', NOW(), '1', NOW(), b'0'),
(2, 'æ°´', '2', 'mes_em_energy_type', 0, 'info', '', '用水量,单位 t', '1', NOW(), '1', NOW(), b'0'),
(3, '燃油', '3', 'mes_em_energy_type', 0, 'warning', '', '燃油用量,单位 L', '1', NOW(), '1', NOW(), b'0'),
(4, '天然气', '4', 'mes_em_energy_type', 0, 'success', '', '天然气用量,单位 m³', '1', NOW(), '1', NOW(), b'0'),
-- è®¡é‡ç‚¹çŠ¶æ€
(1, '启用', '1', 'mes_em_meter_status', 0, 'success', '', '启用', '1', NOW(), '1', NOW(), b'0'),
(2, '停用', '0', 'mes_em_meter_status', 0, 'info', '', '停用', '1', NOW(), '1', NOW(), b'0'),
-- æ•°æ®æ¥æº
(1, '手工录入', '1', 'mes_em_record_source', 0, 'primary', '', '人工抄表录入', '1', NOW(), '1', NOW(), b'0'),
(2, 'Excel å¯¼å…¥', '2', 'mes_em_record_source', 0, 'warning', '', '批量导入', '1', NOW(), '1', NOW(), b'0'),
(3, '自动采集', '3', 'mes_em_record_source', 0, 'success', '', '采集设备自动上报', '1', NOW(), '1', NOW(), b'0'),
-- è€ƒæ ¸å¯¹è±¡ç±»åž‹
(1, '全厂', '0', 'mes_em_quota_object_type', 0, 'info', '', '全厂级考核', '1', NOW(), '1', NOW(), b'0'),
(2, '车间', '1', 'mes_em_quota_object_type', 0, 'primary', '', '车间级考核', '1', NOW(), '1', NOW(), b'0'),
(3, '产线', '2', 'mes_em_quota_object_type', 0, 'warning', '', '产线级考核', '1', NOW(), '1', NOW(), b'0'),
(4, '设备', '3', 'mes_em_quota_object_type', 0, 'danger', '', '重点用能设备考核', '1', NOW(), '1', NOW(), b'0'),
-- è€ƒæ ¸å‘¨æœŸ
(1, '日', '1', 'mes_em_quota_period', 0, 'primary', '', '按日考核', '1', NOW(), '1', NOW(), b'0'),
(2, '月', '2', 'mes_em_quota_period', 0, 'success', '', '按月累计考核', '1', NOW(), '1', NOW(), b'0'),
-- é¢„警类型
(1, '超定额', '1', 'mes_em_alert_type', 0, 'danger', '', '实际用量超过考核定额', '1', NOW(), '1', NOW(), b'0'),
(2, '环比异常', '2', 'mes_em_alert_type', 0, 'warning', '', '较上一周期异常增长', '1', NOW(), '1', NOW(), b'0'),
(3, '同比异常', '3', 'mes_em_alert_type', 0, 'warning', '', '较去年同期异常增长', '1', NOW(), '1', NOW(), b'0'),
(4, '疑似跑冒滴漏', '4', 'mes_em_alert_type', 0, 'danger', '', '单日用量超过计量点设定阈值', '1', NOW(), '1', NOW(), b'0'),
-- é¢„警级别
(1, '提示', '1', 'mes_em_alert_level', 0, 'info', '', '提示级', '1', NOW(), '1', NOW(), b'0'),
(2, '警告', '2', 'mes_em_alert_level', 0, 'warning', '', '警告级', '1', NOW(), '1', NOW(), b'0'),
(3, '严重', '3', 'mes_em_alert_level', 0, 'danger', '', '严重级', '1', NOW(), '1', NOW(), b'0'),
-- é¢„警处理状态
(1, '未处理', '0', 'mes_em_alert_status', 0, 'danger', '', '待处理', '1', NOW(), '1', NOW(), b'0'),
(2, '已处理', '1', 'mes_em_alert_status', 0, 'success', '', '已处理', '1', NOW(), '1', NOW(), b'0');
-- 4. èœå•(父菜单 1065864 èƒ½è€—管理)
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
(1065865, '能耗总览', '', 2, 1, 1065864, 'overview', 'ep:data-analysis', 'energy/overview/index', 'EnergyOverview', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065866, '计量点管理', '', 2, 2, 1065864, 'meter', 'ep:odometer', 'energy/meter/index', 'EnergyMeter', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065867, '能耗数据采集', '', 2, 3, 1065864, 'record', 'ep:edit', 'energy/record/index', 'EnergyRecord', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065868, '考核指标', '', 2, 4, 1065864, 'quota', 'ep:aim', 'energy/quota/index', 'EnergyQuota', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065869, '能耗预警', '', 2, 5, 1065864, 'alert', 'ep:bell', 'energy/alert/index', 'EnergyAlert', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065870, '报表分析', '', 2, 6, 1065864, 'report', 'ep:document', 'energy/report/index', 'EnergyReport', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- 5. æŒ‰é’®æƒé™
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
-- èƒ½è€—总览
(1065871, '能耗总览查询', 'mes:em-overview:query', 3, 1, 1065865, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
-- è®¡é‡ç‚¹ç®¡ç†
(1065872, '计量点查询', 'mes:em-meter:query', 3, 1, 1065866, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065873, '计量点创建', 'mes:em-meter:create', 3, 2, 1065866, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065874, '计量点更新', 'mes:em-meter:update', 3, 3, 1065866, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065875, '计量点删除', 'mes:em-meter:delete', 3, 4, 1065866, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
-- èƒ½è€—数据采集
(1065876, '抄表记录查询', 'mes:em-record:query', 3, 1, 1065867, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065877, '抄表记录创建', 'mes:em-record:create', 3, 2, 1065867, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065878, '抄表记录更新', 'mes:em-record:update', 3, 3, 1065867, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065879, '抄表记录删除', 'mes:em-record:delete', 3, 4, 1065867, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
-- è€ƒæ ¸æŒ‡æ ‡
(1065880, '考核指标查询', 'mes:em-quota:query', 3, 1, 1065868, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065881, '考核指标创建', 'mes:em-quota:create', 3, 2, 1065868, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065882, '考核指标更新', 'mes:em-quota:update', 3, 3, 1065868, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065883, '考核指标删除', 'mes:em-quota:delete', 3, 4, 1065868, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
-- èƒ½è€—预警
(1065884, '能耗预警查询', 'mes:em-alert:query', 3, 1, 1065869, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
(1065885, '能耗预警处理', 'mes:em-alert:handle', 3, 2, 1065869, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0'),
-- æŠ¥è¡¨åˆ†æž
(1065886, '报表分析查询', 'mes:em-report:query', 3, 1, 1065870, '', '', '', '', 0, b'1', b'1', b'1', '1', NOW(), '1', NOW(), b'0');
-- 6. è¶…级管理员菜单授权(角色 1 ç»•过权限校验,这里补齐授权记录便于角色菜单树展示)
--    å…¶ä»–角色如需使用,请在「系统管理 - è§’色管理」中勾选「能耗管理」后保存。
INSERT INTO `system_role_menu` (`role_id`, `menu_id`, `creator`, `create_time`, `updater`, `update_time`, `deleted`)
VALUES
(1, 1065864, '1', NOW(), '1', NOW(), b'0'),
(1, 1065865, '1', NOW(), '1', NOW(), b'0'),
(1, 1065866, '1', NOW(), '1', NOW(), b'0'),
(1, 1065867, '1', NOW(), '1', NOW(), b'0'),
(1, 1065868, '1', NOW(), '1', NOW(), b'0'),
(1, 1065869, '1', NOW(), '1', NOW(), b'0'),
(1, 1065870, '1', NOW(), '1', NOW(), b'0'),
(1, 1065871, '1', NOW(), '1', NOW(), b'0'),
(1, 1065872, '1', NOW(), '1', NOW(), b'0'),
(1, 1065873, '1', NOW(), '1', NOW(), b'0'),
(1, 1065874, '1', NOW(), '1', NOW(), b'0'),
(1, 1065875, '1', NOW(), '1', NOW(), b'0'),
(1, 1065876, '1', NOW(), '1', NOW(), b'0'),
(1, 1065877, '1', NOW(), '1', NOW(), b'0'),
(1, 1065878, '1', NOW(), '1', NOW(), b'0'),
(1, 1065879, '1', NOW(), '1', NOW(), b'0'),
(1, 1065880, '1', NOW(), '1', NOW(), b'0'),
(1, 1065881, '1', NOW(), '1', NOW(), b'0'),
(1, 1065882, '1', NOW(), '1', NOW(), b'0'),
(1, 1065883, '1', NOW(), '1', NOW(), b'0'),
(1, 1065884, '1', NOW(), '1', NOW(), b'0'),
(1, 1065885, '1', NOW(), '1', NOW(), b'0'),
(1, 1065886, '1', NOW(), '1', NOW(), b'0');
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/MesEmAlertController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.alert;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertHandleReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.alert.MesEmAlertDO;
import cn.iocoder.yudao.module.mes.service.em.alert.MesEmAlertService;
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.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.Collections;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES èƒ½è€—预警")
@RestController
@RequestMapping("/mes/em/alert")
@Validated
public class MesEmAlertController {
    @Resource
    private MesEmAlertService alertService;
    @GetMapping("/page")
    @Operation(summary = "获得能耗预警分页")
    @PreAuthorize("@ss.hasPermission('mes:em-alert:query')")
    public CommonResult<PageResult<MesEmAlertRespVO>> getAlertPage(@Valid MesEmAlertPageReqVO pageReqVO) {
        PageResult<MesEmAlertDO> pageResult = alertService.getAlertPage(pageReqVO);
        return success(new PageResult<>(alertService.getAlertRespVOList(pageResult.getList()),
                pageResult.getTotal()));
    }
    @GetMapping("/get")
    @Operation(summary = "获得能耗预警")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:em-alert:query')")
    public CommonResult<MesEmAlertRespVO> getAlert(@RequestParam("id") Long id) {
        MesEmAlertDO alert = alertService.getAlert(id);
        if (alert == null) {
            return success(null);
        }
        return success(alertService.getAlertRespVOList(Collections.singletonList(alert)).get(0));
    }
    @PutMapping("/handle")
    @Operation(summary = "处理能耗预警")
    @PreAuthorize("@ss.hasPermission('mes:em-alert:handle')")
    public CommonResult<Boolean> handleAlert(@Valid @RequestBody MesEmAlertHandleReqVO handleReqVO) {
        alertService.handleAlert(handleReqVO);
        return success(true);
    }
    @PostMapping("/check")
    @Operation(summary = "重新检测指定日期区间的能耗预警")
    @PreAuthorize("@ss.hasPermission('mes:em-alert:handle')")
    public CommonResult<Integer> checkAlerts(
            @RequestParam("beginDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate beginDate,
            @RequestParam("endDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
        return success(alertService.checkRange(beginDate, endDate));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/vo/MesEmAlertHandleReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
@Schema(description = "管理后台 - MES èƒ½è€—预警处理 Request VO")
@Data
public class MesEmAlertHandleReqVO {
    @Schema(description = "预警编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "预警编号不能为空")
    private Long id;
    @Schema(description = "处理说明", example = "已安排检修,更换密封件")
    private String handleRemark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/vo/MesEmAlertPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.alert.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 org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - MES èƒ½è€—预警分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesEmAlertPageReqVO extends PageParam {
    @Schema(description = "计量点名称", example = "变压器车间总电表")
    private String meterName;
    @Schema(description = "能源类型", example = "1")
    private Integer energyType;
    @Schema(description = "预警类型", example = "1")
    private Integer alertType;
    @Schema(description = "预警级别", example = "2")
    private Integer alertLevel;
    @Schema(description = "处理状态", example = "0")
    private Integer status;
    @Schema(description = "预警日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate[] alertDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/alert/vo/MesEmAlertRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,95 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES èƒ½è€—预警 Response VO")
@Data
public class MesEmAlertRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "预警编号", example = "AL20261018-1024-1")
    private String alertNo;
    @Schema(description = "计量点编号", example = "1024")
    private Long meterId;
    @Schema(description = "计量点编码", example = "EM-DL-001")
    private String meterCode;
    @Schema(description = "计量点名称", example = "变压器车间总电表")
    private String meterName;
    @Schema(description = "能源类型", example = "1")
    private Integer energyType;
    @Schema(description = "能源类型名称", example = "电")
    private String energyTypeName;
    @Schema(description = "预警类型", example = "1")
    private Integer alertType;
    @Schema(description = "预警类型名称", example = "超定额")
    private String alertTypeName;
    @Schema(description = "预警级别", example = "2")
    private Integer alertLevel;
    @Schema(description = "预警级别名称", example = "警告")
    private String alertLevelName;
    @Schema(description = "预警日期")
    private LocalDate alertDate;
    @Schema(description = "考核对象类型", example = "1")
    private Integer objectType;
    @Schema(description = "考核对象类型名称", example = "车间")
    private String objectTypeName;
    @Schema(description = "考核对象编号", example = "7")
    private Long objectId;
    @Schema(description = "考核对象名称", example = "变压器制造车间")
    private String objectName;
    @Schema(description = "实际值", example = "1234")
    private BigDecimal actualValue;
    @Schema(description = "阈值", example = "1000")
    private BigDecimal thresholdValue;
    @Schema(description = "计量单位", example = "kWh")
    private String unit;
    @Schema(description = "预警内容")
    private String content;
    @Schema(description = "处理状态", example = "0")
    private Integer status;
    @Schema(description = "处理状态名称", example = "未处理")
    private String statusName;
    @Schema(description = "处理人编号")
    private Long handleUserId;
    @Schema(description = "处理人名称")
    private String handleUserName;
    @Schema(description = "处理时间")
    private LocalDateTime handleTime;
    @Schema(description = "处理说明")
    private String handleRemark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/MesEmMeterController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,107 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.meter;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmOptionRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
import cn.iocoder.yudao.module.mes.service.em.meter.MesEmMeterService;
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.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import java.util.List;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES èƒ½è€—计量点")
@RestController
@RequestMapping("/mes/em/meter")
@Validated
public class MesEmMeterController {
    @Resource
    private MesEmMeterService meterService;
    @PostMapping("/create")
    @Operation(summary = "创建能耗计量点")
    @PreAuthorize("@ss.hasPermission('mes:em-meter:create')")
    public CommonResult<Long> createMeter(@Valid @RequestBody MesEmMeterSaveReqVO createReqVO) {
        return success(meterService.createMeter(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新能耗计量点")
    @PreAuthorize("@ss.hasPermission('mes:em-meter:update')")
    public CommonResult<Boolean> updateMeter(@Valid @RequestBody MesEmMeterSaveReqVO updateReqVO) {
        meterService.updateMeter(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除能耗计量点")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:em-meter:delete')")
    public CommonResult<Boolean> deleteMeter(@RequestParam("id") Long id) {
        meterService.deleteMeter(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得能耗计量点")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:em-meter:query')")
    public CommonResult<MesEmMeterRespVO> getMeter(@RequestParam("id") Long id) {
        MesEmMeterDO meter = meterService.getMeter(id);
        if (meter == null) {
            return success(null);
        }
        return success(meterService.getMeterRespVOList(Collections.singletonList(meter)).get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得能耗计量点分页")
    @PreAuthorize("@ss.hasPermission('mes:em-meter:query')")
    public CommonResult<PageResult<MesEmMeterRespVO>> getMeterPage(@Valid MesEmMeterPageReqVO pageReqVO) {
        PageResult<MesEmMeterDO> pageResult = meterService.getMeterPage(pageReqVO);
        return success(new PageResult<>(meterService.getMeterRespVOList(pageResult.getList()),
                pageResult.getTotal()));
    }
    @GetMapping("/simple-list")
    @Operation(summary = "获得启用中的能耗计量点精简列表")
    @PreAuthorize("@ss.hasPermission('mes:em-meter:query')")
    public CommonResult<List<MesEmOptionRespVO>> getMeterSimpleList() {
        return success(meterService.getMeterSimpleList());
    }
    @GetMapping("/workshop-options")
    @Operation(summary = "获得车间下拉选项")
    @PreAuthorize("@ss.hasPermission('mes:em-meter:query')")
    public CommonResult<List<MesEmOptionRespVO>> getWorkshopOptions() {
        return success(meterService.getWorkshopOptions());
    }
    @GetMapping("/line-options")
    @Operation(summary = "获得产线下拉选项")
    @PreAuthorize("@ss.hasPermission('mes:em-meter:query')")
    public CommonResult<List<MesEmOptionRespVO>> getLineOptions() {
        return success(meterService.getLineOptions());
    }
    @GetMapping("/machinery-options")
    @Operation(summary = "获得设备下拉选项")
    @PreAuthorize("@ss.hasPermission('mes:em-meter:query')")
    public CommonResult<List<MesEmOptionRespVO>> getMachineryOptions() {
        return success(meterService.getMachineryOptions());
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/vo/MesEmMeterPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.meter.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;
@Schema(description = "管理后台 - MES èƒ½è€—计量点分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesEmMeterPageReqVO extends PageParam {
    @Schema(description = "计量点编码", example = "EM-DL-001")
    private String code;
    @Schema(description = "计量点名称", example = "变压器车间总电表")
    private String name;
    @Schema(description = "能源类型", example = "1")
    private Integer energyType;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "状态", example = "1")
    private Integer status;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/vo/MesEmMeterRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,86 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES èƒ½è€—计量点 Response VO")
@Data
public class MesEmMeterRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "计量点编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "EM-DL-001")
    private String code;
    @Schema(description = "计量点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "变压器车间总电表")
    private String name;
    @Schema(description = "能源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer energyType;
    @Schema(description = "能源类型名称", example = "电")
    private String energyTypeName;
    @Schema(description = "计量单位", example = "kWh")
    private String unit;
    @Schema(description = "表号", example = "DTSD341-001")
    private String meterNo;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "所属车间名称", example = "变压器制造车间")
    private String workshopName;
    @Schema(description = "所属产线编号", example = "1")
    private Long lineId;
    @Schema(description = "所属产线名称", example = "变压器生产线")
    private String lineName;
    @Schema(description = "关联设备编号", example = "1001")
    private Long machineryId;
    @Schema(description = "关联设备名称", example = "真空浇注罐")
    private String machineryName;
    @Schema(description = "倍率", example = "1")
    private BigDecimal ratio;
    @Schema(description = "初始读数", example = "0")
    private BigDecimal initialReading;
    @Schema(description = "折标煤系数(kgce/计量单位)", example = "0.1229")
    private BigDecimal coalFactor;
    @Schema(description = "日用量异常上限", example = "500")
    private BigDecimal leakThreshold;
    @Schema(description = "责任人编号", example = "1")
    private Long chargeUserId;
    @Schema(description = "责任人名称", example = "张三")
    private String chargeUserName;
    @Schema(description = "投用日期")
    private LocalDate installDate;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    private Integer status;
    @Schema(description = "状态名称", example = "启用")
    private String statusName;
    @Schema(description = "备注", example = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/meter/vo/MesEmMeterSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES èƒ½è€—计量点新增/修改 Request VO")
@Data
public class MesEmMeterSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "计量点编码", requiredMode = Schema.RequiredMode.REQUIRED, example = "EM-DL-001")
    @NotEmpty(message = "计量点编码不能为空")
    private String code;
    @Schema(description = "计量点名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "变压器车间总电表")
    @NotEmpty(message = "计量点名称不能为空")
    private String name;
    @Schema(description = "能源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "能源类型不能为空")
    private Integer energyType;
    @Schema(description = "计量单位", example = "kWh")
    private String unit;
    @Schema(description = "表号", example = "DTSD341-001")
    private String meterNo;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "所属产线编号", example = "1")
    private Long lineId;
    @Schema(description = "关联设备编号", example = "1001")
    private Long machineryId;
    @Schema(description = "倍率", example = "1")
    private BigDecimal ratio;
    @Schema(description = "初始读数", example = "0")
    private BigDecimal initialReading;
    @Schema(description = "折标煤系数(kgce/计量单位)", example = "0.1229")
    private BigDecimal coalFactor;
    @Schema(description = "日用量异常上限(超过触发疑似跑冒滴漏预警)", example = "500")
    private BigDecimal leakThreshold;
    @Schema(description = "责任人编号", example = "1")
    private Long chargeUserId;
    @Schema(description = "投用日期")
    private LocalDate installDate;
    @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/em/meter/vo/MesEmOptionRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,44 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
/**
 * èƒ½è€—模块下拉选项 VO
 *
 * åŒæ—¶ç”¨äºŽè½¦é—´ã€äº§çº¿ã€è®¾å¤‡ã€è®¡é‡ç‚¹çš„下拉选择。
 *
 * @author è¶…级管理员
 */
@Schema(description = "管理后台 - MES èƒ½è€—下拉选项 Response VO")
@Data
public class MesEmOptionRespVO {
    @Schema(description = "编号")
    private Long id;
    @Schema(description = "编码")
    private String code;
    @Schema(description = "名称")
    private String name;
    @Schema(description = "能源类型(计量点选项使用)")
    private Integer energyType;
    @Schema(description = "计量单位(计量点选项使用)")
    private String unit;
    @Schema(description = "所属车间编号")
    private Long workshopId;
    @Schema(description = "所属车间名称")
    private String workshopName;
    @Schema(description = "所属产线编号")
    private Long lineId;
    @Schema(description = "所属产线名称")
    private String lineName;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/MesEmQuotaController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,77 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.quota;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.quota.MesEmQuotaDO;
import cn.iocoder.yudao.module.mes.service.em.quota.MesEmQuotaService;
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.validation.Valid;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.util.Collections;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES èƒ½è€—考核指标")
@RestController
@RequestMapping("/mes/em/quota")
@Validated
public class MesEmQuotaController {
    @Resource
    private MesEmQuotaService quotaService;
    @PostMapping("/create")
    @Operation(summary = "创建能耗考核指标")
    @PreAuthorize("@ss.hasPermission('mes:em-quota:create')")
    public CommonResult<Long> createQuota(@Valid @RequestBody MesEmQuotaSaveReqVO createReqVO) {
        return success(quotaService.createQuota(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新能耗考核指标")
    @PreAuthorize("@ss.hasPermission('mes:em-quota:update')")
    public CommonResult<Boolean> updateQuota(@Valid @RequestBody MesEmQuotaSaveReqVO updateReqVO) {
        quotaService.updateQuota(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除能耗考核指标")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:em-quota:delete')")
    public CommonResult<Boolean> deleteQuota(@RequestParam("id") Long id) {
        quotaService.deleteQuota(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得能耗考核指标")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:em-quota:query')")
    public CommonResult<MesEmQuotaRespVO> getQuota(@RequestParam("id") Long id) {
        MesEmQuotaDO quota = quotaService.getQuota(id);
        if (quota == null) {
            return success(null);
        }
        return success(quotaService.getQuotaRespVOList(Collections.singletonList(quota)).get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得能耗考核指标分页")
    @PreAuthorize("@ss.hasPermission('mes:em-quota:query')")
    public CommonResult<PageResult<MesEmQuotaRespVO>> getQuotaPage(@Valid MesEmQuotaPageReqVO pageReqVO) {
        PageResult<MesEmQuotaDO> pageResult = quotaService.getQuotaPage(pageReqVO);
        return success(new PageResult<>(quotaService.getQuotaRespVOList(pageResult.getList()),
                pageResult.getTotal()));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/vo/MesEmQuotaPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,30 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.quota.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;
@Schema(description = "管理后台 - MES èƒ½è€—考核指标分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesEmQuotaPageReqVO extends PageParam {
    @Schema(description = "指标名称", example = "变压器车间用电日定额")
    private String name;
    @Schema(description = "能源类型", example = "1")
    private Integer energyType;
    @Schema(description = "考核对象类型", example = "1")
    private Integer objectType;
    @Schema(description = "考核周期", example = "1")
    private Integer periodType;
    @Schema(description = "状态", example = "1")
    private Integer status;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/vo/MesEmQuotaRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,73 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES èƒ½è€—考核指标 Response VO")
@Data
public class MesEmQuotaRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "变压器车间用电日定额")
    private String name;
    @Schema(description = "能源类型", example = "1")
    private Integer energyType;
    @Schema(description = "能源类型名称", example = "电")
    private String energyTypeName;
    @Schema(description = "考核对象类型", example = "1")
    private Integer objectType;
    @Schema(description = "考核对象类型名称", example = "车间")
    private String objectTypeName;
    @Schema(description = "考核对象编号", example = "7")
    private Long objectId;
    @Schema(description = "考核对象名称", example = "变压器制造车间")
    private String objectName;
    @Schema(description = "考核周期", example = "1")
    private Integer periodType;
    @Schema(description = "考核周期名称", example = "日")
    private String periodTypeName;
    @Schema(description = "定额值", example = "1000")
    private BigDecimal quotaValue;
    @Schema(description = "计量单位", example = "kWh")
    private String unit;
    @Schema(description = "预警比例(%)", example = "10")
    private BigDecimal warnRatio;
    @Schema(description = "严重预警比例(%)", example = "20")
    private BigDecimal alarmRatio;
    @Schema(description = "环比异常阈值(%)", example = "30")
    private BigDecimal momWarnRatio;
    @Schema(description = "同比异常阈值(%)", example = "30")
    private BigDecimal yoyWarnRatio;
    @Schema(description = "状态", example = "1")
    private Integer status;
    @Schema(description = "状态名称", example = "启用")
    private String statusName;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/quota/vo/MesEmQuotaSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,64 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.DecimalMin;
import jakarta.validation.constraints.NotEmpty;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
@Schema(description = "管理后台 - MES èƒ½è€—考核指标新增/修改 Request VO")
@Data
public class MesEmQuotaSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "指标名称", requiredMode = Schema.RequiredMode.REQUIRED, example = "变压器车间用电日定额")
    @NotEmpty(message = "指标名称不能为空")
    private String name;
    @Schema(description = "能源类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "能源类型不能为空")
    private Integer energyType;
    @Schema(description = "考核对象类型", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "考核对象类型不能为空")
    private Integer objectType;
    @Schema(description = "考核对象编号(全厂时为空)", example = "7")
    private Long objectId;
    @Schema(description = "考核周期", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "考核周期不能为空")
    private Integer periodType;
    @Schema(description = "定额值", requiredMode = Schema.RequiredMode.REQUIRED, example = "1000")
    @NotNull(message = "定额值不能为空")
    @DecimalMin(value = "0", message = "定额值不能小于 0")
    private BigDecimal quotaValue;
    @Schema(description = "计量单位", example = "kWh")
    private String unit;
    @Schema(description = "预警比例(%)", example = "10")
    private BigDecimal warnRatio;
    @Schema(description = "严重预警比例(%)", example = "20")
    private BigDecimal alarmRatio;
    @Schema(description = "环比异常阈值(%)", example = "30")
    private BigDecimal momWarnRatio;
    @Schema(description = "同比异常阈值(%)", example = "30")
    private BigDecimal yoyWarnRatio;
    @Schema(description = "状态", requiredMode = Schema.RequiredMode.REQUIRED, example = "1")
    @NotNull(message = "状态不能为空")
    private Integer status;
    @Schema(description = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/MesEmRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.record;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmLastReadingRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.record.MesEmRecordDO;
import cn.iocoder.yudao.module.mes.service.em.record.MesEmRecordService;
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.validation.Valid;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*;
import java.time.LocalDate;
import java.util.Collections;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES èƒ½è€—抄表记录")
@RestController
@RequestMapping("/mes/em/record")
@Validated
public class MesEmRecordController {
    @Resource
    private MesEmRecordService recordService;
    @PostMapping("/create")
    @Operation(summary = "创建能耗抄表记录")
    @PreAuthorize("@ss.hasPermission('mes:em-record:create')")
    public CommonResult<Long> createRecord(@Valid @RequestBody MesEmRecordSaveReqVO createReqVO) {
        return success(recordService.createRecord(createReqVO));
    }
    @PutMapping("/update")
    @Operation(summary = "更新能耗抄表记录")
    @PreAuthorize("@ss.hasPermission('mes:em-record:update')")
    public CommonResult<Boolean> updateRecord(@Valid @RequestBody MesEmRecordSaveReqVO updateReqVO) {
        recordService.updateRecord(updateReqVO);
        return success(true);
    }
    @DeleteMapping("/delete")
    @Operation(summary = "删除能耗抄表记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:em-record:delete')")
    public CommonResult<Boolean> deleteRecord(@RequestParam("id") Long id) {
        recordService.deleteRecord(id);
        return success(true);
    }
    @GetMapping("/get")
    @Operation(summary = "获得能耗抄表记录")
    @Parameter(name = "id", description = "编号", required = true)
    @PreAuthorize("@ss.hasPermission('mes:em-record:query')")
    public CommonResult<MesEmRecordRespVO> getRecord(@RequestParam("id") Long id) {
        MesEmRecordDO record = recordService.getRecord(id);
        if (record == null) {
            return success(null);
        }
        return success(recordService.getRecordRespVOList(Collections.singletonList(record)).get(0));
    }
    @GetMapping("/page")
    @Operation(summary = "获得能耗抄表记录分页")
    @PreAuthorize("@ss.hasPermission('mes:em-record:query')")
    public CommonResult<PageResult<MesEmRecordRespVO>> getRecordPage(@Valid MesEmRecordPageReqVO pageReqVO) {
        PageResult<MesEmRecordDO> pageResult = recordService.getRecordPage(pageReqVO);
        return success(new PageResult<>(recordService.getRecordRespVOList(pageResult.getList()),
                pageResult.getTotal()));
    }
    @GetMapping("/last-reading")
    @Operation(summary = "获得计量点上期读数", description = "用于新增抄表记录时自动带入期初读数")
    @PreAuthorize("@ss.hasPermission('mes:em-record:query')")
    public CommonResult<MesEmLastReadingRespVO> getLastReading(
            @RequestParam("meterId") Long meterId,
            @RequestParam("recordDate") @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate recordDate,
            @RequestParam(value = "excludeId", required = false) Long excludeId) {
        return success(recordService.getLastReading(meterId, recordDate, excludeId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmLastReadingRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.record.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.math.BigDecimal;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES èƒ½è€—上期读数 Response VO")
@Data
@NoArgsConstructor
@AllArgsConstructor
public class MesEmLastReadingRespVO {
    @Schema(description = "上期读数", example = "1000")
    private BigDecimal prevReading;
    @Schema(description = "上期抄表日期")
    private LocalDate lastDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmRecordPageReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.record.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 org.springframework.format.annotation.DateTimeFormat;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.util.date.DateUtils.FORMAT_YEAR_MONTH_DAY;
@Schema(description = "管理后台 - MES èƒ½è€—抄表记录分页 Request VO")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
public class MesEmRecordPageReqVO extends PageParam {
    @Schema(description = "计量点编号", example = "1024")
    private Long meterId;
    @Schema(description = "计量点名称", example = "变压器车间总电表")
    private String meterName;
    @Schema(description = "能源类型", example = "1")
    private Integer energyType;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "数据来源", example = "1")
    private Integer source;
    @Schema(description = "抄表日期")
    @DateTimeFormat(pattern = FORMAT_YEAR_MONTH_DAY)
    private LocalDate[] recordDate;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmRecordRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,89 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.record.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
@Schema(description = "管理后台 - MES èƒ½è€—抄表记录 Response VO")
@Data
public class MesEmRecordRespVO {
    @Schema(description = "编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long id;
    @Schema(description = "计量点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    private Long meterId;
    @Schema(description = "计量点编码", example = "EM-DL-001")
    private String meterCode;
    @Schema(description = "计量点名称", example = "变压器车间总电表")
    private String meterName;
    @Schema(description = "能源类型", example = "1")
    private Integer energyType;
    @Schema(description = "能源类型名称", example = "电")
    private String energyTypeName;
    @Schema(description = "计量单位", example = "kWh")
    private String unit;
    @Schema(description = "倍率", example = "1")
    private BigDecimal ratio;
    @Schema(description = "折标煤系数", example = "0.1229")
    private BigDecimal coalFactor;
    @Schema(description = "所属车间编号", example = "7")
    private Long workshopId;
    @Schema(description = "所属车间名称", example = "变压器制造车间")
    private String workshopName;
    @Schema(description = "所属产线编号", example = "1")
    private Long lineId;
    @Schema(description = "所属产线名称", example = "变压器生产线")
    private String lineName;
    @Schema(description = "关联设备编号", example = "1001")
    private Long machineryId;
    @Schema(description = "关联设备名称", example = "真空浇注罐")
    private String machineryName;
    @Schema(description = "抄表日期", requiredMode = Schema.RequiredMode.REQUIRED)
    private LocalDate recordDate;
    @Schema(description = "期初读数", example = "1000")
    private BigDecimal prevReading;
    @Schema(description = "期末读数", example = "1200")
    private BigDecimal reading;
    @Schema(description = "实际用量", example = "200")
    private BigDecimal usageAmount;
    @Schema(description = "单价", example = "0.68")
    private BigDecimal price;
    @Schema(description = "费用", example = "136")
    private BigDecimal cost;
    @Schema(description = "数据来源", example = "1")
    private Integer source;
    @Schema(description = "数据来源名称", example = "手工录入")
    private String sourceName;
    @Schema(description = "备注")
    private String remark;
    @Schema(description = "创建时间")
    private LocalDateTime createTime;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/record/vo/MesEmRecordSaveReqVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.record.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import jakarta.validation.constraints.NotNull;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
@Schema(description = "管理后台 - MES èƒ½è€—抄表记录新增/修改 Request VO")
@Data
public class MesEmRecordSaveReqVO {
    @Schema(description = "编号", example = "1024")
    private Long id;
    @Schema(description = "计量点编号", requiredMode = Schema.RequiredMode.REQUIRED, example = "1024")
    @NotNull(message = "计量点不能为空")
    private Long meterId;
    @Schema(description = "抄表日期", requiredMode = Schema.RequiredMode.REQUIRED)
    @NotNull(message = "抄表日期不能为空")
    private LocalDate recordDate;
    @Schema(description = "期初读数(为空时取上一条记录读数或计量点初始读数)", example = "1000")
    private BigDecimal prevReading;
    @Schema(description = "期末读数", requiredMode = Schema.RequiredMode.REQUIRED, example = "1200")
    @NotNull(message = "期末读数不能为空")
    private BigDecimal reading;
    @Schema(description = "单价(元/计量单位)", example = "0.68")
    private BigDecimal price;
    @Schema(description = "数据来源", example = "1")
    private Integer source;
    @Schema(description = "备注", example = "备注")
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/statistics/MesEmStatisticsController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.statistics;
import cn.iocoder.yudao.framework.common.pojo.CommonResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo.MesEmOverviewRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo.MesEmReportRespVO;
import cn.iocoder.yudao.module.mes.service.em.statistics.MesEmStatisticsService;
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 org.springframework.format.annotation.DateTimeFormat;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import static cn.iocoder.yudao.framework.common.pojo.CommonResult.success;
@Tag(name = "管理后台 - MES èƒ½è€—统计")
@RestController
@RequestMapping("/mes/em/statistics")
@Validated
public class MesEmStatisticsController {
    @Resource
    private MesEmStatisticsService statisticsService;
    @GetMapping("/overview")
    @Operation(summary = "获得能耗总览数据")
    @PreAuthorize("@ss.hasPermission('mes:em-overview:query')")
    public CommonResult<MesEmOverviewRespVO> getOverview(
            @RequestParam(value = "beginDate", required = false)
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate beginDate,
            @RequestParam(value = "endDate", required = false)
            @DateTimeFormat(iso = DateTimeFormat.ISO.DATE) LocalDate endDate) {
        return success(statisticsService.getOverview(beginDate, endDate));
    }
    @GetMapping("/report")
    @Operation(summary = "获得月度能耗报表(含同比、环比)")
    @Parameter(name = "period", description = "月份 yyyy-MM", example = "2026-10")
    @PreAuthorize("@ss.hasPermission('mes:em-report:query')")
    public CommonResult<MesEmReportRespVO> getReport(
            @RequestParam(value = "period", required = false) String period) {
        return success(statisticsService.getReport(period));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/statistics/vo/MesEmOverviewRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,161 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Schema(description = "管理后台 - MES èƒ½è€—总览 Response VO")
@Data
public class MesEmOverviewRespVO {
    @Schema(description = "综合能耗(吨标煤)", example = "12.34")
    private BigDecimal totalTce;
    @Schema(description = "总费用(元)", example = "123456.78")
    private BigDecimal totalCost;
    @Schema(description = "计量点数量", example = "20")
    private Integer meterCount;
    @Schema(description = "未处理预警数量", example = "3")
    private Integer pendingAlertCount;
    @Schema(description = "能源类型统计")
    private List<TypeStat> typeStats;
    @Schema(description = "车间能耗统计")
    private List<WorkshopStat> workshopStats;
    @Schema(description = "能耗趋势")
    private List<TrendPoint> trend;
    @Schema(description = "重点用能设备排行")
    private List<MeterStat> topMeters;
    @Schema(description = "最近预警")
    private List<RecentAlert> recentAlerts;
    @Data
    public static class TypeStat {
        @Schema(description = "能源类型")
        private Integer energyType;
        @Schema(description = "能源类型名称")
        private String energyTypeName;
        @Schema(description = "用量")
        private BigDecimal usage;
        @Schema(description = "计量单位")
        private String unit;
        @Schema(description = "费用")
        private BigDecimal cost;
        @Schema(description = "折标煤(吨)")
        private BigDecimal tce;
    }
    @Data
    public static class WorkshopStat {
        @Schema(description = "车间编号")
        private Long workshopId;
        @Schema(description = "车间名称")
        private String workshopName;
        @Schema(description = "折标煤(吨)")
        private BigDecimal tce;
        @Schema(description = "费用")
        private BigDecimal cost;
    }
    @Data
    public static class TrendPoint {
        @Schema(description = "日期")
        private String date;
        @Schema(description = "综合能耗(吨标煤)")
        private BigDecimal tce;
        @Schema(description = "费用")
        private BigDecimal cost;
        @Schema(description = "用电量")
        private BigDecimal electricity;
        @Schema(description = "用水量")
        private BigDecimal water;
        @Schema(description = "燃油用量")
        private BigDecimal fuel;
        @Schema(description = "天然气用量")
        private BigDecimal gas;
    }
    @Data
    public static class MeterStat {
        @Schema(description = "计量点编号")
        private Long meterId;
        @Schema(description = "计量点名称")
        private String meterName;
        @Schema(description = "能源类型")
        private Integer energyType;
        @Schema(description = "能源类型名称")
        private String energyTypeName;
        @Schema(description = "用量")
        private BigDecimal usage;
        @Schema(description = "计量单位")
        private String unit;
        @Schema(description = "费用")
        private BigDecimal cost;
        @Schema(description = "折标煤(吨)")
        private BigDecimal tce;
    }
    @Data
    public static class RecentAlert {
        @Schema(description = "预警编号")
        private Long id;
        @Schema(description = "预警内容")
        private String content;
        @Schema(description = "预警级别")
        private Integer alertLevel;
        @Schema(description = "预警级别名称")
        private String alertLevelName;
        @Schema(description = "预警日期")
        private String alertDate;
        @Schema(description = "处理状态")
        private Integer status;
        @Schema(description = "处理状态名称")
        private String statusName;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/controller/admin/em/statistics/vo/MesEmReportRespVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
package cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
@Schema(description = "管理后台 - MES èƒ½è€—报表 Response VO")
@Data
public class MesEmReportRespVO {
    @Schema(description = "报表月份", example = "2026-10")
    private String period;
    @Schema(description = "开始日期")
    private LocalDate beginDate;
    @Schema(description = "结束日期")
    private LocalDate endDate;
    @Schema(description = "综合能耗(吨标煤)")
    private BigDecimal totalTce;
    @Schema(description = "总费用")
    private BigDecimal totalCost;
    @Schema(description = "上月综合能耗")
    private BigDecimal lastMonthTce;
    @Schema(description = "环比(%)")
    private BigDecimal momRate;
    @Schema(description = "去年同月综合能耗")
    private BigDecimal lastYearTce;
    @Schema(description = "同比(%)")
    private BigDecimal yoyRate;
    @Schema(description = "按能源类型统计")
    private List<TypeRow> typeRows;
    @Schema(description = "按车间统计")
    private List<WorkshopRow> workshopRows;
    @Data
    public static class TypeRow {
        @Schema(description = "能源类型")
        private Integer energyType;
        @Schema(description = "能源类型名称")
        private String energyTypeName;
        @Schema(description = "本月用量")
        private BigDecimal usage;
        @Schema(description = "计量单位")
        private String unit;
        @Schema(description = "本月费用")
        private BigDecimal cost;
        @Schema(description = "本月折标煤(吨)")
        private BigDecimal tce;
        @Schema(description = "上月用量")
        private BigDecimal lastMonthUsage;
        @Schema(description = "环比(%)")
        private BigDecimal momRate;
        @Schema(description = "去年同月用量")
        private BigDecimal lastYearUsage;
        @Schema(description = "同比(%)")
        private BigDecimal yoyRate;
    }
    @Data
    public static class WorkshopRow {
        @Schema(description = "车间编号")
        private Long workshopId;
        @Schema(description = "车间名称")
        private String workshopName;
        @Schema(description = "本月费用")
        private BigDecimal cost;
        @Schema(description = "本月折标煤(吨)")
        private BigDecimal tce;
        @Schema(description = "上月折标煤(吨)")
        private BigDecimal lastMonthTce;
        @Schema(description = "环比(%)")
        private BigDecimal momRate;
        @Schema(description = "去年同月折标煤(吨)")
        private BigDecimal lastYearTce;
        @Schema(description = "同比(%)")
        private BigDecimal yoyRate;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/alert/MesEmAlertDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.em.alert;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * MES èƒ½è€—预警记录 DO
 *
 * @author è¶…级管理员
 */
@TableName("mes_em_alert")
@KeySequence("mes_em_alert_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesEmAlertDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * é¢„警编号
     */
    private String alertNo;
    /**
     * è®¡é‡ç‚¹ç¼–号(定额预警为 0)
     */
    private Long meterId;
    /**
     * è®¡é‡ç‚¹ç¼–码
     */
    private String meterCode;
    /**
     * è®¡é‡ç‚¹åç§°
     */
    private String meterName;
    /**
     * èƒ½æºç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmEnergyTypeEnum}
     */
    private Integer energyType;
    /**
     * é¢„警类型
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmAlertTypeEnum}
     */
    private Integer alertType;
    /**
     * é¢„警级别
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmAlertLevelEnum}
     */
    private Integer alertLevel;
    /**
     * é¢„警日期
     */
    private LocalDate alertDate;
    /**
     * è€ƒæ ¸å¯¹è±¡ç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmQuotaObjectTypeEnum}
     */
    private Integer objectType;
    /**
     * è€ƒæ ¸å¯¹è±¡ç¼–号(非定额预警为 0)
     */
    private Long objectId;
    /**
     * è€ƒæ ¸å¯¹è±¡åç§°
     */
    private String objectName;
    /**
     * å®žé™…值
     */
    private BigDecimal actualValue;
    /**
     * é˜ˆå€¼
     */
    private BigDecimal thresholdValue;
    /**
     * è®¡é‡å•位
     */
    private String unit;
    /**
     * é¢„警内容
     */
    private String content;
    /**
     * å¤„理状态
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmAlertStatusEnum}
     */
    private Integer status;
    /**
     * å¤„理人编号
     */
    private Long handleUserId;
    /**
     * å¤„理人名称
     */
    private String handleUserName;
    /**
     * å¤„理时间
     */
    private LocalDateTime handleTime;
    /**
     * å¤„理说明
     */
    private String handleRemark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/meter/MesEmMeterDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,115 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.em.meter;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * MES èƒ½è€—计量点 DO
 *
 * ä¸€ä¸ªè®¡é‡ç‚¹å¯ä»¥æŒ‚在车间、产线或重点高耗能设备上,用于采集某一类能源的用量。
 *
 * @author è¶…级管理员
 */
@TableName("mes_em_meter")
@KeySequence("mes_em_meter_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesEmMeterDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * è®¡é‡ç‚¹ç¼–码
     */
    private String code;
    /**
     * è®¡é‡ç‚¹åç§°
     */
    private String name;
    /**
     * èƒ½æºç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmEnergyTypeEnum}
     */
    private Integer energyType;
    /**
     * è®¡é‡å•位
     */
    private String unit;
    /**
     * è¡¨å·ï¼ˆçŽ°åœºè¡¨è®¡ç¼–å·ï¼‰
     */
    private String meterNo;
    /**
     * æ‰€å±žè½¦é—´ç¼–号
     */
    private Long workshopId;
    /**
     * æ‰€å±žè½¦é—´åç§°
     */
    private String workshopName;
    /**
     * æ‰€å±žäº§çº¿ç¼–号
     */
    private Long lineId;
    /**
     * æ‰€å±žäº§çº¿åç§°
     */
    private String lineName;
    /**
     * å…³è”设备编号
     */
    private Long machineryId;
    /**
     * å…³è”设备名称
     */
    private String machineryName;
    /**
     * å€çŽ‡ï¼ˆè¯»æ•° Ã— å€çއ = å®žé™…用量)
     */
    private BigDecimal ratio;
    /**
     * åˆå§‹è¯»æ•°
     */
    private BigDecimal initialReading;
    /**
     * æŠ˜æ ‡ç…¤ç³»æ•°ï¼ˆkgce/计量单位)
     */
    private BigDecimal coalFactor;
    /**
     * æ—¥ç”¨é‡å¼‚常上限(超过后触发疑似跑冒滴漏预警,为空表示不检测)
     */
    private BigDecimal leakThreshold;
    /**
     * è´£ä»»äººç¼–号
     */
    private Long chargeUserId;
    /**
     * æŠ•用日期
     */
    private LocalDate installDate;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmMeterStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/quota/MesEmQuotaDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,98 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.em.quota;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
/**
 * MES èƒ½è€—考核指标 DO
 *
 * æŒ‰å…¨åŽ‚ / è½¦é—´ / äº§çº¿ / è®¾å¤‡é…ç½®æ—¥æˆ–月的能耗定额,并设置超限预警比例。
 *
 * @author è¶…级管理员
 */
@TableName("mes_em_quota")
@KeySequence("mes_em_quota_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesEmQuotaDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * æŒ‡æ ‡åç§°
     */
    private String name;
    /**
     * èƒ½æºç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmEnergyTypeEnum}
     */
    private Integer energyType;
    /**
     * è€ƒæ ¸å¯¹è±¡ç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmQuotaObjectTypeEnum}
     */
    private Integer objectType;
    /**
     * è€ƒæ ¸å¯¹è±¡ç¼–号(全厂时为空)
     */
    private Long objectId;
    /**
     * è€ƒæ ¸å¯¹è±¡åç§°ï¼ˆä¿å­˜æ—¶å†—余)
     */
    private String objectName;
    /**
     * è€ƒæ ¸å‘¨æœŸ
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmQuotaPeriodEnum}
     */
    private Integer periodType;
    /**
     * å®šé¢å€¼
     */
    private BigDecimal quotaValue;
    /**
     * è®¡é‡å•位
     */
    private String unit;
    /**
     * é¢„警比例(超出定额多少百分比触发警告)
     */
    private BigDecimal warnRatio;
    /**
     * ä¸¥é‡é¢„警比例(超出定额多少百分比触发严重预警)
     */
    private BigDecimal alarmRatio;
    /**
     * çŽ¯æ¯”å¼‚å¸¸é˜ˆå€¼ï¼ˆ%)
     */
    private BigDecimal momWarnRatio;
    /**
     * åŒæ¯”异常阈值(%)
     */
    private BigDecimal yoyWarnRatio;
    /**
     * çŠ¶æ€
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmMeterStatusEnum}
     */
    private Integer status;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/dataobject/em/record/MesEmRecordDO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
package cn.iocoder.yudao.module.mes.dal.dataobject.em.record;
import cn.iocoder.yudao.framework.mybatis.core.dataobject.BaseDO;
import com.baomidou.mybatisplus.annotation.KeySequence;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.*;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * MES èƒ½è€—抄表记录 DO
 *
 * è®°å½•保存时会把计量点的名称、车间/产线/设备信息冗余进来,作为历史快照,
 * åŽç»­ä¸»æ•°æ®æ”¹åä¸ä¼šå½±å“åŽ†å²æŠ¥è¡¨ã€‚
 *
 * @author è¶…级管理员
 */
@TableName("mes_em_record")
@KeySequence("mes_em_record_seq")
@Data
@EqualsAndHashCode(callSuper = true)
@ToString(callSuper = true)
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class MesEmRecordDO extends BaseDO {
    /**
     * ç¼–号
     */
    @TableId
    private Long id;
    /**
     * è®¡é‡ç‚¹ç¼–号
     */
    private Long meterId;
    /**
     * è®¡é‡ç‚¹ç¼–码
     */
    private String meterCode;
    /**
     * è®¡é‡ç‚¹åç§°
     */
    private String meterName;
    /**
     * èƒ½æºç±»åž‹
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmEnergyTypeEnum}
     */
    private Integer energyType;
    /**
     * è®¡é‡å•位
     */
    private String unit;
    /**
     * å€çއ
     */
    private BigDecimal ratio;
    /**
     * æŠ˜æ ‡ç…¤ç³»æ•°ï¼ˆkgce/计量单位)
     */
    private BigDecimal coalFactor;
    /**
     * æ‰€å±žè½¦é—´ç¼–号
     */
    private Long workshopId;
    /**
     * æ‰€å±žè½¦é—´åç§°
     */
    private String workshopName;
    /**
     * æ‰€å±žäº§çº¿ç¼–号
     */
    private Long lineId;
    /**
     * æ‰€å±žäº§çº¿åç§°
     */
    private String lineName;
    /**
     * å…³è”设备编号
     */
    private Long machineryId;
    /**
     * å…³è”设备名称
     */
    private String machineryName;
    /**
     * æŠ„表日期
     */
    private LocalDate recordDate;
    /**
     * æœŸåˆè¯»æ•°
     */
    private BigDecimal prevReading;
    /**
     * æœŸæœ«è¯»æ•°
     */
    private BigDecimal reading;
    /**
     * å®žé™…用量((期末读数 - æœŸåˆè¯»æ•°) Ã— å€çŽ‡ï¼‰
     */
    private BigDecimal usageAmount;
    /**
     * å•价(元/计量单位)
     */
    private BigDecimal price;
    /**
     * è´¹ç”¨ï¼ˆå…ƒï¼‰
     */
    private BigDecimal cost;
    /**
     * æ•°æ®æ¥æº
     *
     * æžšä¸¾ {@link cn.iocoder.yudao.module.mes.enums.em.MesEmRecordSourceEnum}
     */
    private Integer source;
    /**
     * å¤‡æ³¨
     */
    private String remark;
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/alert/MesEmAlertMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package cn.iocoder.yudao.module.mes.dal.mysql.em.alert;
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.em.alert.vo.MesEmAlertPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.alert.MesEmAlertDO;
import org.apache.ibatis.annotations.Mapper;
import java.time.LocalDate;
import java.util.List;
/**
 * MES èƒ½è€—预警 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesEmAlertMapper extends BaseMapperX<MesEmAlertDO> {
    default PageResult<MesEmAlertDO> selectPage(MesEmAlertPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesEmAlertDO>()
                .likeIfPresent(MesEmAlertDO::getMeterName, reqVO.getMeterName())
                .eqIfPresent(MesEmAlertDO::getEnergyType, reqVO.getEnergyType())
                .eqIfPresent(MesEmAlertDO::getAlertType, reqVO.getAlertType())
                .eqIfPresent(MesEmAlertDO::getAlertLevel, reqVO.getAlertLevel())
                .eqIfPresent(MesEmAlertDO::getStatus, reqVO.getStatus())
                .betweenIfPresent(MesEmAlertDO::getAlertDate, reqVO.getAlertDate())
                .orderByDesc(MesEmAlertDO::getAlertDate)
                .orderByDesc(MesEmAlertDO::getId));
    }
    default MesEmAlertDO selectByUnique(Long meterId, Integer alertType, LocalDate alertDate, Long objectId) {
        return selectOne(new LambdaQueryWrapperX<MesEmAlertDO>()
                .eq(MesEmAlertDO::getMeterId, meterId)
                .eq(MesEmAlertDO::getAlertType, alertType)
                .eq(MesEmAlertDO::getAlertDate, alertDate)
                .eq(MesEmAlertDO::getObjectId, objectId));
    }
    default List<MesEmAlertDO> selectRecent(LocalDate begin, LocalDate end, int limit) {
        return selectList(new LambdaQueryWrapperX<MesEmAlertDO>()
                .ge(MesEmAlertDO::getAlertDate, begin)
                .le(MesEmAlertDO::getAlertDate, end)
                .orderByDesc(MesEmAlertDO::getAlertDate)
                .orderByDesc(MesEmAlertDO::getId)
                .last("LIMIT " + limit));
    }
    default Long selectPendingCount() {
        return selectCount(new LambdaQueryWrapperX<MesEmAlertDO>()
                .eq(MesEmAlertDO::getStatus, 0));
    }
    default Long selectCountByDateRange(LocalDate begin, LocalDate end) {
        return selectCount(new LambdaQueryWrapperX<MesEmAlertDO>()
                .between(MesEmAlertDO::getAlertDate, begin, end));
    }
    default int deleteByMeterIdAndDate(Long meterId, LocalDate alertDate) {
        return delete(new LambdaQueryWrapperX<MesEmAlertDO>()
                .eq(MesEmAlertDO::getMeterId, meterId)
                .eq(MesEmAlertDO::getAlertDate, alertDate));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/meter/MesEmMeterMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,56 @@
package cn.iocoder.yudao.module.mes.dal.mysql.em.meter;
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.em.meter.vo.MesEmMeterPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmOptionRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.util.List;
/**
 * MES èƒ½è€—计量点 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesEmMeterMapper extends BaseMapperX<MesEmMeterDO> {
    default PageResult<MesEmMeterDO> selectPage(MesEmMeterPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesEmMeterDO>()
                .likeIfPresent(MesEmMeterDO::getCode, reqVO.getCode())
                .likeIfPresent(MesEmMeterDO::getName, reqVO.getName())
                .eqIfPresent(MesEmMeterDO::getEnergyType, reqVO.getEnergyType())
                .eqIfPresent(MesEmMeterDO::getWorkshopId, reqVO.getWorkshopId())
                .eqIfPresent(MesEmMeterDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesEmMeterDO::getId));
    }
    default MesEmMeterDO selectByCode(String code) {
        return selectOne(MesEmMeterDO::getCode, code);
    }
    default List<MesEmMeterDO> selectListByStatus(Integer status) {
        return selectList(new LambdaQueryWrapperX<MesEmMeterDO>()
                .eqIfPresent(MesEmMeterDO::getStatus, status)
                .orderByAsc(MesEmMeterDO::getEnergyType)
                .orderByAsc(MesEmMeterDO::getCode));
    }
    @Select("SELECT id, code, name FROM mes_md_workshop WHERE deleted = 0 ORDER BY id")
    List<MesEmOptionRespVO> selectWorkshopOptions();
    @Select("SELECT id, code, name FROM mes_pro_line WHERE deleted = 0 ORDER BY id")
    List<MesEmOptionRespVO> selectLineOptions();
    @Select("SELECT id, code, name FROM mes_pro_line WHERE id = #{id} AND deleted = 0")
    MesEmOptionRespVO selectLineOptionById(@Param("id") Long id);
    @Select("SELECT id, code, name, workshop_id AS workshopId FROM mes_dv_machinery WHERE deleted = 0 ORDER BY id")
    List<MesEmOptionRespVO> selectMachineryOptions();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/quota/MesEmQuotaMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package cn.iocoder.yudao.module.mes.dal.mysql.em.quota;
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.em.quota.vo.MesEmQuotaPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.quota.MesEmQuotaDO;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
/**
 * MES èƒ½è€—考核指标 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesEmQuotaMapper extends BaseMapperX<MesEmQuotaDO> {
    default PageResult<MesEmQuotaDO> selectPage(MesEmQuotaPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesEmQuotaDO>()
                .likeIfPresent(MesEmQuotaDO::getName, reqVO.getName())
                .eqIfPresent(MesEmQuotaDO::getEnergyType, reqVO.getEnergyType())
                .eqIfPresent(MesEmQuotaDO::getObjectType, reqVO.getObjectType())
                .eqIfPresent(MesEmQuotaDO::getPeriodType, reqVO.getPeriodType())
                .eqIfPresent(MesEmQuotaDO::getStatus, reqVO.getStatus())
                .orderByDesc(MesEmQuotaDO::getId));
    }
    /**
     * æŸ¥è¯¢æŸç±»èƒ½æºä¸‹ã€ä¸ŽæŒ‡å®šè½¦é—´/产线/设备相关的启用指标
     */
    default List<MesEmQuotaDO> selectListByObject(Integer energyType, Integer periodType,
                                                  Long workshopId, Long lineId, Long machineryId) {
        return selectList(new LambdaQueryWrapperX<MesEmQuotaDO>()
                .eq(MesEmQuotaDO::getEnergyType, energyType)
                .eqIfPresent(MesEmQuotaDO::getPeriodType, periodType)
                .eq(MesEmQuotaDO::getStatus, 1)
                .and(wrapper -> wrapper
                        .eq(MesEmQuotaDO::getObjectType, 0)
                        .or(workshopId != null, w -> w.eq(MesEmQuotaDO::getObjectType, 1)
                                .eq(MesEmQuotaDO::getObjectId, workshopId))
                        .or(lineId != null, w -> w.eq(MesEmQuotaDO::getObjectType, 2)
                                .eq(MesEmQuotaDO::getObjectId, lineId))
                        .or(machineryId != null, w -> w.eq(MesEmQuotaDO::getObjectType, 3)
                                .eq(MesEmQuotaDO::getObjectId, machineryId)))
                .orderByAsc(MesEmQuotaDO::getId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/dal/mysql/em/record/MesEmRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,165 @@
package cn.iocoder.yudao.module.mes.dal.mysql.em.record;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.framework.mybatis.core.mapper.BaseMapperX;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordPageReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.record.MesEmRecordDO;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.util.List;
import java.util.Map;
/**
 * MES èƒ½è€—抄表记录 Mapper
 *
 * @author è¶…级管理员
 */
@Mapper
public interface MesEmRecordMapper extends BaseMapperX<MesEmRecordDO> {
    default PageResult<MesEmRecordDO> selectPage(MesEmRecordPageReqVO reqVO) {
        return selectPage(reqVO, new LambdaQueryWrapperX<MesEmRecordDO>()
                .eqIfPresent(MesEmRecordDO::getMeterId, reqVO.getMeterId())
                .likeIfPresent(MesEmRecordDO::getMeterName, reqVO.getMeterName())
                .eqIfPresent(MesEmRecordDO::getEnergyType, reqVO.getEnergyType())
                .eqIfPresent(MesEmRecordDO::getWorkshopId, reqVO.getWorkshopId())
                .eqIfPresent(MesEmRecordDO::getSource, reqVO.getSource())
                .betweenIfPresent(MesEmRecordDO::getRecordDate, reqVO.getRecordDate())
                .orderByDesc(MesEmRecordDO::getRecordDate)
                .orderByDesc(MesEmRecordDO::getId));
    }
    default MesEmRecordDO selectByMeterIdAndDate(Long meterId, LocalDate recordDate) {
        return selectOne(new LambdaQueryWrapperX<MesEmRecordDO>()
                .eq(MesEmRecordDO::getMeterId, meterId)
                .eq(MesEmRecordDO::getRecordDate, recordDate));
    }
    default MesEmRecordDO selectLastByMeterId(Long meterId, LocalDate beforeDate, Long excludeId) {
        return selectOne(new LambdaQueryWrapperX<MesEmRecordDO>()
                .eq(MesEmRecordDO::getMeterId, meterId)
                .lt(MesEmRecordDO::getRecordDate, beforeDate)
                .ne(excludeId != null, MesEmRecordDO::getId, excludeId)
                .orderByDesc(MesEmRecordDO::getRecordDate)
                .orderByDesc(MesEmRecordDO::getId)
                .last("LIMIT 1"));
    }
    default List<MesEmRecordDO> selectListByMeterIdAndDateRange(Long meterId, LocalDate begin, LocalDate end) {
        return selectList(new LambdaQueryWrapperX<MesEmRecordDO>()
                .eq(MesEmRecordDO::getMeterId, meterId)
                .between(MesEmRecordDO::getRecordDate, begin, end));
    }
    /**
     * ç»Ÿè®¡æŸä¸ªè€ƒæ ¸å¯¹è±¡åœ¨æŒ‡å®šåŒºé—´å†…的用量
     *
     * @param objectType 0 å…¨åŽ‚ / 1 è½¦é—´ / 2 äº§çº¿ / 3 è®¾å¤‡
     */
    @Select("""
            <script>
            SELECT COALESCE(SUM(usage_amount), 0)
            FROM mes_em_record
            WHERE deleted = 0
              AND energy_type = #{energyType}
              AND record_date BETWEEN #{begin} AND #{end}
              <if test="objectType == 1">AND workshop_id = #{objectId}</if>
              <if test="objectType == 2">AND line_id = #{objectId}</if>
              <if test="objectType == 3">AND machinery_id = #{objectId}</if>
            </script>
            """)
    BigDecimal sumUsage(@Param("energyType") Integer energyType,
                        @Param("objectType") Integer objectType,
                        @Param("objectId") Long objectId,
                        @Param("begin") LocalDate begin,
                        @Param("end") LocalDate end);
    @Select("""
            SELECT COALESCE(SUM(usage_amount), 0)
            FROM mes_em_record
            WHERE deleted = 0 AND meter_id = #{meterId} AND record_date BETWEEN #{begin} AND #{end}
            """)
    BigDecimal sumMeterUsage(@Param("meterId") Long meterId,
                             @Param("begin") LocalDate begin,
                             @Param("end") LocalDate end);
    @Select("""
            SELECT COALESCE(SUM(usage_amount), 0)
            FROM mes_em_record
            WHERE deleted = 0 AND energy_type = #{energyType} AND record_date BETWEEN #{begin} AND #{end}
            """)
    BigDecimal sumTypeUsage(@Param("energyType") Integer energyType,
                            @Param("begin") LocalDate begin,
                            @Param("end") LocalDate end);
    default List<MesEmRecordDO> selectListByDateRange(LocalDate begin, LocalDate end) {
        return selectList(new LambdaQueryWrapperX<MesEmRecordDO>()
                .between(MesEmRecordDO::getRecordDate, begin, end)
                .orderByAsc(MesEmRecordDO::getRecordDate)
                .orderByAsc(MesEmRecordDO::getId));
    }
    // ==================== ç»Ÿè®¡æŸ¥è¯¢ ====================
    default List<Map<String, Object>> selectTypeStats(LocalDate begin, LocalDate end) {
        return selectMaps(new QueryWrapper<MesEmRecordDO>()
                .select("energy_type AS energyType",
                        "COALESCE(SUM(usage_amount), 0) AS usageAmount",
                        "COALESCE(SUM(cost), 0) AS costAmount",
                        "COALESCE(SUM(usage_amount * coal_factor) / 1000, 0) AS tce")
                .between("record_date", begin, end)
                .groupBy("energy_type")
                .orderByAsc("energy_type"));
    }
    default List<Map<String, Object>> selectWorkshopStats(LocalDate begin, LocalDate end) {
        return selectMaps(new QueryWrapper<MesEmRecordDO>()
                .select("workshop_id AS workshopId",
                        "workshop_name AS workshopName",
                        "COALESCE(SUM(usage_amount * coal_factor) / 1000, 0) AS tce",
                        "COALESCE(SUM(cost), 0) AS costAmount")
                .between("record_date", begin, end)
                .isNotNull("workshop_id")
                .groupBy("workshop_id", "workshop_name")
                .orderByDesc("tce"));
    }
    default List<Map<String, Object>> selectTrendRows(LocalDate begin, LocalDate end) {
        return selectMaps(new QueryWrapper<MesEmRecordDO>()
                .select("record_date AS recordDate",
                        "energy_type AS energyType",
                        "COALESCE(SUM(usage_amount), 0) AS usageAmount",
                        "COALESCE(SUM(cost), 0) AS costAmount",
                        "COALESCE(SUM(usage_amount * coal_factor) / 1000, 0) AS tce")
                .between("record_date", begin, end)
                .groupBy("record_date", "energy_type")
                .orderByAsc("record_date"));
    }
    default List<Map<String, Object>> selectTopMeterStats(LocalDate begin, LocalDate end, int limit) {
        return selectMaps(new QueryWrapper<MesEmRecordDO>()
                .select("meter_id AS meterId",
                        "meter_name AS meterName",
                        "energy_type AS energyType",
                        "unit AS unit",
                        "COALESCE(SUM(usage_amount), 0) AS usageAmount",
                        "COALESCE(SUM(cost), 0) AS costAmount",
                        "COALESCE(SUM(usage_amount * coal_factor) / 1000, 0) AS tce")
                .between("record_date", begin, end)
                .groupBy("meter_id", "meter_name", "energy_type", "unit")
                .orderByDesc("tce")
                .last("LIMIT " + limit));
    }
    default Long selectCountByMeterId(Long meterId) {
        return selectCount(new LambdaQueryWrapperX<MesEmRecordDO>()
                .eq(MesEmRecordDO::getMeterId, meterId));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/DictTypeConstants.java
@@ -107,6 +107,16 @@
    String MES_DV_METERING_CHECK_RESULT = "mes_dv_metering_check_result"; // MES è®¡é‡æ£€æµ‹ç»“论
    String MES_DV_TELEMETRY_ANOMALY = "mes_dv_telemetry_anomaly"; // MES æ•°é‡‡æ˜¯å¦å¼‚常
    // ========== èƒ½è€—管理 (EM) ==========
    String MES_EM_ENERGY_TYPE = "mes_em_energy_type"; // MES èƒ½æºç±»åž‹
    String MES_EM_METER_STATUS = "mes_em_meter_status"; // MES èƒ½è€—计量点状态
    String MES_EM_RECORD_SOURCE = "mes_em_record_source"; // MES èƒ½è€—数据来源
    String MES_EM_QUOTA_OBJECT_TYPE = "mes_em_quota_object_type"; // MES èƒ½è€—考核对象类型
    String MES_EM_QUOTA_PERIOD = "mes_em_quota_period"; // MES èƒ½è€—考核周期
    String MES_EM_ALERT_TYPE = "mes_em_alert_type"; // MES èƒ½è€—预警类型
    String MES_EM_ALERT_LEVEL = "mes_em_alert_level"; // MES èƒ½è€—预警级别
    String MES_EM_ALERT_STATUS = "mes_em_alert_status"; // MES èƒ½è€—预警处理状态
    // ========== æŽ’班日历 (CAL) ==========
    String MES_CAL_HOLIDAY_TYPE = "mes_cal_holiday_type"; // MES å‡æœŸç±»åž‹
    String MES_CAL_SHIFT_TYPE = "mes_cal_shift_type"; // MES è½®ç­æ–¹å¼
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/ErrorCodeConstants.java
@@ -948,6 +948,19 @@
    ErrorCode WM_TRANSFER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_950_014, "导入转移单数据不能为空!");
    ErrorCode WM_PACKAGE_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_950_015, "导入装箱单数据不能为空!");
    // ========== MES èƒ½è€—管理(1-040-902-000) ==========
    ErrorCode EM_METER_NOT_EXISTS = new ErrorCode(1_040_902_000, "能耗计量点不存在");
    ErrorCode EM_METER_CODE_DUPLICATE = new ErrorCode(1_040_902_001, "能耗计量点编码已存在");
    ErrorCode EM_METER_HAS_RECORD = new ErrorCode(1_040_902_002, "能耗计量点已存在抄表记录,无法删除");
    ErrorCode EM_METER_OBJECT_NOT_EXISTS = new ErrorCode(1_040_902_003, "计量点关联的车间/产线/设备不存在");
    ErrorCode EM_RECORD_NOT_EXISTS = new ErrorCode(1_040_902_010, "能耗抄表记录不存在");
    ErrorCode EM_RECORD_DATE_DUPLICATE = new ErrorCode(1_040_902_011, "该计量点在当日已存在抄表记录");
    ErrorCode EM_RECORD_READING_INVALID = new ErrorCode(1_040_902_012, "期末读数不能小于期初读数");
    ErrorCode EM_QUOTA_NOT_EXISTS = new ErrorCode(1_040_902_020, "能耗考核指标不存在");
    ErrorCode EM_QUOTA_OBJECT_NOT_EXISTS = new ErrorCode(1_040_902_021, "考核对象不存在");
    ErrorCode EM_ALERT_NOT_EXISTS = new ErrorCode(1_040_902_030, "能耗预警不存在");
    ErrorCode EM_ALERT_ALREADY_HANDLED = new ErrorCode(1_040_902_031, "能耗预警已处理,请勿重复操作");
    // ========== MES ç”Ÿäº§æ¨¡å—-单据导入(1-040-960-000) ==========
    ErrorCode MES_PRO_WORK_ORDER_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_960_000, "导入生产工单数据不能为空!");
    ErrorCode MES_PRO_FEEDBACK_IMPORT_LIST_IS_EMPTY = new ErrorCode(1_040_960_001, "导入生产报工数据不能为空!");
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmAlertLevelEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES èƒ½è€—预警级别枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmAlertLevelEnum {
    INFO(1, "提示"),
    WARN(2, "警告"),
    SERIOUS(3, "严重");
    /**
     * çº§åˆ«
     */
    private final Integer level;
    /**
     * çº§åˆ«åç§°
     */
    private final String name;
    public static MesEmAlertLevelEnum getByLevel(Integer level) {
        return Arrays.stream(values()).filter(item -> item.getLevel().equals(level)).findFirst().orElse(null);
    }
    public static String getName(Integer level) {
        MesEmAlertLevelEnum item = getByLevel(level);
        return item == null ? null : item.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmAlertStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES èƒ½è€—预警处理状态枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmAlertStatusEnum {
    PENDING(0, "未处理"),
    HANDLED(1, "已处理");
    /**
     * çŠ¶æ€
     */
    private final Integer status;
    /**
     * çŠ¶æ€åç§°
     */
    private final String name;
    public static MesEmAlertStatusEnum getByStatus(Integer status) {
        return Arrays.stream(values()).filter(item -> item.getStatus().equals(status)).findFirst().orElse(null);
    }
    public static String getName(Integer status) {
        MesEmAlertStatusEnum item = getByStatus(status);
        return item == null ? null : item.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmAlertTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES èƒ½è€—预警类型枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmAlertTypeEnum {
    QUOTA(1, "超定额"),
    MOM(2, "环比异常"),
    YOY(3, "同比异常"),
    LEAK(4, "疑似跑冒滴漏");
    /**
     * ç±»åž‹
     */
    private final Integer type;
    /**
     * ç±»åž‹åç§°
     */
    private final String name;
    public static MesEmAlertTypeEnum getByType(Integer type) {
        return Arrays.stream(values()).filter(item -> item.getType().equals(type)).findFirst().orElse(null);
    }
    public static String getName(Integer type) {
        MesEmAlertTypeEnum item = getByType(type);
        return item == null ? null : item.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmEnergyTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.math.BigDecimal;
import java.util.Arrays;
/**
 * MES èƒ½æºç±»åž‹æžšä¸¾
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmEnergyTypeEnum {
    ELECTRICITY(1, "电", "kWh", new BigDecimal("0.1229")),
    WATER(2, "æ°´", "t", new BigDecimal("0.2429")),
    FUEL(3, "燃油", "L", new BigDecimal("1.2000")),
    GAS(4, "天然气", "m³", new BigDecimal("1.3300"));
    /**
     * èƒ½æºç±»åž‹
     */
    private final Integer type;
    /**
     * èƒ½æºç±»åž‹åç§°
     */
    private final String name;
    /**
     * é»˜è®¤è®¡é‡å•位
     */
    private final String unit;
    /**
     * é»˜è®¤æŠ˜æ ‡ç…¤ç³»æ•°ï¼ˆkgce/计量单位)
     */
    private final BigDecimal coalFactor;
    public static MesEmEnergyTypeEnum getByType(Integer type) {
        return Arrays.stream(values()).filter(item -> item.getType().equals(type)).findFirst().orElse(null);
    }
    public static String getName(Integer type) {
        MesEmEnergyTypeEnum energyType = getByType(type);
        return energyType == null ? null : energyType.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmMeterStatusEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES èƒ½è€—计量点状态枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmMeterStatusEnum {
    ENABLE(1, "启用"),
    DISABLE(0, "停用");
    /**
     * çŠ¶æ€
     */
    private final Integer status;
    /**
     * çŠ¶æ€åç§°
     */
    private final String name;
    public static MesEmMeterStatusEnum getByStatus(Integer status) {
        return Arrays.stream(values()).filter(item -> item.getStatus().equals(status)).findFirst().orElse(null);
    }
    public static String getName(Integer status) {
        MesEmMeterStatusEnum item = getByStatus(status);
        return item == null ? null : item.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmQuotaObjectTypeEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES èƒ½è€—考核对象类型枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmQuotaObjectTypeEnum {
    FACTORY(0, "全厂"),
    WORKSHOP(1, "车间"),
    LINE(2, "产线"),
    MACHINERY(3, "设备");
    /**
     * å¯¹è±¡ç±»åž‹
     */
    private final Integer type;
    /**
     * å¯¹è±¡ç±»åž‹åç§°
     */
    private final String name;
    public static MesEmQuotaObjectTypeEnum getByType(Integer type) {
        return Arrays.stream(values()).filter(item -> item.getType().equals(type)).findFirst().orElse(null);
    }
    public static String getName(Integer type) {
        MesEmQuotaObjectTypeEnum item = getByType(type);
        return item == null ? null : item.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmQuotaPeriodEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES èƒ½è€—考核周期枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmQuotaPeriodEnum {
    DAY(1, "日"),
    MONTH(2, "月");
    /**
     * å‘¨æœŸ
     */
    private final Integer period;
    /**
     * å‘¨æœŸåç§°
     */
    private final String name;
    public static MesEmQuotaPeriodEnum getByPeriod(Integer period) {
        return Arrays.stream(values()).filter(item -> item.getPeriod().equals(period)).findFirst().orElse(null);
    }
    public static String getName(Integer period) {
        MesEmQuotaPeriodEnum item = getByPeriod(period);
        return item == null ? null : item.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/enums/em/MesEmRecordSourceEnum.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package cn.iocoder.yudao.module.mes.enums.em;
import lombok.AllArgsConstructor;
import lombok.Getter;
import java.util.Arrays;
/**
 * MES èƒ½è€—数据来源枚举
 *
 * @author è¶…级管理员
 */
@Getter
@AllArgsConstructor
public enum MesEmRecordSourceEnum {
    MANUAL(1, "手工录入"),
    IMPORT(2, "Excel å¯¼å…¥"),
    AUTO(3, "自动采集");
    /**
     * æ¥æº
     */
    private final Integer source;
    /**
     * æ¥æºåç§°
     */
    private final String name;
    public static MesEmRecordSourceEnum getBySource(Integer source) {
        return Arrays.stream(values()).filter(item -> item.getSource().equals(source)).findFirst().orElse(null);
    }
    public static String getName(Integer source) {
        MesEmRecordSourceEnum item = getBySource(source);
        return item == null ? null : item.getName();
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/alert/MesEmAlertService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
package cn.iocoder.yudao.module.mes.service.em.alert;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertHandleReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.alert.MesEmAlertDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.record.MesEmRecordDO;
import java.time.LocalDate;
import java.util.List;
/**
 * MES èƒ½è€—预警 Service
 *
 * @author è¶…级管理员
 */
public interface MesEmAlertService {
    /**
     * æ ¹æ®ä¸€æ¡æŠ„表记录检测并生成预警
     */
    void checkRecord(MesEmRecordDO record);
    /**
     * é‡æ–°æ£€æµ‹æŒ‡å®šæ—¥æœŸåŒºé—´çš„æŠ„表记录
     *
     * @return æ£€æµ‹çš„记录数
     */
    int checkRange(LocalDate begin, LocalDate end);
    /**
     * åˆ é™¤æŸä¸ªè®¡é‡ç‚¹æŸå¤©çš„环比/同比/跑冒滴漏预警(抄表记录删除时调用)
     */
    void deleteMeterAlerts(Long meterId, LocalDate alertDate);
    MesEmAlertDO getAlert(Long id);
    PageResult<MesEmAlertDO> getAlertPage(MesEmAlertPageReqVO pageReqVO);
    List<MesEmAlertRespVO> getAlertRespVOList(List<MesEmAlertDO> list);
    void handleAlert(MesEmAlertHandleReqVO handleReqVO);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/alert/MesEmAlertServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,339 @@
package cn.iocoder.yudao.module.mes.service.em.alert;
import cn.hutool.core.collection.CollUtil;
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.framework.security.core.util.SecurityFrameworkUtils;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertHandleReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.alert.vo.MesEmAlertRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.alert.MesEmAlertDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.quota.MesEmQuotaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.record.MesEmRecordDO;
import cn.iocoder.yudao.module.mes.dal.mysql.em.alert.MesEmAlertMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.meter.MesEmMeterMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.quota.MesEmQuotaMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.record.MesEmRecordMapper;
import cn.iocoder.yudao.module.mes.enums.em.*;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.Collections;
import java.util.Comparator;
import java.util.List;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.EM_ALERT_ALREADY_HANDLED;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.EM_ALERT_NOT_EXISTS;
/**
 * MES èƒ½è€—预警 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesEmAlertServiceImpl implements MesEmAlertService {
    private static final BigDecimal HUNDRED = new BigDecimal("100");
    private static final BigDecimal DEFAULT_MOM_RATIO = new BigDecimal("30");
    private static final BigDecimal DEFAULT_YOY_RATIO = new BigDecimal("30");
    private static final DateTimeFormatter DATE_NO_FORMATTER = DateTimeFormatter.ofPattern("yyyyMMdd");
    @Resource
    private MesEmAlertMapper alertMapper;
    @Resource
    private MesEmRecordMapper recordMapper;
    @Resource
    private MesEmMeterMapper meterMapper;
    @Resource
    private MesEmQuotaMapper quotaMapper;
    @Override
    public void checkRecord(MesEmRecordDO record) {
        MesEmMeterDO meter = meterMapper.selectById(record.getMeterId());
        checkQuota(record, MesEmQuotaPeriodEnum.DAY);
        checkQuota(record, MesEmQuotaPeriodEnum.MONTH);
        checkMomentum(record);
        checkLeak(record, meter);
    }
    /**
     * å®šé¢é¢„警:日定额比较当日用量,月定额比较月初至当日累计用量
     */
    private void checkQuota(MesEmRecordDO record, MesEmQuotaPeriodEnum period) {
        List<MesEmQuotaDO> quotas = quotaMapper.selectListByObject(record.getEnergyType(), period.getPeriod(),
                record.getWorkshopId(), record.getLineId(), record.getMachineryId());
        MesEmQuotaDO quota = mostSpecificQuota(quotas);
        if (quota == null) {
            return;
        }
        LocalDate begin = MesEmQuotaPeriodEnum.MONTH.equals(period)
                ? record.getRecordDate().withDayOfMonth(1) : record.getRecordDate();
        BigDecimal actual = recordMapper.sumUsage(record.getEnergyType(), quota.getObjectType(),
                quota.getObjectId(), begin, record.getRecordDate());
        BigDecimal quotaValue = quota.getQuotaValue() == null ? BigDecimal.ZERO : quota.getQuotaValue();
        BigDecimal warnRatio = defaultIfNull(quota.getWarnRatio(), new BigDecimal("10"));
        BigDecimal alarmRatio = defaultIfNull(quota.getAlarmRatio(), new BigDecimal("20"));
        BigDecimal warnThreshold = quotaValue.multiply(BigDecimal.ONE.add(
                warnRatio.divide(HUNDRED, 6, RoundingMode.HALF_UP)));
        BigDecimal alarmThreshold = quotaValue.multiply(BigDecimal.ONE.add(
                alarmRatio.divide(HUNDRED, 6, RoundingMode.HALF_UP)));
        Long objectId = quota.getObjectId() == null ? 0L : quota.getObjectId();
        if (actual.compareTo(warnThreshold) <= 0) {
            // ç”¨é‡å›žè½åˆ°é˜ˆå€¼å†…时,撤销尚未处理的预警
            deleteIfPending(0L, MesEmAlertTypeEnum.QUOTA.getType(), record.getRecordDate(), objectId);
            return;
        }
        BigDecimal exceedRate = rate(actual, quotaValue);
        String unit = StrUtil.blankToDefault(record.getUnit(), StrUtil.blankToDefault(quota.getUnit(), ""));
        String content = StrUtil.format("{}{} {} ç”¨é‡ {} {},定额 {} {},超出 {}%", period.getName(),
                quota.getObjectName(), MesEmEnergyTypeEnum.getName(record.getEnergyType()),
                stripZeros(actual), unit, stripZeros(quotaValue), unit,
                exceedRate == null ? "-" : stripZeros(exceedRate));
        MesEmAlertDO alert = buildAlert(record, 0L, "", quota.getObjectName(),
                MesEmAlertTypeEnum.QUOTA, actual.compareTo(alarmThreshold) > 0
                        ? MesEmAlertLevelEnum.SERIOUS : MesEmAlertLevelEnum.WARN,
                quota.getObjectType(), objectId, actual, quotaValue, unit, content);
        upsertAlert(alert);
    }
    /**
     * çŽ¯æ¯” / åŒæ¯”异常预警:与上日、去年同日比较
     */
    private void checkMomentum(MesEmRecordDO record) {
        List<MesEmQuotaDO> quotas = quotaMapper.selectListByObject(record.getEnergyType(), null,
                record.getWorkshopId(), record.getLineId(), record.getMachineryId());
        MesEmQuotaDO quota = mostSpecificQuota(quotas);
        BigDecimal momRatio = quota == null ? DEFAULT_MOM_RATIO : defaultIfNull(quota.getMomWarnRatio(), DEFAULT_MOM_RATIO);
        BigDecimal yoyRatio = quota == null ? DEFAULT_YOY_RATIO : defaultIfNull(quota.getYoyWarnRatio(), DEFAULT_YOY_RATIO);
        BigDecimal current = defaultIfNull(record.getUsageAmount(), BigDecimal.ZERO);
        String unit = StrUtil.blankToDefault(record.getUnit(), "");
        // çŽ¯æ¯”ï¼šæ˜¨æ—¥
        LocalDate prevDate = record.getRecordDate().minusDays(1);
        BigDecimal previous = recordMapper.sumMeterUsage(record.getMeterId(), prevDate, prevDate);
        checkMomentumItem(record, current, previous, momRatio, unit,
                MesEmAlertTypeEnum.MOM, "环比", prevDate);
        // åŒæ¯”:去年同日
        LocalDate lastYearDate = record.getRecordDate().minusYears(1);
        BigDecimal lastYear = recordMapper.sumMeterUsage(record.getMeterId(), lastYearDate, lastYearDate);
        checkMomentumItem(record, current, lastYear, yoyRatio, unit,
                MesEmAlertTypeEnum.YOY, "同比", lastYearDate);
    }
    private void checkMomentumItem(MesEmRecordDO record, BigDecimal current, BigDecimal base,
                                   BigDecimal thresholdRatio, String unit, MesEmAlertTypeEnum alertType,
                                   String label, LocalDate baseDate) {
        if (base == null || base.compareTo(BigDecimal.ZERO) <= 0) {
            deleteIfPending(record.getMeterId(), alertType.getType(), record.getRecordDate(), 0L);
            return;
        }
        BigDecimal changeRate = rate(current, base);
        if (changeRate == null || changeRate.compareTo(thresholdRatio) <= 0) {
            deleteIfPending(record.getMeterId(), alertType.getType(), record.getRecordDate(), 0L);
            return;
        }
        MesEmAlertLevelEnum level = changeRate.compareTo(thresholdRatio.multiply(new BigDecimal("2"))) > 0
                ? MesEmAlertLevelEnum.SERIOUS : MesEmAlertLevelEnum.WARN;
        String content = StrUtil.format("{} {} {} ç”¨é‡ {} {},较 {} çš„ {} {} ä¸Šå‡ {}%,超过{}异常阈值 {}%",
                record.getMeterName(), MesEmEnergyTypeEnum.getName(record.getEnergyType()), label,
                stripZeros(current), unit, baseDate, label, stripZeros(base), stripZeros(changeRate),
                label, stripZeros(thresholdRatio));
        MesEmAlertDO alert = buildAlert(record, record.getMeterId(), record.getMeterCode(), record.getMeterName(),
                alertType, level, resolveObjectType(record), resolveObjectId(record),
                current, base, unit, content);
        upsertAlert(alert);
    }
    /**
     * ç–‘似跑冒滴漏预警:当日用量超过计量点配置的异常上限
     */
    private void checkLeak(MesEmRecordDO record, MesEmMeterDO meter) {
        BigDecimal threshold = meter == null ? null : meter.getLeakThreshold();
        BigDecimal current = defaultIfNull(record.getUsageAmount(), BigDecimal.ZERO);
        if (threshold == null || current.compareTo(threshold) <= 0) {
            deleteIfPending(record.getMeterId(), MesEmAlertTypeEnum.LEAK.getType(), record.getRecordDate(), 0L);
            return;
        }
        MesEmAlertLevelEnum level = current.compareTo(threshold.multiply(new BigDecimal("1.5"))) > 0
                ? MesEmAlertLevelEnum.SERIOUS : MesEmAlertLevelEnum.WARN;
        String unit = StrUtil.blankToDefault(record.getUnit(), "");
        String content = StrUtil.format("{} å½“日用量 {} {},超过日用量异常上限 {} {},疑似跑冒滴漏",
                record.getMeterName(), stripZeros(current), unit, stripZeros(threshold), unit);
        MesEmAlertDO alert = buildAlert(record, record.getMeterId(), record.getMeterCode(), record.getMeterName(),
                MesEmAlertTypeEnum.LEAK, level, resolveObjectType(record), resolveObjectId(record),
                current, threshold, unit, content);
        upsertAlert(alert);
    }
    private MesEmAlertDO buildAlert(MesEmRecordDO record, Long meterId, String meterCode, String meterName,
                                    MesEmAlertTypeEnum alertType, MesEmAlertLevelEnum alertLevel,
                                    Integer objectType, Long objectId,
                                    BigDecimal actualValue, BigDecimal thresholdValue,
                                    String unit, String content) {
        MesEmAlertDO alert = new MesEmAlertDO();
        alert.setAlertNo(buildAlertNo(record.getRecordDate(), meterId, alertType.getType(), objectId));
        alert.setMeterId(meterId);
        alert.setMeterCode(meterCode);
        alert.setMeterName(meterName);
        alert.setEnergyType(record.getEnergyType());
        alert.setAlertType(alertType.getType());
        alert.setAlertLevel(alertLevel.getLevel());
        alert.setAlertDate(record.getRecordDate());
        alert.setObjectType(objectType);
        alert.setObjectId(objectId);
        alert.setObjectName(meterName);
        alert.setActualValue(actualValue);
        alert.setThresholdValue(thresholdValue);
        alert.setUnit(unit);
        alert.setContent(content);
        alert.setStatus(MesEmAlertStatusEnum.PENDING.getStatus());
        return alert;
    }
    private void upsertAlert(MesEmAlertDO alert) {
        MesEmAlertDO existing = alertMapper.selectByUnique(alert.getMeterId(), alert.getAlertType(),
                alert.getAlertDate(), alert.getObjectId());
        if (existing == null) {
            alertMapper.insert(alert);
            return;
        }
        alert.setId(existing.getId());
        alert.setAlertNo(existing.getAlertNo());
        alert.setStatus(existing.getStatus());
        alertMapper.updateById(alert);
    }
    private void deleteIfPending(Long meterId, Integer alertType, LocalDate alertDate, Long objectId) {
        MesEmAlertDO existing = alertMapper.selectByUnique(meterId, alertType, alertDate, objectId);
        if (existing != null && MesEmAlertStatusEnum.PENDING.getStatus().equals(existing.getStatus())) {
            alertMapper.deleteById(existing.getId());
        }
    }
    private String buildAlertNo(LocalDate date, Long meterId, Integer alertType, Long objectId) {
        return "AL" + date.format(DATE_NO_FORMATTER) + "-" + meterId + "-" + alertType + "-" + objectId;
    }
    private MesEmQuotaDO mostSpecificQuota(List<MesEmQuotaDO> quotas) {
        if (CollUtil.isEmpty(quotas)) {
            return null;
        }
        return quotas.stream()
                .max(Comparator.comparing(MesEmQuotaDO::getObjectType)
                        .thenComparing(MesEmQuotaDO::getId))
                .orElse(null);
    }
    private Integer resolveObjectType(MesEmRecordDO record) {
        if (record.getMachineryId() != null) {
            return MesEmQuotaObjectTypeEnum.MACHINERY.getType();
        }
        if (record.getLineId() != null) {
            return MesEmQuotaObjectTypeEnum.LINE.getType();
        }
        if (record.getWorkshopId() != null) {
            return MesEmQuotaObjectTypeEnum.WORKSHOP.getType();
        }
        return MesEmQuotaObjectTypeEnum.FACTORY.getType();
    }
    private Long resolveObjectId(MesEmRecordDO record) {
        if (record.getMachineryId() != null) {
            return record.getMachineryId();
        }
        if (record.getLineId() != null) {
            return record.getLineId();
        }
        if (record.getWorkshopId() != null) {
            return record.getWorkshopId();
        }
        return 0L;
    }
    private BigDecimal defaultIfNull(BigDecimal value, BigDecimal defaultValue) {
        return value == null ? defaultValue : value;
    }
    /**
     * è®¡ç®—变化率(%),基数为 0 æ—¶è¿”回 null
     */
    private BigDecimal rate(BigDecimal current, BigDecimal base) {
        if (base == null || base.compareTo(BigDecimal.ZERO) == 0) {
            return null;
        }
        return current.subtract(base).multiply(HUNDRED).divide(base, 2, RoundingMode.HALF_UP);
    }
    private String stripZeros(BigDecimal value) {
        if (value == null) {
            return "0";
        }
        return value.stripTrailingZeros().toPlainString();
    }
    @Override
    public int checkRange(LocalDate begin, LocalDate end) {
        List<MesEmRecordDO> records = recordMapper.selectListByDateRange(begin, end);
        records.forEach(this::checkRecord);
        return records.size();
    }
    @Override
    public void deleteMeterAlerts(Long meterId, LocalDate alertDate) {
        alertMapper.deleteByMeterIdAndDate(meterId, alertDate);
    }
    @Override
    public MesEmAlertDO getAlert(Long id) {
        return alertMapper.selectById(id);
    }
    @Override
    public PageResult<MesEmAlertDO> getAlertPage(MesEmAlertPageReqVO pageReqVO) {
        return alertMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MesEmAlertRespVO> getAlertRespVOList(List<MesEmAlertDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        return BeanUtils.toBean(list, MesEmAlertRespVO.class, vo -> {
            vo.setEnergyTypeName(MesEmEnergyTypeEnum.getName(vo.getEnergyType()));
            vo.setAlertTypeName(MesEmAlertTypeEnum.getName(vo.getAlertType()));
            vo.setAlertLevelName(MesEmAlertLevelEnum.getName(vo.getAlertLevel()));
            vo.setObjectTypeName(MesEmQuotaObjectTypeEnum.getName(vo.getObjectType()));
            vo.setStatusName(MesEmAlertStatusEnum.getName(vo.getStatus()));
        });
    }
    @Override
    public void handleAlert(MesEmAlertHandleReqVO handleReqVO) {
        MesEmAlertDO alert = alertMapper.selectById(handleReqVO.getId());
        if (alert == null) {
            throw exception(EM_ALERT_NOT_EXISTS);
        }
        if (MesEmAlertStatusEnum.HANDLED.getStatus().equals(alert.getStatus())) {
            throw exception(EM_ALERT_ALREADY_HANDLED);
        }
        alertMapper.updateById(new MesEmAlertDO().setId(alert.getId())
                .setStatus(MesEmAlertStatusEnum.HANDLED.getStatus())
                .setHandleUserId(SecurityFrameworkUtils.getLoginUserId())
                .setHandleUserName(SecurityFrameworkUtils.getLoginUserNickname())
                .setHandleTime(LocalDateTime.now())
                .setHandleRemark(handleReqVO.getHandleRemark()));
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/meter/MesEmMeterService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,46 @@
package cn.iocoder.yudao.module.mes.service.em.meter;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmOptionRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
import java.util.Collection;
import java.util.List;
/**
 * MES èƒ½è€—计量点 Service
 *
 * @author è¶…级管理员
 */
public interface MesEmMeterService {
    Long createMeter(MesEmMeterSaveReqVO createReqVO);
    void updateMeter(MesEmMeterSaveReqVO updateReqVO);
    void deleteMeter(Long id);
    MesEmMeterDO getMeter(Long id);
    void validateMeterExists(Long id);
    PageResult<MesEmMeterDO> getMeterPage(MesEmMeterPageReqVO pageReqVO);
    List<MesEmMeterDO> getMeterList();
    List<MesEmMeterDO> getMeterList(Collection<Long> ids);
    List<MesEmMeterRespVO> getMeterRespVOList(List<MesEmMeterDO> list);
    List<MesEmOptionRespVO> getMeterSimpleList();
    List<MesEmOptionRespVO> getWorkshopOptions();
    List<MesEmOptionRespVO> getLineOptions();
    List<MesEmOptionRespVO> getMachineryOptions();
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/meter/MesEmMeterServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,249 @@
package cn.iocoder.yudao.module.mes.service.em.meter;
import cn.hutool.core.collection.CollUtil;
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.collection.MapUtils;
import cn.iocoder.yudao.framework.common.util.object.BeanUtils;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmMeterSaveReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.meter.vo.MesEmOptionRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkshopDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.meter.MesEmMeterMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.record.MesEmRecordMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkshopMapper;
import cn.iocoder.yudao.module.mes.enums.em.MesEmEnergyTypeEnum;
import cn.iocoder.yudao.module.mes.enums.em.MesEmMeterStatusEnum;
import cn.iocoder.yudao.module.system.api.user.AdminUserApi;
import cn.iocoder.yudao.module.system.api.user.dto.AdminUserRespDTO;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.util.Collection;
import java.util.Collections;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
import static cn.iocoder.yudao.framework.common.exception.util.ServiceExceptionUtil.exception;
import static cn.iocoder.yudao.framework.common.util.collection.CollectionUtils.convertSet;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.*;
/**
 * MES èƒ½è€—计量点 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesEmMeterServiceImpl implements MesEmMeterService {
    @Resource
    private MesEmMeterMapper meterMapper;
    @Resource
    private MesEmRecordMapper recordMapper;
    @Resource
    private MesMdWorkshopMapper workshopMapper;
    @Resource
    private MesDvMachineryMapper machineryMapper;
    @Resource
    private AdminUserApi adminUserApi;
    @Override
    public Long createMeter(MesEmMeterSaveReqVO createReqVO) {
        validateMeterCodeUnique(null, createReqVO.getCode());
        MesEmMeterDO meter = BeanUtils.toBean(createReqVO, MesEmMeterDO.class);
        fillMeterDefaultsAndNames(meter);
        meterMapper.insert(meter);
        return meter.getId();
    }
    @Override
    public void updateMeter(MesEmMeterSaveReqVO updateReqVO) {
        validateMeterExists(updateReqVO.getId());
        validateMeterCodeUnique(updateReqVO.getId(), updateReqVO.getCode());
        MesEmMeterDO meter = BeanUtils.toBean(updateReqVO, MesEmMeterDO.class);
        fillMeterDefaultsAndNames(meter);
        meterMapper.updateById(meter);
    }
    @Override
    public void deleteMeter(Long id) {
        validateMeterExists(id);
        if (recordMapper.selectCountByMeterId(id) > 0) {
            throw exception(EM_METER_HAS_RECORD);
        }
        meterMapper.deleteById(id);
    }
    @Override
    public MesEmMeterDO getMeter(Long id) {
        return meterMapper.selectById(id);
    }
    @Override
    public void validateMeterExists(Long id) {
        if (meterMapper.selectById(id) == null) {
            throw exception(EM_METER_NOT_EXISTS);
        }
    }
    private void validateMeterCodeUnique(Long id, String code) {
        if (StrUtil.isBlank(code)) {
            return;
        }
        MesEmMeterDO meter = meterMapper.selectByCode(code);
        if (meter == null) {
            return;
        }
        if (ObjUtil.notEqual(meter.getId(), id)) {
            throw exception(EM_METER_CODE_DUPLICATE);
        }
    }
    /**
     * è¡¥å…¨å•位、倍率、折标煤系数,并冗余车间/产线/设备名称
     */
    private void fillMeterDefaultsAndNames(MesEmMeterDO meter) {
        MesEmEnergyTypeEnum energyType = MesEmEnergyTypeEnum.getByType(meter.getEnergyType());
        if (StrUtil.isBlank(meter.getUnit()) && energyType != null) {
            meter.setUnit(energyType.getUnit());
        }
        if (meter.getRatio() == null) {
            meter.setRatio(BigDecimal.ONE);
        }
        if (meter.getInitialReading() == null) {
            meter.setInitialReading(BigDecimal.ZERO);
        }
        if (meter.getCoalFactor() == null && energyType != null) {
            meter.setCoalFactor(energyType.getCoalFactor());
        }
        if (meter.getStatus() == null) {
            meter.setStatus(MesEmMeterStatusEnum.ENABLE.getStatus());
        }
        // è½¦é—´
        if (meter.getWorkshopId() != null) {
            MesMdWorkshopDO workshop = workshopMapper.selectById(meter.getWorkshopId());
            if (workshop == null) {
                throw exception(EM_METER_OBJECT_NOT_EXISTS);
            }
            meter.setWorkshopName(workshop.getName());
        } else {
            meter.setWorkshopName(null);
        }
        // äº§çº¿
        if (meter.getLineId() != null) {
            MesEmOptionRespVO line = meterMapper.selectLineOptionById(meter.getLineId());
            if (line == null) {
                throw exception(EM_METER_OBJECT_NOT_EXISTS);
            }
            meter.setLineName(line.getName());
        } else {
            meter.setLineName(null);
        }
        // è®¾å¤‡ï¼›æœªé€‰æ‹©è½¦é—´æ—¶è‡ªåŠ¨å¸¦å…¥è®¾å¤‡æ‰€å±žè½¦é—´
        if (meter.getMachineryId() != null) {
            MesDvMachineryDO machinery = machineryMapper.selectById(meter.getMachineryId());
            if (machinery == null) {
                throw exception(EM_METER_OBJECT_NOT_EXISTS);
            }
            meter.setMachineryName(machinery.getName());
            if (meter.getWorkshopId() == null && machinery.getWorkshopId() != null) {
                MesMdWorkshopDO workshop = workshopMapper.selectById(machinery.getWorkshopId());
                if (workshop != null) {
                    meter.setWorkshopId(workshop.getId());
                    meter.setWorkshopName(workshop.getName());
                }
            }
        } else {
            meter.setMachineryName(null);
        }
    }
    @Override
    public PageResult<MesEmMeterDO> getMeterPage(MesEmMeterPageReqVO pageReqVO) {
        return meterMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MesEmMeterDO> getMeterList() {
        return meterMapper.selectList();
    }
    @Override
    public List<MesEmMeterDO> getMeterList(Collection<Long> ids) {
        if (CollUtil.isEmpty(ids)) {
            return Collections.emptyList();
        }
        return meterMapper.selectByIds(ids);
    }
    @Override
    public List<MesEmMeterRespVO> getMeterRespVOList(List<MesEmMeterDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        Map<Long, AdminUserRespDTO> userMap = CollUtil.isNotEmpty(convertSet(list, MesEmMeterDO::getChargeUserId))
                ? adminUserApi.getUserMap(convertSet(list, MesEmMeterDO::getChargeUserId))
                : Collections.emptyMap();
        return BeanUtils.toBean(list, MesEmMeterRespVO.class, vo -> {
            vo.setEnergyTypeName(MesEmEnergyTypeEnum.getName(vo.getEnergyType()));
            vo.setStatusName(MesEmMeterStatusEnum.getName(vo.getStatus()));
            MapUtils.findAndThen(userMap, vo.getChargeUserId(), user -> vo.setChargeUserName(user.getNickname()));
        });
    }
    @Override
    public List<MesEmOptionRespVO> getMeterSimpleList() {
        List<MesEmMeterDO> list = meterMapper.selectListByStatus(MesEmMeterStatusEnum.ENABLE.getStatus());
        return list.stream().map(meter -> {
            MesEmOptionRespVO option = new MesEmOptionRespVO();
            option.setId(meter.getId());
            option.setCode(meter.getCode());
            option.setName(meter.getName());
            option.setEnergyType(meter.getEnergyType());
            option.setUnit(meter.getUnit());
            option.setWorkshopId(meter.getWorkshopId());
            option.setWorkshopName(meter.getWorkshopName());
            option.setLineId(meter.getLineId());
            option.setLineName(meter.getLineName());
            return option;
        }).collect(Collectors.toList());
    }
    @Override
    public List<MesEmOptionRespVO> getWorkshopOptions() {
        return meterMapper.selectWorkshopOptions();
    }
    @Override
    public List<MesEmOptionRespVO> getLineOptions() {
        return meterMapper.selectLineOptions();
    }
    @Override
    public List<MesEmOptionRespVO> getMachineryOptions() {
        List<MesEmOptionRespVO> options = meterMapper.selectMachineryOptions();
        if (CollUtil.isEmpty(options)) {
            return options;
        }
        Map<Long, MesMdWorkshopDO> workshopMap = workshopMapper.selectList().stream()
                .collect(Collectors.toMap(MesMdWorkshopDO::getId, Function.identity(), (a, b) -> a));
        options.forEach(option -> {
            MesMdWorkshopDO workshop = workshopMap.get(option.getWorkshopId());
            if (workshop != null) {
                option.setWorkshopName(workshop.getName());
            }
        });
        return options;
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/quota/MesEmQuotaService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package cn.iocoder.yudao.module.mes.service.em.quota;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.quota.MesEmQuotaDO;
import java.util.List;
/**
 * MES èƒ½è€—考核指标 Service
 *
 * @author è¶…级管理员
 */
public interface MesEmQuotaService {
    Long createQuota(MesEmQuotaSaveReqVO createReqVO);
    void updateQuota(MesEmQuotaSaveReqVO updateReqVO);
    void deleteQuota(Long id);
    MesEmQuotaDO getQuota(Long id);
    void validateQuotaExists(Long id);
    PageResult<MesEmQuotaDO> getQuotaPage(MesEmQuotaPageReqVO pageReqVO);
    List<MesEmQuotaRespVO> getQuotaRespVOList(List<MesEmQuotaDO> list);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/quota/MesEmQuotaServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,157 @@
package cn.iocoder.yudao.module.mes.service.em.quota;
import cn.hutool.core.collection.CollUtil;
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.em.meter.vo.MesEmOptionRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.quota.vo.MesEmQuotaSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.dv.machinery.MesDvMachineryDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.quota.MesEmQuotaDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.md.workstation.MesMdWorkshopDO;
import cn.iocoder.yudao.module.mes.dal.mysql.dv.machinery.MesDvMachineryMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.meter.MesEmMeterMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.quota.MesEmQuotaMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.md.workstation.MesMdWorkshopMapper;
import cn.iocoder.yudao.module.mes.enums.em.MesEmEnergyTypeEnum;
import cn.iocoder.yudao.module.mes.enums.em.MesEmMeterStatusEnum;
import cn.iocoder.yudao.module.mes.enums.em.MesEmQuotaObjectTypeEnum;
import cn.iocoder.yudao.module.mes.enums.em.MesEmQuotaPeriodEnum;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
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.EM_QUOTA_NOT_EXISTS;
import static cn.iocoder.yudao.module.mes.enums.ErrorCodeConstants.EM_QUOTA_OBJECT_NOT_EXISTS;
/**
 * MES èƒ½è€—考核指标 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesEmQuotaServiceImpl implements MesEmQuotaService {
    @Resource
    private MesEmQuotaMapper quotaMapper;
    @Resource
    private MesEmMeterMapper meterMapper;
    @Resource
    private MesMdWorkshopMapper workshopMapper;
    @Resource
    private MesDvMachineryMapper machineryMapper;
    @Override
    public Long createQuota(MesEmQuotaSaveReqVO createReqVO) {
        MesEmQuotaDO quota = BeanUtils.toBean(createReqVO, MesEmQuotaDO.class);
        fillQuotaDefaultsAndObjectName(quota);
        quotaMapper.insert(quota);
        return quota.getId();
    }
    @Override
    public void updateQuota(MesEmQuotaSaveReqVO updateReqVO) {
        validateQuotaExists(updateReqVO.getId());
        MesEmQuotaDO quota = BeanUtils.toBean(updateReqVO, MesEmQuotaDO.class);
        fillQuotaDefaultsAndObjectName(quota);
        quotaMapper.updateById(quota);
    }
    @Override
    public void deleteQuota(Long id) {
        validateQuotaExists(id);
        quotaMapper.deleteById(id);
    }
    @Override
    public MesEmQuotaDO getQuota(Long id) {
        return quotaMapper.selectById(id);
    }
    @Override
    public void validateQuotaExists(Long id) {
        if (quotaMapper.selectById(id) == null) {
            throw exception(EM_QUOTA_NOT_EXISTS);
        }
    }
    private void fillQuotaDefaultsAndObjectName(MesEmQuotaDO quota) {
        MesEmEnergyTypeEnum energyType = MesEmEnergyTypeEnum.getByType(quota.getEnergyType());
        if (StrUtil.isBlank(quota.getUnit()) && energyType != null) {
            quota.setUnit(energyType.getUnit());
        }
        if (quota.getWarnRatio() == null) {
            quota.setWarnRatio(new BigDecimal("10"));
        }
        if (quota.getAlarmRatio() == null) {
            quota.setAlarmRatio(new BigDecimal("20"));
        }
        if (quota.getMomWarnRatio() == null) {
            quota.setMomWarnRatio(new BigDecimal("30"));
        }
        if (quota.getYoyWarnRatio() == null) {
            quota.setYoyWarnRatio(new BigDecimal("30"));
        }
        if (quota.getStatus() == null) {
            quota.setStatus(MesEmMeterStatusEnum.ENABLE.getStatus());
        }
        // è€ƒæ ¸å¯¹è±¡åç§°
        Integer objectType = quota.getObjectType();
        if (MesEmQuotaObjectTypeEnum.FACTORY.getType().equals(objectType)) {
            quota.setObjectId(0L);
            quota.setObjectName("全厂");
            return;
        }
        if (quota.getObjectId() == null) {
            throw exception(EM_QUOTA_OBJECT_NOT_EXISTS);
        }
        if (MesEmQuotaObjectTypeEnum.WORKSHOP.getType().equals(objectType)) {
            MesMdWorkshopDO workshop = workshopMapper.selectById(quota.getObjectId());
            if (workshop == null) {
                throw exception(EM_QUOTA_OBJECT_NOT_EXISTS);
            }
            quota.setObjectName(workshop.getName());
        } else if (MesEmQuotaObjectTypeEnum.LINE.getType().equals(objectType)) {
            MesEmOptionRespVO line = meterMapper.selectLineOptionById(quota.getObjectId());
            if (line == null) {
                throw exception(EM_QUOTA_OBJECT_NOT_EXISTS);
            }
            quota.setObjectName(line.getName());
        } else if (MesEmQuotaObjectTypeEnum.MACHINERY.getType().equals(objectType)) {
            MesDvMachineryDO machinery = machineryMapper.selectById(quota.getObjectId());
            if (machinery == null) {
                throw exception(EM_QUOTA_OBJECT_NOT_EXISTS);
            }
            quota.setObjectName(machinery.getName());
        } else {
            throw exception(EM_QUOTA_OBJECT_NOT_EXISTS);
        }
    }
    @Override
    public PageResult<MesEmQuotaDO> getQuotaPage(MesEmQuotaPageReqVO pageReqVO) {
        return quotaMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MesEmQuotaRespVO> getQuotaRespVOList(List<MesEmQuotaDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        return BeanUtils.toBean(list, MesEmQuotaRespVO.class, vo -> {
            vo.setEnergyTypeName(MesEmEnergyTypeEnum.getName(vo.getEnergyType()));
            vo.setObjectTypeName(MesEmQuotaObjectTypeEnum.getName(vo.getObjectType()));
            vo.setPeriodTypeName(MesEmQuotaPeriodEnum.getName(vo.getPeriodType()));
            vo.setStatusName(MesEmMeterStatusEnum.getName(vo.getStatus()));
        });
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/record/MesEmRecordService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package cn.iocoder.yudao.module.mes.service.em.record;
import cn.iocoder.yudao.framework.common.pojo.PageResult;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmLastReadingRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.record.MesEmRecordDO;
import java.time.LocalDate;
import java.util.List;
/**
 * MES èƒ½è€—抄表记录 Service
 *
 * @author è¶…级管理员
 */
public interface MesEmRecordService {
    Long createRecord(MesEmRecordSaveReqVO createReqVO);
    void updateRecord(MesEmRecordSaveReqVO updateReqVO);
    void deleteRecord(Long id);
    MesEmRecordDO getRecord(Long id);
    PageResult<MesEmRecordDO> getRecordPage(MesEmRecordPageReqVO pageReqVO);
    List<MesEmRecordRespVO> getRecordRespVOList(List<MesEmRecordDO> list);
    MesEmLastReadingRespVO getLastReading(Long meterId, LocalDate recordDate, Long excludeId);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/record/MesEmRecordServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,178 @@
package cn.iocoder.yudao.module.mes.service.em.record;
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.mes.controller.admin.em.record.vo.MesEmLastReadingRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordPageReqVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.record.vo.MesEmRecordSaveReqVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.record.MesEmRecordDO;
import cn.iocoder.yudao.module.mes.dal.mysql.em.record.MesEmRecordMapper;
import cn.iocoder.yudao.module.mes.enums.em.MesEmEnergyTypeEnum;
import cn.iocoder.yudao.module.mes.enums.em.MesEmRecordSourceEnum;
import cn.iocoder.yudao.module.mes.service.em.alert.MesEmAlertService;
import cn.iocoder.yudao.module.mes.service.em.meter.MesEmMeterService;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
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.*;
/**
 * MES èƒ½è€—抄表记录 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesEmRecordServiceImpl implements MesEmRecordService {
    @Resource
    private MesEmRecordMapper recordMapper;
    @Resource
    private MesEmMeterService meterService;
    @Resource
    private MesEmAlertService alertService;
    @Override
    public Long createRecord(MesEmRecordSaveReqVO createReqVO) {
        MesEmMeterDO meter = meterService.getMeter(createReqVO.getMeterId());
        if (meter == null) {
            throw exception(EM_METER_NOT_EXISTS);
        }
        validateRecordDateUnique(null, createReqVO.getMeterId(), createReqVO.getRecordDate());
        MesEmRecordDO record = buildRecord(createReqVO, meter, null);
        recordMapper.insert(record);
        // å†™å…¥åŽæŒ‰å®šé¢ã€çŽ¯æ¯”ã€åŒæ¯”ã€è·‘å†’æ»´æ¼è§„åˆ™æ£€æµ‹é¢„è­¦
        alertService.checkRecord(record);
        return record.getId();
    }
    @Override
    public void updateRecord(MesEmRecordSaveReqVO updateReqVO) {
        validateRecordExists(updateReqVO.getId());
        MesEmMeterDO meter = meterService.getMeter(updateReqVO.getMeterId());
        if (meter == null) {
            throw exception(EM_METER_NOT_EXISTS);
        }
        validateRecordDateUnique(updateReqVO.getId(), updateReqVO.getMeterId(), updateReqVO.getRecordDate());
        MesEmRecordDO record = buildRecord(updateReqVO, meter, updateReqVO.getId());
        recordMapper.updateById(record);
        alertService.checkRecord(record);
    }
    @Override
    public void deleteRecord(Long id) {
        MesEmRecordDO record = validateRecordExists(id);
        recordMapper.deleteById(id);
        // åŒæ­¥æ¸…理该记录产生的环比/同比/跑冒滴漏预警
        alertService.deleteMeterAlerts(record.getMeterId(), record.getRecordDate());
    }
    @Override
    public MesEmRecordDO getRecord(Long id) {
        return recordMapper.selectById(id);
    }
    private MesEmRecordDO validateRecordExists(Long id) {
        MesEmRecordDO record = recordMapper.selectById(id);
        if (record == null) {
            throw exception(EM_RECORD_NOT_EXISTS);
        }
        return record;
    }
    private void validateRecordDateUnique(Long id, Long meterId, LocalDate recordDate) {
        MesEmRecordDO record = recordMapper.selectByMeterIdAndDate(meterId, recordDate);
        if (record != null && ObjUtil.notEqual(record.getId(), id)) {
            throw exception(EM_RECORD_DATE_DUPLICATE);
        }
    }
    /**
     * æž„建抄表记录:自动计算用量与费用,并冗余计量点/车间/产线/设备快照
     */
    private MesEmRecordDO buildRecord(MesEmRecordSaveReqVO reqVO, MesEmMeterDO meter, Long id) {
        // æœŸåˆè¯»æ•°ï¼šæœªå¡«å†™æ—¶å–上一条记录读数,没有历史记录则取计量点初始读数
        BigDecimal prevReading = reqVO.getPrevReading();
        if (prevReading == null) {
            MesEmRecordDO lastRecord = recordMapper.selectLastByMeterId(
                    reqVO.getMeterId(), reqVO.getRecordDate(), id);
            prevReading = lastRecord != null ? lastRecord.getReading() : meter.getInitialReading();
        }
        if (prevReading == null) {
            prevReading = BigDecimal.ZERO;
        }
        if (reqVO.getReading().compareTo(prevReading) < 0) {
            throw exception(EM_RECORD_READING_INVALID);
        }
        BigDecimal ratio = meter.getRatio() == null ? BigDecimal.ONE : meter.getRatio();
        BigDecimal usage = reqVO.getReading().subtract(prevReading).multiply(ratio);
        BigDecimal price = reqVO.getPrice() == null ? BigDecimal.ZERO : reqVO.getPrice();
        BigDecimal cost = usage.multiply(price).setScale(2, RoundingMode.HALF_UP);
        MesEmRecordDO record = BeanUtils.toBean(reqVO, MesEmRecordDO.class);
        record.setId(id);
        record.setMeterCode(meter.getCode());
        record.setMeterName(meter.getName());
        record.setEnergyType(meter.getEnergyType());
        record.setUnit(meter.getUnit());
        record.setRatio(ratio);
        record.setCoalFactor(meter.getCoalFactor());
        record.setWorkshopId(meter.getWorkshopId());
        record.setWorkshopName(meter.getWorkshopName());
        record.setLineId(meter.getLineId());
        record.setLineName(meter.getLineName());
        record.setMachineryId(meter.getMachineryId());
        record.setMachineryName(meter.getMachineryName());
        record.setPrevReading(prevReading);
        record.setUsageAmount(usage);
        record.setPrice(price);
        record.setCost(cost);
        record.setSource(reqVO.getSource() == null ? MesEmRecordSourceEnum.MANUAL.getSource() : reqVO.getSource());
        return record;
    }
    @Override
    public PageResult<MesEmRecordDO> getRecordPage(MesEmRecordPageReqVO pageReqVO) {
        return recordMapper.selectPage(pageReqVO);
    }
    @Override
    public List<MesEmRecordRespVO> getRecordRespVOList(List<MesEmRecordDO> list) {
        if (CollUtil.isEmpty(list)) {
            return Collections.emptyList();
        }
        return BeanUtils.toBean(list, MesEmRecordRespVO.class, vo -> {
            vo.setEnergyTypeName(MesEmEnergyTypeEnum.getName(vo.getEnergyType()));
            vo.setSourceName(MesEmRecordSourceEnum.getName(vo.getSource()));
        });
    }
    @Override
    public MesEmLastReadingRespVO getLastReading(Long meterId, LocalDate recordDate, Long excludeId) {
        MesEmRecordDO lastRecord = recordMapper.selectLastByMeterId(meterId, recordDate, excludeId);
        if (lastRecord != null) {
            return new MesEmLastReadingRespVO(lastRecord.getReading(), lastRecord.getRecordDate());
        }
        MesEmMeterDO meter = meterService.getMeter(meterId);
        if (meter == null) {
            throw exception(EM_METER_NOT_EXISTS);
        }
        return new MesEmLastReadingRespVO(
                meter.getInitialReading() == null ? BigDecimal.ZERO : meter.getInitialReading(), null);
    }
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/statistics/MesEmStatisticsService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package cn.iocoder.yudao.module.mes.service.em.statistics;
import cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo.MesEmOverviewRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo.MesEmReportRespVO;
import java.time.LocalDate;
/**
 * MES èƒ½è€—统计 Service
 *
 * @author è¶…级管理员
 */
public interface MesEmStatisticsService {
    MesEmOverviewRespVO getOverview(LocalDate beginDate, LocalDate endDate);
    MesEmReportRespVO getReport(String period);
}
yudao-module-mes/src/main/java/cn/iocoder/yudao/module/mes/service/em/statistics/MesEmStatisticsServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,331 @@
package cn.iocoder.yudao.module.mes.service.em.statistics;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.util.StrUtil;
import cn.iocoder.yudao.framework.mybatis.core.query.LambdaQueryWrapperX;
import cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo.MesEmOverviewRespVO;
import cn.iocoder.yudao.module.mes.controller.admin.em.statistics.vo.MesEmReportRespVO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.alert.MesEmAlertDO;
import cn.iocoder.yudao.module.mes.dal.dataobject.em.meter.MesEmMeterDO;
import cn.iocoder.yudao.module.mes.dal.mysql.em.alert.MesEmAlertMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.meter.MesEmMeterMapper;
import cn.iocoder.yudao.module.mes.dal.mysql.em.record.MesEmRecordMapper;
import cn.iocoder.yudao.module.mes.enums.em.*;
import jakarta.annotation.Resource;
import org.springframework.stereotype.Service;
import org.springframework.validation.annotation.Validated;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.YearMonth;
import java.util.*;
/**
 * MES èƒ½è€—统计 Service å®žçŽ°ç±»
 *
 * @author è¶…级管理员
 */
@Service
@Validated
public class MesEmStatisticsServiceImpl implements MesEmStatisticsService {
    private static final BigDecimal ZERO = BigDecimal.ZERO;
    private static final BigDecimal HUNDRED = new BigDecimal("100");
    @Resource
    private MesEmRecordMapper recordMapper;
    @Resource
    private MesEmMeterMapper meterMapper;
    @Resource
    private MesEmAlertMapper alertMapper;
    @Override
    public MesEmOverviewRespVO getOverview(LocalDate beginDate, LocalDate endDate) {
        LocalDate begin = beginDate == null ? LocalDate.now().withDayOfMonth(1) : beginDate;
        LocalDate end = endDate == null ? LocalDate.now() : endDate;
        if (begin.isAfter(end)) {
            LocalDate temp = begin;
            begin = end;
            end = temp;
        }
        MesEmOverviewRespVO respVO = new MesEmOverviewRespVO();
        // 1. èƒ½æºç±»åž‹ç»Ÿè®¡
        Map<Integer, Map<String, Object>> typeMap = toMapBy(recordMapper.selectTypeStats(begin, end), "energyType");
        List<MesEmOverviewRespVO.TypeStat> typeStats = new ArrayList<>();
        BigDecimal totalTce = ZERO;
        BigDecimal totalCost = ZERO;
        for (MesEmEnergyTypeEnum energyType : MesEmEnergyTypeEnum.values()) {
            Map<String, Object> row = typeMap.getOrDefault(energyType.getType(), Collections.emptyMap());
            MesEmOverviewRespVO.TypeStat stat = new MesEmOverviewRespVO.TypeStat();
            stat.setEnergyType(energyType.getType());
            stat.setEnergyTypeName(energyType.getName());
            stat.setUnit(energyType.getUnit());
            stat.setUsage(toDecimal(row, "usageAmount"));
            stat.setCost(toDecimal(row, "costAmount"));
            stat.setTce(toDecimal(row, "tce"));
            typeStats.add(stat);
            totalTce = totalTce.add(stat.getTce());
            totalCost = totalCost.add(stat.getCost());
        }
        respVO.setTypeStats(typeStats);
        respVO.setTotalTce(scale(totalTce));
        respVO.setTotalCost(scale(totalCost));
        // 2. è½¦é—´ç»Ÿè®¡
        respVO.setWorkshopStats(parseWorkshopStats(recordMapper.selectWorkshopStats(begin, end)));
        // 3. èƒ½è€—趋势
        respVO.setTrend(parseTrend(recordMapper.selectTrendRows(begin, end)));
        // 4. é‡ç‚¹ç”¨èƒ½è®¾å¤‡æŽ’行
        respVO.setTopMeters(parseTopMeters(recordMapper.selectTopMeterStats(begin, end, 10)));
        // 5. é¢„警概览
        respVO.setMeterCount(Math.toIntExact(meterMapper.selectCount(
                new LambdaQueryWrapperX<MesEmMeterDO>()
                        .eq(MesEmMeterDO::getStatus, MesEmMeterStatusEnum.ENABLE.getStatus()))));
        respVO.setPendingAlertCount(Math.toIntExact(alertMapper.selectPendingCount()));
        respVO.setRecentAlerts(parseRecentAlerts(alertMapper.selectRecent(begin, end, 5)));
        return respVO;
    }
    @Override
    public MesEmReportRespVO getReport(String period) {
        YearMonth yearMonth = StrUtil.isBlank(period) ? YearMonth.now() : YearMonth.parse(period);
        LocalDate begin = yearMonth.atDay(1);
        LocalDate end = yearMonth.atEndOfMonth();
        YearMonth lastMonth = yearMonth.minusMonths(1);
        YearMonth lastYear = yearMonth.minusYears(1);
        Map<Integer, Map<String, Object>> currentTypeMap = toMapBy(recordMapper.selectTypeStats(begin, end), "energyType");
        Map<Integer, Map<String, Object>> lastMonthTypeMap = toMapBy(
                recordMapper.selectTypeStats(lastMonth.atDay(1), lastMonth.atEndOfMonth()), "energyType");
        Map<Integer, Map<String, Object>> lastYearTypeMap = toMapBy(
                recordMapper.selectTypeStats(lastYear.atDay(1), lastYear.atEndOfMonth()), "energyType");
        MesEmReportRespVO respVO = new MesEmReportRespVO();
        respVO.setPeriod(yearMonth.toString());
        respVO.setBeginDate(begin);
        respVO.setEndDate(end);
        // 1. æŒ‰èƒ½æºç±»åž‹
        List<MesEmReportRespVO.TypeRow> typeRows = new ArrayList<>();
        BigDecimal totalTce = ZERO;
        BigDecimal totalCost = ZERO;
        BigDecimal lastMonthTce = ZERO;
        BigDecimal lastYearTce = ZERO;
        for (MesEmEnergyTypeEnum energyType : MesEmEnergyTypeEnum.values()) {
            Map<String, Object> current = currentTypeMap.getOrDefault(energyType.getType(), Collections.emptyMap());
            Map<String, Object> lastMonthRow = lastMonthTypeMap.getOrDefault(energyType.getType(), Collections.emptyMap());
            Map<String, Object> lastYearRow = lastYearTypeMap.getOrDefault(energyType.getType(), Collections.emptyMap());
            MesEmReportRespVO.TypeRow row = new MesEmReportRespVO.TypeRow();
            row.setEnergyType(energyType.getType());
            row.setEnergyTypeName(energyType.getName());
            row.setUnit(energyType.getUnit());
            row.setUsage(toDecimal(current, "usageAmount"));
            row.setCost(toDecimal(current, "costAmount"));
            row.setTce(toDecimal(current, "tce"));
            row.setLastMonthUsage(toDecimal(lastMonthRow, "usageAmount"));
            row.setMomRate(rate(row.getUsage(), row.getLastMonthUsage()));
            row.setLastYearUsage(toDecimal(lastYearRow, "usageAmount"));
            row.setYoyRate(rate(row.getUsage(), row.getLastYearUsage()));
            typeRows.add(row);
            totalTce = totalTce.add(row.getTce());
            totalCost = totalCost.add(row.getCost());
            lastMonthTce = lastMonthTce.add(toDecimal(lastMonthRow, "tce"));
            lastYearTce = lastYearTce.add(toDecimal(lastYearRow, "tce"));
        }
        respVO.setTypeRows(typeRows);
        // 2. æŒ‰è½¦é—´
        respVO.setWorkshopRows(parseReportWorkshopRows(
                recordMapper.selectWorkshopStats(begin, end),
                recordMapper.selectWorkshopStats(lastMonth.atDay(1), lastMonth.atEndOfMonth()),
                recordMapper.selectWorkshopStats(lastYear.atDay(1), lastYear.atEndOfMonth())));
        // 3. åˆè®¡
        respVO.setTotalTce(scale(totalTce));
        respVO.setTotalCost(scale(totalCost));
        respVO.setLastMonthTce(scale(lastMonthTce));
        respVO.setMomRate(rate(totalTce, lastMonthTce));
        respVO.setLastYearTce(scale(lastYearTce));
        respVO.setYoyRate(rate(totalTce, lastYearTce));
        return respVO;
    }
    // ==================== æ•°æ®è½¬æ¢ ====================
    private Map<Integer, Map<String, Object>> toMapBy(List<Map<String, Object>> rows, String key) {
        Map<Integer, Map<String, Object>> result = new HashMap<>();
        for (Map<String, Object> row : rows) {
            Integer type = Convert.toInt(row.get(key));
            if (type != null) {
                result.put(type, row);
            }
        }
        return result;
    }
    private List<MesEmOverviewRespVO.WorkshopStat> parseWorkshopStats(List<Map<String, Object>> rows) {
        List<MesEmOverviewRespVO.WorkshopStat> result = new ArrayList<>();
        for (Map<String, Object> row : rows) {
            MesEmOverviewRespVO.WorkshopStat stat = new MesEmOverviewRespVO.WorkshopStat();
            stat.setWorkshopId(Convert.toLong(row.get("workshopId")));
            stat.setWorkshopName(Convert.toStr(row.get("workshopName")));
            stat.setTce(scale(Convert.toBigDecimal(row.get("tce"), ZERO)));
            stat.setCost(scale(Convert.toBigDecimal(row.get("costAmount"), ZERO)));
            result.add(stat);
        }
        return result;
    }
    private List<MesEmOverviewRespVO.TrendPoint> parseTrend(List<Map<String, Object>> rows) {
        Map<String, MesEmOverviewRespVO.TrendPoint> pointMap = new TreeMap<>();
        for (Map<String, Object> row : rows) {
            String date = toDateStr(row.get("recordDate"));
            if (StrUtil.isBlank(date)) {
                continue;
            }
            MesEmOverviewRespVO.TrendPoint point = pointMap.computeIfAbsent(date, key -> {
                MesEmOverviewRespVO.TrendPoint item = new MesEmOverviewRespVO.TrendPoint();
                item.setDate(key);
                item.setTce(ZERO);
                item.setCost(ZERO);
                item.setElectricity(ZERO);
                item.setWater(ZERO);
                item.setFuel(ZERO);
                item.setGas(ZERO);
                return item;
            });
            Integer energyType = Convert.toInt(row.get("energyType"));
            BigDecimal usage = Convert.toBigDecimal(row.get("usageAmount"), ZERO);
            point.setTce(point.getTce().add(Convert.toBigDecimal(row.get("tce"), ZERO)));
            point.setCost(point.getCost().add(Convert.toBigDecimal(row.get("costAmount"), ZERO)));
            if (MesEmEnergyTypeEnum.ELECTRICITY.getType().equals(energyType)) {
                point.setElectricity(usage);
            } else if (MesEmEnergyTypeEnum.WATER.getType().equals(energyType)) {
                point.setWater(usage);
            } else if (MesEmEnergyTypeEnum.FUEL.getType().equals(energyType)) {
                point.setFuel(usage);
            } else if (MesEmEnergyTypeEnum.GAS.getType().equals(energyType)) {
                point.setGas(usage);
            }
        }
        pointMap.values().forEach(point -> {
            point.setTce(scale(point.getTce()));
            point.setCost(scale(point.getCost()));
        });
        return new ArrayList<>(pointMap.values());
    }
    private List<MesEmOverviewRespVO.MeterStat> parseTopMeters(List<Map<String, Object>> rows) {
        List<MesEmOverviewRespVO.MeterStat> result = new ArrayList<>();
        for (Map<String, Object> row : rows) {
            MesEmOverviewRespVO.MeterStat stat = new MesEmOverviewRespVO.MeterStat();
            stat.setMeterId(Convert.toLong(row.get("meterId")));
            stat.setMeterName(Convert.toStr(row.get("meterName")));
            stat.setEnergyType(Convert.toInt(row.get("energyType")));
            stat.setEnergyTypeName(MesEmEnergyTypeEnum.getName(stat.getEnergyType()));
            stat.setUnit(Convert.toStr(row.get("unit")));
            stat.setUsage(Convert.toBigDecimal(row.get("usageAmount"), ZERO));
            stat.setCost(scale(Convert.toBigDecimal(row.get("costAmount"), ZERO)));
            stat.setTce(scale(Convert.toBigDecimal(row.get("tce"), ZERO)));
            result.add(stat);
        }
        return result;
    }
    private List<MesEmOverviewRespVO.RecentAlert> parseRecentAlerts(List<MesEmAlertDO> alerts) {
        List<MesEmOverviewRespVO.RecentAlert> result = new ArrayList<>();
        for (MesEmAlertDO alert : alerts) {
            MesEmOverviewRespVO.RecentAlert item = new MesEmOverviewRespVO.RecentAlert();
            item.setId(alert.getId());
            item.setContent(alert.getContent());
            item.setAlertLevel(alert.getAlertLevel());
            item.setAlertLevelName(MesEmAlertLevelEnum.getName(alert.getAlertLevel()));
            item.setAlertDate(alert.getAlertDate() == null ? null : alert.getAlertDate().toString());
            item.setStatus(alert.getStatus());
            item.setStatusName(MesEmAlertStatusEnum.getName(alert.getStatus()));
            result.add(item);
        }
        return result;
    }
    private List<MesEmReportRespVO.WorkshopRow> parseReportWorkshopRows(
            List<Map<String, Object>> currentRows, List<Map<String, Object>> lastMonthRows,
            List<Map<String, Object>> lastYearRows) {
        Map<Long, Map<String, Object>> currentMap = toMapByWorkshop(currentRows);
        Map<Long, Map<String, Object>> lastMonthMap = toMapByWorkshop(lastMonthRows);
        Map<Long, Map<String, Object>> lastYearMap = toMapByWorkshop(lastYearRows);
        Set<Long> workshopIds = new LinkedHashSet<>(currentMap.keySet());
        workshopIds.addAll(lastMonthMap.keySet());
        workshopIds.addAll(lastYearMap.keySet());
        List<MesEmReportRespVO.WorkshopRow> result = new ArrayList<>();
        for (Long workshopId : workshopIds) {
            Map<String, Object> current = currentMap.getOrDefault(workshopId, Collections.emptyMap());
            Map<String, Object> lastMonth = lastMonthMap.getOrDefault(workshopId, Collections.emptyMap());
            Map<String, Object> lastYear = lastYearMap.getOrDefault(workshopId, Collections.emptyMap());
            MesEmReportRespVO.WorkshopRow row = new MesEmReportRespVO.WorkshopRow();
            row.setWorkshopId(workshopId);
            row.setWorkshopName(Convert.toStr(current.getOrDefault("workshopName",
                    lastMonth.getOrDefault("workshopName", lastYear.get("workshopName")))));
            row.setCost(scale(Convert.toBigDecimal(current.get("costAmount"), ZERO)));
            row.setTce(scale(Convert.toBigDecimal(current.get("tce"), ZERO)));
            row.setLastMonthTce(scale(Convert.toBigDecimal(lastMonth.get("tce"), ZERO)));
            row.setMomRate(rate(row.getTce(), row.getLastMonthTce()));
            row.setLastYearTce(scale(Convert.toBigDecimal(lastYear.get("tce"), ZERO)));
            row.setYoyRate(rate(row.getTce(), row.getLastYearTce()));
            result.add(row);
        }
        return result;
    }
    private Map<Long, Map<String, Object>> toMapByWorkshop(List<Map<String, Object>> rows) {
        Map<Long, Map<String, Object>> result = new HashMap<>();
        for (Map<String, Object> row : rows) {
            Long workshopId = Convert.toLong(row.get("workshopId"));
            if (workshopId != null) {
                result.put(workshopId, row);
            }
        }
        return result;
    }
    private BigDecimal toDecimal(Map<String, Object> row, String key) {
        return scale(Convert.toBigDecimal(row.get(key), ZERO));
    }
    private String toDateStr(Object value) {
        if (value == null) {
            return null;
        }
        if (value instanceof LocalDate localDate) {
            return localDate.toString();
        }
        if (value instanceof java.sql.Date sqlDate) {
            return sqlDate.toLocalDate().toString();
        }
        String text = Convert.toStr(value);
        return text != null && text.length() > 10 ? text.substring(0, 10) : text;
    }
    /**
     * è®¡ç®—环比/同比(%),基数为 0 æ—¶è¿”回 null,前端显示为 -
     */
    private BigDecimal rate(BigDecimal current, BigDecimal base) {
        if (base == null || base.compareTo(ZERO) == 0) {
            return null;
        }
        return current.subtract(base).multiply(HUNDRED).divide(base, 2, RoundingMode.HALF_UP);
    }
    private BigDecimal scale(BigDecimal value) {
        return value == null ? ZERO : value.setScale(2, RoundingMode.HALF_UP);
    }
}