gongchunyi
21 小时以前 e9b4ceeea014f9d81263672f3d9c46a567c39591
feat: 能耗管理
已添加63个文件
已修改4个文件
8702 ■■■■■ 文件已修改
demo/energy-meter-export/energy_meter_all.sql 851 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
doc/水电表接入/sql/水电表接入.sql 333 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java 5 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/config/EnergyMeterExecutorConfig.java 43 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/config/EnergyMeterProperties.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/constant/EnergyMeterConstants.java 208 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/controller/EnergyMeterController.java 244 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/controller/EnergyMonitorController.java 247 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/DailyUsageRow.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/DataRangeRow.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/EnergyMeterDeviceQuery.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterArchive.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterBindRequest.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterCollectRequest.java 36 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterManualBindRequest.java 38 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterQuarterPoint.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterReadingPoint.java 31 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterReadingRequest.java 29 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterUpdateRequest.java 23 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterUsagePoint.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterUsageRow.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MeterVerifyRequest.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/MonthlyUsageRow.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/dto/TaskStatusCount.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/job/EnergyMeterDailySyncJob.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/job/EnergyMeterRecoveryJob.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/job/EnergyMeterSchedulerRegistrar.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterDeviceMapper.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterMeasurementMapper.java 114 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterSyncCheckpointMapper.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterSyncRunMapper.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterSyncTaskMapper.java 78 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterDevice.java 164 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterMeasurement.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterSyncCheckpoint.java 49 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterSyncRun.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterSyncTask.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/provider/MeterProviderClient.java 52 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/provider/MeterProviderException.java 25 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/provider/YunJiChaoBiaoProviderClient.java 706 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/ElectricMeterDashboardService.java 17 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/EnergyMeterDemoService.java 16 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/EnergyMeterDeviceService.java 87 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/EnergyMeterStatService.java 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/EnergyMeterSyncService.java 42 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/EnergyMonitorService.java 28 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/impl/ElectricMeterDashboardServiceImpl.java 353 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterDemoServiceImpl.java 132 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterDeviceServiceImpl.java 692 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterStatServiceImpl.java 389 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterSyncServiceImpl.java 768 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMonitorServiceImpl.java 822 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/util/EnergyMeterSupport.java 55 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/vo/ElectricMeterDashboardVO.java 124 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/vo/EnergyCollectionVO.java 133 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/vo/EnergyMeterOverviewVO.java 137 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/vo/EnergyRealtimeVO.java 180 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/vo/EnergyReportVO.java 118 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/vo/MeterArchiveVO.java 47 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/energymeter/vo/MeterReadingResultVO.java 41 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-dev.yml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application-xswh.yml 30 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/application.yml 2 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/energymeter/EnergyMeterMeasurementMapper.xml 111 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/energymeter/EnergyMeterSyncCheckpointMapper.xml 18 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/energymeter/EnergyMeterSyncRunMapper.xml 24 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/energymeter/EnergyMeterSyncTaskMapper.xml 88 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
demo/energy-meter-export/energy_meter_all.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,851 @@
-- èƒ½è€—管理(水表/电表/气表)数据导出
-- æºæ•°æ®åº“:product-inventory-management-xswh
-- å¯¼å‡ºæ—¶é—´ï¼š2026-11-04 23:26:05
-- å­—符集:UTF-8 (utf8mb4)
-- è¯´æ˜Žï¼šåŒ…含建表语句(IF NOT EXISTS)与数据,不会 DROP ä»»ä½•表。
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- energy_meter_device
-- ----------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_device` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '本地设备ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `meter_type` varchar(16) NOT NULL COMMENT '能源类型:WATER水表 / ELECTRIC电表 / GAS气表',
  `source_type` varchar(16) NOT NULL DEFAULT 'PROVIDER' COMMENT '数据来源:PROVIDER第三方接入 / MANUAL手工抄表',
  `meter_address` varchar(64) NOT NULL COMMENT '通信地址或表号,按字符串保存,保留前导零',
  `provider_meter_id` varchar(64) DEFAULT NULL COMMENT '第三方设备ID',
  `provider_name` varchar(128) DEFAULT NULL COMMENT '第三方设备名称',
  `display_name` varchar(128) NOT NULL COMMENT '本系统显示名称',
  `provider_area_id` bigint DEFAULT NULL COMMENT '第三方区域ID',
  `provider_area_name` varchar(128) DEFAULT NULL COMMENT '第三方区域名称',
  `parent_provider_id` varchar(64) DEFAULT NULL COMMENT '第三方总表ID',
  `parent_meter_id` bigint DEFAULT NULL COMMENT '本地总表ID,确认映射后维护',
  `location` varchar(255) DEFAULT NULL COMMENT '安装位置',
  `online_status` tinyint DEFAULT NULL COMMENT '1在线 0离线 NULL未知',
  `bind_status` varchar(16) NOT NULL DEFAULT 'VERIFIED' COMMENT 'VERIFIED/INVALID',
  `enabled` tinyint NOT NULL DEFAULT '1' COMMENT '1启用 0停用',
  `is_seed` tinyint NOT NULL DEFAULT '0' COMMENT '1初始化设备(禁止删除)0用户新增',
  `simulated` tinyint NOT NULL DEFAULT '0' COMMENT '1演示数据(每日模拟生成)0真实设备',
  `initial_reading` decimal(20,6) DEFAULT NULL COMMENT '手工抄表起始表码',
  `initial_reading_date` date DEFAULT NULL COMMENT '起始表码对应业务日期',
  `sync_start_date` date NOT NULL COMMENT '本设备最早需要同步的业务日期',
  `last_success_at` datetime(3) DEFAULT NULL COMMENT '最近成功采集/抄表时间',
  `last_error_code` varchar(64) DEFAULT NULL COMMENT '最近采集错误类别',
  `last_error_message` varchar(255) DEFAULT NULL COMMENT '脱敏错误摘要',
  `create_user` bigint DEFAULT NULL COMMENT '创建人',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_user` bigint DEFAULT NULL COMMENT '修改人',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_device_tenant_type_address` (`tenant_id`,`meter_type`,`meter_address`),
  KEY `idx_em_device_scan` (`enabled`,`bind_status`,`meter_type`,`id`),
  KEY `idx_em_device_parent` (`parent_meter_id`),
  KEY `idx_em_device_source` (`tenant_id`,`source_type`,`meter_type`,`enabled`)
) ENGINE=InnoDB AUTO_INCREMENT=20 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='设备台账:水表/电表/气表';
-- ----------------------------
-- energy_meter_measurement
-- ----------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_measurement` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '采集数据ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `meter_id` bigint NOT NULL COMMENT '本地设备ID',
  `metric_code` varchar(32) NOT NULL COMMENT 'WATER_USAGE/WATER_READING/ELECTRIC_USAGE/ELECTRIC_READING/ELECTRIC_COST/GAS_USAGE/GAS_READING',
  `granularity` varchar(8) NOT NULL COMMENT 'D日/M月/Yå¹´/POINT时点',
  `measure_time` datetime NOT NULL COMMENT '期间起点或示值时点,Asia/Shanghai',
  `metric_value` decimal(20,6) NOT NULL COMMENT '实际期间用量、示值或费用',
  `unit` varchar(16) NOT NULL COMMENT 'm3/kWh/CNY',
  `source_api` varchar(64) NOT NULL COMMENT '来源标识:第三方接口名或 MANUAL',
  `source_record_time` datetime DEFAULT NULL COMMENT '第三方原始ReadingDate解析值;手工抄表为抄表日期',
  `collected_at` datetime(3) NOT NULL COMMENT '本系统采集时间',
  `remark` varchar(255) DEFAULT NULL COMMENT '备注:手工抄表说明',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次入库时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后修正时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_measurement_point` (`meter_id`,`metric_code`,`granularity`,`measure_time`),
  KEY `idx_em_measurement_query` (`tenant_id`,`meter_id`,`metric_code`,`granularity`,`measure_time`),
  KEY `idx_em_measurement_time` (`tenant_id`,`metric_code`,`granularity`,`measure_time`),
  KEY `idx_em_measurement_source` (`meter_id`,`source_api`,`granularity`,`measure_time`)
) ENGINE=InnoDB AUTO_INCREMENT=8503 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='用量、示值和已验证费用';
-- ----------------------------
-- energy_meter_sync_run
-- ----------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_sync_run` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '同步批次ID',
  `run_key` varchar(80) NOT NULL COMMENT '自动批次AUTO:yyyyMMdd;手动批次使用UUID',
  `trigger_type` varchar(16) NOT NULL COMMENT 'SCHEDULED/MANUAL/BACKFILL',
  `schedule_date` date DEFAULT NULL COMMENT '计划执行日,北京时间;非定时批次为空',
  `range_start` date NOT NULL COMMENT '同步开始日期,含',
  `range_end` date NOT NULL COMMENT '同步结束日期,含',
  `status` varchar(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RUNNING/SUCCESS/PARTIAL/FAILED',
  `total_tasks` int NOT NULL DEFAULT '0' COMMENT '任务总数',
  `success_tasks` int NOT NULL DEFAULT '0' COMMENT '成功任务数',
  `empty_tasks` int NOT NULL DEFAULT '0' COMMENT '确认无数据任务数',
  `failed_tasks` int NOT NULL DEFAULT '0' COMMENT '失败任务数',
  `started_at` datetime(3) DEFAULT NULL COMMENT '开始时间',
  `finished_at` datetime(3) DEFAULT NULL COMMENT '结束时间',
  `error_summary` varchar(500) DEFAULT NULL COMMENT '脱敏错误摘要',
  `create_user` bigint DEFAULT NULL COMMENT '触发人;系统任务为空',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_sync_run_key` (`run_key`),
  KEY `idx_em_sync_run_schedule` (`trigger_type`,`schedule_date`,`status`),
  KEY `idx_em_sync_run_created` (`create_time`)
) ENGINE=InnoDB AUTO_INCREMENT=10 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='同步执行批次';
-- ----------------------------
-- energy_meter_sync_task
-- ----------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_sync_task` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '单设备单接口同步任务ID',
  `run_id` bigint NOT NULL COMMENT '所属批次ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `meter_id` bigint NOT NULL COMMENT '本地设备ID',
  `source_api` varchar(64) NOT NULL COMMENT '第三方接口标识',
  `granularity` varchar(8) NOT NULL COMMENT 'D/M/Y/POINT',
  `range_start` date NOT NULL COMMENT '请求开始日期,含',
  `range_end` date NOT NULL COMMENT '请求结束日期,含',
  `status` varchar(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RUNNING/SUCCESS/EMPTY/RETRY/FAILED',
  `attempt_count` int NOT NULL DEFAULT '0' COMMENT '已尝试次数',
  `next_retry_at` datetime(3) DEFAULT NULL COMMENT '下次重试时间',
  `fetched_count` int NOT NULL DEFAULT '0' COMMENT '上游返回条数',
  `inserted_count` int NOT NULL DEFAULT '0' COMMENT '新增条数',
  `updated_count` int NOT NULL DEFAULT '0' COMMENT '修正条数',
  `error_code` varchar(64) DEFAULT NULL COMMENT '脱敏错误类别',
  `error_message` varchar(500) DEFAULT NULL COMMENT '脱敏错误信息',
  `started_at` datetime(3) DEFAULT NULL COMMENT '开始时间',
  `finished_at` datetime(3) DEFAULT NULL COMMENT '完成时间',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_sync_task_unit` (`run_id`,`meter_id`,`source_api`,`granularity`,`range_start`,`range_end`),
  KEY `idx_em_sync_task_retry` (`status`,`next_retry_at`),
  KEY `idx_em_sync_task_meter` (`tenant_id`,`meter_id`,`create_time`),
  KEY `idx_em_sync_task_run` (`run_id`,`status`)
) ENGINE=InnoDB AUTO_INCREMENT=41 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='单设备同步任务及重试状态';
-- ----------------------------
-- energy_meter_sync_checkpoint
-- ----------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_sync_checkpoint` (
  `id` bigint NOT NULL AUTO_INCREMENT COMMENT '补采进度ID',
  `tenant_id` bigint NOT NULL COMMENT '租户ID',
  `meter_id` bigint NOT NULL COMMENT '本地设备ID',
  `source_api` varchar(64) NOT NULL COMMENT '第三方接口标识',
  `granularity` varchar(8) NOT NULL COMMENT 'D/M/Y/POINT',
  `last_complete_date` date DEFAULT NULL COMMENT '连续完成到的业务日期,含;不可跨过失败日期',
  `last_success_at` datetime(3) DEFAULT NULL COMMENT '最近成功确认时间',
  `last_run_id` bigint DEFAULT NULL COMMENT '最近推进进度的批次ID',
  `create_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_sync_checkpoint_unit` (`meter_id`,`source_api`,`granularity`),
  KEY `idx_em_sync_checkpoint_tenant` (`tenant_id`,`meter_id`)
) ENGINE=InnoDB AUTO_INCREMENT=35 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='每接口连续同步进度';
-- energy_meter_device: 15 rows
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (1, 208, 'WATER', 'PROVIDER', '12541588057907', '32134', '二楼灭菌西北', '二楼灭菌西北', NULL, '兴盛旺海', NULL, NULL, '二楼灭菌西北', 1, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-10-13', '2026-11-04 15:17:39.697000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 15:52:07.417000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (2, 208, 'WATER', 'PROVIDER', '12541588045261', '32133', '二楼生产用水总水表', '二楼生产用水总水表', NULL, '兴盛旺海', NULL, NULL, '二楼生产用水总水表', 1, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-10-13', '2026-11-04 15:17:41.645000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 15:17:41.654000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (3, 208, 'WATER', 'PROVIDER', '12541588057898', '32132', '煮毛肚水表', '煮毛肚水表', NULL, '兴盛旺海', NULL, NULL, '煮毛肚水表', 1, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-10-13', '2026-11-04 15:17:43.133000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 15:17:43.141000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (4, 208, 'WATER', 'PROVIDER', '12541588057901', '32131', '提升机洗毛肚处', '提升机洗毛肚处', NULL, '兴盛旺海', NULL, NULL, '提升机洗毛肚处', 1, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-10-13', '2026-11-04 15:17:44.657000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 15:17:44.669000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (5, 208, 'WATER', 'PROVIDER', '12541588057897', '32130', '分切毛肚', '分切毛肚', NULL, '兴盛旺海', NULL, NULL, '分切毛肚', 1, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-10-13', '2026-11-04 15:17:46.156000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 15:17:46.164000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (6, 208, 'WATER', 'PROVIDER', '12541588057908', '32123', '一楼纯净水总表', '一楼纯净水总表', NULL, '兴盛旺海', NULL, NULL, '一楼纯净水总表', 1, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-10-13', '2026-11-04 15:19:54.095000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 15:19:54.102000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (7, 208, 'ELECTRIC', 'PROVIDER', '712606021272', '270946', '二楼总电表', '二楼总电表', NULL, '电表', NULL, NULL, '二楼总电表', 0, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-11-04', '2026-11-04 19:00:01.774000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 19:00:01.787000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (8, 208, 'ELECTRIC', 'PROVIDER', '712606021277', '270949', '污水电表', '污水电表', NULL, '电表', NULL, NULL, '污水电表', 0, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-11-04', '2026-11-04 19:00:01.984000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 19:00:01.995000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (9, 208, 'ELECTRIC', 'PROVIDER', '712606021273', '270951', '一楼总电表', '一楼总电表', NULL, '电表', NULL, NULL, '一楼总电表', 0, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-11-04', '2026-11-04 19:00:02.129000', NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 19:00:02.137000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (10, 208, 'WATER', 'PROVIDER', '12541588057808', '32758', '二楼南排煮锅纯净水', '二楼南排煮锅纯净水', NULL, '水表', NULL, NULL, '二楼南排煮锅纯净水', 0, 'VERIFIED', 1, 1, 0, NULL, NULL, '2026-11-04', NULL, NULL, NULL, 1, '2026-11-04 15:08:50.606000', NULL, '2026-11-04 15:08:50.606000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (11, 208, 'GAS', 'MANUAL', '862873080705632', NULL, NULL, '灭菌处气表', NULL, NULL, NULL, NULL, '灭菌处', NULL, 'VERIFIED', 1, 0, 1, 0.000000, '2026-11-04', '2025-09-01', '2026-11-04 00:00:00', NULL, NULL, 1, '2026-11-04 15:08:50.612000', NULL, '2026-11-04 17:02:17.039000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (12, 208, 'GAS', 'MANUAL', '865619085854645', NULL, NULL, '煮锅中气表', NULL, NULL, NULL, NULL, '煮锅中', NULL, 'VERIFIED', 1, 0, 1, 0.000000, '2026-11-04', '2025-09-01', '2026-11-04 00:00:00', NULL, NULL, 1, '2026-11-04 15:08:50.612000', NULL, '2026-11-04 17:02:17.039000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (13, 208, 'GAS', 'MANUAL', '862873080702613', NULL, NULL, '煮锅南气表', NULL, NULL, NULL, NULL, '煮锅南', NULL, 'VERIFIED', 1, 0, 1, 0.000000, '2026-11-04', '2025-09-01', '2026-11-04 00:00:00', NULL, NULL, 1, '2026-11-04 15:08:50.612000', NULL, '2026-11-04 17:02:17.039000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (14, 208, 'GAS', 'MANUAL', '866960071744294', NULL, NULL, '煮锅北气表', NULL, NULL, NULL, NULL, '煮锅北', NULL, 'VERIFIED', 1, 0, 1, 0.000000, '2026-11-04', '2025-09-01', '2026-11-04 00:00:00', NULL, NULL, 1, '2026-11-04 15:08:50.612000', NULL, '2026-11-04 17:02:17.039000');
INSERT INTO `energy_meter_device` (`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`, `provider_name`, `display_name`, `provider_area_id`, `provider_area_name`, `parent_provider_id`, `parent_meter_id`, `location`, `online_status`, `bind_status`, `enabled`, `is_seed`, `simulated`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`, `last_error_code`, `last_error_message`, `create_user`, `create_time`, `update_user`, `update_time`) VALUES (15, 208, 'GAS', 'MANUAL', '862873080704353', NULL, NULL, '一楼煮毛肚气表', NULL, NULL, NULL, NULL, '一楼煮毛肚', NULL, 'VERIFIED', 1, 0, 1, 0.000000, '2026-11-04', '2025-09-01', '2026-11-04 00:00:00', NULL, NULL, 1, '2026-11-04 15:08:50.612000', NULL, '2026-11-04 17:02:17.039000');
-- energy_meter_measurement: 274 rows
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8198, 208, 1, 'WATER_READING', 'POINT', '2026-10-26 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-26 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8199, 208, 1, 'WATER_READING', 'POINT', '2026-10-25 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-25 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8200, 208, 1, 'WATER_READING', 'POINT', '2026-10-24 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-24 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8201, 208, 1, 'WATER_READING', 'POINT', '2026-10-23 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-23 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8202, 208, 1, 'WATER_READING', 'POINT', '2026-10-22 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-22 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8203, 208, 1, 'WATER_READING', 'POINT', '2026-10-21 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-21 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8204, 208, 1, 'WATER_READING', 'POINT', '2026-10-20 10:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-20 10:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8205, 208, 1, 'WATER_READING', 'POINT', '2026-10-20 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-20 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8206, 208, 1, 'WATER_READING', 'POINT', '2026-10-19 03:00:00', 18.000000, 'm3', 'GetWaterRawData', '2026-10-19 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8207, 208, 1, 'WATER_READING', 'POINT', '2026-10-18 03:00:00', 18.000000, 'm3', 'GetWaterRawData', '2026-10-18 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8208, 208, 1, 'WATER_READING', 'POINT', '2026-10-17 03:00:00', 18.000000, 'm3', 'GetWaterRawData', '2026-10-17 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8209, 208, 1, 'WATER_READING', 'POINT', '2026-10-16 03:00:00', 18.000000, 'm3', 'GetWaterRawData', '2026-10-16 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8210, 208, 1, 'WATER_READING', 'POINT', '2026-10-15 03:00:00', 17.000000, 'm3', 'GetWaterRawData', '2026-10-15 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8211, 208, 1, 'WATER_READING', 'POINT', '2026-10-14 03:00:00', 17.000000, 'm3', 'GetWaterRawData', '2026-10-14 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8212, 208, 1, 'WATER_READING', 'POINT', '2026-10-13 03:00:00', 17.000000, 'm3', 'GetWaterRawData', '2026-10-13 03:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:38.999000', '2026-11-04 15:17:38.999000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8213, 208, 1, 'WATER_USAGE', 'D', '2026-11-03 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-03 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8214, 208, 1, 'WATER_USAGE', 'D', '2026-11-02 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-02 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8215, 208, 1, 'WATER_USAGE', 'D', '2026-11-01 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-01 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8216, 208, 1, 'WATER_USAGE', 'D', '2026-10-31 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-31 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8217, 208, 1, 'WATER_USAGE', 'D', '2026-10-30 00:00:00', 1.000000, 'm3', 'GetUseWater', '2026-10-30 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8218, 208, 1, 'WATER_USAGE', 'D', '2026-10-29 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-29 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8219, 208, 1, 'WATER_USAGE', 'D', '2026-10-28 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-28 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8220, 208, 1, 'WATER_USAGE', 'D', '2026-10-27 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-27 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8221, 208, 1, 'WATER_USAGE', 'D', '2026-10-26 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-26 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8222, 208, 1, 'WATER_USAGE', 'D', '2026-10-25 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-25 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8223, 208, 1, 'WATER_USAGE', 'D', '2026-10-24 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-24 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8224, 208, 1, 'WATER_USAGE', 'D', '2026-10-23 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-23 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8225, 208, 1, 'WATER_USAGE', 'D', '2026-10-22 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-22 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8226, 208, 1, 'WATER_USAGE', 'D', '2026-10-21 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-21 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8227, 208, 1, 'WATER_USAGE', 'D', '2026-10-20 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-20 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8228, 208, 1, 'WATER_USAGE', 'D', '2026-10-19 00:00:00', 1.000000, 'm3', 'GetUseWater', '2026-10-19 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8229, 208, 1, 'WATER_USAGE', 'D', '2026-10-18 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-18 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8230, 208, 1, 'WATER_USAGE', 'D', '2026-10-17 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-17 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8231, 208, 1, 'WATER_USAGE', 'D', '2026-10-16 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-16 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8232, 208, 1, 'WATER_USAGE', 'D', '2026-10-15 00:00:00', 1.000000, 'm3', 'GetUseWater', '2026-10-15 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8233, 208, 1, 'WATER_USAGE', 'D', '2026-10-14 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-14 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8234, 208, 1, 'WATER_USAGE', 'D', '2026-10-13 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-13 00:00:00', '2026-11-04 15:17:38.525000', NULL, '2026-11-04 15:17:39.148000', '2026-11-04 15:17:39.148000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8235, 208, 1, 'WATER_READING', 'POINT', '2026-11-02 03:00:00', 20.000000, 'm3', 'GetWaterRawData', '2026-11-02 03:00:00', '2026-11-04 15:17:39.028000', NULL, '2026-11-04 15:17:39.702000', '2026-11-04 15:17:39.702000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8236, 208, 1, 'WATER_READING', 'POINT', '2026-11-01 03:00:00', 20.000000, 'm3', 'GetWaterRawData', '2026-11-01 03:00:00', '2026-11-04 15:17:39.028000', NULL, '2026-11-04 15:17:39.702000', '2026-11-04 15:17:39.702000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8237, 208, 1, 'WATER_READING', 'POINT', '2026-10-31 03:00:00', 20.000000, 'm3', 'GetWaterRawData', '2026-10-31 03:00:00', '2026-11-04 15:17:39.028000', NULL, '2026-11-04 15:17:39.702000', '2026-11-04 15:17:39.702000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8238, 208, 1, 'WATER_READING', 'POINT', '2026-10-30 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-30 03:00:00', '2026-11-04 15:17:39.028000', NULL, '2026-11-04 15:17:39.702000', '2026-11-04 15:17:39.702000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8239, 208, 1, 'WATER_READING', 'POINT', '2026-10-29 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-29 03:00:00', '2026-11-04 15:17:39.028000', NULL, '2026-11-04 15:17:39.702000', '2026-11-04 15:17:39.702000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8240, 208, 1, 'WATER_READING', 'POINT', '2026-10-28 03:00:00', 19.000000, 'm3', 'GetWaterRawData', '2026-10-28 03:00:00', '2026-11-04 15:17:39.028000', NULL, '2026-11-04 15:17:39.702000', '2026-11-04 15:17:39.702000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8241, 208, 2, 'WATER_USAGE', 'D', '2026-11-03 00:00:00', 73.000000, 'm3', 'GetUseWater', '2026-11-03 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8242, 208, 2, 'WATER_USAGE', 'D', '2026-11-02 00:00:00', 24.000000, 'm3', 'GetUseWater', '2026-11-02 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8243, 208, 2, 'WATER_USAGE', 'D', '2026-11-01 00:00:00', 26.000000, 'm3', 'GetUseWater', '2026-11-01 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8244, 208, 2, 'WATER_USAGE', 'D', '2026-10-31 00:00:00', 34.000000, 'm3', 'GetUseWater', '2026-10-31 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8245, 208, 2, 'WATER_USAGE', 'D', '2026-10-30 00:00:00', 48.000000, 'm3', 'GetUseWater', '2026-10-30 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8246, 208, 2, 'WATER_USAGE', 'D', '2026-10-29 00:00:00', 46.000000, 'm3', 'GetUseWater', '2026-10-29 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8247, 208, 2, 'WATER_USAGE', 'D', '2026-10-28 00:00:00', 55.000000, 'm3', 'GetUseWater', '2026-10-28 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8248, 208, 2, 'WATER_USAGE', 'D', '2026-10-27 00:00:00', 62.000000, 'm3', 'GetUseWater', '2026-10-27 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8249, 208, 2, 'WATER_USAGE', 'D', '2026-10-26 00:00:00', 55.000000, 'm3', 'GetUseWater', '2026-10-26 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8250, 208, 2, 'WATER_USAGE', 'D', '2026-10-25 00:00:00', 58.000000, 'm3', 'GetUseWater', '2026-10-25 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8251, 208, 2, 'WATER_USAGE', 'D', '2026-10-24 00:00:00', 58.000000, 'm3', 'GetUseWater', '2026-10-24 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8252, 208, 2, 'WATER_USAGE', 'D', '2026-10-23 00:00:00', 68.000000, 'm3', 'GetUseWater', '2026-10-23 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8253, 208, 2, 'WATER_USAGE', 'D', '2026-10-22 00:00:00', 61.000000, 'm3', 'GetUseWater', '2026-10-22 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8254, 208, 2, 'WATER_USAGE', 'D', '2026-10-21 00:00:00', 68.000000, 'm3', 'GetUseWater', '2026-10-21 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8255, 208, 2, 'WATER_USAGE', 'D', '2026-10-20 00:00:00', 71.000000, 'm3', 'GetUseWater', '2026-10-20 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8256, 208, 2, 'WATER_USAGE', 'D', '2026-10-19 00:00:00', 55.000000, 'm3', 'GetUseWater', '2026-10-19 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8257, 208, 2, 'WATER_USAGE', 'D', '2026-10-18 00:00:00', 61.000000, 'm3', 'GetUseWater', '2026-10-18 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8258, 208, 2, 'WATER_USAGE', 'D', '2026-10-17 00:00:00', 27.000000, 'm3', 'GetUseWater', '2026-10-17 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8259, 208, 2, 'WATER_USAGE', 'D', '2026-10-16 00:00:00', 23.000000, 'm3', 'GetUseWater', '2026-10-16 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8260, 208, 2, 'WATER_USAGE', 'D', '2026-10-15 00:00:00', 42.000000, 'm3', 'GetUseWater', '2026-10-15 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8261, 208, 2, 'WATER_USAGE', 'D', '2026-10-14 00:00:00', 40.000000, 'm3', 'GetUseWater', '2026-10-14 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8262, 208, 2, 'WATER_USAGE', 'D', '2026-10-13 00:00:00', 39.000000, 'm3', 'GetUseWater', '2026-10-13 00:00:00', '2026-11-04 15:17:39.175000', NULL, '2026-11-04 15:17:40.387000', '2026-11-04 15:17:40.387000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8263, 208, 2, 'WATER_READING', 'POINT', '2026-10-26 07:00:00', 3055.000000, 'm3', 'GetWaterRawData', '2026-10-26 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8264, 208, 2, 'WATER_READING', 'POINT', '2026-10-25 07:00:00', 2997.000000, 'm3', 'GetWaterRawData', '2026-10-25 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8265, 208, 2, 'WATER_READING', 'POINT', '2026-10-24 07:00:00', 2939.000000, 'm3', 'GetWaterRawData', '2026-10-24 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8266, 208, 2, 'WATER_READING', 'POINT', '2026-10-23 07:00:00', 2871.000000, 'm3', 'GetWaterRawData', '2026-10-23 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8267, 208, 2, 'WATER_READING', 'POINT', '2026-10-22 07:00:00', 2810.000000, 'm3', 'GetWaterRawData', '2026-10-22 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8268, 208, 2, 'WATER_READING', 'POINT', '2026-10-21 07:00:00', 2742.000000, 'm3', 'GetWaterRawData', '2026-10-21 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8269, 208, 2, 'WATER_READING', 'POINT', '2026-10-20 07:00:00', 2671.000000, 'm3', 'GetWaterRawData', '2026-10-20 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8270, 208, 2, 'WATER_READING', 'POINT', '2026-10-19 07:00:00', 2616.000000, 'm3', 'GetWaterRawData', '2026-10-19 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8271, 208, 2, 'WATER_READING', 'POINT', '2026-10-18 07:00:00', 2555.000000, 'm3', 'GetWaterRawData', '2026-10-18 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8272, 208, 2, 'WATER_READING', 'POINT', '2026-10-17 07:00:00', 2528.000000, 'm3', 'GetWaterRawData', '2026-10-17 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8273, 208, 2, 'WATER_READING', 'POINT', '2026-10-16 07:00:00', 2505.000000, 'm3', 'GetWaterRawData', '2026-10-16 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8274, 208, 2, 'WATER_READING', 'POINT', '2026-10-15 07:00:00', 2463.000000, 'm3', 'GetWaterRawData', '2026-10-15 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8275, 208, 2, 'WATER_READING', 'POINT', '2026-10-14 07:00:00', 2423.000000, 'm3', 'GetWaterRawData', '2026-10-14 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8276, 208, 2, 'WATER_READING', 'POINT', '2026-10-13 07:00:00', 2384.000000, 'm3', 'GetWaterRawData', '2026-10-13 07:00:00', '2026-11-04 15:17:39.726000', NULL, '2026-11-04 15:17:40.642000', '2026-11-04 15:17:40.642000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8277, 208, 3, 'WATER_USAGE', 'D', '2026-11-03 00:00:00', 4.000000, 'm3', 'GetUseWater', '2026-11-03 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8278, 208, 3, 'WATER_USAGE', 'D', '2026-11-02 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-02 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8279, 208, 3, 'WATER_USAGE', 'D', '2026-11-01 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-01 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8280, 208, 3, 'WATER_USAGE', 'D', '2026-10-31 00:00:00', 13.000000, 'm3', 'GetUseWater', '2026-10-31 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8281, 208, 3, 'WATER_USAGE', 'D', '2026-10-30 00:00:00', 11.000000, 'm3', 'GetUseWater', '2026-10-30 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8282, 208, 3, 'WATER_USAGE', 'D', '2026-10-29 00:00:00', 9.000000, 'm3', 'GetUseWater', '2026-10-29 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8283, 208, 3, 'WATER_USAGE', 'D', '2026-10-28 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-28 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8284, 208, 3, 'WATER_USAGE', 'D', '2026-10-27 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-27 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8285, 208, 3, 'WATER_USAGE', 'D', '2026-10-26 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-26 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8286, 208, 3, 'WATER_USAGE', 'D', '2026-10-25 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-25 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8287, 208, 3, 'WATER_USAGE', 'D', '2026-10-24 00:00:00', 16.000000, 'm3', 'GetUseWater', '2026-10-24 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8288, 208, 3, 'WATER_USAGE', 'D', '2026-10-23 00:00:00', 16.000000, 'm3', 'GetUseWater', '2026-10-23 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8289, 208, 3, 'WATER_USAGE', 'D', '2026-10-22 00:00:00', 10.000000, 'm3', 'GetUseWater', '2026-10-22 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8290, 208, 3, 'WATER_USAGE', 'D', '2026-10-21 00:00:00', 5.000000, 'm3', 'GetUseWater', '2026-10-21 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8291, 208, 3, 'WATER_USAGE', 'D', '2026-10-20 00:00:00', 6.000000, 'm3', 'GetUseWater', '2026-10-20 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8292, 208, 3, 'WATER_USAGE', 'D', '2026-10-19 00:00:00', 1.000000, 'm3', 'GetUseWater', '2026-10-19 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8293, 208, 3, 'WATER_USAGE', 'D', '2026-10-18 00:00:00', 3.000000, 'm3', 'GetUseWater', '2026-10-18 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8294, 208, 3, 'WATER_USAGE', 'D', '2026-10-17 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-17 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8295, 208, 3, 'WATER_USAGE', 'D', '2026-10-16 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-16 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8296, 208, 3, 'WATER_USAGE', 'D', '2026-10-15 00:00:00', 12.000000, 'm3', 'GetUseWater', '2026-10-15 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8297, 208, 3, 'WATER_USAGE', 'D', '2026-10-14 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-14 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8298, 208, 3, 'WATER_USAGE', 'D', '2026-10-13 00:00:00', 4.000000, 'm3', 'GetUseWater', '2026-10-13 00:00:00', '2026-11-04 15:17:40.418000', NULL, '2026-11-04 15:17:41.138000', '2026-11-04 15:17:41.138000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8299, 208, 2, 'WATER_READING', 'POINT', '2026-11-02 07:00:00', 3381.000000, 'm3', 'GetWaterRawData', '2026-11-02 07:00:00', '2026-11-04 15:17:40.672000', NULL, '2026-11-04 15:17:41.650000', '2026-11-04 15:17:41.650000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8300, 208, 2, 'WATER_READING', 'POINT', '2026-11-01 07:00:00', 3355.000000, 'm3', 'GetWaterRawData', '2026-11-01 07:00:00', '2026-11-04 15:17:40.672000', NULL, '2026-11-04 15:17:41.650000', '2026-11-04 15:17:41.650000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8301, 208, 2, 'WATER_READING', 'POINT', '2026-10-31 07:00:00', 3321.000000, 'm3', 'GetWaterRawData', '2026-10-31 07:00:00', '2026-11-04 15:17:40.672000', NULL, '2026-11-04 15:17:41.650000', '2026-11-04 15:17:41.650000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8302, 208, 2, 'WATER_READING', 'POINT', '2026-10-30 07:00:00', 3273.000000, 'm3', 'GetWaterRawData', '2026-10-30 07:00:00', '2026-11-04 15:17:40.672000', NULL, '2026-11-04 15:17:41.650000', '2026-11-04 15:17:41.650000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8303, 208, 2, 'WATER_READING', 'POINT', '2026-10-29 07:00:00', 3227.000000, 'm3', 'GetWaterRawData', '2026-10-29 07:00:00', '2026-11-04 15:17:40.672000', NULL, '2026-11-04 15:17:41.650000', '2026-11-04 15:17:41.650000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8304, 208, 2, 'WATER_READING', 'POINT', '2026-10-28 07:00:00', 3172.000000, 'm3', 'GetWaterRawData', '2026-10-28 07:00:00', '2026-11-04 15:17:40.672000', NULL, '2026-11-04 15:17:41.650000', '2026-11-04 15:17:41.650000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8305, 208, 3, 'WATER_READING', 'POINT', '2026-10-26 01:00:00', 370.000000, 'm3', 'GetWaterRawData', '2026-10-26 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8306, 208, 3, 'WATER_READING', 'POINT', '2026-10-25 01:00:00', 370.000000, 'm3', 'GetWaterRawData', '2026-10-25 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8307, 208, 3, 'WATER_READING', 'POINT', '2026-10-24 01:00:00', 354.000000, 'm3', 'GetWaterRawData', '2026-10-24 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8308, 208, 3, 'WATER_READING', 'POINT', '2026-10-23 01:00:00', 338.000000, 'm3', 'GetWaterRawData', '2026-10-23 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8309, 208, 3, 'WATER_READING', 'POINT', '2026-10-22 01:00:00', 328.000000, 'm3', 'GetWaterRawData', '2026-10-22 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8310, 208, 3, 'WATER_READING', 'POINT', '2026-10-21 01:00:00', 323.000000, 'm3', 'GetWaterRawData', '2026-10-21 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8311, 208, 3, 'WATER_READING', 'POINT', '2026-10-20 01:00:00', 317.000000, 'm3', 'GetWaterRawData', '2026-10-20 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8312, 208, 3, 'WATER_READING', 'POINT', '2026-10-19 01:00:00', 316.000000, 'm3', 'GetWaterRawData', '2026-10-19 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8313, 208, 3, 'WATER_READING', 'POINT', '2026-10-18 01:00:00', 313.000000, 'm3', 'GetWaterRawData', '2026-10-18 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8314, 208, 3, 'WATER_READING', 'POINT', '2026-10-17 01:00:00', 313.000000, 'm3', 'GetWaterRawData', '2026-10-17 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8315, 208, 3, 'WATER_READING', 'POINT', '2026-10-16 01:00:00', 313.000000, 'm3', 'GetWaterRawData', '2026-10-16 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8316, 208, 3, 'WATER_READING', 'POINT', '2026-10-15 01:00:00', 301.000000, 'm3', 'GetWaterRawData', '2026-10-15 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8317, 208, 3, 'WATER_READING', 'POINT', '2026-10-14 08:00:00', 301.000000, 'm3', 'GetWaterRawData', '2026-10-14 08:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8318, 208, 3, 'WATER_READING', 'POINT', '2026-10-14 01:00:00', 301.000000, 'm3', 'GetWaterRawData', '2026-10-14 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8319, 208, 3, 'WATER_READING', 'POINT', '2026-10-13 08:00:00', 298.000000, 'm3', 'GetWaterRawData', '2026-10-13 08:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8320, 208, 3, 'WATER_READING', 'POINT', '2026-10-13 01:00:00', 297.000000, 'm3', 'GetWaterRawData', '2026-10-13 01:00:00', '2026-11-04 15:17:41.167000', NULL, '2026-11-04 15:17:42.176000', '2026-11-04 15:17:42.176000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8321, 208, 4, 'WATER_USAGE', 'D', '2026-11-03 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-03 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8322, 208, 4, 'WATER_USAGE', 'D', '2026-11-02 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-02 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8323, 208, 4, 'WATER_USAGE', 'D', '2026-11-01 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-11-01 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8324, 208, 4, 'WATER_USAGE', 'D', '2026-10-31 00:00:00', 28.000000, 'm3', 'GetUseWater', '2026-10-31 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8325, 208, 4, 'WATER_USAGE', 'D', '2026-10-30 00:00:00', 27.000000, 'm3', 'GetUseWater', '2026-10-30 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8326, 208, 4, 'WATER_USAGE', 'D', '2026-10-29 00:00:00', 27.000000, 'm3', 'GetUseWater', '2026-10-29 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8327, 208, 4, 'WATER_USAGE', 'D', '2026-10-28 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-28 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8328, 208, 4, 'WATER_USAGE', 'D', '2026-10-27 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-27 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8329, 208, 4, 'WATER_USAGE', 'D', '2026-10-26 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-26 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8330, 208, 4, 'WATER_USAGE', 'D', '2026-10-25 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-25 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8331, 208, 4, 'WATER_USAGE', 'D', '2026-10-24 00:00:00', 35.000000, 'm3', 'GetUseWater', '2026-10-24 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8332, 208, 4, 'WATER_USAGE', 'D', '2026-10-23 00:00:00', 32.000000, 'm3', 'GetUseWater', '2026-10-23 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8333, 208, 4, 'WATER_USAGE', 'D', '2026-10-22 00:00:00', 32.000000, 'm3', 'GetUseWater', '2026-10-22 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8334, 208, 4, 'WATER_USAGE', 'D', '2026-10-21 00:00:00', 20.000000, 'm3', 'GetUseWater', '2026-10-21 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8335, 208, 4, 'WATER_USAGE', 'D', '2026-10-20 00:00:00', 19.000000, 'm3', 'GetUseWater', '2026-10-20 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8336, 208, 4, 'WATER_USAGE', 'D', '2026-10-19 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-19 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8337, 208, 4, 'WATER_USAGE', 'D', '2026-10-18 00:00:00', 11.000000, 'm3', 'GetUseWater', '2026-10-18 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8338, 208, 4, 'WATER_USAGE', 'D', '2026-10-17 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-17 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8339, 208, 4, 'WATER_USAGE', 'D', '2026-10-16 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-16 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8340, 208, 4, 'WATER_USAGE', 'D', '2026-10-15 00:00:00', 39.000000, 'm3', 'GetUseWater', '2026-10-15 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8341, 208, 4, 'WATER_USAGE', 'D', '2026-10-14 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-14 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8342, 208, 4, 'WATER_USAGE', 'D', '2026-10-13 00:00:00', 5.000000, 'm3', 'GetUseWater', '2026-10-13 00:00:00', '2026-11-04 15:17:41.673000', NULL, '2026-11-04 15:17:42.667000', '2026-11-04 15:17:42.667000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8343, 208, 3, 'WATER_READING', 'POINT', '2026-11-02 01:00:00', 403.000000, 'm3', 'GetWaterRawData', '2026-11-02 01:00:00', '2026-11-04 15:17:42.201000', NULL, '2026-11-04 15:17:43.137000', '2026-11-04 15:17:43.137000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8344, 208, 3, 'WATER_READING', 'POINT', '2026-11-01 01:00:00', 403.000000, 'm3', 'GetWaterRawData', '2026-11-01 01:00:00', '2026-11-04 15:17:42.201000', NULL, '2026-11-04 15:17:43.137000', '2026-11-04 15:17:43.137000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8345, 208, 3, 'WATER_READING', 'POINT', '2026-10-31 01:00:00', 390.000000, 'm3', 'GetWaterRawData', '2026-10-31 01:00:00', '2026-11-04 15:17:42.201000', NULL, '2026-11-04 15:17:43.137000', '2026-11-04 15:17:43.137000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8346, 208, 3, 'WATER_READING', 'POINT', '2026-10-30 01:00:00', 379.000000, 'm3', 'GetWaterRawData', '2026-10-30 01:00:00', '2026-11-04 15:17:42.201000', NULL, '2026-11-04 15:17:43.137000', '2026-11-04 15:17:43.137000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8347, 208, 3, 'WATER_READING', 'POINT', '2026-10-29 01:00:00', 370.000000, 'm3', 'GetWaterRawData', '2026-10-29 01:00:00', '2026-11-04 15:17:42.201000', NULL, '2026-11-04 15:17:43.137000', '2026-11-04 15:17:43.137000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8348, 208, 3, 'WATER_READING', 'POINT', '2026-10-28 01:00:00', 370.000000, 'm3', 'GetWaterRawData', '2026-10-28 01:00:00', '2026-11-04 15:17:42.201000', NULL, '2026-11-04 15:17:43.137000', '2026-11-04 15:17:43.137000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8349, 208, 4, 'WATER_READING', 'POINT', '2026-10-26 06:00:00', 913.000000, 'm3', 'GetWaterRawData', '2026-10-26 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8350, 208, 4, 'WATER_READING', 'POINT', '2026-10-25 06:00:00', 913.000000, 'm3', 'GetWaterRawData', '2026-10-25 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8351, 208, 4, 'WATER_READING', 'POINT', '2026-10-24 06:00:00', 878.000000, 'm3', 'GetWaterRawData', '2026-10-24 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8352, 208, 4, 'WATER_READING', 'POINT', '2026-10-23 06:00:00', 846.000000, 'm3', 'GetWaterRawData', '2026-10-23 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8353, 208, 4, 'WATER_READING', 'POINT', '2026-10-22 06:00:00', 814.000000, 'm3', 'GetWaterRawData', '2026-10-22 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8354, 208, 4, 'WATER_READING', 'POINT', '2026-10-21 06:00:00', 794.000000, 'm3', 'GetWaterRawData', '2026-10-21 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8355, 208, 4, 'WATER_READING', 'POINT', '2026-10-20 06:00:00', 775.000000, 'm3', 'GetWaterRawData', '2026-10-20 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8356, 208, 4, 'WATER_READING', 'POINT', '2026-10-19 06:00:00', 775.000000, 'm3', 'GetWaterRawData', '2026-10-19 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8357, 208, 4, 'WATER_READING', 'POINT', '2026-10-18 12:00:00', 764.000000, 'm3', 'GetWaterRawData', '2026-10-18 12:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8358, 208, 4, 'WATER_READING', 'POINT', '2026-10-18 06:00:00', 764.000000, 'm3', 'GetWaterRawData', '2026-10-18 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8359, 208, 4, 'WATER_READING', 'POINT', '2026-10-17 12:00:00', 764.000000, 'm3', 'GetWaterRawData', '2026-10-17 12:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8360, 208, 4, 'WATER_READING', 'POINT', '2026-10-17 06:00:00', 764.000000, 'm3', 'GetWaterRawData', '2026-10-17 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8361, 208, 4, 'WATER_READING', 'POINT', '2026-10-16 06:00:00', 764.000000, 'm3', 'GetWaterRawData', '2026-10-16 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8362, 208, 4, 'WATER_READING', 'POINT', '2026-10-15 06:00:00', 725.000000, 'm3', 'GetWaterRawData', '2026-10-15 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8363, 208, 4, 'WATER_READING', 'POINT', '2026-10-14 06:00:00', 725.000000, 'm3', 'GetWaterRawData', '2026-10-14 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8364, 208, 4, 'WATER_READING', 'POINT', '2026-10-13 06:00:00', 720.000000, 'm3', 'GetWaterRawData', '2026-10-13 06:00:00', '2026-11-04 15:17:42.690000', NULL, '2026-11-04 15:17:43.684000', '2026-11-04 15:17:43.684000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8365, 208, 5, 'WATER_USAGE', 'D', '2026-11-03 00:00:00', 3.000000, 'm3', 'GetUseWater', '2026-11-03 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8366, 208, 5, 'WATER_USAGE', 'D', '2026-11-02 00:00:00', 28.000000, 'm3', 'GetUseWater', '2026-11-02 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8367, 208, 5, 'WATER_USAGE', 'D', '2026-11-01 00:00:00', 28.000000, 'm3', 'GetUseWater', '2026-11-01 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8368, 208, 5, 'WATER_USAGE', 'D', '2026-10-31 00:00:00', 70.000000, 'm3', 'GetUseWater', '2026-10-31 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8369, 208, 5, 'WATER_USAGE', 'D', '2026-10-30 00:00:00', 81.000000, 'm3', 'GetUseWater', '2026-10-30 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8370, 208, 5, 'WATER_USAGE', 'D', '2026-10-29 00:00:00', 9.000000, 'm3', 'GetUseWater', '2026-10-29 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8371, 208, 5, 'WATER_USAGE', 'D', '2026-10-28 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-28 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8372, 208, 5, 'WATER_USAGE', 'D', '2026-10-27 00:00:00', 4.000000, 'm3', 'GetUseWater', '2026-10-27 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8373, 208, 5, 'WATER_USAGE', 'D', '2026-10-26 00:00:00', 24.000000, 'm3', 'GetUseWater', '2026-10-26 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8374, 208, 5, 'WATER_USAGE', 'D', '2026-10-25 00:00:00', 40.000000, 'm3', 'GetUseWater', '2026-10-25 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8375, 208, 5, 'WATER_USAGE', 'D', '2026-10-24 00:00:00', 84.000000, 'm3', 'GetUseWater', '2026-10-24 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8376, 208, 5, 'WATER_USAGE', 'D', '2026-10-23 00:00:00', 94.000000, 'm3', 'GetUseWater', '2026-10-23 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8377, 208, 5, 'WATER_USAGE', 'D', '2026-10-22 00:00:00', 31.000000, 'm3', 'GetUseWater', '2026-10-22 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8378, 208, 5, 'WATER_USAGE', 'D', '2026-10-21 00:00:00', 20.000000, 'm3', 'GetUseWater', '2026-10-21 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8379, 208, 5, 'WATER_USAGE', 'D', '2026-10-20 00:00:00', 6.000000, 'm3', 'GetUseWater', '2026-10-20 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8380, 208, 5, 'WATER_USAGE', 'D', '2026-10-19 00:00:00', 37.000000, 'm3', 'GetUseWater', '2026-10-19 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8381, 208, 5, 'WATER_USAGE', 'D', '2026-10-18 00:00:00', 8.000000, 'm3', 'GetUseWater', '2026-10-18 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8382, 208, 5, 'WATER_USAGE', 'D', '2026-10-17 00:00:00', 0.000000, 'm3', 'GetUseWater', '2026-10-17 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8383, 208, 5, 'WATER_USAGE', 'D', '2026-10-16 00:00:00', 53.000000, 'm3', 'GetUseWater', '2026-10-16 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8384, 208, 5, 'WATER_USAGE', 'D', '2026-10-15 00:00:00', 11.000000, 'm3', 'GetUseWater', '2026-10-15 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8385, 208, 5, 'WATER_USAGE', 'D', '2026-10-14 00:00:00', 6.000000, 'm3', 'GetUseWater', '2026-10-14 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8386, 208, 5, 'WATER_USAGE', 'D', '2026-10-13 00:00:00', 3.000000, 'm3', 'GetUseWater', '2026-10-13 00:00:00', '2026-11-04 15:17:43.159000', NULL, '2026-11-04 15:17:44.167000', '2026-11-04 15:17:44.167000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8387, 208, 4, 'WATER_READING', 'POINT', '2026-11-02 06:00:00', 995.000000, 'm3', 'GetWaterRawData', '2026-11-02 06:00:00', '2026-11-04 15:17:43.707000', NULL, '2026-11-04 15:17:44.662000', '2026-11-04 15:17:44.662000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8388, 208, 4, 'WATER_READING', 'POINT', '2026-11-01 06:00:00', 995.000000, 'm3', 'GetWaterRawData', '2026-11-01 06:00:00', '2026-11-04 15:17:43.707000', NULL, '2026-11-04 15:17:44.662000', '2026-11-04 15:17:44.662000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8389, 208, 4, 'WATER_READING', 'POINT', '2026-10-31 06:00:00', 967.000000, 'm3', 'GetWaterRawData', '2026-10-31 06:00:00', '2026-11-04 15:17:43.707000', NULL, '2026-11-04 15:17:44.662000', '2026-11-04 15:17:44.662000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8390, 208, 4, 'WATER_READING', 'POINT', '2026-10-30 06:00:00', 940.000000, 'm3', 'GetWaterRawData', '2026-10-30 06:00:00', '2026-11-04 15:17:43.707000', NULL, '2026-11-04 15:17:44.662000', '2026-11-04 15:17:44.662000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8391, 208, 4, 'WATER_READING', 'POINT', '2026-10-29 06:00:00', 913.000000, 'm3', 'GetWaterRawData', '2026-10-29 06:00:00', '2026-11-04 15:17:43.707000', NULL, '2026-11-04 15:17:44.662000', '2026-11-04 15:17:44.662000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8392, 208, 4, 'WATER_READING', 'POINT', '2026-10-28 06:00:00', 913.000000, 'm3', 'GetWaterRawData', '2026-10-28 06:00:00', '2026-11-04 15:17:43.707000', NULL, '2026-11-04 15:17:44.662000', '2026-11-04 15:17:44.662000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8393, 208, 5, 'WATER_READING', 'POINT', '2026-10-26 05:00:00', 2110.000000, 'm3', 'GetWaterRawData', '2026-10-26 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8394, 208, 5, 'WATER_READING', 'POINT', '2026-10-25 05:00:00', 2070.000000, 'm3', 'GetWaterRawData', '2026-10-25 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8395, 208, 5, 'WATER_READING', 'POINT', '2026-10-24 05:00:00', 1986.000000, 'm3', 'GetWaterRawData', '2026-10-24 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8396, 208, 5, 'WATER_READING', 'POINT', '2026-10-23 05:00:00', 1892.000000, 'm3', 'GetWaterRawData', '2026-10-23 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8397, 208, 5, 'WATER_READING', 'POINT', '2026-10-22 05:00:00', 1861.000000, 'm3', 'GetWaterRawData', '2026-10-22 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8398, 208, 5, 'WATER_READING', 'POINT', '2026-10-21 05:00:00', 1841.000000, 'm3', 'GetWaterRawData', '2026-10-21 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8399, 208, 5, 'WATER_READING', 'POINT', '2026-10-20 05:00:00', 1835.000000, 'm3', 'GetWaterRawData', '2026-10-20 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8400, 208, 5, 'WATER_READING', 'POINT', '2026-10-19 05:00:00', 1798.000000, 'm3', 'GetWaterRawData', '2026-10-19 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8401, 208, 5, 'WATER_READING', 'POINT', '2026-10-18 05:00:00', 1790.000000, 'm3', 'GetWaterRawData', '2026-10-18 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8402, 208, 5, 'WATER_READING', 'POINT', '2026-10-17 05:00:00', 1790.000000, 'm3', 'GetWaterRawData', '2026-10-17 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8403, 208, 5, 'WATER_READING', 'POINT', '2026-10-16 05:00:00', 1737.000000, 'm3', 'GetWaterRawData', '2026-10-16 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8404, 208, 5, 'WATER_READING', 'POINT', '2026-10-15 05:00:00', 1726.000000, 'm3', 'GetWaterRawData', '2026-10-15 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8405, 208, 5, 'WATER_READING', 'POINT', '2026-10-14 05:00:00', 1720.000000, 'm3', 'GetWaterRawData', '2026-10-14 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8406, 208, 5, 'WATER_READING', 'POINT', '2026-10-13 05:00:00', 1717.000000, 'm3', 'GetWaterRawData', '2026-10-13 05:00:00', '2026-11-04 15:17:44.197000', NULL, '2026-11-04 15:17:45.142000', '2026-11-04 15:17:45.142000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8407, 208, 6, 'WATER_USAGE', 'D', '2026-11-03 00:00:00', 4.000000, 'm3', 'GetUseWater', '2026-11-03 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8408, 208, 6, 'WATER_USAGE', 'D', '2026-11-02 00:00:00', 27.000000, 'm3', 'GetUseWater', '2026-11-02 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8409, 208, 6, 'WATER_USAGE', 'D', '2026-11-01 00:00:00', 35.000000, 'm3', 'GetUseWater', '2026-11-01 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8410, 208, 6, 'WATER_USAGE', 'D', '2026-10-31 00:00:00', 140.000000, 'm3', 'GetUseWater', '2026-10-31 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8411, 208, 6, 'WATER_USAGE', 'D', '2026-10-30 00:00:00', 139.000000, 'm3', 'GetUseWater', '2026-10-30 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8412, 208, 6, 'WATER_USAGE', 'D', '2026-10-29 00:00:00', 67.000000, 'm3', 'GetUseWater', '2026-10-29 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8413, 208, 6, 'WATER_USAGE', 'D', '2026-10-28 00:00:00', 28.000000, 'm3', 'GetUseWater', '2026-10-28 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8414, 208, 6, 'WATER_USAGE', 'D', '2026-10-27 00:00:00', 4.000000, 'm3', 'GetUseWater', '2026-10-27 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8415, 208, 6, 'WATER_USAGE', 'D', '2026-10-26 00:00:00', 24.000000, 'm3', 'GetUseWater', '2026-10-26 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8416, 208, 6, 'WATER_USAGE', 'D', '2026-10-25 00:00:00', 39.000000, 'm3', 'GetUseWater', '2026-10-25 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8417, 208, 6, 'WATER_USAGE', 'D', '2026-10-24 00:00:00', 171.000000, 'm3', 'GetUseWater', '2026-10-24 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8418, 208, 6, 'WATER_USAGE', 'D', '2026-10-23 00:00:00', 171.000000, 'm3', 'GetUseWater', '2026-10-23 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8419, 208, 6, 'WATER_USAGE', 'D', '2026-10-22 00:00:00', 100.000000, 'm3', 'GetUseWater', '2026-10-22 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8420, 208, 6, 'WATER_USAGE', 'D', '2026-10-21 00:00:00', 52.000000, 'm3', 'GetUseWater', '2026-10-21 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8421, 208, 6, 'WATER_USAGE', 'D', '2026-10-20 00:00:00', 34.000000, 'm3', 'GetUseWater', '2026-10-20 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8422, 208, 6, 'WATER_USAGE', 'D', '2026-10-19 00:00:00', 38.000000, 'm3', 'GetUseWater', '2026-10-19 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8423, 208, 6, 'WATER_USAGE', 'D', '2026-10-18 00:00:00', 33.000000, 'm3', 'GetUseWater', '2026-10-18 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8424, 208, 6, 'WATER_USAGE', 'D', '2026-10-17 00:00:00', 2.000000, 'm3', 'GetUseWater', '2026-10-17 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8425, 208, 6, 'WATER_USAGE', 'D', '2026-10-16 00:00:00', 67.000000, 'm3', 'GetUseWater', '2026-10-16 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8426, 208, 6, 'WATER_USAGE', 'D', '2026-10-15 00:00:00', 88.000000, 'm3', 'GetUseWater', '2026-10-15 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8427, 208, 6, 'WATER_USAGE', 'D', '2026-10-14 00:00:00', 10.000000, 'm3', 'GetUseWater', '2026-10-14 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8428, 208, 6, 'WATER_USAGE', 'D', '2026-10-13 00:00:00', 10.000000, 'm3', 'GetUseWater', '2026-10-13 00:00:00', '2026-11-04 15:17:44.687000', NULL, '2026-11-04 15:17:45.670000', '2026-11-04 15:17:45.670000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8429, 208, 5, 'WATER_READING', 'POINT', '2026-11-02 05:00:00', 2326.000000, 'm3', 'GetWaterRawData', '2026-11-02 05:00:00', '2026-11-04 15:17:45.168000', NULL, '2026-11-04 15:17:46.160000', '2026-11-04 15:17:46.160000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8430, 208, 5, 'WATER_READING', 'POINT', '2026-11-01 05:00:00', 2298.000000, 'm3', 'GetWaterRawData', '2026-11-01 05:00:00', '2026-11-04 15:17:45.168000', NULL, '2026-11-04 15:17:46.160000', '2026-11-04 15:17:46.160000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8431, 208, 5, 'WATER_READING', 'POINT', '2026-10-31 05:00:00', 2228.000000, 'm3', 'GetWaterRawData', '2026-10-31 05:00:00', '2026-11-04 15:17:45.168000', NULL, '2026-11-04 15:17:46.160000', '2026-11-04 15:17:46.160000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8432, 208, 5, 'WATER_READING', 'POINT', '2026-10-30 05:00:00', 2147.000000, 'm3', 'GetWaterRawData', '2026-10-30 05:00:00', '2026-11-04 15:17:45.168000', NULL, '2026-11-04 15:17:46.160000', '2026-11-04 15:17:46.160000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8433, 208, 5, 'WATER_READING', 'POINT', '2026-10-29 05:00:00', 2138.000000, 'm3', 'GetWaterRawData', '2026-10-29 05:00:00', '2026-11-04 15:17:45.168000', NULL, '2026-11-04 15:17:46.160000', '2026-11-04 15:17:46.160000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8434, 208, 5, 'WATER_READING', 'POINT', '2026-10-28 05:00:00', 2138.000000, 'm3', 'GetWaterRawData', '2026-10-28 05:00:00', '2026-11-04 15:17:45.168000', NULL, '2026-11-04 15:17:46.160000', '2026-11-04 15:17:46.160000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8435, 208, 6, 'WATER_READING', 'POINT', '2026-10-26 01:00:00', 3989.000000, 'm3', 'GetWaterRawData', '2026-10-26 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8436, 208, 6, 'WATER_READING', 'POINT', '2026-10-25 01:00:00', 3950.000000, 'm3', 'GetWaterRawData', '2026-10-25 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8437, 208, 6, 'WATER_READING', 'POINT', '2026-10-24 01:00:00', 3779.000000, 'm3', 'GetWaterRawData', '2026-10-24 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8438, 208, 6, 'WATER_READING', 'POINT', '2026-10-23 01:00:00', 3608.000000, 'm3', 'GetWaterRawData', '2026-10-23 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8439, 208, 6, 'WATER_READING', 'POINT', '2026-10-22 01:00:00', 3508.000000, 'm3', 'GetWaterRawData', '2026-10-22 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8440, 208, 6, 'WATER_READING', 'POINT', '2026-10-21 01:00:00', 3456.000000, 'm3', 'GetWaterRawData', '2026-10-21 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8441, 208, 6, 'WATER_READING', 'POINT', '2026-10-20 01:00:00', 3422.000000, 'm3', 'GetWaterRawData', '2026-10-20 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8442, 208, 6, 'WATER_READING', 'POINT', '2026-10-19 01:00:00', 3384.000000, 'm3', 'GetWaterRawData', '2026-10-19 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8443, 208, 6, 'WATER_READING', 'POINT', '2026-10-18 01:00:00', 3351.000000, 'm3', 'GetWaterRawData', '2026-10-18 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8444, 208, 6, 'WATER_READING', 'POINT', '2026-10-17 01:00:00', 3349.000000, 'm3', 'GetWaterRawData', '2026-10-17 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8445, 208, 6, 'WATER_READING', 'POINT', '2026-10-16 01:00:00', 3282.000000, 'm3', 'GetWaterRawData', '2026-10-16 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8446, 208, 6, 'WATER_READING', 'POINT', '2026-10-15 08:00:00', 3197.000000, 'm3', 'GetWaterRawData', '2026-10-15 08:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8447, 208, 6, 'WATER_READING', 'POINT', '2026-10-15 01:00:00', 3194.000000, 'm3', 'GetWaterRawData', '2026-10-15 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8448, 208, 6, 'WATER_READING', 'POINT', '2026-10-14 01:00:00', 3184.000000, 'm3', 'GetWaterRawData', '2026-10-14 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8449, 208, 6, 'WATER_READING', 'POINT', '2026-10-13 01:00:00', 3174.000000, 'm3', 'GetWaterRawData', '2026-10-13 01:00:00', '2026-11-04 15:19:53.392000', NULL, '2026-11-04 15:19:53.668000', '2026-11-04 15:19:53.668000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8450, 208, 6, 'WATER_READING', 'POINT', '2026-11-02 01:00:00', 4426.000000, 'm3', 'GetWaterRawData', '2026-11-02 01:00:00', '2026-11-04 15:19:53.693000', NULL, '2026-11-04 15:19:54.098000', '2026-11-04 15:19:54.098000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8451, 208, 6, 'WATER_READING', 'POINT', '2026-11-01 01:00:00', 4391.000000, 'm3', 'GetWaterRawData', '2026-11-01 01:00:00', '2026-11-04 15:19:53.693000', NULL, '2026-11-04 15:19:54.098000', '2026-11-04 15:19:54.098000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8452, 208, 6, 'WATER_READING', 'POINT', '2026-10-31 01:00:00', 4251.000000, 'm3', 'GetWaterRawData', '2026-10-31 01:00:00', '2026-11-04 15:19:53.693000', NULL, '2026-11-04 15:19:54.098000', '2026-11-04 15:19:54.098000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8453, 208, 6, 'WATER_READING', 'POINT', '2026-10-30 01:00:00', 4112.000000, 'm3', 'GetWaterRawData', '2026-10-30 01:00:00', '2026-11-04 15:19:53.693000', NULL, '2026-11-04 15:19:54.098000', '2026-11-04 15:19:54.098000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8454, 208, 6, 'WATER_READING', 'POINT', '2026-10-29 01:00:00', 4045.000000, 'm3', 'GetWaterRawData', '2026-10-29 01:00:00', '2026-11-04 15:19:53.693000', NULL, '2026-11-04 15:19:54.098000', '2026-11-04 15:19:54.098000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8455, 208, 6, 'WATER_READING', 'POINT', '2026-10-28 01:00:00', 4017.000000, 'm3', 'GetWaterRawData', '2026-10-28 01:00:00', '2026-11-04 15:19:53.693000', NULL, '2026-11-04 15:19:54.098000', '2026-11-04 15:19:54.098000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8467, 208, 11, 'GAS_USAGE', 'D', '2026-11-04 00:00:00', 17.030000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.938000', NULL, '2026-11-04 17:09:26.949000', '2026-11-04 17:09:26.949000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8468, 208, 11, 'GAS_READING', 'POINT', '2026-11-04 00:00:00', 17.030000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.938000', NULL, '2026-11-04 17:09:26.949000', '2026-11-04 17:09:26.949000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8469, 208, 12, 'GAS_USAGE', 'D', '2026-11-04 00:00:00', 22.320000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.963000', NULL, '2026-11-04 17:09:26.967000', '2026-11-04 17:09:26.967000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8470, 208, 12, 'GAS_READING', 'POINT', '2026-11-04 00:00:00', 22.320000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.963000', NULL, '2026-11-04 17:09:26.967000', '2026-11-04 17:09:26.967000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8471, 208, 13, 'GAS_USAGE', 'D', '2026-11-04 00:00:00', 27.610000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.981000', NULL, '2026-11-04 17:09:26.986000', '2026-11-04 17:09:26.986000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8472, 208, 13, 'GAS_READING', 'POINT', '2026-11-04 00:00:00', 27.610000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.981000', NULL, '2026-11-04 17:09:26.986000', '2026-11-04 17:09:26.986000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8473, 208, 14, 'GAS_USAGE', 'D', '2026-11-04 00:00:00', 29.900000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.998000', NULL, '2026-11-04 17:09:27.003000', '2026-11-04 17:09:27.003000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8474, 208, 14, 'GAS_READING', 'POINT', '2026-11-04 00:00:00', 29.900000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:26.998000', NULL, '2026-11-04 17:09:27.003000', '2026-11-04 17:09:27.003000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8475, 208, 15, 'GAS_USAGE', 'D', '2026-11-04 00:00:00', 12.270000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:27.014000', NULL, '2026-11-04 17:09:27.019000', '2026-11-04 17:09:27.019000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8476, 208, 15, 'GAS_READING', 'POINT', '2026-11-04 00:00:00', 12.270000, 'm3', 'MANUAL', '2026-11-04 00:00:00', '2026-11-04 17:09:27.014000', NULL, '2026-11-04 17:09:27.019000', '2026-11-04 17:09:27.019000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8483, 208, 7, 'ELECTRIC_USAGE', 'D', '2026-11-04 00:00:00', 2.330000, 'kWh', 'ElectricQuantity', '2026-11-04 00:00:00', '2026-11-04 19:00:00.122000', NULL, '2026-11-04 17:31:29.349000', '2026-11-04 19:00:01.781000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8484, 208, 7, 'ELECTRIC_READING', 'POINT', '2026-11-03 00:00:00', 72.970000, 'kWh', 'FrozenElectricity', '2026-11-03 00:00:00', '2026-11-04 17:31:29.114000', NULL, '2026-11-04 17:31:29.826000', '2026-11-04 17:31:29.826000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8485, 208, 8, 'ELECTRIC_USAGE', 'D', '2026-11-04 00:00:00', 4.130000, 'kWh', 'ElectricQuantity', '2026-11-04 00:00:00', '2026-11-04 19:00:00.127000', NULL, '2026-11-04 17:31:30.230000', '2026-11-04 19:00:01.991000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8486, 208, 8, 'ELECTRIC_READING', 'POINT', '2026-11-03 00:00:00', 469.070000, 'kWh', 'FrozenElectricity', '2026-11-03 00:00:00', '2026-11-04 17:31:29.872000', NULL, '2026-11-04 17:31:30.781000', '2026-11-04 17:31:30.781000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8487, 208, 9, 'ELECTRIC_USAGE', 'D', '2026-11-04 00:00:00', 2.690000, 'kWh', 'ElectricQuantity', '2026-11-04 00:00:00', '2026-11-04 18:41:22.396000', NULL, '2026-11-04 17:31:31.251000', '2026-11-04 18:41:22.979000');
INSERT INTO `energy_meter_measurement` (`id`, `tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`, `metric_value`, `unit`, `source_api`, `source_record_time`, `collected_at`, `remark`, `create_time`, `update_time`) VALUES (8488, 208, 9, 'ELECTRIC_READING', 'POINT', '2026-11-03 00:00:00', 303.120000, 'kWh', 'FrozenElectricity', '2026-11-03 00:00:00', '2026-11-04 17:31:30.819000', NULL, '2026-11-04 17:31:31.794000', '2026-11-04 17:31:31.794000');
-- energy_meter_sync_run: 7 rows
INSERT INTO `energy_meter_sync_run` (`id`, `run_key`, `trigger_type`, `schedule_date`, `range_start`, `range_end`, `status`, `total_tasks`, `success_tasks`, `empty_tasks`, `failed_tasks`, `started_at`, `finished_at`, `error_summary`, `create_user`, `create_time`, `update_time`) VALUES (1, 'AUTO:20261104', 'SCHEDULED', '2026-11-04', '2026-10-13', '2026-11-03', 'SUCCESS', 18, 18, 0, 0, '2026-11-04 15:17:38.498000', '2026-11-04 15:19:54.128000', NULL, 1, '2026-11-04 15:17:38.357000', '2026-11-04 15:19:54.130000');
INSERT INTO `energy_meter_sync_run` (`id`, `run_key`, `trigger_type`, `schedule_date`, `range_start`, `range_end`, `status`, `total_tasks`, `success_tasks`, `empty_tasks`, `failed_tasks`, `started_at`, `finished_at`, `error_summary`, `create_user`, `create_time`, `update_time`) VALUES (2, 'MANUAL:4ee77a32-e7fc-4588-97c8-2ca17fd3f69d', 'MANUAL', NULL, '2026-11-01', '2026-11-03', 'SUCCESS', 2, 1, 1, 0, '2026-11-04 17:23:25.151000', '2026-11-04 17:30:48.120000', NULL, 1, '2026-11-04 17:23:25.109000', '2026-11-04 17:30:48.130000');
INSERT INTO `energy_meter_sync_run` (`id`, `run_key`, `trigger_type`, `schedule_date`, `range_start`, `range_end`, `status`, `total_tasks`, `success_tasks`, `empty_tasks`, `failed_tasks`, `started_at`, `finished_at`, `error_summary`, `create_user`, `create_time`, `update_time`) VALUES (3, 'MANUAL:94bf000f-8a1d-4e67-9cbd-0e17daae76b1', 'MANUAL', NULL, '2026-11-01', '2026-11-03', 'SUCCESS', 2, 1, 1, 0, '2026-11-04 17:23:30.959000', '2026-11-04 17:30:48.434000', NULL, 1, '2026-11-04 17:23:30.932000', '2026-11-04 17:30:48.438000');
INSERT INTO `energy_meter_sync_run` (`id`, `run_key`, `trigger_type`, `schedule_date`, `range_start`, `range_end`, `status`, `total_tasks`, `success_tasks`, `empty_tasks`, `failed_tasks`, `started_at`, `finished_at`, `error_summary`, `create_user`, `create_time`, `update_time`) VALUES (4, 'MANUAL:f3235037-1361-4935-9fe9-1dc5d893fdca', 'MANUAL', NULL, '2026-11-01', '2026-11-03', 'SUCCESS', 2, 1, 1, 0, '2026-11-04 17:23:33.884000', '2026-11-04 17:30:48.943000', NULL, 1, '2026-11-04 17:23:33.857000', '2026-11-04 17:30:48.947000');
INSERT INTO `energy_meter_sync_run` (`id`, `run_key`, `trigger_type`, `schedule_date`, `range_start`, `range_end`, `status`, `total_tasks`, `success_tasks`, `empty_tasks`, `failed_tasks`, `started_at`, `finished_at`, `error_summary`, `create_user`, `create_time`, `update_time`) VALUES (5, 'MANUAL:76414df1-60a3-4ad9-96c6-7eb6d92a26be', 'MANUAL', NULL, '2026-11-01', '2026-11-04', 'SUCCESS', 6, 6, 0, 0, '2026-11-04 17:31:29.076000', '2026-11-04 17:31:31.838000', NULL, 1, '2026-11-04 17:31:29.003000', '2026-11-04 17:31:31.841000');
INSERT INTO `energy_meter_sync_run` (`id`, `run_key`, `trigger_type`, `schedule_date`, `range_start`, `range_end`, `status`, `total_tasks`, `success_tasks`, `empty_tasks`, `failed_tasks`, `started_at`, `finished_at`, `error_summary`, `create_user`, `create_time`, `update_time`) VALUES (7, 'MANUAL:264a56f2-a48c-418a-b500-275b20c3bce6', 'MANUAL', NULL, '2026-11-01', '2026-11-03', 'SUCCESS', 2, 1, 1, 0, '2026-11-04 18:48:51.045000', '2026-11-04 18:48:51.739000', NULL, 1, '2026-11-04 18:48:51.010000', '2026-11-04 18:48:51.742000');
INSERT INTO `energy_meter_sync_run` (`id`, `run_key`, `trigger_type`, `schedule_date`, `range_start`, `range_end`, `status`, `total_tasks`, `success_tasks`, `empty_tasks`, `failed_tasks`, `started_at`, `finished_at`, `error_summary`, `create_user`, `create_time`, `update_time`) VALUES (8, 'MANUAL:dacf586e-88cc-4376-901a-353aa7a71371', 'MANUAL', NULL, '2026-11-01', '2026-11-03', 'SUCCESS', 2, 1, 1, 0, '2026-11-04 18:48:53.686000', '2026-11-04 18:48:54.367000', NULL, 1, '2026-11-04 18:48:53.654000', '2026-11-04 18:48:54.369000');
-- energy_meter_sync_task: 28 rows
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (1, 1, 208, 1, 'GetUseWater', 'D', '2026-10-13', '2026-11-03', 'SUCCESS', 1, NULL, 22, 22, 0, NULL, NULL, '2026-11-04 15:17:38.511000', '2026-11-04 15:17:39.138000', '2026-11-04 15:17:38.372000', '2026-11-04 15:17:39.152000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (2, 1, 208, 1, 'GetWaterRawData', 'POINT', '2026-10-13', '2026-10-27', 'SUCCESS', 1, NULL, 15, 15, 0, NULL, NULL, '2026-11-04 15:17:38.511000', '2026-11-04 15:17:38.986000', '2026-11-04 15:17:38.378000', '2026-11-04 15:17:39.003000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (3, 1, 208, 1, 'GetWaterRawData', 'POINT', '2026-10-28', '2026-11-03', 'SUCCESS', 1, NULL, 6, 6, 0, NULL, NULL, '2026-11-04 15:17:39.016000', '2026-11-04 15:17:39.697000', '2026-11-04 15:17:38.385000', '2026-11-04 15:17:39.705000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (4, 1, 208, 2, 'GetUseWater', 'D', '2026-10-13', '2026-11-03', 'SUCCESS', 1, NULL, 22, 22, 0, NULL, NULL, '2026-11-04 15:17:39.162000', '2026-11-04 15:17:40.379000', '2026-11-04 15:17:38.391000', '2026-11-04 15:17:40.392000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (5, 1, 208, 2, 'GetWaterRawData', 'POINT', '2026-10-13', '2026-10-27', 'SUCCESS', 1, NULL, 14, 14, 0, NULL, NULL, '2026-11-04 15:17:39.715000', '2026-11-04 15:17:40.634000', '2026-11-04 15:17:38.397000', '2026-11-04 15:17:40.647000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (6, 1, 208, 2, 'GetWaterRawData', 'POINT', '2026-10-28', '2026-11-03', 'SUCCESS', 1, NULL, 6, 6, 0, NULL, NULL, '2026-11-04 15:17:40.659000', '2026-11-04 15:17:41.645000', '2026-11-04 15:17:38.403000', '2026-11-04 15:17:41.653000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (7, 1, 208, 3, 'GetUseWater', 'D', '2026-10-13', '2026-11-03', 'SUCCESS', 1, NULL, 22, 22, 0, NULL, NULL, '2026-11-04 15:17:40.405000', '2026-11-04 15:17:41.131000', '2026-11-04 15:17:38.410000', '2026-11-04 15:17:41.142000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (8, 1, 208, 3, 'GetWaterRawData', 'POINT', '2026-10-13', '2026-10-27', 'SUCCESS', 1, NULL, 16, 16, 0, NULL, NULL, '2026-11-04 15:17:41.154000', '2026-11-04 15:17:42.170000', '2026-11-04 15:17:38.416000', '2026-11-04 15:17:42.180000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (9, 1, 208, 3, 'GetWaterRawData', 'POINT', '2026-10-28', '2026-11-03', 'SUCCESS', 1, NULL, 6, 6, 0, NULL, NULL, '2026-11-04 15:17:42.190000', '2026-11-04 15:17:43.133000', '2026-11-04 15:17:38.422000', '2026-11-04 15:17:43.140000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (10, 1, 208, 4, 'GetUseWater', 'D', '2026-10-13', '2026-11-03', 'SUCCESS', 1, NULL, 22, 22, 0, NULL, NULL, '2026-11-04 15:17:41.662000', '2026-11-04 15:17:42.658000', '2026-11-04 15:17:38.429000', '2026-11-04 15:17:42.670000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (11, 1, 208, 4, 'GetWaterRawData', 'POINT', '2026-10-13', '2026-10-27', 'SUCCESS', 1, NULL, 16, 16, 0, NULL, NULL, '2026-11-04 15:17:42.681000', '2026-11-04 15:17:43.678000', '2026-11-04 15:17:38.435000', '2026-11-04 15:17:43.688000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (12, 1, 208, 4, 'GetWaterRawData', 'POINT', '2026-10-28', '2026-11-03', 'SUCCESS', 1, NULL, 6, 6, 0, NULL, NULL, '2026-11-04 15:17:43.696000', '2026-11-04 15:17:44.657000', '2026-11-04 15:17:38.441000', '2026-11-04 15:17:44.667000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (13, 1, 208, 5, 'GetUseWater', 'D', '2026-10-13', '2026-11-03', 'SUCCESS', 1, NULL, 22, 22, 0, NULL, NULL, '2026-11-04 15:17:43.149000', '2026-11-04 15:17:44.161000', '2026-11-04 15:17:38.448000', '2026-11-04 15:17:44.173000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (14, 1, 208, 5, 'GetWaterRawData', 'POINT', '2026-10-13', '2026-10-27', 'SUCCESS', 1, NULL, 14, 14, 0, NULL, NULL, '2026-11-04 15:17:44.186000', '2026-11-04 15:17:45.136000', '2026-11-04 15:17:38.453000', '2026-11-04 15:17:45.147000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (15, 1, 208, 5, 'GetWaterRawData', 'POINT', '2026-10-28', '2026-11-03', 'SUCCESS', 1, NULL, 6, 6, 0, NULL, NULL, '2026-11-04 15:17:45.156000', '2026-11-04 15:17:46.156000', '2026-11-04 15:17:38.459000', '2026-11-04 15:17:46.162000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (16, 1, 208, 6, 'GetUseWater', 'D', '2026-10-13', '2026-11-03', 'SUCCESS', 1, NULL, 22, 22, 0, NULL, NULL, '2026-11-04 15:17:44.677000', '2026-11-04 15:17:45.661000', '2026-11-04 15:17:38.466000', '2026-11-04 15:17:45.678000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (17, 1, 208, 6, 'GetWaterRawData', 'POINT', '2026-10-13', '2026-10-27', 'SUCCESS', 2, NULL, 15, 15, 0, NULL, NULL, '2026-11-04 15:19:53.380000', '2026-11-04 15:19:53.660000', '2026-11-04 15:17:38.471000', '2026-11-04 15:19:53.671000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (18, 1, 208, 6, 'GetWaterRawData', 'POINT', '2026-10-28', '2026-11-03', 'SUCCESS', 2, NULL, 6, 6, 0, NULL, NULL, '2026-11-04 15:19:53.682000', '2026-11-04 15:19:54.095000', '2026-11-04 15:17:38.477000', '2026-11-04 15:19:54.101000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (25, 5, 208, 7, 'FrozenElectricity', 'D', '2026-11-01', '2026-11-04', 'SUCCESS', 1, NULL, 1, 1, 0, NULL, NULL, '2026-11-04 17:31:29.094000', '2026-11-04 17:31:29.338000', '2026-11-04 17:31:29.017000', '2026-11-04 17:31:29.355000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (26, 5, 208, 7, 'FrozenElectricity', 'POINT', '2026-11-01', '2026-11-04', 'SUCCESS', 1, NULL, 1, 1, 0, NULL, NULL, '2026-11-04 17:31:29.094000', '2026-11-04 17:31:29.819000', '2026-11-04 17:31:29.025000', '2026-11-04 17:31:29.831000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (27, 5, 208, 8, 'FrozenElectricity', 'D', '2026-11-01', '2026-11-04', 'SUCCESS', 1, NULL, 1, 1, 0, NULL, NULL, '2026-11-04 17:31:29.373000', '2026-11-04 17:31:30.214000', '2026-11-04 17:31:29.033000', '2026-11-04 17:31:30.233000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (28, 5, 208, 8, 'FrozenElectricity', 'POINT', '2026-11-01', '2026-11-04', 'SUCCESS', 1, NULL, 1, 1, 0, NULL, NULL, '2026-11-04 17:31:29.849000', '2026-11-04 17:31:30.773000', '2026-11-04 17:31:29.041000', '2026-11-04 17:31:30.785000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (29, 5, 208, 9, 'FrozenElectricity', 'D', '2026-11-01', '2026-11-04', 'SUCCESS', 1, NULL, 1, 1, 0, NULL, NULL, '2026-11-04 17:31:30.252000', '2026-11-04 17:31:31.233000', '2026-11-04 17:31:29.049000', '2026-11-04 17:31:31.255000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (30, 5, 208, 9, 'FrozenElectricity', 'POINT', '2026-11-01', '2026-11-04', 'SUCCESS', 1, NULL, 1, 1, 0, NULL, NULL, '2026-11-04 17:31:30.802000', '2026-11-04 17:31:31.779000', '2026-11-04 17:31:29.057000', '2026-11-04 17:31:31.798000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (34, 7, 208, 7, 'ElectricQuantity', 'D', '2026-11-01', '2026-11-03', 'EMPTY', 1, NULL, 0, 0, 0, NULL, NULL, '2026-11-04 18:48:51.059000', '2026-11-04 18:48:51.697000', '2026-11-04 18:48:51.019000', '2026-11-04 18:48:51.702000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (35, 7, 208, 7, 'FrozenElectricity', 'POINT', '2026-11-01', '2026-11-03', 'SUCCESS', 1, NULL, 1, 0, 0, NULL, NULL, '2026-11-04 18:48:51.059000', '2026-11-04 18:48:51.323000', '2026-11-04 18:48:51.027000', '2026-11-04 18:48:51.331000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (36, 8, 208, 7, 'ElectricQuantity', 'D', '2026-11-01', '2026-11-03', 'EMPTY', 1, NULL, 0, 0, 0, NULL, NULL, '2026-11-04 18:48:53.700000', '2026-11-04 18:48:54.324000', '2026-11-04 18:48:53.661000', '2026-11-04 18:48:54.330000');
INSERT INTO `energy_meter_sync_task` (`id`, `run_id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`, `status`, `attempt_count`, `next_retry_at`, `fetched_count`, `inserted_count`, `updated_count`, `error_code`, `error_message`, `started_at`, `finished_at`, `create_time`, `update_time`) VALUES (37, 8, 208, 7, 'FrozenElectricity', 'POINT', '2026-11-01', '2026-11-03', 'SUCCESS', 1, NULL, 1, 0, 0, NULL, NULL, '2026-11-04 18:48:53.700000', '2026-11-04 18:48:53.870000', '2026-11-04 18:48:53.669000', '2026-11-04 18:48:53.877000');
-- energy_meter_sync_checkpoint: 19 rows
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (1, 208, 1, 'GetWaterRawData', 'POINT', '2026-11-03', '2026-11-04 15:17:39.697000', 1, '2026-11-04 15:17:39.008000', '2026-11-04 15:17:39.710000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (2, 208, 1, 'GetUseWater', 'D', '2026-11-03', '2026-11-04 15:17:39.138000', 1, '2026-11-04 15:17:39.157000', '2026-11-04 15:17:39.157000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (4, 208, 2, 'GetUseWater', 'D', '2026-11-03', '2026-11-04 15:17:40.379000', 1, '2026-11-04 15:17:40.399000', '2026-11-04 15:17:40.399000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (5, 208, 2, 'GetWaterRawData', 'POINT', '2026-11-03', '2026-11-04 15:17:41.645000', 1, '2026-11-04 15:17:40.654000', '2026-11-04 15:17:41.658000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (6, 208, 3, 'GetUseWater', 'D', '2026-11-03', '2026-11-04 15:17:41.131000', 1, '2026-11-04 15:17:41.148000', '2026-11-04 15:17:41.148000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (8, 208, 3, 'GetWaterRawData', 'POINT', '2026-11-03', '2026-11-04 15:17:43.133000', 1, '2026-11-04 15:17:42.183000', '2026-11-04 15:17:43.143000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (9, 208, 4, 'GetUseWater', 'D', '2026-11-03', '2026-11-04 15:17:42.658000', 1, '2026-11-04 15:17:42.674000', '2026-11-04 15:17:42.674000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (11, 208, 4, 'GetWaterRawData', 'POINT', '2026-11-03', '2026-11-04 15:17:44.657000', 1, '2026-11-04 15:17:43.692000', '2026-11-04 15:17:44.672000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (12, 208, 5, 'GetUseWater', 'D', '2026-11-03', '2026-11-04 15:17:44.161000', 1, '2026-11-04 15:17:44.180000', '2026-11-04 15:17:44.180000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (14, 208, 5, 'GetWaterRawData', 'POINT', '2026-11-03', '2026-11-04 15:17:46.156000', 1, '2026-11-04 15:17:45.151000', '2026-11-04 15:17:46.167000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (15, 208, 6, 'GetUseWater', 'D', '2026-11-03', '2026-11-04 15:17:45.661000', 1, '2026-11-04 15:17:45.685000', '2026-11-04 15:17:45.685000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (17, 208, 6, 'GetWaterRawData', 'POINT', '2026-11-03', '2026-11-04 15:19:54.095000', 1, '2026-11-04 15:19:53.676000', '2026-11-04 15:19:54.104000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (19, 208, 7, 'FrozenElectricity', 'POINT', '2026-11-04', '2026-11-04 18:48:53.870000', 8, '2026-11-04 17:23:25.512000', '2026-11-04 18:48:53.886000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (20, 208, 8, 'FrozenElectricity', 'POINT', '2026-11-04', '2026-11-04 17:31:30.773000', 5, '2026-11-04 17:23:31.211000', '2026-11-04 17:31:30.796000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (21, 208, 9, 'FrozenElectricity', 'POINT', '2026-11-04', '2026-11-04 17:31:31.779000', 5, '2026-11-04 17:23:34.112000', '2026-11-04 17:31:31.808000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (22, 208, 7, 'FrozenElectricity', 'D', '2026-11-04', '2026-11-04 17:31:29.338000', 5, '2026-11-04 17:30:48.070000', '2026-11-04 17:31:29.366000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (23, 208, 8, 'FrozenElectricity', 'D', '2026-11-04', '2026-11-04 17:31:30.214000', 5, '2026-11-04 17:30:48.401000', '2026-11-04 17:31:30.246000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (24, 208, 9, 'FrozenElectricity', 'D', '2026-11-04', '2026-11-04 17:31:31.233000', 5, '2026-11-04 17:30:48.913000', '2026-11-04 17:31:31.265000');
INSERT INTO `energy_meter_sync_checkpoint` (`id`, `tenant_id`, `meter_id`, `source_api`, `granularity`, `last_complete_date`, `last_success_at`, `last_run_id`, `create_time`, `update_time`) VALUES (32, 208, 7, 'ElectricQuantity', 'D', '2026-11-03', '2026-11-04 18:48:54.324000', 8, '2026-11-04 18:48:51.713000', '2026-11-04 18:48:54.340000');
SET FOREIGN_KEY_CHECKS = 1;
doc/Ë®µç±í½ÓÈë/sql/Ë®µç±í½ÓÈë.sql
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,333 @@
-- ============================================================
-- æ°´ç”µè¡¨æŽ¥å…¥ â€”— å®Œæ•´è„šæœ¬ï¼ˆMySQL 8 / InnoDB / utf8mb4)
-- æ‰§è¡Œé¡ºåºï¼šåˆ é™¤æ—§è¡¨ â†’ å»ºè¡¨ â†’ æ¸…理菜单 â†’ åˆå§‹åŒ–设备
-- æ³¨æ„ï¼šæœ¬è„šæœ¬ä¼šæ¸…ç©º energy_meter_* å…¨éƒ¨æ•°æ®ï¼Œä»…开发/演示库直接执行;
--       ç”Ÿäº§åº“请改写为 ALTER TABLE å¢žé‡è„šæœ¬ï¼Œä¸è¦ç›´æŽ¥è·‘æœ¬æ–‡ä»¶ã€‚
-- è¯´æ˜Žï¼šè®¾å¤‡è¡¨å·ä¸Žç¬¬ä¸‰æ–¹ ID æ¥è‡ªé›†ç¤¾äº‘真实档案;真实用量需配置环境变量 NH_ENERGY_API_KEY åŽé‡‡é›†ã€‚
-- ============================================================
-- ============================================================
-- ä¸€ã€åˆ é™¤æ—§è¡¨ï¼ˆä¼šæ¸…空 energy_meter_* å…¨éƒ¨æ•°æ®ï¼‰
-- ============================================================
-- ============================================================
-- æ°´ç”µè¡¨æŽ¥å…¥ï¼ˆè®¾å¤‡å°è´¦ + æ‰‹å·¥æŠ„表 + æ°”表)—— åˆ è¡¨
-- æ‰§è¡ŒçŽ¯å¢ƒï¼šä»…å¼€å‘/演示库;会清空 energy_meter_* å…¨éƒ¨æ•°æ®
-- åˆ è¡¨é¡ºåºï¼šä»»åŠ¡ â†’ è¿›åº¦ â†’ æ‰¹æ¬¡ â†’ é‡‡é›†æ•°æ® â†’ è®¾å¤‡
-- ============================================================
-- 1. å•设备同步任务
DROP TABLE IF EXISTS `energy_meter_sync_task`;
-- 2. æ¯æŽ¥å£è¿žç»­åŒæ­¥è¿›åº¦
DROP TABLE IF EXISTS `energy_meter_sync_checkpoint`;
-- 3. åŒæ­¥æ‰§è¡Œæ‰¹æ¬¡
DROP TABLE IF EXISTS `energy_meter_sync_run`;
-- 4. ç”¨é‡ã€ç¤ºå€¼ä¸Žè´¹ç”¨
DROP TABLE IF EXISTS `energy_meter_measurement`;
-- 5. æœ¬åœ°ç»‘定档案
DROP TABLE IF EXISTS `energy_meter_device`;
-- ============================================================
-- äºŒã€å»ºè¡¨ï¼ˆè®¾å¤‡å°è´¦ / é‡‡é›†æ•°æ® / åŒæ­¥æ‰¹æ¬¡ / ä»»åŠ¡ / è¿›åº¦ï¼‰
-- ============================================================
-- ============================================================
-- æ°´ç”µè¡¨æŽ¥å…¥ï¼ˆè®¾å¤‡å°è´¦ + æ‰‹å·¥æŠ„表 + æ°”表)—— å»ºè¡¨
-- å¼•擎 InnoDB / å­—符集 utf8mb4;时间统一 Asia/Shanghai
-- äº”张表:设备台账、采集数据、同步批次、同步任务、同步进度
-- ä¸Žæ—§ç‰ˆå·®å¼‚:设备表增加来源/初始化标记/起始表码,采集表增加 remark,
--             èƒ½æºç±»åž‹å¢žåŠ æ°”è¡¨ï¼ˆGAS)
-- ============================================================
-- ------------------------------------------------------------
-- 1. è®¾å¤‡å°è´¦ï¼ˆæ°´è¡¨ / ç”µè¡¨ / æ°”表)
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_device` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '本地设备ID',
  `tenant_id` BIGINT NOT NULL COMMENT '租户ID',
  `meter_type` VARCHAR(16) NOT NULL COMMENT '能源类型:WATER水表 / ELECTRIC电表 / GAS气表',
  `source_type` VARCHAR(16) NOT NULL DEFAULT 'PROVIDER' COMMENT '数据来源:PROVIDER第三方接入 / MANUAL手工抄表',
  `meter_address` VARCHAR(64) NOT NULL COMMENT '通信地址或表号,按字符串保存,保留前导零',
  `provider_meter_id` VARCHAR(64) DEFAULT NULL COMMENT '第三方设备ID',
  `provider_name` VARCHAR(128) DEFAULT NULL COMMENT '第三方设备名称',
  `display_name` VARCHAR(128) NOT NULL COMMENT '本系统显示名称',
  `provider_area_id` BIGINT DEFAULT NULL COMMENT '第三方区域ID',
  `provider_area_name` VARCHAR(128) DEFAULT NULL COMMENT '第三方区域名称',
  `parent_provider_id` VARCHAR(64) DEFAULT NULL COMMENT '第三方总表ID',
  `parent_meter_id` BIGINT DEFAULT NULL COMMENT '本地总表ID,确认映射后维护',
  `location` VARCHAR(255) DEFAULT NULL COMMENT '安装位置',
  `online_status` TINYINT DEFAULT NULL COMMENT '1在线 0离线 NULL未知',
  `bind_status` VARCHAR(16) NOT NULL DEFAULT 'VERIFIED' COMMENT 'VERIFIED/INVALID',
  `enabled` TINYINT NOT NULL DEFAULT 1 COMMENT '1启用 0停用',
  `is_seed` TINYINT NOT NULL DEFAULT 0 COMMENT '1初始化设备(禁止删除)0用户新增',
  `simulated` TINYINT NOT NULL DEFAULT 0 COMMENT '1演示数据(每日模拟生成)0真实设备',
  `initial_reading` DECIMAL(20,6) DEFAULT NULL COMMENT '手工抄表起始表码',
  `initial_reading_date` DATE DEFAULT NULL COMMENT '起始表码对应业务日期',
  `sync_start_date` DATE NOT NULL COMMENT '本设备最早需要同步的业务日期',
  `last_success_at` DATETIME(3) DEFAULT NULL COMMENT '最近成功采集/抄表时间',
  `last_error_code` VARCHAR(64) DEFAULT NULL COMMENT '最近采集错误类别',
  `last_error_message` VARCHAR(255) DEFAULT NULL COMMENT '脱敏错误摘要',
  `create_user` BIGINT DEFAULT NULL COMMENT '创建人',
  `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_user` BIGINT DEFAULT NULL COMMENT '修改人',
  `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_device_tenant_type_address` (`tenant_id`, `meter_type`, `meter_address`),
  KEY `idx_em_device_scan` (`enabled`, `bind_status`, `meter_type`, `id`),
  KEY `idx_em_device_parent` (`parent_meter_id`),
  KEY `idx_em_device_source` (`tenant_id`, `source_type`, `meter_type`, `enabled`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='设备台账:水表/电表/气表';
-- ------------------------------------------------------------
-- 2. ç”¨é‡ã€ç¤ºå€¼ä¸Žè´¹ç”¨
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_measurement` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '采集数据ID',
  `tenant_id` BIGINT NOT NULL COMMENT '租户ID',
  `meter_id` BIGINT NOT NULL COMMENT '本地设备ID',
  `metric_code` VARCHAR(32) NOT NULL COMMENT 'WATER_USAGE/WATER_READING/ELECTRIC_USAGE/ELECTRIC_READING/ELECTRIC_COST/GAS_USAGE/GAS_READING',
  `granularity` VARCHAR(8) NOT NULL COMMENT 'D日/M月/Yå¹´/POINT时点',
  `measure_time` DATETIME NOT NULL COMMENT '期间起点或示值时点,Asia/Shanghai',
  `metric_value` DECIMAL(20,6) NOT NULL COMMENT '实际期间用量、示值或费用',
  `unit` VARCHAR(16) NOT NULL COMMENT 'm3/kWh/CNY',
  `source_api` VARCHAR(64) NOT NULL COMMENT '来源标识:第三方接口名或 MANUAL',
  `source_record_time` DATETIME DEFAULT NULL COMMENT '第三方原始ReadingDate解析值;手工抄表为抄表日期',
  `collected_at` DATETIME(3) NOT NULL COMMENT '本系统采集时间',
  `remark` VARCHAR(255) DEFAULT NULL COMMENT '备注:手工抄表说明',
  `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '首次入库时间',
  `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '最后修正时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_measurement_point` (`meter_id`, `metric_code`, `granularity`, `measure_time`),
  KEY `idx_em_measurement_query` (`tenant_id`, `meter_id`, `metric_code`, `granularity`, `measure_time`),
  KEY `idx_em_measurement_time` (`tenant_id`, `metric_code`, `granularity`, `measure_time`),
  KEY `idx_em_measurement_source` (`meter_id`, `source_api`, `granularity`, `measure_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用量、示值和已验证费用';
-- ------------------------------------------------------------
-- 3. åŒæ­¥æ‰§è¡Œæ‰¹æ¬¡
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_sync_run` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '同步批次ID',
  `run_key` VARCHAR(80) NOT NULL COMMENT '自动批次AUTO:yyyyMMdd;手动批次使用UUID',
  `trigger_type` VARCHAR(16) NOT NULL COMMENT 'SCHEDULED/MANUAL/BACKFILL',
  `schedule_date` DATE DEFAULT NULL COMMENT '计划执行日,北京时间;非定时批次为空',
  `range_start` DATE NOT NULL COMMENT '同步开始日期,含',
  `range_end` DATE NOT NULL COMMENT '同步结束日期,含',
  `status` VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RUNNING/SUCCESS/PARTIAL/FAILED',
  `total_tasks` INT NOT NULL DEFAULT 0 COMMENT '任务总数',
  `success_tasks` INT NOT NULL DEFAULT 0 COMMENT '成功任务数',
  `empty_tasks` INT NOT NULL DEFAULT 0 COMMENT '确认无数据任务数',
  `failed_tasks` INT NOT NULL DEFAULT 0 COMMENT '失败任务数',
  `started_at` DATETIME(3) DEFAULT NULL COMMENT '开始时间',
  `finished_at` DATETIME(3) DEFAULT NULL COMMENT '结束时间',
  `error_summary` VARCHAR(500) DEFAULT NULL COMMENT '脱敏错误摘要',
  `create_user` BIGINT DEFAULT NULL COMMENT '触发人;系统任务为空',
  `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_sync_run_key` (`run_key`),
  KEY `idx_em_sync_run_schedule` (`trigger_type`, `schedule_date`, `status`),
  KEY `idx_em_sync_run_created` (`create_time`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='同步执行批次';
-- ------------------------------------------------------------
-- 4. å•设备同步任务
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_sync_task` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '单设备单接口同步任务ID',
  `run_id` BIGINT NOT NULL COMMENT '所属批次ID',
  `tenant_id` BIGINT NOT NULL COMMENT '租户ID',
  `meter_id` BIGINT NOT NULL COMMENT '本地设备ID',
  `source_api` VARCHAR(64) NOT NULL COMMENT '第三方接口标识',
  `granularity` VARCHAR(8) NOT NULL COMMENT 'D/M/Y/POINT',
  `range_start` DATE NOT NULL COMMENT '请求开始日期,含',
  `range_end` DATE NOT NULL COMMENT '请求结束日期,含',
  `status` VARCHAR(16) NOT NULL DEFAULT 'PENDING' COMMENT 'PENDING/RUNNING/SUCCESS/EMPTY/RETRY/FAILED',
  `attempt_count` INT NOT NULL DEFAULT 0 COMMENT '已尝试次数',
  `next_retry_at` DATETIME(3) DEFAULT NULL COMMENT '下次重试时间',
  `fetched_count` INT NOT NULL DEFAULT 0 COMMENT '上游返回条数',
  `inserted_count` INT NOT NULL DEFAULT 0 COMMENT '新增条数',
  `updated_count` INT NOT NULL DEFAULT 0 COMMENT '修正条数',
  `error_code` VARCHAR(64) DEFAULT NULL COMMENT '脱敏错误类别',
  `error_message` VARCHAR(500) DEFAULT NULL COMMENT '脱敏错误信息',
  `started_at` DATETIME(3) DEFAULT NULL COMMENT '开始时间',
  `finished_at` DATETIME(3) DEFAULT NULL COMMENT '完成时间',
  `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_sync_task_unit` (`run_id`, `meter_id`, `source_api`, `granularity`, `range_start`, `range_end`),
  KEY `idx_em_sync_task_retry` (`status`, `next_retry_at`),
  KEY `idx_em_sync_task_meter` (`tenant_id`, `meter_id`, `create_time`),
  KEY `idx_em_sync_task_run` (`run_id`, `status`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='单设备同步任务及重试状态';
-- ------------------------------------------------------------
-- 5. æ¯æŽ¥å£è¿žç»­åŒæ­¥è¿›åº¦
-- ------------------------------------------------------------
CREATE TABLE IF NOT EXISTS `energy_meter_sync_checkpoint` (
  `id` BIGINT NOT NULL AUTO_INCREMENT COMMENT '补采进度ID',
  `tenant_id` BIGINT NOT NULL COMMENT '租户ID',
  `meter_id` BIGINT NOT NULL COMMENT '本地设备ID',
  `source_api` VARCHAR(64) NOT NULL COMMENT '第三方接口标识',
  `granularity` VARCHAR(8) NOT NULL COMMENT 'D/M/Y/POINT',
  `last_complete_date` DATE DEFAULT NULL COMMENT '连续完成到的业务日期,含;不可跨过失败日期',
  `last_success_at` DATETIME(3) DEFAULT NULL COMMENT '最近成功确认时间',
  `last_run_id` BIGINT DEFAULT NULL COMMENT '最近推进进度的批次ID',
  `create_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) COMMENT '创建时间',
  `update_time` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3) COMMENT '修改时间',
  PRIMARY KEY (`id`),
  UNIQUE KEY `uk_em_sync_checkpoint_unit` (`meter_id`, `source_api`, `granularity`),
  KEY `idx_em_sync_checkpoint_tenant` (`tenant_id`, `meter_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='每接口连续同步进度';
-- ============================================================
-- ä¸‰ã€æ¸…理数据库中的能耗菜单(菜单改为前端固定路由,不在此维护)
-- ============================================================
-- ============================================================
-- æ°´ç”µè¡¨æŽ¥å…¥ â€”— èœå•清理
-- è¯´æ˜Žï¼šèƒ½è€—管理(设备台账、水表/电表/气表数据,以及原有的设备能耗、能源趋势、
--       ç”¨ç”µåŒºåŸŸã€ç”¨ç”µæ—¶æ®µï¼‰å·²æ”¹ä¸ºå‰ç«¯å›ºå®šè·¯ç”±ï¼Œè§
--       product-inventory-management/src/router/index.js ä¸­çš„ EnergyManagement,
--       ä¸å†ä¾èµ– sys_menu ä¸Žè§’色授权,所有登录用户都能看到。
--       æœ¬è„šæœ¬åˆ é™¤æ•°æ®åº“ä¸­é—ç•™çš„èƒ½è€—èœå•ä¸ŽæŽˆæƒï¼Œé¿å…ä¾§è¾¹æ å‡ºçŽ°é‡å¤åˆ†ç»„ã€‚
-- å¯é‡å¤æ‰§è¡Œã€‚
-- ============================================================
DELETE FROM `sys_role_menu`
WHERE `menu_id` IN (2064, 2065, 2066, 2076, 2080, 2081,
                    2333, 2334, 2335, 2336,
                    2400, 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2409);
DELETE FROM `sys_menu`
WHERE `menu_id` IN (2064, 2065, 2066, 2076, 2080, 2081,
                    2333, 2334, 2335, 2336,
                    2400, 2401, 2402, 2403, 2404, 2405, 2406, 2407, 2408, 2409);
-- ============================================================
-- å››ã€åˆå§‹åŒ–设备(7 å°æ°´è¡¨ + 3 å°ç”µè¡¨ï¼ŒçœŸå®žè¡¨å·ï¼Œä¸å†™æ¨¡æ‹Ÿæ•°æ®ï¼‰
-- ============================================================
-- ============================================================
-- æ°´ç”µè¡¨æŽ¥å…¥ â€”— åˆå§‹åŒ–设备
-- è®¾å¤‡æ¥è‡ª æ•°æ®/设备地址.md çš„真实表号,第三方 ID/区域/入网日期已按集社云档案回填:
--   æ°´è¡¨ 7 å°ï¼ˆç¬¬ä¸‰æ–¹ ID 32123~32134、32758;6 å°å…¥ç½‘ 2026-10-12,采集起点 2026-10-13;
--              äºŒæ¥¼å—排煮锅纯净水入网 2026-11-04,采集起点 2026-11-04)
--   ç”µè¡¨ 3 å°ï¼ˆç¬¬ä¸‰æ–¹ ID 270946 / 270949 / 270951,2026-11-04 é‡æ–°æ³¨å†Œï¼Œé‡‡é›†èµ·ç‚¹ 2026-11-04,区域均为“电表”)
-- è¯´æ˜Žï¼š
--   1. å…¨éƒ¨ is_seed=1,前端与后端都不允许删除,只能停用;
--   2. åŒæ­¥èµ·ç‚¹å–入网次日的完整自然日,之前的日期供应商没有数据,不必补采;
--   3. çœŸå®žç”¨é‡éœ€è¦é…ç½®çŽ¯å¢ƒå˜é‡ NH_ENERGY_API_KEY,本脚本不写任何模拟数据;
--   4. å¯é‡å¤æ‰§è¡Œï¼Œæ‰§è¡Œå‰ä¼šæ¸…空 5 å¼ è¡¨ã€‚
-- ============================================================
SET @tenant := 208;
-- æ¸…空(顺序:任务 â†’ è¿›åº¦ â†’ æ‰¹æ¬¡ â†’ é‡‡é›† â†’ è®¾å¤‡ï¼‰
DELETE FROM `energy_meter_sync_task`;
DELETE FROM `energy_meter_sync_checkpoint`;
DELETE FROM `energy_meter_sync_run`;
DELETE FROM `energy_meter_measurement`;
DELETE FROM `energy_meter_device`;
INSERT INTO `energy_meter_device`
(`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`,
 `provider_meter_id`, `provider_name`, `display_name`, `provider_area_name`,
 `location`, `online_status`, `bind_status`, `enabled`,
 `is_seed`, `initial_reading`, `initial_reading_date`, `sync_start_date`,
 `last_success_at`, `create_user`, `create_time`, `update_time`)
VALUES
-- ---------------- æ°´è¡¨ 7 å° ----------------
(1, @tenant, 'WATER', 'PROVIDER', '12541588057907',
 '32134', '二楼灭菌西北', '二楼灭菌西北', '兴盛旺海',
 '二楼灭菌西北', 1, 'VERIFIED', 1,
 1, NULL, NULL, '2026-10-13', NULL, 1, NOW(3), NOW(3)),
(2, @tenant, 'WATER', 'PROVIDER', '12541588045261',
 '32133', '二楼生产用水总水表', '二楼生产用水总水表', '兴盛旺海',
 '二楼生产用水总水表', 1, 'VERIFIED', 1,
 1, NULL, NULL, '2026-10-13', NULL, 1, NOW(3), NOW(3)),
(3, @tenant, 'WATER', 'PROVIDER', '12541588057898',
 '32132', '煮毛肚水表', '煮毛肚水表', '兴盛旺海',
 '煮毛肚水表', 1, 'VERIFIED', 1,
 1, NULL, NULL, '2026-10-13', NULL, 1, NOW(3), NOW(3)),
(4, @tenant, 'WATER', 'PROVIDER', '12541588057901',
 '32131', '提升机洗毛肚处', '提升机洗毛肚处', '兴盛旺海',
 '提升机洗毛肚处', 1, 'VERIFIED', 1,
 1, NULL, NULL, '2026-10-13', NULL, 1, NOW(3), NOW(3)),
(5, @tenant, 'WATER', 'PROVIDER', '12541588057897',
 '32130', '分切毛肚', '分切毛肚', '兴盛旺海',
 '分切毛肚', 1, 'VERIFIED', 1,
 1, NULL, NULL, '2026-10-13', NULL, 1, NOW(3), NOW(3)),
(6, @tenant, 'WATER', 'PROVIDER', '12541588057908',
 '32123', '一楼纯净水总表', '一楼纯净水总表', '兴盛旺海',
 '一楼纯净水总表', 1, 'VERIFIED', 1,
 1, NULL, NULL, '2026-10-13', NULL, 1, NOW(3), NOW(3)),
(10, @tenant, 'WATER', 'PROVIDER', '12541588057808',
 '32758', '二楼南排煮锅纯净水', '二楼南排煮锅纯净水', '水表',
 '二楼南排煮锅纯净水', 0, 'VERIFIED', 1,
 1, NULL, NULL, '2026-11-04', NULL, 1, NOW(3), NOW(3)),
-- ---------------- ç”µè¡¨ 3 å° ----------------
(7, @tenant, 'ELECTRIC', 'PROVIDER', '712606021272',
 '270946', '二楼总电表', '二楼总电表', '电表',
 '二楼总电表', 0, 'VERIFIED', 1,
 1, NULL, NULL, '2026-11-04', NULL, 1, NOW(3), NOW(3)),
(8, @tenant, 'ELECTRIC', 'PROVIDER', '712606021277',
 '270949', '污水电表', '污水电表', '电表',
 '污水电表', 0, 'VERIFIED', 1,
 1, NULL, NULL, '2026-11-04', NULL, 1, NOW(3), NOW(3)),
(9, @tenant, 'ELECTRIC', 'PROVIDER', '712606021273',
 '270951', '一楼总电表', '一楼总电表', '电表',
 '一楼总电表', 0, 'VERIFIED', 1,
 1, NULL, NULL, '2026-11-04', NULL, 1, NOW(3), NOW(3));
-- ============================================================
-- äº”、气表演示设备(可选)
-- è¯´æ˜Žï¼šè¿™ 5 å°æ°”表未在集社云登记,按“手工抄表”建档并标记 simulated=1;
--       æ¯å¤© 08:00 çš„同步作业会为它们生成当天的模拟表码与用量,不再一次性灌历史数据。
--       æŽ¥å…¥çœŸå®žæ°”表后,删除本段生成的设备与数据即可(见文末说明)。
--       è¡¨å·å–自现场表身条码,is_seed=0(允许删除)。
-- ============================================================
-- 5.1 è®¾å¤‡ï¼ˆé‡å¤æ‰§è¡Œå…ˆæ¸…理)
SET @tenant := 208;
DELETE FROM `energy_meter_measurement` WHERE `meter_id` IN (11, 12, 13, 14, 15);
DELETE FROM `energy_meter_device` WHERE `id` IN (11, 12, 13, 14, 15);
INSERT INTO `energy_meter_device`
(`id`, `tenant_id`, `meter_type`, `source_type`, `meter_address`, `provider_meter_id`,
 `provider_name`, `display_name`, `provider_area_name`, `location`, `online_status`, `bind_status`,
 `enabled`, `is_seed`, `initial_reading`, `initial_reading_date`, `sync_start_date`, `last_success_at`,
 `create_user`, `create_time`, `update_time`)
VALUES
(11, @tenant, 'GAS', 'MANUAL', '862873080705632', NULL,
 NULL, '灭菌处气表', NULL, '灭菌处', NULL, 'VERIFIED',
 1, 0, 0.000, '2026-11-04', '2026-11-04', NULL,
 1, NOW(3), NOW(3)),
(12, @tenant, 'GAS', 'MANUAL', '865619085854645', NULL,
 NULL, '煮锅中气表', NULL, '煮锅中', NULL, 'VERIFIED',
 1, 0, 0.000, '2026-11-04', '2026-11-04', NULL,
 1, NOW(3), NOW(3)),
(13, @tenant, 'GAS', 'MANUAL', '862873080702613', NULL,
 NULL, '煮锅南气表', NULL, '煮锅南', NULL, 'VERIFIED',
 1, 0, 0.000, '2026-11-04', '2026-11-04', NULL,
 1, NOW(3), NOW(3)),
(14, @tenant, 'GAS', 'MANUAL', '866960071744294', NULL,
 NULL, '煮锅北气表', NULL, '煮锅北', NULL, 'VERIFIED',
 1, 0, 0.000, '2026-11-04', '2026-11-04', NULL,
 1, NOW(3), NOW(3)),
(15, @tenant, 'GAS', 'MANUAL', '862873080704353', NULL,
 NULL, '一楼煮毛肚气表', NULL, '一楼煮毛肚', NULL, 'VERIFIED',
 1, 0, 0.000, '2026-11-04', '2026-11-04', NULL,
 1, NOW(3), NOW(3));
-- 5.2 æ ‡è®°ä¸ºæ¼”示数据:每天 08:00 çš„同步作业会为 simulated=1 çš„设备生成当天的模拟表码与用量,
--     ä¸å†ä¸€æ¬¡æ€§çŒåŽ†å²æ•°æ®ï¼›éœ€è¦è¡¥æŸä¸€å¤©æ—¶è°ƒç”¨ POST /energyMeter/sync/gasSimulate?date=yyyy-MM-dd
UPDATE `energy_meter_device` SET `simulated` = 1 WHERE `id` IN (11, 12, 13, 14, 15);
-- è¯´æ˜Žï¼šæŽ¥å…¥çœŸå®žæ°”表后,执行下面两句即可清掉这 5 å°æ¼”示设备及其数据:
--   DELETE FROM `energy_meter_measurement` WHERE `meter_id` IN (11, 12, 13, 14, 15);
--   DELETE FROM `energy_meter_device` WHERE `id` IN (11, 12, 13, 14, 15);
src/main/java/com/ruoyi/common/config/IgnoreTableConfig.java
@@ -39,6 +39,11 @@
        IGNORE_TABLES.add("product_model");
        IGNORE_TABLES.add("ai_chat_session");
        IGNORE_TABLES.add("product");
        IGNORE_TABLES.add("energy_meter_device");
        IGNORE_TABLES.add("energy_meter_measurement");
        IGNORE_TABLES.add("energy_meter_sync_run");
        IGNORE_TABLES.add("energy_meter_sync_task");
        IGNORE_TABLES.add("energy_meter_sync_checkpoint");
    }
}
src/main/java/com/ruoyi/energymeter/config/EnergyMeterExecutorConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,43 @@
package com.ruoyi.energymeter.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import java.util.concurrent.ThreadPoolExecutor;
/**
 * æ°´ç”µè¡¨åŒæ­¥çº¿ç¨‹æ± ï¼šä»»åŠ¡æ± é™åˆ¶ç¬¬ä¸‰æ–¹å¹¶å‘ï¼Œç¼–æŽ’æ± é¿å…åŒä¸€æ‰¹æ¬¡é˜»å¡žä»»åŠ¡æ‰§è¡Œã€‚
 */
@Configuration
public class EnergyMeterExecutorConfig {
    @Bean(name = "energyMeterSyncExecutor")
    public ThreadPoolTaskExecutor energyMeterSyncExecutor(EnergyMeterProperties properties) {
        int concurrency = Math.max(1, properties.getConcurrency());
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(concurrency);
        executor.setMaxPoolSize(concurrency);
        executor.setQueueCapacity(5000);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("energy-meter-task-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        return executor;
    }
    @Bean(name = "energyMeterRunExecutor")
    public ThreadPoolTaskExecutor energyMeterRunExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(2);
        executor.setMaxPoolSize(4);
        executor.setQueueCapacity(500);
        executor.setKeepAliveSeconds(60);
        executor.setThreadNamePrefix("energy-meter-run-");
        executor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());
        executor.setWaitForTasksToCompleteOnShutdown(true);
        executor.setAwaitTerminationSeconds(30);
        return executor;
    }
}
src/main/java/com/ruoyi/energymeter/config/EnergyMeterProperties.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package com.ruoyi.energymeter.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * æ°´ç”µè¡¨æŽ¥å…¥é…ç½®ã€‚
 *
 * <p>授权码只从环境变量或安全配置读取,禁止写入源码、前端与数据库业务表。</p>
 */
@Data
@Component
@ConfigurationProperties(prefix = "energy-meter")
public class EnergyMeterProperties {
    /** ç¬¬ä¸‰æ–¹ä¾›åº”商配置。 */
    private Provider provider = new Provider();
    /** æ¯å¤©è‡ªåŠ¨åŒæ­¥æ—¶å›žæŸ¥çš„å¤©æ•°ï¼ˆå«æ˜¨å¤©ï¼‰ã€‚ */
    private int lookbackDays = 3;
    /** åŽ†å²è¡¥é‡‡å•ä¸ªåˆ†ç‰‡çš„æœ€å¤§è‡ªç„¶æ—¥ã€‚ */
    private int maxShardDays = 30;
    /** ç¤ºå€¼æŽ¥å£å•个分片的最大自然日:集社云 GetWaterRawData é™åˆ¶æŸ¥è¯¢è·¨åº¦ä¸è¶…过 15 å¤©ã€‚ */
    private int maxReadingShardDays = 15;
    /** ç¬¬ä¸‰æ–¹è¯·æ±‚最大并发数。 */
    private int concurrency = 3;
    /** å¤±è´¥ä»»åŠ¡æœ€å¤§å°è¯•æ¬¡æ•°ã€‚ */
    private int maxAttempts = 3;
    /** RUNNING ä»»åŠ¡è¶…æ—¶é˜ˆå€¼ï¼ˆåˆ†é’Ÿï¼‰ï¼Œè¶…æ—¶åŽè¿›å…¥é‡è¯•ã€‚ */
    private int runningTimeoutMinutes = 30;
    /** æ˜¯å¦å¯ç”¨åŽŸå§‹ç¤ºå€¼ä»»åŠ¡ï¼ˆæ°´è¡¨åˆ†é¡µèƒ½åŠ›æœªç¡®è®¤æ—¶å¯å…³é—­ï¼‰ã€‚ */
    private boolean rawReadingEnabled = true;
    /** æ˜¯å¦æ³¨å†Œ Quartz ä½œä¸šã€‚ */
    private boolean schedulerEnabled = true;
    /** æ¯å¤©åŒæ­¥ Cron,固定北京时间 08:00。 */
    private String syncCron = "0 0 8 * * ?";
    /** æ¢å¤ä»»åŠ¡ Cron,每 10 åˆ†é’Ÿã€‚ */
    private String recoveryCron = "0 0/10 * * * ?";
    /** ç»‘定时默认补采起点:上一自然年 1 æœˆ 1 æ—¥ã€‚ */
    private boolean backfillFromLastYear = true;
    @Data
    public static class Provider {
        /** æ­£å¼çŽ¯å¢ƒåŸºç¡€åœ°å€ï¼Œéƒ¨ç½²å‰é¡»ç¡®è®¤ HTTPS æˆ–安全代理方案。 */
        private String baseUrl = "http://nhapi.yunjichaobiao.com/WebApi";
        /** ç¬¬ä¸‰æ–¹æŽˆæƒç ï¼Œåªå…è®¸é€šè¿‡çŽ¯å¢ƒå˜é‡æ³¨å…¥ã€‚ */
        private String apiKey = "";
        /** æ¼”示数据模式:未配置授权码时自动开启,仅在本地联调使用。 */
        private boolean mockEnabled = false;
        /** ä¸¤æ¬¡ç¬¬ä¸‰æ–¹è¯·æ±‚之间的最小间隔(毫秒),用于规避供应商限流(429)。 */
        private long minIntervalMs = 500;
        private int connectTimeoutSeconds = 5;
        private int readTimeoutSeconds = 30;
    }
}
src/main/java/com/ruoyi/energymeter/constant/EnergyMeterConstants.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,208 @@
package com.ruoyi.energymeter.constant;
import java.time.ZoneId;
/**
 * èƒ½è€—表(水表/电表/气表)接入模块常量。
 *
 * <p>所有业务日期均按 Asia/Shanghai è®¡ç®—,与服务器默认时区无关。</p>
 */
public final class EnergyMeterConstants {
    private EnergyMeterConstants() {
    }
    public static final ZoneId ZONE = ZoneId.of("Asia/Shanghai");
    /* ---------------- èƒ½æºç±»åž‹ ---------------- */
    public static final String TYPE_WATER = "WATER";
    public static final String TYPE_ELECTRIC = "ELECTRIC";
    public static final String TYPE_GAS = "GAS";
    /* ---------------- æ•°æ®æ¥æº ---------------- */
    /** ç¬¬ä¸‰æ–¹æŽ¥å£æŽ¥å…¥ï¼Œç”±å®šæ—¶ä»»åŠ¡/手动查表落库。 */
    public static final String SOURCE_TYPE_PROVIDER = "PROVIDER";
    /** æ‰‹å·¥æŠ„表,由用户在表具台账中录入表码,用量按期差计算。 */
    public static final String SOURCE_TYPE_MANUAL = "MANUAL";
    /** æ‰‹å·¥æŠ„表数据在采集表中的来源标识。 */
    public static final String SOURCE_MANUAL = "MANUAL";
    /* ---------------- æŒ‡æ ‡ç¼–码 ---------------- */
    public static final String METRIC_WATER_USAGE = "WATER_USAGE";
    public static final String METRIC_WATER_READING = "WATER_READING";
    public static final String METRIC_ELECTRIC_USAGE = "ELECTRIC_USAGE";
    public static final String METRIC_ELECTRIC_READING = "ELECTRIC_READING";
    public static final String METRIC_ELECTRIC_COST = "ELECTRIC_COST";
    public static final String METRIC_GAS_USAGE = "GAS_USAGE";
    public static final String METRIC_GAS_READING = "GAS_READING";
    /* ---------------- ç²’度 ---------------- */
    public static final String GRANULARITY_DAY = "D";
    public static final String GRANULARITY_MONTH = "M";
    public static final String GRANULARITY_YEAR = "Y";
    public static final String GRANULARITY_POINT = "POINT";
    /* ---------------- å•位 ---------------- */
    public static final String UNIT_WATER = "m3";
    public static final String UNIT_ELECTRIC = "kWh";
    public static final String UNIT_GAS = "m3";
    public static final String UNIT_COST = "CNY";
    /* ---------------- ç¬¬ä¸‰æ–¹æŽ¥å£æ ‡è¯† ---------------- */
    public static final String SOURCE_GET_USE_WATER = "GetUseWater";
    public static final String SOURCE_GET_WATER_RAW = "GetWaterRawData";
    public static final String SOURCE_FROZEN_ELECTRICITY = "FrozenElectricity";
    public static final String SOURCE_ELECTRIC_QUANTITY = "ElectricQuantity";
    public static final String SOURCE_GAS_USE = "GetGasUseData";
    public static final String SOURCE_GAS_RAW = "GetGasRawData";
    /* ---------------- ç»‘定与启用 ---------------- */
    public static final String BIND_VERIFIED = "VERIFIED";
    public static final String BIND_INVALID = "INVALID";
    /* ---------------- æ‰¹æ¬¡ç±»åž‹ ---------------- */
    public static final String RUN_SCHEDULED = "SCHEDULED";
    public static final String RUN_MANUAL = "MANUAL";
    public static final String RUN_BACKFILL = "BACKFILL";
    /* ---------------- æ‰¹æ¬¡çŠ¶æ€ ---------------- */
    public static final String RUN_PENDING = "PENDING";
    public static final String RUN_RUNNING = "RUNNING";
    public static final String RUN_SUCCESS = "SUCCESS";
    public static final String RUN_PARTIAL = "PARTIAL";
    public static final String RUN_FAILED = "FAILED";
    /* ---------------- ä»»åŠ¡çŠ¶æ€ ---------------- */
    public static final String TASK_PENDING = "PENDING";
    public static final String TASK_RUNNING = "RUNNING";
    public static final String TASK_SUCCESS = "SUCCESS";
    public static final String TASK_EMPTY = "EMPTY";
    public static final String TASK_RETRY = "RETRY";
    public static final String TASK_FAILED = "FAILED";
    /* ---------------- é”™è¯¯ç±»åˆ« ---------------- */
    public static final String ERROR_UPSTREAM = "UPSTREAM_ERROR";
    public static final String ERROR_TIMEOUT = "UPSTREAM_TIMEOUT";
    public static final String ERROR_PARSE = "PARSE_ERROR";
    public static final String ERROR_PAGE_INCOMPLETE = "PAGE_INCOMPLETE";
    public static final String ERROR_ADDRESS_MISMATCH = "ADDRESS_MISMATCH";
    /** è‡ªåŠ¨æ‰¹æ¬¡ run_key å‰ç¼€ã€‚ */
    public static final String AUTO_RUN_KEY_PREFIX = "AUTO:";
    /** é‡è¯•间隔(分钟),下标对应第几次失败。 */
    public static final int[] RETRY_DELAY_MINUTES = {1, 5, 15};
    /** ä¸€ä¸ªè®¾å¤‡æœ€å¤šæ”¯æŒçš„补采跨度(自然日),超出时按分片拆分。 */
    public static final int MAX_RANGE_DAYS = 366 * 3;
    public static boolean isWater(String meterType) {
        return TYPE_WATER.equalsIgnoreCase(meterType);
    }
    public static boolean isElectric(String meterType) {
        return TYPE_ELECTRIC.equalsIgnoreCase(meterType);
    }
    public static boolean isGas(String meterType) {
        return TYPE_GAS.equalsIgnoreCase(meterType);
    }
    public static boolean isManual(String sourceType) {
        return SOURCE_TYPE_MANUAL.equalsIgnoreCase(sourceType);
    }
    /**
     * æ˜¯å¦å±žäºŽè‡ªåŠ¨é‡‡é›†ï¼šç¬¬ä¸‰æ–¹æŽ¥å…¥ï¼Œæˆ–ç”±ç³»ç»Ÿæ¯æ—¥è‡ªåŠ¨ç”Ÿæˆæ•°æ®çš„æ¨¡æ‹Ÿè¡¨ï¼ˆå¦‚è’¸æ±½è¡¨æ¼”ç¤ºè®¾å¤‡ï¼‰ã€‚
     *
     * <p>模拟表的 source_type ä»æ˜¯ MANUAL(不参与第三方查表),但业务上按自动采集展示与统计。</p>
     */
    public static boolean isAutoCollect(String sourceType, Integer simulated) {
        return SOURCE_TYPE_PROVIDER.equalsIgnoreCase(sourceType) || Integer.valueOf(1).equals(simulated);
    }
    public static boolean isSimulated(Integer simulated) {
        return Integer.valueOf(1).equals(simulated);
    }
    /** è§„范化能源类型,未知类型返回 null,由调用方抛出业务异常。 */
    public static String canonicalType(String meterType) {
        String type = meterType == null ? null : meterType.trim();
        if (TYPE_WATER.equalsIgnoreCase(type)) {
            return TYPE_WATER;
        }
        if (TYPE_ELECTRIC.equalsIgnoreCase(type)) {
            return TYPE_ELECTRIC;
        }
        if (TYPE_GAS.equalsIgnoreCase(type)) {
            return TYPE_GAS;
        }
        return null;
    }
    public static String typeName(String meterType) {
        if (isWater(meterType)) {
            return "水表";
        }
        if (isGas(meterType)) {
            return "气表";
        }
        if (isElectric(meterType)) {
            return "电表";
        }
        return meterType;
    }
    public static String usageMetric(String meterType) {
        if (isWater(meterType)) {
            return METRIC_WATER_USAGE;
        }
        if (isGas(meterType)) {
            return METRIC_GAS_USAGE;
        }
        return METRIC_ELECTRIC_USAGE;
    }
    public static String readingMetric(String meterType) {
        if (isWater(meterType)) {
            return METRIC_WATER_READING;
        }
        if (isGas(meterType)) {
            return METRIC_GAS_READING;
        }
        return METRIC_ELECTRIC_READING;
    }
    public static String unit(String meterType) {
        if (isWater(meterType)) {
            return UNIT_WATER;
        }
        if (isGas(meterType)) {
            return UNIT_GAS;
        }
        return UNIT_ELECTRIC;
    }
    /** æœŸé—´ç”¨é‡çš„æ¥æºæ ‡è¯†ã€‚ */
    public static String usageSource(String meterType) {
        if (isWater(meterType)) {
            return SOURCE_GET_USE_WATER;
        }
        if (isGas(meterType)) {
            return SOURCE_GAS_USE;
        }
        // ç”µè¡¨ç”¨é‡å– ElectricQuantity çš„æ­£å‘有功电量;FrozenElectricity çš„ TotalKwh å¯èƒ½æ’为 null
        return SOURCE_ELECTRIC_QUANTITY;
    }
    /** æ—¶ç‚¹ç¤ºå€¼çš„æ¥æºæ ‡è¯†ã€‚ */
    public static String readingSource(String meterType) {
        if (isWater(meterType)) {
            return SOURCE_GET_WATER_RAW;
        }
        if (isGas(meterType)) {
            return SOURCE_GAS_RAW;
        }
        return SOURCE_FROZEN_ELECTRICITY;
    }
}
src/main/java/com/ruoyi/energymeter/controller/EnergyMeterController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,244 @@
package com.ruoyi.energymeter.controller;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.energymeter.dto.EnergyMeterDeviceQuery;
import com.ruoyi.energymeter.dto.MeterBindRequest;
import com.ruoyi.energymeter.dto.MeterCollectRequest;
import com.ruoyi.energymeter.dto.MeterManualBindRequest;
import com.ruoyi.energymeter.dto.MeterReadingRequest;
import com.ruoyi.energymeter.dto.MeterUpdateRequest;
import com.ruoyi.energymeter.dto.MeterVerifyRequest;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.service.EnergyMeterDeviceService;
import com.ruoyi.energymeter.service.EnergyMeterDemoService;
import com.ruoyi.energymeter.service.EnergyMeterStatService;
import com.ruoyi.energymeter.service.EnergyMeterSyncService;
import com.ruoyi.energymeter.service.ElectricMeterDashboardService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import com.ruoyi.framework.aspectj.lang.annotation.Log;
import com.ruoyi.framework.aspectj.lang.enums.BusinessType;
import com.ruoyi.framework.web.controller.BaseController;
import com.ruoyi.framework.web.domain.AjaxResult;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDate;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
 * æ°´ç”µè¡¨æŽ¥å…¥ä¸ŽæŸ¥è¡¨æŽ¥å£ã€‚
 *
 * <p>前端只调用本系统后端,第三方授权码不会下发到浏览器。</p>
 */
@RestController
@Tag(name = "水电表监测")
@RequestMapping("/energyMeter")
@RequiredArgsConstructor
public class EnergyMeterController extends BaseController {
    private final EnergyMeterDeviceService deviceService;
    private final EnergyMeterSyncService syncService;
    private final EnergyMeterStatService statService;
    private final EnergyMeterDemoService demoService;
    private final ElectricMeterDashboardService electricDashboardService;
    @GetMapping("/devices")
    @Operation(summary = "设备列表-分页查询")
    @Log(title = "水电表监测-设备列表", businessType = BusinessType.OTHER)
    public AjaxResult devices(Page<EnergyMeterDevice> page, EnergyMeterDeviceQuery query) {
        IPage<EnergyMeterDevice> result = deviceService.pageDevices(page, query);
        return AjaxResult.success(result);
    }
    @GetMapping("/devices/options")
    @Operation(summary = "设备下拉选项")
    public AjaxResult deviceOptions(@RequestParam(required = false) String meterType) {
        return AjaxResult.success(deviceService.listDeviceOptions(meterType));
    }
    @PostMapping("/devices/verify")
    @Operation(summary = "验证并读取第三方档案")
    @Log(title = "水电表监测-档案验证", businessType = BusinessType.OTHER)
    public AjaxResult verify(@RequestBody MeterVerifyRequest request) {
        return AjaxResult.success(deviceService.verifyArchive(request.getMeterType(), request.getMeterAddress()));
    }
    @PostMapping("/devices")
    @Operation(summary = "新增本地绑定")
    @Log(title = "水电表监测-新增绑定", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody MeterBindRequest request) {
        return AjaxResult.success(deviceService.bind(request));
    }
    @PutMapping("/devices/{id}")
    @Operation(summary = "修改设备本地字段/启停用")
    @Log(title = "表具台账-修改设备", businessType = BusinessType.UPDATE)
    public AjaxResult update(@PathVariable Long id, @RequestBody MeterUpdateRequest request) {
        return AjaxResult.success(deviceService.updateLocal(id, request));
    }
    @PostMapping("/devices/manual")
    @Operation(summary = "新增手工抄表设备(水表/电表/气表)")
    @Log(title = "表具台账-新增手工抄表设备", businessType = BusinessType.INSERT)
    public AjaxResult addManual(@RequestBody MeterManualBindRequest request) {
        return AjaxResult.success(deviceService.addManual(request));
    }
    @DeleteMapping("/devices/{id}")
    @Operation(summary = "删除设备(初始化设备与第三方接入设备不允许删除)")
    @Log(title = "表具台账-删除设备", businessType = BusinessType.DELETE)
    public AjaxResult remove(@PathVariable Long id) {
        deviceService.removeDevice(id);
        return AjaxResult.success();
    }
    @GetMapping("/devices/{id}/readings")
    @Operation(summary = "手工抄表记录分页")
    public AjaxResult readings(@PathVariable Long id,
                               @RequestParam(defaultValue = "1") long current,
                               @RequestParam(defaultValue = "20") long size) {
        return AjaxResult.success(deviceService.pageReadings(id, current, size));
    }
    @PostMapping("/devices/{id}/readings")
    @Operation(summary = "录入手工抄表表码")
    @Log(title = "表具台账-手工录表", businessType = BusinessType.INSERT)
    public AjaxResult addReading(@PathVariable Long id, @RequestBody MeterReadingRequest request) {
        return AjaxResult.success(deviceService.addReading(id, request));
    }
    @DeleteMapping("/devices/{id}/readings/{readingId}")
    @Operation(summary = "删除手工抄表记录")
    @Log(title = "表具台账-删除抄表记录", businessType = BusinessType.DELETE)
    public AjaxResult removeReading(@PathVariable Long id, @PathVariable Long readingId) {
        deviceService.removeReading(id, readingId);
        return AjaxResult.success();
    }
    @PostMapping("/devices/{id}/collect")
    @Operation(summary = "单台设备手动查表")
    @Log(title = "水电表监测-手动查表", businessType = BusinessType.OTHER)
    public AjaxResult collect(@PathVariable Long id,
                              @RequestBody(required = false) MeterCollectRequest request) {
        LocalDate start = request == null ? null : request.getStartDate();
        LocalDate end = request == null ? null : request.getEndDate();
        Boolean includeReadings = request == null ? null : request.getIncludeReadings();
        Long runId = syncService.triggerManual(List.of(id), start, end, includeReadings);
        return AjaxResult.success(runResult(runId));
    }
    @PostMapping("/devices/collect")
    @Operation(summary = "批量手动查表")
    @Log(title = "水电表监测-批量查表", businessType = BusinessType.OTHER)
    public AjaxResult collectBatch(@RequestBody MeterCollectRequest request) {
        List<Long> meterIds = new ArrayList<>();
        if (request.getMeterIds() != null) {
            meterIds.addAll(request.getMeterIds());
        }
        if (request.getMeterId() != null && !meterIds.contains(request.getMeterId())) {
            meterIds.add(request.getMeterId());
        }
        Long runId = syncService.triggerManual(meterIds, request.getStartDate(), request.getEndDate(),
                request.getIncludeReadings());
        return AjaxResult.success(runResult(runId));
    }
    @GetMapping("/devices/{id}/measurements")
    @Operation(summary = "设备读数/用量明细")
    public AjaxResult measurements(@PathVariable Long id,
                                   @RequestParam(required = false) String metricCode,
                                   @RequestParam(required = false) String granularity,
                                   @RequestParam(required = false) String start,
                                   @RequestParam(required = false) String end,
                                   @RequestParam(defaultValue = "1") long current,
                                   @RequestParam(defaultValue = "20") long size) {
        IPage<EnergyMeterMeasurement> page = statService.pageMeasurements(id, metricCode, granularity,
                parseDate(start), parseDate(end), current, size);
        return AjaxResult.success(page);
    }
    @GetMapping("/overview")
    @Operation(summary = "用能概览")
    public AjaxResult overview(@RequestParam(defaultValue = "WATER") String meterType,
                               @RequestParam(required = false) Long meterId,
                               @RequestParam(required = false) String month,
                               @RequestParam(required = false) Integer year) {
        return AjaxResult.success(statService.overview(meterType, meterId, month, year));
    }
    @GetMapping("/electric/dashboard")
    @Operation(summary = "电表实时看板:今日15分钟曲线与电能质量")
    public AjaxResult electricDashboard(@RequestParam(required = false) Long meterId,
                                        @RequestParam(defaultValue = "0") Integer weekOffset) {
        return AjaxResult.success(electricDashboardService.dashboard(meterId, weekOffset));
    }
    @PostMapping("/sync/trigger")
    @Operation(summary = "手动触发一次每日同步(排障用)")
    @Log(title = "水电表监测-触发同步", businessType = BusinessType.OTHER)
    public AjaxResult triggerDailySync() {
        return AjaxResult.success(runResult(syncService.triggerScheduledSync()));
    }
    @PostMapping("/sync/recover")
    @Operation(summary = "手动触发一次重试/超时恢复(排障用)")
    @Log(title = "水电表监测-触发恢复", businessType = BusinessType.OTHER)
    public AjaxResult recoverSync() {
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("handled", syncService.recoverDueTasks());
        return AjaxResult.success(data);
    }
    @GetMapping("/providerStatus")
    @Operation(summary = "第三方接入模式")
    public AjaxResult providerStatus() {
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("mockMode", deviceService.mockMode());
        return AjaxResult.success(data);
    }
    @PostMapping("/sync/gasSimulate")
    @Operation(summary = "生成气表演示数据(按天,默认今天;同一天重复执行不会重复写入)")
    @Log(title = "表具台账-气表演示数据", businessType = BusinessType.OTHER)
    public AjaxResult gasSimulate(@RequestParam(required = false) String date) {
        LocalDate day = StringUtils.isBlank(date) ? EnergyMeterSupport.today() : parseDate(date);
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("date", day.toString());
        data.put("generated", demoService.generateGasDemo(day));
        return AjaxResult.success(data);
    }
    private Map<String, Object> runResult(Long runId) {
        Map<String, Object> data = new LinkedHashMap<>();
        data.put("runId", runId);
        return data;
    }
    private LocalDate parseDate(String value) {
        if (StringUtils.isBlank(value)) {
            return null;
        }
        try {
            return LocalDate.parse(value.trim());
        } catch (Exception e) {
            throw new ServiceException("日期格式应为 yyyy-MM-dd");
        }
    }
}
src/main/java/com/ruoyi/energymeter/controller/EnergyMonitorController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,247 @@
package com.ruoyi.energymeter.controller;
import com.ruoyi.framework.web.domain.AjaxResult;
import com.ruoyi.energymeter.service.EnergyMonitorService;
import com.ruoyi.energymeter.vo.EnergyCollectionVO;
import com.ruoyi.energymeter.vo.EnergyRealtimeVO;
import com.ruoyi.energymeter.vo.EnergyReportVO;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import org.apache.poi.ss.usermodel.BorderStyle;
import org.apache.poi.ss.usermodel.Cell;
import org.apache.poi.ss.usermodel.CellStyle;
import org.apache.poi.ss.usermodel.FillPatternType;
import org.apache.poi.ss.usermodel.Font;
import org.apache.poi.ss.usermodel.HorizontalAlignment;
import org.apache.poi.ss.usermodel.IndexedColors;
import org.apache.poi.ss.usermodel.Row;
import org.apache.poi.ss.usermodel.Sheet;
import org.apache.poi.ss.usermodel.VerticalAlignment;
import org.apache.poi.ss.usermodel.Workbook;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.io.IOException;
import java.math.BigDecimal;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.List;
/**
 * èƒ½è€—数据采集、实时监控与统计报表接口。
 *
 * <p>菜单固定在前端路由,不写数据库菜单、不配角色权限;所有登录用户可见。</p>
 */
@RestController
@Tag(name = "能耗监控与统计")
@RequestMapping("/energyMeter")
@RequiredArgsConstructor
public class EnergyMonitorController {
    private final EnergyMonitorService monitorService;
    @GetMapping("/collection/overview")
    @Operation(summary = "能耗数据采集概览:三类介质接入设备、归集进度与最近批次")
    public AjaxResult collectionOverview() {
        EnergyCollectionVO vo = monitorService.collection();
        return AjaxResult.success(vo);
    }
    @GetMapping("/realtime/overview")
    @Operation(summary = "能耗实时监控:三类介质最新用量、趋势与电表实时曲线")
    public AjaxResult realtimeOverview(
            @RequestParam(required = false) Long meterId) {
        EnergyRealtimeVO vo = monitorService.realtime(meterId);
        return AjaxResult.success(vo);
    }
    @GetMapping("/report/summary")
    @Operation(summary = "能耗统计分析:按日/月、按介质/设备汇总")
    public AjaxResult reportSummary(
            @RequestParam(required = false) String startDate,
            @RequestParam(required = false) String endDate,
            @RequestParam(defaultValue = "DAY") String granularity,
            @RequestParam(defaultValue = "ALL") String meterType,
            @RequestParam(defaultValue = "MEDIUM") String dimension) {
        EnergyReportVO vo = monitorService.report(startDate, endDate, granularity, meterType, dimension);
        return AjaxResult.success(vo);
    }
    @RequestMapping(value = "/report/export", method = {RequestMethod.GET, RequestMethod.POST})
    @Operation(summary = "能耗统计报表导出 Excel")
    public void reportExport(@RequestParam(required = false) String startDate,
                             @RequestParam(required = false) String endDate,
                             @RequestParam(defaultValue = "DAY") String granularity,
                             @RequestParam(defaultValue = "ALL") String meterType,
                             HttpServletResponse response) throws IOException {
        EnergyReportVO report = monitorService.report(startDate, endDate, granularity, meterType, "MEDIUM");
        String fileName = URLEncoder.encode("能耗统计报表_" + report.getStart() + "_" + report.getEnd() + ".xlsx",
                StandardCharsets.UTF_8).replace("+", "%20");
        response.setContentType("application/vnd.openxmlformats-officedocument.spreadsheetml.sheet");
        response.setCharacterEncoding("utf-8");
        response.setHeader("Content-Disposition", "attachment; filename=\"" + fileName + "\"");
        response.setHeader("Access-Control-Expose-Headers", "Content-Disposition");
        try (Workbook workbook = new XSSFWorkbook()) {
            writeMediumSheet(workbook, report);
            writeDeviceSheet(workbook, report);
            workbook.write(response.getOutputStream());
        }
    }
    /* ------------------------------------------------------------------ */
    /* Excel                                                               */
    /* ------------------------------------------------------------------ */
    private void writeMediumSheet(Workbook workbook, EnergyReportVO report) {
        Sheet sheet = workbook.createSheet("介质汇总");
        CellStyle headStyle = headStyle(workbook);
        CellStyle numberStyle = numberStyle(workbook);
        CellStyle textStyle = textStyle(workbook);
        String[] headers = {"统计周期", "水表用量(m³)", "电表用量(度)", "蒸汽表用量(m³)"};
        Row head = sheet.createRow(0);
        for (int i = 0; i < headers.length; i++) {
            Cell cell = head.createCell(i);
            cell.setCellValue(headers[i]);
            cell.setCellStyle(headStyle);
        }
        int rowIndex = 1;
        for (EnergyReportVO.PeriodRow row : report.getRows()) {
            Row dataRow = sheet.createRow(rowIndex++);
            dataRow.createCell(0).setCellValue(row.getPeriod());
            dataRow.getCell(0).setCellStyle(textStyle);
            writeNumber(dataRow, 1, row.getWater(), numberStyle);
            writeNumber(dataRow, 2, row.getElectric(), numberStyle);
            writeNumber(dataRow, 3, row.getGas(), numberStyle);
        }
        Row totalRow = sheet.createRow(rowIndex);
        Cell totalLabel = totalRow.createCell(0);
        totalLabel.setCellValue("合计");
        totalLabel.setCellStyle(headStyle);
        for (EnergyReportVO.MediumTotal total : report.getTotals()) {
            int column = columnOf(total.getMeterType());
            if (column > 0) {
                writeNumber(totalRow, column, total.getTotal(), numberStyle);
            }
        }
        sheet.setColumnWidth(0, 18 * 256);
        for (int i = 1; i <= 3; i++) {
            sheet.setColumnWidth(i, 20 * 256);
        }
        sheet.createFreezePane(0, 1);
        sheet.setAutoFilter(new org.apache.poi.ss.util.CellRangeAddress(0, Math.max(0, rowIndex - 1), 0, 3));
    }
    private void writeDeviceSheet(Workbook workbook, EnergyReportVO report) {
        Sheet sheet = workbook.createSheet("设备明细");
        CellStyle headStyle = headStyle(workbook);
        CellStyle numberStyle = numberStyle(workbook);
        CellStyle textStyle = textStyle(workbook);
        String[] headers = {"设备名称", "表号", "介质", "采集方式", "期间用量", "单位", "占介质比例(%)"};
        Row head = sheet.createRow(0);
        for (int i = 0; i < headers.length; i++) {
            Cell cell = head.createCell(i);
            cell.setCellValue(headers[i]);
            cell.setCellStyle(headStyle);
        }
        int rowIndex = 1;
        for (EnergyReportVO.DeviceRow row : report.getDevices()) {
            Row dataRow = sheet.createRow(rowIndex++);
            createText(dataRow, 0, row.getMeterName(), textStyle);
            createText(dataRow, 1, row.getMeterAddress(), textStyle);
            createText(dataRow, 2, row.getMeterTypeName(), textStyle);
            createText(dataRow, 3, row.getSourceTypeName(), textStyle);
            writeNumber(dataRow, 4, row.getTotal(), numberStyle);
            createText(dataRow, 5, excelUnit(row.getUnit()), textStyle);
            writeNumber(dataRow, 6, row.getShare(), numberStyle);
        }
        sheet.setColumnWidth(0, 24 * 256);
        sheet.setColumnWidth(1, 22 * 256);
        for (int i = 2; i <= 6; i++) {
            sheet.setColumnWidth(i, 14 * 256);
        }
        sheet.createFreezePane(0, 1);
        sheet.setAutoFilter(new org.apache.poi.ss.util.CellRangeAddress(0, Math.max(0, rowIndex - 1), 0, 6));
    }
    private int columnOf(String meterType) {
        if ("WATER".equals(meterType)) {
            return 1;
        }
        if ("ELECTRIC".equals(meterType)) {
            return 2;
        }
        if ("GAS".equals(meterType)) {
            return 3;
        }
        return 0;
    }
    /** ç•Œé¢ä¸ŽæŠ¥è¡¨ç»Ÿä¸€ç”¨ã€Œåº¦ã€ï¼Œåº•层仍按 kWh å­˜å‚¨ã€‚ */
    private String excelUnit(String unit) {
        if ("kWh".equals(unit)) {
            return "度";
        }
        if ("m3".equals(unit)) {
            return "m³";
        }
        return unit;
    }
    private void createText(Row row, int column, String value, CellStyle style) {
        Cell cell = row.createCell(column);
        cell.setCellValue(value == null ? "" : value);
        cell.setCellStyle(style);
    }
    private void writeNumber(Row row, int column, BigDecimal value, CellStyle style) {
        Cell cell = row.createCell(column);
        if (value != null) {
            cell.setCellValue(value.doubleValue());
        }
        cell.setCellStyle(style);
    }
    private CellStyle headStyle(Workbook workbook) {
        CellStyle style = baseStyle(workbook);
        style.setFillForegroundColor(IndexedColors.GREY_25_PERCENT.getIndex());
        style.setFillPattern(FillPatternType.SOLID_FOREGROUND);
        style.setAlignment(HorizontalAlignment.CENTER);
        Font font = workbook.createFont();
        font.setBold(true);
        style.setFont(font);
        return style;
    }
    private CellStyle textStyle(Workbook workbook) {
        CellStyle style = baseStyle(workbook);
        style.setAlignment(HorizontalAlignment.LEFT);
        return style;
    }
    private CellStyle numberStyle(Workbook workbook) {
        CellStyle style = baseStyle(workbook);
        style.setAlignment(HorizontalAlignment.RIGHT);
        style.setDataFormat(workbook.createDataFormat().getFormat("#,##0.000"));
        return style;
    }
    private CellStyle baseStyle(Workbook workbook) {
        CellStyle style = workbook.createCellStyle();
        style.setBorderTop(BorderStyle.THIN);
        style.setBorderBottom(BorderStyle.THIN);
        style.setBorderLeft(BorderStyle.THIN);
        style.setBorderRight(BorderStyle.THIN);
        style.setVerticalAlignment(VerticalAlignment.CENTER);
        return style;
    }
}
src/main/java/com/ruoyi/energymeter/dto/DailyUsageRow.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * èšåˆæŸ¥è¯¢ï¼šæŒ‰æ—¥ç”¨é‡ã€‚
 */
@Data
public class DailyUsageRow implements Serializable {
    private static final long serialVersionUID = 1L;
    private String day;
    private BigDecimal total;
}
src/main/java/com/ruoyi/energymeter/dto/DataRangeRow.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDateTime;
/**
 * å·²é‡‡é›†æ•°æ®çš„æ—¶é—´èŒƒå›´ã€‚
 */
@Data
public class DataRangeRow implements Serializable {
    private static final long serialVersionUID = 1L;
    private LocalDateTime minTime;
    private LocalDateTime maxTime;
}
src/main/java/com/ruoyi/energymeter/dto/EnergyMeterDeviceQuery.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.ruoyi.energymeter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
 * è®¾å¤‡åˆ—表筛选条件。
 */
@Data
@Schema(description = "设备列表筛选")
public class EnergyMeterDeviceQuery implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "能源类型 WATER/ELECTRIC/GAS")
    private String meterType;
    @Schema(description = "数据来源 PROVIDER第三方接入/MANUAL手工抄表")
    private String sourceType;
    @Schema(description = "是否演示自动生成 1是 0否")
    private Integer simulated;
    @Schema(description = "采集方式 AUTO自动采集 / MANUAL手动抄表")
    private String collectionMode;
    @Schema(description = "名称或通信地址关键字")
    private String keyword;
    @Schema(description = "通信地址精确匹配")
    private String meterAddress;
    @Schema(description = "启停用 1/0")
    private Integer enabled;
    @Schema(description = "绑定状态 VERIFIED/INVALID")
    private String bindStatus;
}
src/main/java/com/ruoyi/energymeter/dto/MeterArchive.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
/**
 * ç¬¬ä¸‰æ–¹è¡¨è®¡æ¡£æ¡ˆï¼ˆåªä¿ç•™æœ¬ç³»ç»Ÿéœ€è¦çš„字段,绝不包含第三方 Password)。
 */
@Data
public class MeterArchive implements Serializable {
    private static final long serialVersionUID = 1L;
    /** èƒ½æºç±»åž‹ã€‚ */
    private String meterType;
    /** é€šä¿¡åœ°å€ï¼Œç¬¬ä¸‰æ–¹è¿”回原值。 */
    private String meterAddress;
    /** ç¬¬ä¸‰æ–¹è®¾å¤‡ID。 */
    private String providerMeterId;
    /** ç¬¬ä¸‰æ–¹è®¾å¤‡åç§°ã€‚ */
    private String providerName;
    /** ç¬¬ä¸‰æ–¹åŒºåŸŸID。 */
    private Long areaId;
    /** ç¬¬ä¸‰æ–¹åŒºåŸŸåç§°ã€‚ */
    private String areaName;
    /** åœ¨çº¿çŠ¶æ€ï¼š1在线 0离线 null未知。 */
    private Integer onlineStatus;
    /** ç¬¬ä¸‰æ–¹æ€»è¡¨ID。 */
    private String parentProviderId;
    /** ç¬¬ä¸‰æ–¹åŽŸå§‹çŠ¶æ€æ–‡æœ¬ï¼Œä»…ç”¨äºŽå±•ç¤ºã€‚ */
    private String stateText;
}
src/main/java/com/ruoyi/energymeter/dto/MeterBindRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.energymeter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
 * æ–°å¢žæœ¬åœ°ç»‘定请求。
 */
@Data
@Schema(description = "新增绑定请求")
public class MeterBindRequest implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "能源类型 WATER/ELECTRIC", requiredMode = Schema.RequiredMode.REQUIRED)
    private String meterType;
    @Schema(description = "第三方通信地址", requiredMode = Schema.RequiredMode.REQUIRED)
    private String meterAddress;
    @Schema(description = "本系统显示名称,为空时使用第三方名称")
    private String displayName;
    @Schema(description = "安装位置")
    private String location;
}
src/main/java/com/ruoyi/energymeter/dto/MeterCollectRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,36 @@
package com.ruoyi.energymeter.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.util.List;
/**
 * æ‰‹åŠ¨æŸ¥è¡¨è¯·æ±‚ï¼šå¯æŒ‡å®šå•å°æˆ–æ‰¹é‡è®¾å¤‡ï¼Œèµ·æ­¢æ—¥æœŸå‡åŒ…å«ã€‚
 */
@Data
@Schema(description = "手动查表请求")
public class MeterCollectRequest implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "单台设备ID")
    private Long meterId;
    @Schema(description = "批量设备ID列表")
    private List<Long> meterIds;
    @Schema(description = "开始日期(含)")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate startDate;
    @Schema(description = "结束日期(含)")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate endDate;
    @Schema(description = "是否包含时点示值任务,默认 true")
    private Boolean includeReadings;
}
src/main/java/com/ruoyi/energymeter/dto/MeterManualBindRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,38 @@
package com.ruoyi.energymeter.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * æ–°å¢žæ‰‹å·¥æŠ„表设备请求(水表/电表/气表均可)。
 */
@Data
@Schema(description = "新增手工抄表设备请求")
public class MeterManualBindRequest implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "能源类型 WATER/ELECTRIC/GAS", requiredMode = Schema.RequiredMode.REQUIRED)
    private String meterType;
    @Schema(description = "表号(通信地址),按字符串保存,保留前导零", requiredMode = Schema.RequiredMode.REQUIRED)
    private String meterAddress;
    @Schema(description = "本系统显示名称")
    private String displayName;
    @Schema(description = "安装位置")
    private String location;
    @Schema(description = "起始表码", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal initialReading;
    @Schema(description = "起始表码日期,默认今天")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate initialReadingDate;
}
src/main/java/com/ruoyi/energymeter/dto/MeterQuarterPoint.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ç¬¬ä¸‰æ–¹ 15 åˆ†é’Ÿç²’度电量数据点。
 *
 * <p>active / reactive æ˜¯è¿™ä¸€æ—¶é—´ç‰‡å†…的电量(kWh / kvarh),用于绘制 15 åˆ†é’ŸæŸ±çŠ¶å›¾ï¼›
 * activeReading / reactiveReading æ˜¯æ—¶ç‚¹ç´¯è®¡è¡¨ç ï¼Œåªç”¨äºŽæŽ’查,不参与求和。</p>
 */
@Data
public class MeterQuarterPoint implements Serializable {
    private static final long serialVersionUID = 1L;
    /** æ—¶é—´ç‰‡ç»“束时间,例如 2026-11-04 17:45。 */
    private LocalDateTime time;
    /** æ­£å‘有功电量(本时间片)。 */
    private BigDecimal active;
    /** æ­£å‘无功电量(本时间片)。 */
    private BigDecimal reactive;
    /** æ­£å‘有功总表码。 */
    private BigDecimal activeReading;
    /** æ­£å‘无功总表码。 */
    private BigDecimal reactiveReading;
    /** ç¬¬ä¸‰æ–¹è¿”回的通信地址。 */
    private String address;
    /** åŽŸå§‹æ—¶é—´æ–‡æœ¬ï¼Œä¾¿äºŽæŽ’æŸ¥ã€‚ */
    private String rawTime;
}
src/main/java/com/ruoyi/energymeter/dto/MeterReadingPoint.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,31 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ç¬¬ä¸‰æ–¹æ—¶ç‚¹ç¤ºå€¼æ•°æ®ç‚¹ã€‚
 */
@Data
public class MeterReadingPoint implements Serializable {
    private static final long serialVersionUID = 1L;
    /** ç¤ºå€¼æ—¶ç‚¹ã€‚ */
    private LocalDateTime time;
    /** ç´¯è®¡è¡¨ç ã€‚ */
    private BigDecimal reading;
    /** ç¬¬ä¸‰æ–¹åŒæ—¶è¿”回的期间用量,仅用于口径核对。 */
    private BigDecimal usage;
    /** ç¬¬ä¸‰æ–¹è¿”回的通信地址。 */
    private String address;
    /** åŽŸå§‹æ—¶é—´æ–‡æœ¬ã€‚ */
    private String rawTime;
}
src/main/java/com/ruoyi/energymeter/dto/MeterReadingRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,29 @@
package com.ruoyi.energymeter.dto;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
/**
 * æ‰‹å·¥å½•表请求。
 */
@Data
@Schema(description = "手工录表请求")
public class MeterReadingRequest implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "抄表日期(业务日期)", requiredMode = Schema.RequiredMode.REQUIRED)
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate readingDate;
    @Schema(description = "本次表码", requiredMode = Schema.RequiredMode.REQUIRED)
    private BigDecimal readingValue;
    @Schema(description = "抄表备注")
    private String remark;
}
src/main/java/com/ruoyi/energymeter/dto/MeterUpdateRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,23 @@
package com.ruoyi.energymeter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
 * ä¿®æ”¹æœ¬åœ°å±•示字段请求;不修改通信地址与第三方档案字段。
 */
@Data
@Schema(description = "修改设备本地字段请求")
public class MeterUpdateRequest implements Serializable {
    private static final long serialVersionUID = 1L;
    private String displayName;
    private String location;
    @Schema(description = "启停用:1启用 0停用,为空表示不修改")
    private Integer enabled;
}
src/main/java/com/ruoyi/energymeter/dto/MeterUsagePoint.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * ç¬¬ä¸‰æ–¹æœŸé—´ç”¨é‡æ•°æ®ç‚¹ã€‚
 */
@Data
public class MeterUsagePoint implements Serializable {
    private static final long serialVersionUID = 1L;
    /** ä¸šåŠ¡æ—¶é—´ï¼ˆæ—¥ç”¨é‡å½’ä¸€ä¸ºå½“å¤© 00:00)。 */
    private LocalDateTime time;
    /** æœŸé—´ç”¨é‡ã€‚ */
    private BigDecimal value;
    /** ç¬¬ä¸‰æ–¹è¿”回的通信地址。 */
    private String address;
    /** åŽŸå§‹æ—¶é—´æ–‡æœ¬ï¼Œä¾¿äºŽæŽ’æŸ¥ã€‚ */
    private String rawTime;
}
src/main/java/com/ruoyi/energymeter/dto/MeterUsageRow.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * èšåˆæŸ¥è¯¢ï¼šæŒ‰è®¾å¤‡æ±‡æ€»ç”¨é‡ã€‚
 */
@Data
public class MeterUsageRow implements Serializable {
    private static final long serialVersionUID = 1L;
    private Long meterId;
    private BigDecimal total;
}
src/main/java/com/ruoyi/energymeter/dto/MeterVerifyRequest.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.energymeter.dto;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
 * éªŒè¯å¹¶è¯»å–第三方档案的请求。
 */
@Data
@Schema(description = "档案验证请求")
public class MeterVerifyRequest implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "能源类型 WATER/ELECTRIC", requiredMode = Schema.RequiredMode.REQUIRED)
    private String meterType;
    @Schema(description = "第三方通信地址", requiredMode = Schema.RequiredMode.REQUIRED)
    private String meterAddress;
}
src/main/java/com/ruoyi/energymeter/dto/MonthlyUsageRow.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
/**
 * èšåˆæŸ¥è¯¢ï¼šæŒ‰æœˆç”¨é‡ã€‚
 */
@Data
public class MonthlyUsageRow implements Serializable {
    private static final long serialVersionUID = 1L;
    private String month;
    private BigDecimal total;
}
src/main/java/com/ruoyi/energymeter/dto/TaskStatusCount.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.energymeter.dto;
import lombok.Data;
import java.io.Serializable;
/**
 * æ‰¹æ¬¡å†…任务状态统计。
 */
@Data
public class TaskStatusCount implements Serializable {
    private static final long serialVersionUID = 1L;
    private Integer total;
    private Integer success;
    private Integer empty;
    private Integer failed;
    private Integer retry;
    private Integer pending;
    private Integer running;
}
src/main/java/com/ruoyi/energymeter/job/EnergyMeterDailySyncJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package com.ruoyi.energymeter.job;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.energymeter.service.EnergyMeterDemoService;
import com.ruoyi.energymeter.service.EnergyMeterSyncService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * æ¯å¤© 08:00(Asia/Shanghai)自动同步作业。
 *
 * <p>禁止并发执行;错过触发时按 FireAndProceed è¡¥è§¦å‘一次,缺口由进度表逐日补齐。</p>
 */
@DisallowConcurrentExecution
public class EnergyMeterDailySyncJob implements Job {
    private static final Logger log = LoggerFactory.getLogger(EnergyMeterDailySyncJob.class);
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        Exception syncError = null;
        try {
            EnergyMeterSyncService syncService = SpringUtils.getBean(EnergyMeterSyncService.class);
            Long runId = syncService.triggerScheduledSync();
            log.info("水电表 08:00 è‡ªåŠ¨åŒæ­¥å®Œæˆ runId={}", runId);
        } catch (Exception e) {
            syncError = e;
            log.error("水电表 08:00 è‡ªåŠ¨åŒæ­¥å¤±è´¥", e);
        }
        // æ°”表演示设备与真实同步互不影响:即使第三方同步失败,也照常生成前一天的数据
        try {
            EnergyMeterDemoService demoService = SpringUtils.getBean(EnergyMeterDemoService.class);
            int generated = demoService.generateGasDemo(EnergyMeterSupport.today().minusDays(1));
            log.info("气表演示数据生成完成 è®¾å¤‡æ•°={}", generated);
        } catch (Exception e) {
            log.error("气表演示数据生成失败", e);
        }
        if (syncError != null) {
            throw new JobExecutionException(syncError, false);
        }
    }
}
src/main/java/com/ruoyi/energymeter/job/EnergyMeterRecoveryJob.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.energymeter.job;
import com.ruoyi.common.utils.spring.SpringUtils;
import com.ruoyi.energymeter.service.EnergyMeterSyncService;
import org.quartz.DisallowConcurrentExecution;
import org.quartz.Job;
import org.quartz.JobExecutionContext;
import org.quartz.JobExecutionException;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
 * æ¯ 10 åˆ†é’Ÿæ‰§è¡Œä¸€æ¬¡ï¼šå¤„理到期重试任务与超时 RUNNING ä»»åŠ¡ã€‚
 */
@DisallowConcurrentExecution
public class EnergyMeterRecoveryJob implements Job {
    private static final Logger log = LoggerFactory.getLogger(EnergyMeterRecoveryJob.class);
    @Override
    public void execute(JobExecutionContext context) throws JobExecutionException {
        try {
            EnergyMeterSyncService syncService = SpringUtils.getBean(EnergyMeterSyncService.class);
            int handled = syncService.recoverDueTasks();
            if (handled > 0) {
                log.info("水电表同步恢复作业完成,处理任务数={}", handled);
            }
        } catch (Exception e) {
            log.error("水电表同步恢复作业失败", e);
            throw new JobExecutionException(e, false);
        }
    }
}
src/main/java/com/ruoyi/energymeter/job/EnergyMeterSchedulerRegistrar.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
package com.ruoyi.energymeter.job;
import com.ruoyi.energymeter.config.EnergyMeterProperties;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.quartz.CronScheduleBuilder;
import org.quartz.CronTrigger;
import org.quartz.Job;
import org.quartz.JobBuilder;
import org.quartz.JobDetail;
import org.quartz.JobKey;
import org.quartz.Scheduler;
import org.quartz.SchedulerException;
import org.quartz.Trigger;
import org.quartz.TriggerBuilder;
import org.quartz.TriggerKey;
import org.springframework.boot.ApplicationArguments;
import org.springframework.boot.ApplicationRunner;
import org.springframework.stereotype.Component;
import java.util.TimeZone;
/**
 * å¯åŠ¨æ—¶ä»¥å›ºå®š JobKey/TriggerKey æ³¨å†Œæˆ–更新两个 Quartz ä½œä¸šï¼Œé‡å¤å¯åŠ¨ä¸ä¼šäº§ç”Ÿé‡å¤è§¦å‘å™¨ã€‚
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class EnergyMeterSchedulerRegistrar implements ApplicationRunner {
    private static final String GROUP = "energyMeter";
    private static final String TIME_ZONE = "Asia/Shanghai";
    private final Scheduler scheduler;
    private final EnergyMeterProperties properties;
    @Override
    public void run(ApplicationArguments args) {
        if (!properties.isSchedulerEnabled()) {
            log.info("水电表定时任务已关闭(energy-meter.scheduler-enabled=false)");
            return;
        }
        try {
            // æ¸…理已下线的电表整点同步作业,避免持久化 Quartz å­˜å‚¨é‡Œçš„æ—§è§¦å‘器继续按小时触发
            removeIfExists("energyMeterElectricHourlySync", "energyMeterElectricHourlySyncTrigger");
            register("energyMeterDailySync", "energyMeterDailySyncTrigger",
                    EnergyMeterDailySyncJob.class, properties.getSyncCron(), "水电表每天08:00自动同步");
            register("energyMeterRecovery", "energyMeterRecoveryTrigger",
                    EnergyMeterRecoveryJob.class, properties.getRecoveryCron(), "水电表每10分钟恢复重试");
        } catch (Exception e) {
            log.error("注册水电表定时任务失败", e);
        }
    }
    private void removeIfExists(String jobName, String triggerName) throws SchedulerException {
        TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, GROUP);
        if (scheduler.checkExists(triggerKey)) {
            scheduler.unscheduleJob(triggerKey);
            log.info("已移除下线的水电表定时任务触发器 {}", triggerName);
        }
        JobKey jobKey = JobKey.jobKey(jobName, GROUP);
        if (scheduler.checkExists(jobKey)) {
            scheduler.deleteJob(jobKey);
            log.info("已移除下线的水电表定时任务 {}", jobName);
        }
    }
    private void register(String jobName, String triggerName, Class<? extends Job> jobClass,
                          String cron, String description) throws SchedulerException {
        JobKey jobKey = JobKey.jobKey(jobName, GROUP);
        TriggerKey triggerKey = TriggerKey.triggerKey(triggerName, GROUP);
        if (!scheduler.checkExists(jobKey)) {
            JobDetail jobDetail = JobBuilder.newJob(jobClass)
                    .withIdentity(jobKey)
                    .withDescription(description)
                    .storeDurably()
                    .requestRecovery()
                    .build();
            Trigger trigger = buildTrigger(triggerKey, jobKey, cron, description);
            scheduler.scheduleJob(jobDetail, trigger);
            log.info("已注册水电表定时任务 {}({},{})", jobName, cron, TIME_ZONE);
            return;
        }
        Trigger existing = scheduler.getTrigger(triggerKey);
        boolean sameCron = existing instanceof CronTrigger cronTrigger
                && cron.equals(cronTrigger.getCronExpression());
        Trigger.TriggerState state = existing == null
                ? Trigger.TriggerState.NONE : scheduler.getTriggerState(triggerKey);
        // ç‰ˆæœ¬å‡çº§åŽæ—§å®žä¾‹å¯èƒ½ä¸è®¤è¯†æ–° Job ç±»è€ŒæŠŠè§¦å‘器置为 ERROR,这里重建触发器使其恢复调度
        if (!sameCron || state == Trigger.TriggerState.ERROR) {
            Trigger trigger = buildTrigger(triggerKey, jobKey, cron, description);
            if (existing != null) {
                scheduler.unscheduleJob(triggerKey);
            }
            scheduler.scheduleJob(trigger);
            log.info("已重建水电表定时任务 {} çš„触发器(cron={},原状态={})", jobName, cron, state);
        } else {
            log.info("水电表定时任务 {} å·²å­˜åœ¨ä¸”规则一致,跳过重复注册", jobName);
        }
    }
    private Trigger buildTrigger(TriggerKey triggerKey, JobKey jobKey, String cron, String description) {
        return TriggerBuilder.newTrigger()
                .withIdentity(triggerKey)
                .withDescription(description)
                .forJob(jobKey)
                .withSchedule(CronScheduleBuilder.cronSchedule(cron)
                        .inTimeZone(TimeZone.getTimeZone(TIME_ZONE))
                        .withMisfireHandlingInstructionFireAndProceed())
                .build();
    }
}
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterDeviceMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package com.ruoyi.energymeter.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.List;
/**
 * æ°´ç”µè¡¨ç»‘定设备 Mapper。
 */
@Mapper
public interface EnergyMeterDeviceMapper extends BaseMapper<EnergyMeterDevice> {
    /**
     * æ‰«æå…¨éƒ¨ç§Ÿæˆ·ä¸‹å·²å¯ç”¨ã€å·²éªŒè¯ã€ä¸”为第三方接入的设备,用于系统级自动同步。
     * æ‰‹å·¥æŠ„表设备由用户录表,不参与定时采集。
     */
    @Select("SELECT * FROM energy_meter_device WHERE enabled = 1 AND bind_status = 'VERIFIED' "
            + "AND source_type = 'PROVIDER' ORDER BY id")
    List<EnergyMeterDevice> selectScannableDevices();
    /**
     * è®°å½•采集成功并清空上次错误。
     */
    @Update("UPDATE energy_meter_device SET last_success_at = #{now}, last_error_code = NULL, "
            + "last_error_message = NULL WHERE id = #{id}")
    int markCollectSuccess(@Param("id") Long id, @Param("now") LocalDateTime now);
    /**
     * è®°å½•采集失败的脱敏错误类别与摘要。
     */
    @Update("UPDATE energy_meter_device SET last_error_code = #{code}, last_error_message = #{message} WHERE id = #{id}")
    int markCollectFailure(@Param("id") Long id, @Param("code") String code, @Param("message") String message);
}
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterMeasurementMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,114 @@
package com.ruoyi.energymeter.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.energymeter.dto.DailyUsageRow;
import com.ruoyi.energymeter.dto.DataRangeRow;
import com.ruoyi.energymeter.dto.MeterUsageRow;
import com.ruoyi.energymeter.dto.MonthlyUsageRow;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
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.LocalDateTime;
import java.util.Collection;
import java.util.List;
/**
 * æ°´ç”µè¡¨é‡‡é›†æ•°æ® Mapper。
 */
@Mapper
public interface EnergyMeterMeasurementMapper extends BaseMapper<EnergyMeterMeasurement> {
    /**
     * æŒ‰æ—¥æ±‡æ€»æœŸé—´ç”¨é‡ã€‚只统计 D ç²’度的用量指标,绝不累加 POINT è¡¨ç ã€‚
     */
    List<DailyUsageRow> sumDailyUsage(@Param("meterIds") Collection<Long> meterIds,
                                      @Param("metricCode") String metricCode,
                                      @Param("start") LocalDateTime start,
                                      @Param("end") LocalDateTime end);
    /**
     * æŒ‰è®¾å¤‡æ±‡æ€»ä¸€æ®µæ—¶é—´å†…的用量,用于设备列表展示最近一天用量。
     */
    List<MeterUsageRow> sumUsageByMeter(@Param("meterIds") Collection<Long> meterIds,
                                        @Param("metricCode") String metricCode,
                                        @Param("start") LocalDateTime start,
                                        @Param("end") LocalDateTime end);
    /**
     * æŒ‰æœˆæ±‡æ€»æœŸé—´ç”¨é‡ã€‚
     */
    List<MonthlyUsageRow> sumMonthlyUsage(@Param("meterIds") Collection<Long> meterIds,
                                          @Param("metricCode") String metricCode,
                                          @Param("start") LocalDateTime start,
                                          @Param("end") LocalDateTime end);
    /**
     * æŸ¥è¯¢æŒ‡å®šè®¾å¤‡ã€æŒ‡æ ‡ã€ç²’度在时间区间内已存在的采集点,用于幂等比对。
     */
    List<EnergyMeterMeasurement> selectExistingPoints(@Param("meterId") Long meterId,
                                                       @Param("metricCode") String metricCode,
                                                       @Param("granularity") String granularity,
                                                       @Param("start") LocalDateTime start,
                                                       @Param("end") LocalDateTime end);
    /**
     * æ‰¹é‡å¹‚等写入,唯一键冲突时更新指标值与采集时间。
     */
    int upsertBatch(@Param("list") List<EnergyMeterMeasurement> list);
    /**
     * æŸ¥è¯¢å·²é‡‡é›†æ•°æ®çš„æ—¶é—´èŒƒå›´ã€‚
     */
    DataRangeRow selectDataRange(@Param("meterIds") Collection<Long> meterIds,
                                 @Param("metricCode") String metricCode);
    /**
     * åŒºé—´åˆè®¡ã€‚
     */
    BigDecimal sumTotal(@Param("meterIds") Collection<Long> meterIds,
                        @Param("metricCode") String metricCode,
                        @Param("start") LocalDateTime start,
                        @Param("end") LocalDateTime end);
    @Select("SELECT * FROM energy_meter_measurement WHERE meter_id = #{meterId} AND metric_code = #{metricCode} "
            + "ORDER BY measure_time DESC LIMIT 1")
    EnergyMeterMeasurement selectLatestPoint(@Param("meterId") Long meterId,
                                             @Param("metricCode") String metricCode);
    /**
     * æŸ¥è¯¢æŒ‡å®šæ—¶é—´ç‚¹ä¹‹å‰æœ€è¿‘的一条时点示值(手工抄表用于计算期差)。
     */
    @Select("SELECT * FROM energy_meter_measurement WHERE meter_id = #{meterId} AND metric_code = #{metricCode} "
            + "AND granularity = 'POINT' AND measure_time < #{time} ORDER BY measure_time DESC LIMIT 1")
    EnergyMeterMeasurement selectPreviousPoint(@Param("meterId") Long meterId,
                                               @Param("metricCode") String metricCode,
                                               @Param("time") LocalDateTime time);
    /**
     * æŸ¥è¯¢æŒ‡å®šæ—¶é—´ç‚¹ä¹‹åŽæœ€è¿‘的一条时点示值(补录/删除后重算下一期用量)。
     */
    @Select("SELECT * FROM energy_meter_measurement WHERE meter_id = #{meterId} AND metric_code = #{metricCode} "
            + "AND granularity = 'POINT' AND measure_time > #{time} ORDER BY measure_time ASC LIMIT 1")
    EnergyMeterMeasurement selectNextPoint(@Param("meterId") Long meterId,
                                           @Param("metricCode") String metricCode,
                                           @Param("time") LocalDateTime time);
    @Select("SELECT COUNT(DISTINCT DATE(measure_time)) FROM energy_meter_measurement "
            + "WHERE meter_id = #{meterId} AND metric_code = #{metricCode} AND granularity = 'D' "
            + "AND measure_time >= #{start} AND measure_time < #{end}")
    Integer countCollectedDays(@Param("meterId") Long meterId,
                               @Param("metricCode") String metricCode,
                               @Param("start") LocalDateTime start,
                               @Param("end") LocalDateTime end);
    /**
     * æŒ‡å®šæœŸé—´å†…已有日用量数据的设备台数,用于采集页展示“今日已归集”。
     */
    Integer countMetersWithUsage(@Param("meterIds") Collection<Long> meterIds,
                                 @Param("metricCode") String metricCode,
                                 @Param("start") LocalDateTime start,
                                 @Param("end") LocalDateTime end);
}
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterSyncCheckpointMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.energymeter.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncCheckpoint;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
/**
 * åŒæ­¥è¿›åº¦ Mapper。
 */
@Mapper
public interface EnergyMeterSyncCheckpointMapper extends BaseMapper<EnergyMeterSyncCheckpoint> {
    @Select("SELECT * FROM energy_meter_sync_checkpoint WHERE meter_id = #{meterId} "
            + "AND source_api = #{sourceApi} AND granularity = #{granularity} LIMIT 1")
    EnergyMeterSyncCheckpoint selectUnit(@Param("meterId") Long meterId,
                                         @Param("sourceApi") String sourceApi,
                                         @Param("granularity") String granularity);
    /**
     * å¹‚等写入进度,只允许前进,不因最近几天回查而后退。
     */
    int upsert(@Param("cp") EnergyMeterSyncCheckpoint cp);
}
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterSyncRunMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.energymeter.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncRun;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
/**
 * åŒæ­¥æ‰¹æ¬¡ Mapper。
 */
@Mapper
public interface EnergyMeterSyncRunMapper extends BaseMapper<EnergyMeterSyncRun> {
    @Select("SELECT * FROM energy_meter_sync_run WHERE run_key = #{runKey} LIMIT 1")
    EnergyMeterSyncRun selectByRunKey(@Param("runKey") String runKey);
    /**
     * æ±‡æ€»æ‰¹æ¬¡ä»»åŠ¡çŠ¶æ€å¹¶å›žå†™ç»Ÿè®¡å­—æ®µã€‚
     */
    int refreshTotals(@Param("runId") Long runId);
    /**
     * æŸ¥è¯¢é•¿æ—¶é—´åœåœ¨ RUNNING、但已没有待执行任务的批次,用于崩溃后兜底汇总。
     */
    @Select("SELECT r.* FROM energy_meter_sync_run r WHERE r.status = 'RUNNING' AND r.create_time < #{deadline} "
            + "AND NOT EXISTS (SELECT 1 FROM energy_meter_sync_task t WHERE t.run_id = r.id "
            + "AND t.status IN ('PENDING', 'RUNNING', 'RETRY')) ORDER BY r.id LIMIT #{limit}")
    List<EnergyMeterSyncRun> selectStuckRuns(@Param("deadline") LocalDateTime deadline,
                                             @Param("limit") int limit);
}
src/main/java/com/ruoyi/energymeter/mapper/EnergyMeterSyncTaskMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,78 @@
package com.ruoyi.energymeter.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.energymeter.dto.TaskStatusCount;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncTask;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;
import java.time.LocalDateTime;
import java.util.Collection;
import java.util.List;
/**
 * å•设备同步任务 Mapper。
 */
@Mapper
public interface EnergyMeterSyncTaskMapper extends BaseMapper<EnergyMeterSyncTask> {
    /**
     * æŒ‰å”¯ä¸€é”®å¹‚等插入任务。
     */
    int insertIgnore(@Param("task") EnergyMeterSyncTask task);
    /**
     * æ¡ä»¶é¢†å–任务:只有把状态从 PENDING/RETRY æ›´æ–°ä¸º RUNNING æˆåŠŸçš„æ‰§è¡Œè€…æ‰å‘è¯·æ±‚ã€‚
     */
    int claim(@Param("id") Long id, @Param("now") LocalDateTime now);
    /**
     * åˆ°æœŸå¾…重试任务。
     */
    List<EnergyMeterSyncTask> selectDueRetry(@Param("now") LocalDateTime now, @Param("limit") int limit);
    /**
     * è¶…时仍处于 RUNNING çš„任务。
     */
    List<EnergyMeterSyncTask> selectStaleRunning(@Param("deadline") LocalDateTime deadline, @Param("limit") int limit);
    /**
     * æŠŠè¶…时任务重置为待重试。
     */
    int resetToRetry(@Param("id") Long id,
                     @Param("now") LocalDateTime now,
                     @Param("nextRetryAt") LocalDateTime nextRetryAt,
                     @Param("reason") String reason);
    List<EnergyMeterSyncTask> selectByRunId(@Param("runId") Long runId);
    List<EnergyMeterSyncTask> selectLatestByMeterIds(@Param("meterIds") Collection<Long> meterIds);
    TaskStatusCount countByRunId(@Param("runId") Long runId);
    /**
     * æ ‡è®°ä»»åŠ¡æˆåŠŸ/空结果,并清空错误与重试信息。
     */
    @Update("UPDATE energy_meter_sync_task SET status = #{status}, fetched_count = #{fetched}, "
            + "inserted_count = #{inserted}, updated_count = #{updated}, finished_at = #{now}, "
            + "error_code = NULL, error_message = NULL, next_retry_at = NULL WHERE id = #{id}")
    int markFinished(@Param("id") Long id,
                     @Param("status") String status,
                     @Param("fetched") int fetched,
                     @Param("inserted") int inserted,
                     @Param("updated") int updated,
                     @Param("now") LocalDateTime now);
    /**
     * æ ‡è®°ä»»åŠ¡å¤±è´¥æˆ–è½¬å…¥é‡è¯•ã€‚
     */
    @Update("UPDATE energy_meter_sync_task SET status = #{status}, error_code = #{code}, "
            + "error_message = #{message}, next_retry_at = #{nextRetryAt}, finished_at = #{now} WHERE id = #{id}")
    int markFailed(@Param("id") Long id,
                   @Param("status") String status,
                   @Param("code") String code,
                   @Param("message") String message,
                   @Param("nextRetryAt") LocalDateTime nextRetryAt,
                   @Param("now") LocalDateTime now);
}
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterDevice.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,164 @@
package com.ruoyi.energymeter.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * æ°´ç”µè¡¨æœ¬åœ°ç»‘定档案。
 */
@Data
@TableName("energy_meter_device")
@Schema(description = "水电表绑定设备")
public class EnergyMeterDevice implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "租户ID")
    @TableField(fill = FieldFill.INSERT)
    private Long tenantId;
    @Schema(description = "能源类型 WATER/ELECTRIC/GAS")
    private String meterType;
    @Schema(description = "数据来源 PROVIDER第三方接入/MANUAL手工抄表")
    private String sourceType;
    @Schema(description = "第三方通信地址或表号,保留前导零")
    private String meterAddress;
    @Schema(description = "第三方设备ID")
    private String providerMeterId;
    @Schema(description = "第三方设备名称")
    private String providerName;
    @Schema(description = "本系统显示名称")
    private String displayName;
    @Schema(description = "第三方区域ID")
    private Long providerAreaId;
    @Schema(description = "第三方区域名称")
    private String providerAreaName;
    @Schema(description = "第三方总表ID")
    private String parentProviderId;
    @Schema(description = "本地总表ID")
    private Long parentMeterId;
    @Schema(description = "安装位置")
    private String location;
    @Schema(description = "在线状态 1在线 0离线")
    private Integer onlineStatus;
    @Schema(description = "绑定状态 VERIFIED/INVALID")
    private String bindStatus;
    @Schema(description = "是否启用 1启用 0停用")
    private Integer enabled;
    @Schema(description = "是否初始化设备 1是(禁止删除)0否")
    private Integer isSeed;
    @Schema(description = "是否演示数据 1是(每日同步时生成模拟用量)0否")
    private Integer simulated;
    @Schema(description = "手工抄表起始表码")
    private BigDecimal initialReading;
    @Schema(description = "手工抄表起始表码日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate initialReadingDate;
    @Schema(description = "最早需要同步的业务日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate syncStartDate;
    @Schema(description = "最近成功采集时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime lastSuccessAt;
    @Schema(description = "最近采集错误类别")
    private String lastErrorCode;
    @Schema(description = "最近采集错误摘要")
    private String lastErrorMessage;
    @TableField(fill = FieldFill.INSERT)
    private Long createUser;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    private Long updateUser;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    /* ---------------- åˆ—表展示用非持久化字段 ---------------- */
    @Schema(description = "最近一次任务状态")
    @TableField(exist = false)
    private String lastTaskStatus;
    @Schema(description = "最近一次任务时间")
    @TableField(exist = false)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime lastTaskTime;
    @Schema(description = "最近一天用量")
    @TableField(exist = false)
    private BigDecimal lastDayUsage;
    @Schema(description = "最新表码读数")
    @TableField(exist = false)
    private BigDecimal latestReading;
    @Schema(description = "上期表码读数")
    @TableField(exist = false)
    private BigDecimal previousReading;
    @Schema(description = "最近一天用量的业务日期")
    @TableField(exist = false)
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate lastDayUsageDate;
    @Schema(description = "采集状态说明")
    @TableField(exist = false)
    private String collectStateText;
    @Schema(description = "能源类型中文名")
    @TableField(exist = false)
    private String meterTypeName;
    @Schema(description = "数据来源中文名")
    @TableField(exist = false)
    private String sourceTypeName;
    @Schema(description = "指标单位")
    @TableField(exist = false)
    private String unit;
    @Schema(description = "是否允许删除")
    @TableField(exist = false)
    private Boolean deletable;
}
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterMeasurement.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package com.ruoyi.energymeter.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * æ°´ç”µè¡¨ç”¨é‡ã€ç¤ºå€¼ä¸Žå·²éªŒè¯è´¹ç”¨ã€‚
 */
@Data
@TableName("energy_meter_measurement")
@Schema(description = "水电表采集数据")
public class EnergyMeterMeasurement implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "租户ID")
    private Long tenantId;
    @Schema(description = "本地设备ID")
    private Long meterId;
    @Schema(description = "指标编码")
    private String metricCode;
    @Schema(description = "粒度 D/POINT")
    private String granularity;
    @Schema(description = "期间起点或示值时点")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime measureTime;
    @Schema(description = "指标值")
    private BigDecimal metricValue;
    @Schema(description = "单位")
    private String unit;
    @Schema(description = "第三方来源接口标识")
    private String sourceApi;
    @Schema(description = "第三方原始时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime sourceRecordTime;
    @Schema(description = "采集时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime collectedAt;
    @Schema(description = "备注:手工抄表说明")
    private String remark;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    /* ---------------- æ˜Žç»†å±•示用非持久化字段 ---------------- */
    @Schema(description = "设备名称")
    @TableField(exist = false)
    private String meterName;
    @Schema(description = "能源类型")
    @TableField(exist = false)
    private String meterType;
    @Schema(description = "通信地址")
    @TableField(exist = false)
    private String meterAddress;
    @Schema(description = "本条抄表对应的期间用量(仅抄表记录返回)")
    @TableField(exist = false)
    private BigDecimal usageValue;
}
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterSyncCheckpoint.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,49 @@
package com.ruoyi.energymeter.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * æŒ‰è®¾å¤‡ã€ç¬¬ä¸‰æ–¹æŽ¥å£ã€ç²’度记录连续完成到的日期。
 */
@Data
@TableName("energy_meter_sync_checkpoint")
@Schema(description = "水电表同步进度")
public class EnergyMeterSyncCheckpoint implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private Long tenantId;
    private Long meterId;
    private String sourceApi;
    private String granularity;
    @Schema(description = "连续完成到的业务日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate lastCompleteDate;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime lastSuccessAt;
    private Long lastRunId;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterSyncRun.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package com.ruoyi.energymeter.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * ä¸€æ¬¡è‡ªåŠ¨ã€æ‰‹åŠ¨æˆ–åŽ†å²è¡¥é‡‡çš„åŒæ­¥æ‰¹æ¬¡ã€‚
 */
@Data
@TableName("energy_meter_sync_run")
@Schema(description = "水电表同步批次")
public class EnergyMeterSyncRun implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    @Schema(description = "批次唯一键")
    private String runKey;
    @Schema(description = "SCHEDULED/MANUAL/BACKFILL")
    private String triggerType;
    @Schema(description = "计划执行日")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate scheduleDate;
    @Schema(description = "同步开始日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate rangeStart;
    @Schema(description = "同步结束日期")
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate rangeEnd;
    @Schema(description = "批次状态")
    private String status;
    private Integer totalTasks;
    private Integer successTasks;
    private Integer emptyTasks;
    private Integer failedTasks;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startedAt;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime finishedAt;
    @Schema(description = "脱敏错误摘要")
    private String errorSummary;
    private Long createUser;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
}
src/main/java/com/ruoyi/energymeter/pojo/EnergyMeterSyncTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
package com.ruoyi.energymeter.pojo;
import com.baomidou.mybatisplus.annotation.FieldFill;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.time.LocalDate;
import java.time.LocalDateTime;
/**
 * ä¸€å°è®¾å¤‡ã€ä¸€ä¸ªç¬¬ä¸‰æ–¹æŽ¥å£ã€ä¸€ä¸ªæ—¶é—´åˆ†ç‰‡çš„æ‰§è¡ŒçŠ¶æ€ã€‚
 */
@Data
@TableName("energy_meter_sync_task")
@Schema(description = "水电表同步任务")
public class EnergyMeterSyncTask implements Serializable {
    private static final long serialVersionUID = 1L;
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    private Long runId;
    private Long tenantId;
    private Long meterId;
    @Schema(description = "第三方接口标识")
    private String sourceApi;
    @Schema(description = "粒度 D/POINT")
    private String granularity;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate rangeStart;
    @JsonFormat(pattern = "yyyy-MM-dd")
    private LocalDate rangeEnd;
    @Schema(description = "任务状态")
    private String status;
    private Integer attemptCount;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime nextRetryAt;
    private Integer fetchedCount;
    private Integer insertedCount;
    private Integer updatedCount;
    @Schema(description = "脱敏错误类别")
    private String errorCode;
    @Schema(description = "脱敏错误信息")
    private String errorMessage;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime startedAt;
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime finishedAt;
    @TableField(fill = FieldFill.INSERT)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;
    @TableField(fill = FieldFill.INSERT_UPDATE)
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    /* ---------------- å±•示用非持久化字段 ---------------- */
    @TableField(exist = false)
    private String meterName;
    @TableField(exist = false)
    private String meterType;
    @TableField(exist = false)
    private String meterAddress;
}
src/main/java/com/ruoyi/energymeter/provider/MeterProviderClient.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,52 @@
package com.ruoyi.energymeter.provider;
import com.ruoyi.energymeter.dto.MeterArchive;
import com.ruoyi.energymeter.dto.MeterQuarterPoint;
import com.ruoyi.energymeter.dto.MeterReadingPoint;
import com.ruoyi.energymeter.dto.MeterUsagePoint;
import java.time.LocalDate;
import java.util.List;
/**
 * ç¬¬ä¸‰æ–¹è¡¨è®¡æ•°æ®æºã€‚
 */
public interface MeterProviderClient {
    /**
     * æŸ¥è¯¢å¹¶ä¸¥æ ¼åŒ¹é…ä¸€æ¡è¡¨è®¡æ¡£æ¡ˆï¼›åŒ¹é…ä¸åˆ°æ—¶æŠ›å‡º NOT_FOUND。
     */
    MeterArchive queryArchive(String meterType, String address);
    /**
     * æŸ¥è¯¢æœŸé—´æ—¥ç”¨é‡ã€‚
     *
     * @param providerMeterId ç¬¬ä¸‰æ–¹è®¾å¤‡ID,气表用量接口要求传 ID è€Œä¸æ˜¯é€šä¿¡åœ°å€
     */
    List<MeterUsagePoint> queryDailyUsage(String meterType, String address, String providerMeterId,
                                         LocalDate start, LocalDate end);
    /**
     * æŸ¥è¯¢æ—¶ç‚¹ç¤ºå€¼ã€‚
     */
    List<MeterReadingPoint> queryReadings(String meterType, String address, String providerMeterId,
                                          LocalDate start, LocalDate end);
    /**
     * æŸ¥è¯¢ 15 åˆ†é’Ÿç²’度电量(当前仅电表支持,来自 ElectricQuantity çš„ mi15 æ•°æ®ï¼‰ã€‚
     *
     * <p>返回的时间是时间片结束时间;active / reactive ä¸ºè¯¥æ—¶é—´ç‰‡å†…的电量。</p>
     */
    List<MeterQuarterPoint> queryQuarterUsage(String meterType, String address, String providerMeterId,
                                              LocalDate start, LocalDate end);
    /**
     * æ˜¯å¦å¤„于演示数据模式。
     */
    boolean mockMode();
    /**
     * æ˜¯å¦å·²é…ç½®ç¬¬ä¸‰æ–¹æŽˆæƒç ï¼›æœªé…ç½®ä¸”非演示模式时不允许创建采集任务。
     */
    boolean hasApiKey();
}
src/main/java/com/ruoyi/energymeter/provider/MeterProviderException.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,25 @@
package com.ruoyi.energymeter.provider;
/**
 * ç¬¬ä¸‰æ–¹æŽ¥å£è°ƒç”¨å¼‚常,携带可入库的脱敏错误类别。
 */
public class MeterProviderException extends RuntimeException {
    private static final long serialVersionUID = 1L;
    private final String code;
    public MeterProviderException(String code, String message) {
        super(message);
        this.code = code;
    }
    public MeterProviderException(String code, String message, Throwable cause) {
        super(message, cause);
        this.code = code;
    }
    public String getCode() {
        return code;
    }
}
src/main/java/com/ruoyi/energymeter/provider/YunJiChaoBiaoProviderClient.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,706 @@
package com.ruoyi.energymeter.provider;
import cn.hutool.http.HttpException;
import cn.hutool.http.HttpRequest;
import cn.hutool.http.HttpResponse;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.energymeter.config.EnergyMeterProperties;
import com.ruoyi.energymeter.constant.EnergyMeterConstants;
import com.ruoyi.energymeter.dto.MeterArchive;
import com.ruoyi.energymeter.dto.MeterQuarterPoint;
import com.ruoyi.energymeter.dto.MeterReadingPoint;
import com.ruoyi.energymeter.dto.MeterUsagePoint;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.format.DateTimeParseException;
import java.util.ArrayList;
import java.util.HashSet;
import java.util.List;
import java.util.Objects;
import java.util.Set;
/**
 * é›†ç¤¾äº‘能源监控 API å®¢æˆ·ç«¯ã€‚
 *
 * <p>真实调用:POST application/json,请求头传 API_KEY。授权码只从配置读取,日志不输出授权码与响应原文。</p>
 * <p>演示模式:未配置授权码或显式开启 mock æ—¶ï¼Œç”Ÿæˆå¯é‡å¤çš„æœ¬åœ°æ•°æ®ï¼Œä»…用于本地联调与页面演示。</p>
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class YunJiChaoBiaoProviderClient implements MeterProviderClient {
    private static final String PATH_ELECTRIC_ARCHIVE = "/NH2/AmmeterBasicInfo";
    private static final String PATH_WATER_ARCHIVE = "/NH2/WatermeterBasicInfo";
    private static final String PATH_WATER_USAGE = "/NH2/GetUseWater";
    private static final String PATH_WATER_RAW = "/NH2/GetWaterRawData";
    private static final String PATH_FROZEN_ELECTRICITY = "/NH2/FrozenElectricity";
    private static final String PATH_GAS_ARCHIVE = "/NH2/GasmeterBasicInfo";
    private static final String PATH_GAS_USAGE = "/NH2/GetGasUseData";
    private static final String PATH_GAS_RAW = "/NH2/GetGasRawData";
    private static final String PATH_ELECTRIC_QUANTITY = "/NH2/ElectricQuantity";
    /** æ¼”示读数基准日期与基准表码,保证重复查询结果一致。 */
    private static final LocalDate MOCK_READING_EPOCH = LocalDate.of(2025, 9, 1);
    private static final BigDecimal MOCK_WATER_BASE = BigDecimal.valueOf(1000);
    private static final BigDecimal MOCK_ELECTRIC_BASE = BigDecimal.valueOf(50000);
    private static final DateTimeFormatter[] DATE_TIME_FORMATS = new DateTimeFormatter[]{
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"),
            DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm"),
            DateTimeFormatter.ofPattern("yyyy-MM-dd'T'HH:mm:ss"),
            DateTimeFormatter.ofPattern("yyyy/MM/dd HH:mm:ss"),
            DateTimeFormatter.ofPattern("yyyy-MM-dd"),
            DateTimeFormatter.ofPattern("yyyy/MM/dd")
    };
    private final EnergyMeterProperties properties;
    /** ä¸Šä¸€æ¬¡è¯·æ±‚时间,用于控制最小请求间隔,规避供应商限流。 */
    private long lastRequestAt = 0L;
    /**
     * å…¨å±€æœ€å°è¯·æ±‚间隔:供应商对短时间内的连续调用会返回 429,这里串行控速。
     */
    private synchronized void throttle(long minIntervalMs) {
        if (minIntervalMs <= 0) {
            return;
        }
        long wait = minIntervalMs - (System.currentTimeMillis() - lastRequestAt);
        if (wait > 0) {
            try {
                Thread.sleep(wait);
            } catch (InterruptedException e) {
                Thread.currentThread().interrupt();
            }
        }
        lastRequestAt = System.currentTimeMillis();
    }
    @Override
    public boolean mockMode() {
        // åªæœ‰æ˜¾å¼å¼€å¯ mock æ‰ä½¿ç”¨æ¼”示数据;未配置授权码时直接报错,避免把模拟值当成真实用量
        return properties.getProvider().isMockEnabled();
    }
    @Override
    public boolean hasApiKey() {
        return StringUtils.isNotBlank(properties.getProvider().getApiKey());
    }
    @Override
    public MeterArchive queryArchive(String meterType, String address) {
        String trimmed = StringUtils.trim(address);
        if (StringUtils.isBlank(trimmed)) {
            throw new MeterProviderException("INVALID_ADDRESS", "通信地址不能为空");
        }
        if (mockMode()) {
            return mockArchive(meterType, trimmed);
        }
        boolean water = EnergyMeterConstants.isWater(meterType);
        boolean gas = EnergyMeterConstants.isGas(meterType);
        JSONObject payload = new JSONObject();
        if (water || gas) {
            payload.put("address", trimmed);
        } else {
            payload.put("meterAddress", trimmed);
        }
        String archivePath = gas ? PATH_GAS_ARCHIVE
                : (water ? PATH_WATER_ARCHIVE : PATH_ELECTRIC_ARCHIVE);
        List<JSONObject> items = callList(archivePath, payload);
        JSONObject matched = items.stream()
                .filter(item -> trimmed.equals(StringUtils.trim(firstString(item, "Address", "address"))))
                .findFirst()
                .orElse(null);
        if (matched == null) {
            throw new MeterProviderException("NOT_FOUND", "第三方未找到该表计");
        }
        MeterArchive archive = new MeterArchive();
        archive.setMeterType(EnergyMeterConstants.canonicalType(meterType));
        archive.setMeterAddress(trimmed);
        archive.setProviderMeterId(firstString(matched, "ID", "Id", "id"));
        archive.setProviderName(water || gas
                ? firstString(matched, "Name", "name")
                : firstString(matched, "AmmeterName", "Name", "name"));
        archive.setAreaName(firstString(matched, "AreaName", "areaName"));
        archive.setParentProviderId(firstString(matched, "ParentID", "ParentId", "parentId"));
        archive.setStateText(firstString(matched, "State", "LineState", "OnlineState"));
        archive.setOnlineStatus(parseOnline(water || gas
                ? firstString(matched, "LineState", "State", "OnlineState")
                : firstString(matched, "OnlineState", "State")));
        Long areaId = firstLong(matched, "AreaID", "AreaId", "areaId");
        archive.setAreaId(areaId);
        return archive;
    }
    @Override
    public List<MeterUsagePoint> queryDailyUsage(String meterType, String address, String providerMeterId,
                                                 LocalDate start, LocalDate end) {
        boolean water = EnergyMeterConstants.isWater(meterType);
        boolean gas = EnergyMeterConstants.isGas(meterType);
        if (mockMode()) {
            return mockUsage(meterType, address, start, end);
        }
        JSONObject payload = new JSONObject();
        payload.put("dateType", "D");
        payload.put("startTime", start.toString());
        payload.put("endTime", end.toString());
        String path;
        if (water) {
            payload.put("address", address);
            path = PATH_WATER_USAGE;
        } else if (gas) {
            // æ°”表用量接口按第三方设备ID查询,不认通信地址
            payload.put("gasmeterID", requireMeterId(meterType, providerMeterId));
            path = PATH_GAS_USAGE;
        } else {
            // ç”µè¡¨æœŸé—´ç”¨é‡ç”¨ ElectricQuantity çš„ ZYGDN(正向有功电量);
            // FrozenElectricity çš„ TotalKwh åœ¨éƒ¨åˆ†è¡¨ä¸Šæ’为 null,不能作为用量来源
            payload.put("meterAddress", address);
            payload.put("valueType", "SJZ");
            path = PATH_ELECTRIC_QUANTITY;
        }
        List<JSONObject> items = callList(path, payload);
        List<MeterUsagePoint> points = new ArrayList<>(items.size());
        for (JSONObject item : items) {
            String rawTime = firstString(item, "ReadingDate", "readingDate", "Time", "time", "Date");
            LocalDateTime time = parseDateTime(rawTime);
            if (time == null) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE,
                        "无法识别用量时间:" + rawTime);
            }
            BigDecimal value;
            if (water) {
                value = firstDecimal(item, "Value", "value", "UseNumber");
            } else if (gas) {
                value = firstDecimal(item, "UseNumber", "useNumber");
            } else {
                value = firstDecimal(item, "ZYGDN", "zygdn", "TotalKwh", "totalKwh");
            }
            if (value == null) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "用量字段缺失");
            }
            MeterUsagePoint point = new MeterUsagePoint();
            point.setTime(time.toLocalDate().atStartOfDay());
            point.setValue(value);
            point.setAddress(firstString(item, "Address", "address"));
            point.setRawTime(rawTime);
            points.add(point);
        }
        return points;
    }
    @Override
    public List<MeterQuarterPoint> queryQuarterUsage(String meterType, String address, String providerMeterId,
                                                     LocalDate start, LocalDate end) {
        if (!EnergyMeterConstants.isElectric(meterType)) {
            throw new MeterProviderException("UNSUPPORTED",
                    "15 åˆ†é’Ÿç”µé‡ç›®å‰åªæ”¯æŒç”µè¡¨");
        }
        if (mockMode()) {
            return mockQuarterUsage(address, start, end);
        }
        JSONObject payload = new JSONObject();
        payload.put("dateType", "mi15");
        payload.put("startTime", start.toString());
        payload.put("endTime", end.toString());
        payload.put("meterAddress", address);
        payload.put("valueType", "SJZ");
        List<JSONObject> items = callList(PATH_ELECTRIC_QUANTITY, payload);
        List<MeterQuarterPoint> points = new ArrayList<>(items.size());
        for (JSONObject item : items) {
            String rawTime = firstString(item, "ReadingDate", "readingDate", "Time", "time", "Date");
            LocalDateTime time = parseDateTime(rawTime);
            if (time == null) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE,
                        "无法识别 15 åˆ†é’Ÿç”µé‡æ—¶é—´ï¼š" + rawTime);
            }
            BigDecimal active = firstDecimal(item, "ZYGDN", "zygdn");
            // æœ€åŽä¸€ä¸ªæœªé—­åˆæ—¶é—´ç‰‡çš„ ZYGDN ä¸ºç©ºï¼Œåªä¿ç•™å·²ç»ç»“算的时间片
            if (active == null) {
                continue;
            }
            MeterQuarterPoint point = new MeterQuarterPoint();
            point.setTime(time);
            point.setActive(active);
            point.setReactive(firstDecimal(item, "ZWGDN", "zwgdn"));
            point.setActiveReading(firstDecimal(item, "ZYGDNSZ", "zygdnsz"));
            point.setReactiveReading(firstDecimal(item, "ZWGDNSZ", "zwgdnsz"));
            point.setAddress(firstString(item, "Address", "address"));
            point.setRawTime(rawTime);
            points.add(point);
        }
        return points;
    }
    @Override
    public List<MeterReadingPoint> queryReadings(String meterType, String address, String providerMeterId,
                                                 LocalDate start, LocalDate end) {
        boolean water = EnergyMeterConstants.isWater(meterType);
        if (mockMode()) {
            return mockReadings(meterType, address, start, end);
        }
        if (EnergyMeterConstants.isGas(meterType)) {
            return queryGasReadings(address, providerMeterId, start, end);
        }
        JSONObject payload = new JSONObject();
        payload.put("startTime", start.toString());
        payload.put("endTime", end.toString());
        if (water) {
            payload.put("address", address);
            List<JSONObject> items = callList(PATH_WATER_RAW, payload);
            List<MeterReadingPoint> points = new ArrayList<>(items.size());
            for (JSONObject item : items) {
                String rawTime = firstString(item, "ReadingDate", "readingDate", "Time", "time", "Date");
                LocalDateTime time = parseDateTime(rawTime);
                if (time == null) {
                    throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "无法识别示值时间:" + rawTime);
                }
                BigDecimal reading = firstDecimal(item, "ReadNumber", "readNumber");
                if (reading == null) {
                    throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "示值字段缺失");
                }
                MeterReadingPoint point = new MeterReadingPoint();
                point.setTime(time);
                point.setReading(reading);
                point.setUsage(firstDecimal(item, "UseNumber", "useNumber"));
                point.setAddress(firstString(item, "Address", "address"));
                point.setRawTime(rawTime);
                points.add(point);
            }
            return points;
        }
        payload.put("meterAddress", address);
        payload.put("dateType", "D");
        payload.put("valueType", "SJZ");
        List<JSONObject> items = callList(PATH_FROZEN_ELECTRICITY, payload);
        List<MeterReadingPoint> points = new ArrayList<>(items.size());
        for (JSONObject item : items) {
            // FrozenElectricity åŒä¸€å¤©ä¼šè¿”回 Type=D(表码)与 Type=DF(日冻结电量)两行,
            // åªæœ‰ Type=D çš„ TotalNowKwh æ‰æ˜¯ç´¯è®¡è¡¨ç 
            String freezeType = firstString(item, "Type", "type");
            if (StringUtils.isNotBlank(freezeType) && !"D".equalsIgnoreCase(freezeType)) {
                continue;
            }
            String rawTime = firstString(item, "ReadingDate", "readingDate", "Time", "time", "Date");
            LocalDateTime time = parseDateTime(rawTime);
            if (time == null) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "无法识别示值时间:" + rawTime);
            }
            BigDecimal reading = firstDecimal(item, "TotalNowKwh", "totalNowKwh", "ZYGDNSZ", "zygdnsz");
            if (reading == null) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "示值字段缺失");
            }
            MeterReadingPoint point = new MeterReadingPoint();
            point.setTime(time);
            point.setReading(reading);
            point.setUsage(firstDecimal(item, "TotalKwh", "totalKwh"));
            point.setAddress(firstString(item, "Address", "address"));
            point.setRawTime(rawTime);
            points.add(point);
        }
        return points;
    }
    /**
     * æ°”表示值:/NH2/GetGasRawData,按第三方设备ID或通信地址查询。
     */
    private List<MeterReadingPoint> queryGasReadings(String address, String providerMeterId,
                                                     LocalDate start, LocalDate end) {
        JSONObject payload = new JSONObject();
        payload.put("pageIndex", "1");
        payload.put("pageSize", "100");
        payload.put("dateType", "D");
        payload.put("areaID", "0");
        payload.put("gasmeterID", requireMeterId(EnergyMeterConstants.TYPE_GAS, providerMeterId));
        payload.put("address", StringUtils.defaultString(address));
        payload.put("startTime", start.toString());
        payload.put("endTime", end.toString());
        List<JSONObject> items = callList(PATH_GAS_RAW, payload);
        List<MeterReadingPoint> points = new ArrayList<>(items.size());
        for (JSONObject item : items) {
            String rawTime = firstString(item, "ReadingDate", "readingDate", "RealReadingDate", "Time", "time");
            LocalDateTime time = parseDateTime(rawTime);
            if (time == null) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "无法识别气表示值时间:" + rawTime);
            }
            BigDecimal reading = firstDecimal(item, "ReadNumber", "readNumber");
            if (reading == null) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "气表示值字段缺失");
            }
            MeterReadingPoint point = new MeterReadingPoint();
            point.setTime(time);
            point.setReading(reading);
            point.setUsage(firstDecimal(item, "UseNumber", "useNumber"));
            point.setAddress(firstString(item, "Address", "address"));
            point.setRawTime(rawTime);
            points.add(point);
        }
        return points;
    }
    /**
     * æ°”表接口要求传第三方设备ID(数字),这里统一校验。
     */
    private long requireMeterId(String meterType, String providerMeterId) {
        if (StringUtils.isBlank(providerMeterId)) {
            throw new MeterProviderException("MISSING_METER_ID",
                    EnergyMeterConstants.typeName(meterType) + "缺少第三方设备ID,请重新验证档案后再采集");
        }
        try {
            return Long.parseLong(providerMeterId.trim());
        } catch (NumberFormatException e) {
            throw new MeterProviderException("MISSING_METER_ID",
                    "第三方设备ID格式不正确:" + providerMeterId);
        }
    }
    /* ------------------------------------------------------------------ */
    /* HTTP è°ƒç”¨ä¸Žå“åº”解析                                                  */
    /* ------------------------------------------------------------------ */
    private List<JSONObject> callList(String path, JSONObject payload) {
        JSONObject root = call(path, payload);
        JSONArray array = findArray(root);
        if (array == null) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "第三方响应缺少数据列表");
        }
        List<JSONObject> items = new ArrayList<>(array.size());
        for (int i = 0; i < array.size(); i++) {
            JSONObject item = array.getJSONObject(i);
            if (item != null) {
                items.add(item);
            }
        }
        int pageCount = firstInt(root, 1, "pageCount", "pagecount", "pages", "totalPage", "totalPages");
        if (pageCount > 1) {
            int pageSize = firstInt(root, Math.max(items.size(), 1), "pageSize", "pagesize", "size");
            int total = firstInt(root, -1, "total", "totalCount", "totalRows", "count");
            Set<String> fingerprints = new HashSet<>();
            items.forEach(item -> fingerprints.add(item.toJSONString()));
            for (int page = 2; page <= pageCount; page++) {
                JSONObject nextPayload = new JSONObject(payload);
                nextPayload.put("pageIndex", page);
                nextPayload.put("pageSize", pageSize);
                nextPayload.put("pageNum", page);
                JSONArray nextArray = findArray(call(path, nextPayload));
                if (nextArray == null || nextArray.isEmpty()) {
                    throw new MeterProviderException(EnergyMeterConstants.ERROR_PAGE_INCOMPLETE,
                            "第三方分页第 " + page + " é¡µä¸ºç©ºï¼Œæ— æ³•确认全量数据");
                }
                for (int i = 0; i < nextArray.size(); i++) {
                    JSONObject item = nextArray.getJSONObject(i);
                    if (item != null && fingerprints.add(item.toJSONString())) {
                        items.add(item);
                    }
                }
            }
            if (total > 0 && items.size() < total) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_PAGE_INCOMPLETE,
                        "第三方分页条数不完整:" + items.size() + "/" + total);
            }
        }
        return items;
    }
    private JSONObject call(String path, JSONObject payload) {
        EnergyMeterProperties.Provider provider = properties.getProvider();
        if (StringUtils.isBlank(provider.getApiKey())) {
            throw new MeterProviderException("NO_API_KEY",
                    "未配置第三方授权码(环境变量 NH_ENERGY_API_KEY),无法获取真实用量");
        }
        String url = joinUrl(provider.getBaseUrl(), path);
        String body;
        HttpResponse response = null;
        try {
            throttle(provider.getMinIntervalMs());
            response = HttpRequest.post(url)
                    .header("API_KEY", provider.getApiKey())
                    .contentType("application/json")
                    .body(payload.toJSONString())
                    .timeout(provider.getReadTimeoutSeconds() * 1000)
                    .setConnectionTimeout(provider.getConnectTimeoutSeconds() * 1000)
                    .execute();
            int status = response.getStatus();
            if (status == 429) {
                throw new MeterProviderException("RATE_LIMITED", "第三方接口限流(429),请降低采集频率后重试");
            }
            if (status >= 400) {
                throw new MeterProviderException(EnergyMeterConstants.ERROR_UPSTREAM,
                        "第三方接口返回状态码:" + status + "(" + path + ")");
            }
            body = response.body();
        } catch (MeterProviderException e) {
            throw e;
        } catch (HttpException e) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_TIMEOUT,
                    "第三方接口请求超时或网络异常:" + path, e);
        } catch (Exception e) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_UPSTREAM,
                    "第三方接口调用失败:" + path, e);
        } finally {
            if (response != null) {
                response.close();
            }
        }
        if (StringUtils.isBlank(body)) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "第三方响应为空");
        }
        Object parsed;
        try {
            parsed = JSON.parse(body);
        } catch (Exception e) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "第三方响应不是合法 JSON", e);
        }
        JSONObject root;
        if (parsed instanceof JSONArray array) {
            root = new JSONObject();
            root.put("list", array);
        } else if (parsed instanceof JSONObject object) {
            root = object;
        } else {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "第三方响应结构无法识别");
        }
        checkBusinessError(root);
        return root;
    }
    private void checkBusinessError(JSONObject root) {
        Boolean success = null;
        for (String key : new String[]{"success", "Success"}) {
            if (root.containsKey(key)) {
                success = root.getBoolean(key);
                break;
            }
        }
        Integer code = null;
        for (String key : new String[]{"code", "Code", "errcode", "errCode", "status"}) {
            if (root.containsKey(key)) {
                code = root.getInteger(key);
                break;
            }
        }
        String message = firstString(root, "msg", "message", "Message", "errmsg", "error", "Error");
        if (Boolean.FALSE.equals(success)) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_UPSTREAM,
                    "第三方返回失败:" + StringUtils.defaultString(message, "未知原因"));
        }
        if (code != null && code != 0 && code != 200 && code != 1) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_UPSTREAM,
                    "第三方返回错误码 " + code + ":" + StringUtils.defaultString(message, "未知原因"));
        }
    }
    private JSONArray findArray(JSONObject root) {
        if (root == null) {
            return null;
        }
        for (String key : new String[]{"list", "data", "result", "rows", "records", "items", "List", "Data"}) {
            Object value = root.get(key);
            if (value instanceof JSONArray array) {
                return array;
            }
            if (value instanceof JSONObject object) {
                JSONArray nested = findArray(object);
                if (nested != null) {
                    return nested;
                }
            }
        }
        return null;
    }
    private String joinUrl(String baseUrl, String path) {
        String base = StringUtils.trim(baseUrl);
        if (StringUtils.isBlank(base)) {
            throw new MeterProviderException("CONFIG_ERROR", "未配置第三方基础地址 energy-meter.provider.base-url");
        }
        while (base.endsWith("/")) {
            base = base.substring(0, base.length() - 1);
        }
        return base + path;
    }
    /* ------------------------------------------------------------------ */
    /* æ¼”示数据                                                             */
    /* ------------------------------------------------------------------ */
    private MeterArchive mockArchive(String meterType, String address) {
        boolean water = EnergyMeterConstants.isWater(meterType);
        MeterArchive archive = new MeterArchive();
        archive.setMeterType(meterType);
        archive.setMeterAddress(address);
        archive.setProviderMeterId("MOCK-" + Integer.toHexString(Math.abs(address.hashCode())).toUpperCase());
        archive.setProviderName((water ? "演示水表 " : "演示电表 ") + address);
        archive.setAreaName("演示区域");
        archive.setOnlineStatus(1);
        archive.setStateText("在线");
        return archive;
    }
    private List<MeterUsagePoint> mockUsage(String meterType, String address, LocalDate start, LocalDate end) {
        List<MeterUsagePoint> points = new ArrayList<>();
        for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
            MeterUsagePoint point = new MeterUsagePoint();
            point.setTime(date.atStartOfDay());
            point.setValue(mockDailyValue(meterType, address, date));
            point.setAddress(address);
            point.setRawTime(date.toString());
            points.add(point);
        }
        return points;
    }
    private List<MeterQuarterPoint> mockQuarterUsage(String address, LocalDate start, LocalDate end) {
        List<MeterQuarterPoint> points = new ArrayList<>();
        for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
            BigDecimal daily = mockDailyValue(EnergyMeterConstants.TYPE_ELECTRIC, address, date);
            BigDecimal quarterBase = daily.divide(BigDecimal.valueOf(96), 6, RoundingMode.HALF_UP);
            for (int index = 1; index <= 96; index++) {
                LocalDateTime time = date.atStartOfDay().plusMinutes(index * 15L);
                // ç™½å¤©è´Ÿè·é«˜ã€å¤œé—´è´Ÿè·ä½Žï¼Œä¿è¯æ›²çº¿ç¨³å®šä¸”可重复
                double hour = (index * 15.0) / 60.0;
                double shape = 0.45 + 0.75 * Math.sin(Math.PI * Math.min(Math.max((hour - 6) / 16.0, 0), 1));
                BigDecimal active = quarterBase.multiply(BigDecimal.valueOf(shape))
                        .setScale(3, RoundingMode.HALF_UP);
                MeterQuarterPoint point = new MeterQuarterPoint();
                point.setTime(time);
                point.setActive(active);
                point.setReactive(active.multiply(BigDecimal.valueOf(0.18)).setScale(3, RoundingMode.HALF_UP));
                point.setActiveReading(BigDecimal.valueOf(MOCK_ELECTRIC_BASE.doubleValue())
                        .add(daily.multiply(BigDecimal.valueOf(index / 96.0))).setScale(3, RoundingMode.HALF_UP));
                point.setReactiveReading(point.getActiveReading().multiply(BigDecimal.valueOf(0.18))
                        .setScale(3, RoundingMode.HALF_UP));
                point.setAddress(address);
                point.setRawTime(time.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm")));
                points.add(point);
            }
        }
        return points;
    }
    private List<MeterReadingPoint> mockReadings(String meterType, String address, LocalDate start, LocalDate end) {
        boolean water = EnergyMeterConstants.isWater(meterType);
        BigDecimal base = water ? MOCK_WATER_BASE : MOCK_ELECTRIC_BASE;
        List<MeterReadingPoint> points = new ArrayList<>();
        for (LocalDate date = start; !date.isAfter(end); date = date.plusDays(1)) {
            BigDecimal cumulative = base;
            for (LocalDate cursor = MOCK_READING_EPOCH; !cursor.isAfter(date); cursor = cursor.plusDays(1)) {
                cumulative = cumulative.add(mockDailyValue(meterType, address, cursor));
            }
            MeterReadingPoint point = new MeterReadingPoint();
            point.setTime(date.atStartOfDay());
            point.setReading(cumulative.setScale(3, RoundingMode.HALF_UP));
            point.setUsage(mockDailyValue(meterType, address, date));
            point.setAddress(address);
            point.setRawTime(date.toString());
            points.add(point);
        }
        return points;
    }
    /**
     * æ¼”示用量:同一设备同一日期恒定,保证重复查表结果一致(便于验证幂等)。
     */
    private BigDecimal mockDailyValue(String meterType, String address, LocalDate date) {
        boolean water = EnergyMeterConstants.isWater(meterType);
        long seed = Math.abs(Objects.hash(address, date.toString()));
        double seasonal = 1 + 0.16 * Math.sin((date.getDayOfYear() - 80) / 365.0 * 2 * Math.PI);
        double base = water ? 3.2 : 55.0;
        double variance = water ? 1.6 : 30.0;
        double value = (base + (seed % 1000) / 1000.0 * variance) * seasonal;
        return BigDecimal.valueOf(value).setScale(3, RoundingMode.HALF_UP);
    }
    /* ------------------------------------------------------------------ */
    /* è§£æžå·¥å…·                                                             */
    /* ------------------------------------------------------------------ */
    private String firstString(JSONObject object, String... keys) {
        if (object == null) {
            return null;
        }
        for (String key : keys) {
            Object value = object.get(key);
            if (value != null && StringUtils.isNotBlank(String.valueOf(value))) {
                return String.valueOf(value).trim();
            }
        }
        return null;
    }
    private BigDecimal firstDecimal(JSONObject object, String... keys) {
        if (object == null) {
            return null;
        }
        for (String key : keys) {
            Object value = object.get(key);
            if (value == null) {
                continue;
            }
            try {
                return new BigDecimal(String.valueOf(value).trim());
            } catch (NumberFormatException ignored) {
                // ç»§ç»­å°è¯•下一个候选字段
            }
        }
        return null;
    }
    private Long firstLong(JSONObject object, String... keys) {
        BigDecimal value = firstDecimal(object, keys);
        return value == null ? null : value.longValue();
    }
    private int firstInt(JSONObject object, int defaultValue, String... keys) {
        BigDecimal value = firstDecimal(object, keys);
        return value == null ? defaultValue : value.intValue();
    }
    private Integer parseOnline(String state) {
        if (StringUtils.isBlank(state)) {
            return null;
        }
        String normalized = state.trim();
        if ("1".equals(normalized) || "true".equalsIgnoreCase(normalized)
                || normalized.contains("在线") || "on".equalsIgnoreCase(normalized)) {
            return 1;
        }
        if ("0".equals(normalized) || "false".equalsIgnoreCase(normalized)
                || normalized.contains("离线") || "off".equalsIgnoreCase(normalized)) {
            return 0;
        }
        return null;
    }
    private LocalDateTime parseDateTime(String raw) {
        if (StringUtils.isBlank(raw)) {
            return null;
        }
        String text = raw.trim();
        for (DateTimeFormatter formatter : DATE_TIME_FORMATS) {
            try {
                if (text.length() <= 10) {
                    return LocalDate.parse(text, formatter).atStartOfDay();
                }
                return LocalDateTime.parse(text, formatter);
            } catch (DateTimeParseException ignored) {
                // å°è¯•下一种格式
            }
        }
        return null;
    }
}
src/main/java/com/ruoyi/energymeter/service/ElectricMeterDashboardService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,17 @@
package com.ruoyi.energymeter.service;
import com.ruoyi.energymeter.vo.ElectricMeterDashboardVO;
/**
 * ç”µè¡¨å®žæ—¶çœ‹æ¿æœåŠ¡ã€‚
 */
public interface ElectricMeterDashboardService {
    /**
     * ä»Šæ—¥ 15 åˆ†é’Ÿæ›²çº¿ + è¿‘一周/上一周电能质量。
     *
     * @param meterId    å•台设备,空表示全部已启用电表
     * @param weekOffset 0 è¿‘一周,1 ä¸Šä¸€å‘¨
     */
    ElectricMeterDashboardVO dashboard(Long meterId, Integer weekOffset);
}
src/main/java/com/ruoyi/energymeter/service/EnergyMeterDemoService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,16 @@
package com.ruoyi.energymeter.service;
import java.time.LocalDate;
/**
 * æ¼”示数据服务:气表未接入第三方时,按天生成模拟用量。
 *
 * <p>只处理 `simulated=1` çš„设备,且同一天重复调用不会产生重复记录。</p>
 */
public interface EnergyMeterDemoService {
    /**
     * ä¸ºæŒ‡å®šä¸šåŠ¡æ—¥æœŸç”Ÿæˆæ¼”ç¤ºè¡¨ç ä¸Žç”¨é‡ï¼Œè¿”å›žå®žé™…å†™å…¥çš„è®¾å¤‡æ•°ã€‚
     */
    int generateGasDemo(LocalDate date);
}
src/main/java/com/ruoyi/energymeter/service/EnergyMeterDeviceService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,87 @@
package com.ruoyi.energymeter.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.IService;
import com.ruoyi.energymeter.dto.EnergyMeterDeviceQuery;
import com.ruoyi.energymeter.dto.MeterBindRequest;
import com.ruoyi.energymeter.dto.MeterManualBindRequest;
import com.ruoyi.energymeter.dto.MeterReadingRequest;
import com.ruoyi.energymeter.dto.MeterUpdateRequest;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.vo.MeterArchiveVO;
import com.ruoyi.energymeter.vo.MeterReadingResultVO;
import java.util.List;
/**
 * æ°´ç”µè¡¨ç»‘定设备服务。
 */
public interface EnergyMeterDeviceService extends IService<EnergyMeterDevice> {
    /**
     * æŒ‰ç±»åž‹ã€å…³é”®å­—、状态分页查询当前租户可见设备,并补充最近采集结果。
     */
    IPage<EnergyMeterDevice> pageDevices(Page<EnergyMeterDevice> page, EnergyMeterDeviceQuery query);
    /**
     * è®¾å¤‡ä¸‹æ‹‰é€‰é¡¹ï¼ˆä»…已启用设备)。
     */
    List<EnergyMeterDevice> listDeviceOptions(String meterType);
    /**
     * è°ƒç”¨ç¬¬ä¸‰æ–¹æ¡£æ¡ˆæŽ¥å£éªŒè¯å¹¶ä¸¥æ ¼åŒ¹é…é€šä¿¡åœ°å€ã€‚
     */
    MeterArchiveVO verifyArchive(String meterType, String meterAddress);
    /**
     * æ–°å¢žæœ¬åœ°ç»‘定,成功后异步创建历史补采批次。
     */
    EnergyMeterDevice bind(MeterBindRequest request);
    /**
     * æ–°å¢žæ‰‹å·¥æŠ„表设备(水表/电表/气表),写入起始表码,不调用第三方接口。
     */
    EnergyMeterDevice addManual(MeterManualBindRequest request);
    /**
     * åˆ é™¤è®¾å¤‡ï¼šä»…允许删除用户新增的手工抄表设备,初始化设备禁止删除。
     */
    void removeDevice(Long id);
    /**
     * æ‰‹å·¥æŠ„表记录分页(表码 + æœ¬æœŸç”¨é‡ï¼‰ã€‚
     */
    IPage<EnergyMeterMeasurement> pageReadings(Long meterId, long current, long size);
    /**
     * å½•入一条手工抄表记录,按期差生成/修正对应日用量。
     */
    MeterReadingResultVO addReading(Long meterId, MeterReadingRequest request);
    /**
     * åˆ é™¤ä¸€æ¡æ‰‹å·¥æŠ„表记录,并重算受影响的日用量。
     */
    void removeReading(Long meterId, Long measurementId);
    /**
     * ä¿®æ”¹æœ¬åœ°æ˜¾ç¤ºå­—段与启停用状态;通信地址不可在此修改。
     */
    EnergyMeterDevice updateLocal(Long id, MeterUpdateRequest request);
    /**
     * èŽ·å–å½“å‰ç§Ÿæˆ·ä¸‹çš„ä¸€å°è®¾å¤‡ï¼Œæ‰¾ä¸åˆ°æ—¶æŠ›ä¸šåŠ¡å¼‚å¸¸ã€‚
     */
    EnergyMeterDevice getOwnedDevice(Long id);
    /**
     * å½“前租户下启用的设备(按类型筛选,可为空)。
     */
    List<EnergyMeterDevice> listEnabledDevices(String meterType);
    /**
     * æ¼”示数据模式提示。
     */
    boolean mockMode();
}
src/main/java/com/ruoyi/energymeter/service/EnergyMeterStatService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
package com.ruoyi.energymeter.service;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.vo.EnergyMeterOverviewVO;
import java.time.LocalDate;
/**
 * æ¦‚览与明细查询服务,只读本地采集表,不直连第三方。
 */
public interface EnergyMeterStatService {
    /**
     * ç”¨èƒ½æ¦‚览:卡片、按日/按月对比序列、采集状态。
     */
    EnergyMeterOverviewVO overview(String meterType, Long meterId, String month, Integer year);
    /**
     * å•台设备的读数/用量明细。
     */
    IPage<EnergyMeterMeasurement> pageMeasurements(Long meterId, String metricCode, String granularity,
                                                   LocalDate start, LocalDate end, long current, long size);
}
src/main/java/com/ruoyi/energymeter/service/EnergyMeterSyncService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,42 @@
package com.ruoyi.energymeter.service;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import java.time.LocalDate;
import java.util.List;
/**
 * åŒæ­¥è°ƒåº¦æœåŠ¡ï¼šæ‰¹æ¬¡åˆ›å»ºã€ä»»åŠ¡æ‰§è¡Œã€å¤±è´¥é‡è¯•ä¸Žè¿›åº¦æŽ¨è¿›ã€‚
 */
public interface EnergyMeterSyncService {
    /**
     * æ¯å¤© 08:00 çš„自动同步:同一北京日期只创建一个批次。
     */
    Long triggerScheduledSync();
    /**
     * æ–°ç»‘定设备的历史补采。
     */
    void triggerBackfill(EnergyMeterDevice device, LocalDate start, LocalDate end);
    /**
     * åœç”¨åŽé‡æ–°å¯ç”¨æ—¶æŒ‰è¿›åº¦è¡¥é‡‡åœç”¨æœŸé—´æ•°æ®ã€‚
     */
    Long triggerBackfillGap(EnergyMeterDevice device);
    /**
     * æ‰‹åŠ¨æŸ¥è¡¨ï¼Œè¿”å›žæ‰¹æ¬¡ID。
     */
    Long triggerManual(List<Long> meterIds, LocalDate start, LocalDate end, Boolean includeReadings);
    /**
     * å¼‚步执行一个批次。
     */
    void executeRunAsync(Long runId);
    /**
     * å¤„理到期的重试任务与超时的 RUNNING ä»»åŠ¡ï¼Œè¿”å›žå¤„ç†æ•°é‡ã€‚
     */
    int recoverDueTasks();
}
src/main/java/com/ruoyi/energymeter/service/EnergyMonitorService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,28 @@
package com.ruoyi.energymeter.service;
import com.ruoyi.energymeter.vo.EnergyCollectionVO;
import com.ruoyi.energymeter.vo.EnergyRealtimeVO;
import com.ruoyi.energymeter.vo.EnergyReportVO;
/**
 * èƒ½è€—数据采集、实时监控与统计分析。
 */
public interface EnergyMonitorService {
    /**
     * æ•°æ®é‡‡é›†æ¦‚览:三类介质的接入设备、归集进度与最近采集批次。
     */
    EnergyCollectionVO collection();
    /**
     * å®žæ—¶ç›‘控:三类介质最新用量、近 7 æ—¥è¶‹åŠ¿ä¸Žç”µè¡¨ 15 åˆ†é’Ÿå®žæ—¶æ•°æ®ã€‚
     *
     * @param meterId å•台设备,空表示全部已启用设备
     */
    EnergyRealtimeVO realtime(Long meterId);
    /**
     * ç»Ÿè®¡åˆ†æžï¼šæŒ‰æ—¥/月、按介质/设备汇总水、电、蒸汽用量。
     */
    EnergyReportVO report(String startDate, String endDate, String granularity, String meterType, String dimension);
}
src/main/java/com/ruoyi/energymeter/service/impl/ElectricMeterDashboardServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,353 @@
package com.ruoyi.energymeter.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.energymeter.constant.EnergyMeterConstants;
import com.ruoyi.energymeter.dto.MeterQuarterPoint;
import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.provider.MeterProviderClient;
import com.ruoyi.energymeter.provider.MeterProviderException;
import com.ruoyi.energymeter.service.ElectricMeterDashboardService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import com.ruoyi.energymeter.vo.ElectricMeterDashboardVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.TreeMap;
import java.util.concurrent.ConcurrentHashMap;
/**
 * ç”µè¡¨çœ‹æ¿ï¼šæŠŠé›†ç¤¾äº‘ ElectricQuantity çš„ mi15 æ•°æ®èšåˆæˆä»Šæ—¥æ›²çº¿ä¸Žç”µèƒ½è´¨é‡ã€‚
 *
 * <p>15 åˆ†é’Ÿæ•°æ®åªåœ¨é¡µé¢æŸ¥çœ‹æ—¶å®žæ—¶æ‹‰å–,不落库;日/月累计仍以采集表的 D ç²’度为准。
 * åªè®¡ç®—电量、功率和功率因数,不涉及任何费用。</p>
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class ElectricMeterDashboardServiceImpl implements ElectricMeterDashboardService {
    private static final DateTimeFormatter DAY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter LABEL_FORMAT = DateTimeFormatter.ofPattern("MM-dd HH:mm");
    private static final int SLOT_COUNT = 96;
    /** ç¬¬ä¸‰æ–¹é™æµè¾ƒä¸¥ï¼Œé¡µé¢æ•°æ®ç¼“å­˜ 2 åˆ†é’Ÿï¼Œé¿å…é¢‘繁切换设备/周次时反复请求。 */
    private static final long CACHE_TTL_MS = 120_000L;
    private final EnergyMeterDeviceMapper deviceMapper;
    private final MeterProviderClient provider;
    private final Map<String, CachedPoints> cache = new ConcurrentHashMap<>();
    @Override
    public ElectricMeterDashboardVO dashboard(Long meterId, Integer weekOffset) {
        LocalDate today = EnergyMeterSupport.today();
        int offset = weekOffset != null && weekOffset == 1 ? 1 : 0;
        LocalDate qualityStart = offset == 1 ? today.minusDays(13) : today.minusDays(6);
        LocalDate qualityEnd = offset == 1 ? today.minusDays(7) : today;
        List<EnergyMeterDevice> devices = resolveDevices(meterId);
        ElectricMeterDashboardVO vo = new ElectricMeterDashboardVO();
        vo.setUnit(EnergyMeterConstants.UNIT_ELECTRIC);
        vo.setMeterId(meterId);
        vo.setDeviceCount(devices.size());
        vo.setScopeLabel(scopeLabel(devices, meterId));
        vo.setMockMode(provider.mockMode());
        ElectricMeterDashboardVO.Today todayVo = new ElectricMeterDashboardVO.Today();
        ElectricMeterDashboardVO.Quality qualityVo = new ElectricMeterDashboardVO.Quality();
        if (devices.isEmpty()) {
            fillEmptyToday(todayVo, today);
            fillEmptyQuality(qualityVo, qualityStart, qualityEnd, offset);
            vo.setToday(todayVo);
            vo.setQuality(qualityVo);
            return vo;
        }
        List<MeterQuarterPoint> qualityPoints;
        List<MeterQuarterPoint> todayPoints;
        if (offset == 1) {
            // ä¸Šä¸€å‘¨ä¸Žä»Šæ—¥æ²¡æœ‰äº¤é›†ï¼Œåˆ†åˆ«æŸ¥è¯¢ï¼šä»Šæ—¥æ›²çº¿ä»ç„¶å–自最近两天
            qualityPoints = queryDevices(devices, qualityStart, qualityEnd);
            todayPoints = queryDevices(devices, today.minusDays(1), today);
        } else {
            // è¿‘一周已包含昨天与今天,一次查询即可同时满足两块图表
            qualityPoints = queryDevices(devices, qualityStart, qualityEnd);
            todayPoints = qualityPoints;
        }
        fillToday(todayVo, today, todayPoints);
        fillQuality(qualityVo, qualityStart, qualityEnd, offset, qualityPoints);
        vo.setToday(todayVo);
        vo.setQuality(qualityVo);
        return vo;
    }
    /* ------------------------------------------------------------------ */
    /* ä»Šæ—¥æ›²çº¿                                                             */
    /* ------------------------------------------------------------------ */
    private void fillToday(ElectricMeterDashboardVO.Today todayVo, LocalDate today,
                           List<MeterQuarterPoint> points) {
        Map<Integer, BigDecimal[]> todaySlots = new HashMap<>();
        Map<Integer, BigDecimal[]> yesterdaySlots = new HashMap<>();
        LocalDate yesterday = today.minusDays(1);
        for (MeterQuarterPoint point : points) {
            if (point.getTime() == null || point.getActive() == null) {
                continue;
            }
            LocalDate day = point.getTime().toLocalDate();
            Map<Integer, BigDecimal[]> target;
            if (day.equals(today)) {
                target = todaySlots;
            } else if (day.equals(yesterday)) {
                target = yesterdaySlots;
            } else {
                continue;
            }
            int slot = slotIndex(point.getTime());
            BigDecimal[] values = target.computeIfAbsent(slot, key -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO});
            values[0] = values[0].add(point.getActive());
            values[1] = values[1].add(point.getReactive() == null ? BigDecimal.ZERO : point.getReactive());
        }
        todayVo.setDate(today.format(DAY_FORMAT));
        todayVo.setRangeStart("00:00");
        todayVo.setSeries(buildSlots(todaySlots, 1, SLOT_COUNT));
        todayVo.setYesterdaySeries(buildSlots(yesterdaySlots, 1, SLOT_COUNT));
        BigDecimal todayTotal = sumActive(todaySlots);
        BigDecimal yesterdayTotal = sumActive(yesterdaySlots);
        todayVo.setTotal(todaySlots.isEmpty() ? null : todayTotal);
        todayVo.setYesterdayTotal(yesterdayTotal);
        int lastSlot = lastDataSlot(todaySlots);
        BigDecimal yesterdaySame = lastSlot <= 0 ? null : sumActive(yesterdaySlots, lastSlot);
        todayVo.setYesterdaySamePeriodTotal(yesterdaySame);
        if (lastSlot > 0 && yesterdaySame != null && yesterdaySame.compareTo(BigDecimal.ZERO) > 0) {
            todayVo.setCompareRate(todayTotal.subtract(yesterdaySame)
                    .multiply(BigDecimal.valueOf(100))
                    .divide(yesterdaySame, 1, RoundingMode.HALF_UP));
        }
        todayVo.setRangeEnd(lastSlot > 0 ? labelOfSlot(lastSlot) : "--");
    }
    private List<ElectricMeterDashboardVO.QuarterPoint> buildSlots(Map<Integer, BigDecimal[]> slots,
                                                                   int fromSlot, int toSlot) {
        List<ElectricMeterDashboardVO.QuarterPoint> result = new ArrayList<>(toSlot - fromSlot + 1);
        for (int slot = fromSlot; slot <= toSlot; slot++) {
            ElectricMeterDashboardVO.QuarterPoint point = new ElectricMeterDashboardVO.QuarterPoint();
            point.setTime(labelOfSlot(slot));
            BigDecimal[] values = slots.get(slot);
            if (values != null) {
                point.setActive(values[0].setScale(3, RoundingMode.HALF_UP));
                point.setReactive(values[1].setScale(3, RoundingMode.HALF_UP));
            }
            result.add(point);
        }
        return result;
    }
    private void fillEmptyToday(ElectricMeterDashboardVO.Today todayVo, LocalDate today) {
        todayVo.setDate(today.format(DAY_FORMAT));
        todayVo.setRangeStart("00:00");
        todayVo.setRangeEnd("--");
        todayVo.setSeries(buildSlots(Map.of(), 1, SLOT_COUNT));
        todayVo.setYesterdaySeries(buildSlots(Map.of(), 1, SLOT_COUNT));
    }
    private BigDecimal sumActive(Map<Integer, BigDecimal[]> slots) {
        return sumActive(slots, SLOT_COUNT);
    }
    private BigDecimal sumActive(Map<Integer, BigDecimal[]> slots, int maxSlot) {
        BigDecimal total = BigDecimal.ZERO;
        for (Map.Entry<Integer, BigDecimal[]> entry : slots.entrySet()) {
            if (entry.getKey() <= maxSlot) {
                total = total.add(entry.getValue()[0]);
            }
        }
        return total.setScale(3, RoundingMode.HALF_UP);
    }
    private int lastDataSlot(Map<Integer, BigDecimal[]> slots) {
        int last = 0;
        for (Integer slot : slots.keySet()) {
            if (slot > last) {
                last = slot;
            }
        }
        return last;
    }
    /** æ—¶é—´ç‰‡ç»“束时间 â†’ 1..96;24:00 è®°ä¸ºç¬¬ 96 ç‰‡ã€‚ */
    private int slotIndex(LocalDateTime time) {
        int minute = time.getHour() * 60 + time.getMinute();
        if (minute == 0) {
            return SLOT_COUNT;
        }
        return Math.min(SLOT_COUNT, Math.max(1, (minute + 14) / 15));
    }
    private String labelOfSlot(int slot) {
        if (slot >= SLOT_COUNT) {
            return "23:59";
        }
        int minute = slot * 15;
        return String.format("%02d:%02d", minute / 60, minute % 60);
    }
    /* ------------------------------------------------------------------ */
    /* ç”µèƒ½è´¨é‡                                                             */
    /* ------------------------------------------------------------------ */
    private void fillQuality(ElectricMeterDashboardVO.Quality qualityVo, LocalDate start, LocalDate end,
                             int offset, List<MeterQuarterPoint> points) {
        Map<LocalDateTime, BigDecimal[]> hourMap = new TreeMap<>();
        for (MeterQuarterPoint point : points) {
            if (point.getTime() == null || point.getActive() == null) {
                continue;
            }
            LocalDate day = point.getTime().toLocalDate();
            if (day.isBefore(start) || day.isAfter(end)) {
                continue;
            }
            LocalDateTime hour = point.getTime().truncatedTo(ChronoUnit.HOURS);
            BigDecimal[] values = hourMap.computeIfAbsent(hour,
                    key -> new BigDecimal[]{BigDecimal.ZERO, BigDecimal.ZERO, BigDecimal.ZERO});
            values[0] = values[0].add(point.getActive());
            values[1] = values[1].add(point.getReactive() == null ? BigDecimal.ZERO : point.getReactive());
            // è®°å½•该小时实际有几个 15 åˆ†é’Ÿç‚¹ï¼šæœ€åŽä¸€ä¸ªå°æ—¶å¯èƒ½è¿˜æ²¡èµ°å®Œï¼Œä¸èƒ½ç›´æŽ¥æŒ‰ 1 å°æ—¶å¹³å‡
            values[2] = values[2].add(BigDecimal.ONE);
        }
        List<ElectricMeterDashboardVO.PowerPoint> series = new ArrayList<>(hourMap.size());
        for (Map.Entry<LocalDateTime, BigDecimal[]> entry : hourMap.entrySet()) {
            BigDecimal activeEnergy = entry.getValue()[0];
            BigDecimal reactiveEnergy = entry.getValue()[1];
            double hours = Math.max(entry.getValue()[2].doubleValue() * 0.25, 0.25);
            double p = activeEnergy.doubleValue() / hours;
            double q = reactiveEnergy.doubleValue() / hours;
            ElectricMeterDashboardVO.PowerPoint powerPoint = new ElectricMeterDashboardVO.PowerPoint();
            powerPoint.setTime(entry.getKey().format(LABEL_FORMAT));
            powerPoint.setActivePower(BigDecimal.valueOf(p).setScale(3, RoundingMode.HALF_UP));
            powerPoint.setReactivePower(BigDecimal.valueOf(q).setScale(3, RoundingMode.HALF_UP));
            if (p > 0 || q > 0) {
                double factor = p / Math.sqrt(p * p + q * q);
                powerPoint.setPowerFactor(BigDecimal.valueOf(Math.min(factor, 1.0))
                        .setScale(3, RoundingMode.HALF_UP));
            }
            series.add(powerPoint);
        }
        qualityVo.setWeekStart(start.format(DAY_FORMAT));
        qualityVo.setWeekEnd(end.format(DAY_FORMAT));
        qualityVo.setLabel(offset == 1 ? "上一周" : "近一周");
        qualityVo.setSeries(series);
    }
    private void fillEmptyQuality(ElectricMeterDashboardVO.Quality qualityVo, LocalDate start, LocalDate end,
                                  int offset) {
        qualityVo.setWeekStart(start.format(DAY_FORMAT));
        qualityVo.setWeekEnd(end.format(DAY_FORMAT));
        qualityVo.setLabel(offset == 1 ? "上一周" : "近一周");
        qualityVo.setSeries(new ArrayList<>());
    }
    /* ------------------------------------------------------------------ */
    /* æ•°æ®èŽ·å–                                                             */
    /* ------------------------------------------------------------------ */
    private List<MeterQuarterPoint> queryDevices(List<EnergyMeterDevice> devices, LocalDate start, LocalDate end) {
        List<MeterQuarterPoint> all = new ArrayList<>();
        for (EnergyMeterDevice device : devices) {
            // æ‰‹å·¥æŠ„表设备没有 15 åˆ†é’Ÿæ•°æ®ï¼Œä¸å‘起第三方请求
            if (EnergyMeterConstants.isManual(device.getSourceType())) {
                continue;
            }
            try {
                all.addAll(queryDevice(device, start, end));
            } catch (MeterProviderException e) {
                // å•台设备失败不影响其他设备;页面仍然展示已有数据
                log.warn("电表看板查询 15 åˆ†é’Ÿæ•°æ®å¤±è´¥ meterId={} {}~{}: {}",
                        device.getId(), start, end, e.getMessage());
            }
        }
        return all;
    }
    private List<MeterQuarterPoint> queryDevice(EnergyMeterDevice device, LocalDate start, LocalDate end) {
        String key = device.getId() + ":" + start + ":" + end;
        CachedPoints cached = cache.get(key);
        long now = System.currentTimeMillis();
        if (cached != null && now - cached.at < CACHE_TTL_MS) {
            return cached.points;
        }
        List<MeterQuarterPoint> points = provider.queryQuarterUsage(EnergyMeterConstants.TYPE_ELECTRIC,
                device.getMeterAddress(), device.getProviderMeterId(), start, end);
        // ç¬¬ä¸‰æ–¹è¿”回的通信地址必须与本地绑定一致,避免把别的表算进来
        String expected = StringUtils.trim(device.getMeterAddress());
        if (StringUtils.isNotBlank(expected)) {
            points.removeIf(point -> StringUtils.isNotBlank(point.getAddress())
                    && !expected.equals(StringUtils.trim(point.getAddress())));
        }
        if (cache.size() > 128) {
            cache.clear();
        }
        cache.put(key, new CachedPoints(now, points));
        return points;
    }
    private List<EnergyMeterDevice> resolveDevices(Long meterId) {
        if (meterId != null) {
            EnergyMeterDevice device = deviceMapper.selectById(meterId);
            if (device == null) {
                throw new ServiceException("设备不存在或已删除");
            }
            if (!EnergyMeterConstants.isElectric(device.getMeterType())) {
                throw new ServiceException("该设备不是电表");
            }
            Long tenantId = EnergyMeterSupport.currentTenantId();
            if (tenantId != null && device.getTenantId() != null && !tenantId.equals(device.getTenantId())) {
                throw new ServiceException("无权访问该设备");
            }
            return List.of(device);
        }
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null) {
            wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
        }
        wrapper.eq(EnergyMeterDevice::getMeterType, EnergyMeterConstants.TYPE_ELECTRIC)
                .eq(EnergyMeterDevice::getEnabled, 1)
                .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED)
                .orderByAsc(EnergyMeterDevice::getId);
        return deviceMapper.selectList(wrapper);
    }
    private String scopeLabel(List<EnergyMeterDevice> devices, Long meterId) {
        if (meterId != null && !devices.isEmpty()) {
            EnergyMeterDevice device = devices.get(0);
            return device.getDisplayName() + "(" + device.getMeterAddress() + ")";
        }
        return "全部已绑定电表(" + devices.size() + " å°ï¼‰";
    }
    private record CachedPoints(long at, List<MeterQuarterPoint> points) {
    }
}
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterDemoServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,132 @@
package com.ruoyi.energymeter.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.energymeter.constant.EnergyMeterConstants;
import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.service.EnergyMeterDemoService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.List;
/**
 * æ¼”示数据实现:气表按天生成模拟表码与用量。
 *
 * <p>模拟值是“设备ID + æ—¥æœŸâ€çš„确定性函数,重复生成同一天结果一致,便于核对。</p>
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class EnergyMeterDemoServiceImpl implements EnergyMeterDemoService {
    private final EnergyMeterDeviceMapper deviceMapper;
    private final EnergyMeterMeasurementMapper measurementMapper;
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int generateGasDemo(LocalDate date) {
        if (date == null) {
            return 0;
        }
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(EnergyMeterDevice::getMeterType, EnergyMeterConstants.TYPE_GAS)
                .eq(EnergyMeterDevice::getSimulated, 1)
                .eq(EnergyMeterDevice::getEnabled, 1);
        List<EnergyMeterDevice> devices = deviceMapper.selectList(wrapper);
        if (devices.isEmpty()) {
            return 0;
        }
        int generated = 0;
        LocalDateTime measureTime = date.atStartOfDay();
        for (EnergyMeterDevice device : devices) {
            if (hasUsage(device.getId(), measureTime)) {
                // æ•°æ®å·²å­˜åœ¨æ—¶åªåˆ·æ–°é‡‡é›†æ—¶é—´ï¼Œä¿è¯â€œæœ€è¿‘采集/在线”反映最近一次自动生成
                deviceMapper.markCollectSuccess(device.getId(), EnergyMeterSupport.now());
                continue;
            }
            BigDecimal usage = simulatedUsage(device.getId(), date);
            BigDecimal previous = previousReading(device, measureTime);
            BigDecimal reading = previous.add(usage);
            EnergyMeterMeasurement usagePoint = new EnergyMeterMeasurement();
            usagePoint.setTenantId(device.getTenantId());
            usagePoint.setMeterId(device.getId());
            usagePoint.setMetricCode(EnergyMeterConstants.METRIC_GAS_USAGE);
            usagePoint.setGranularity(EnergyMeterConstants.GRANULARITY_DAY);
            usagePoint.setMeasureTime(measureTime);
            usagePoint.setMetricValue(usage);
            usagePoint.setUnit(EnergyMeterConstants.UNIT_GAS);
            usagePoint.setSourceApi(EnergyMeterConstants.SOURCE_MANUAL);
            usagePoint.setSourceRecordTime(measureTime);
            usagePoint.setCollectedAt(EnergyMeterSupport.now());
            usagePoint.setRemark("模拟数据:每日用量");
            EnergyMeterMeasurement readingPoint = new EnergyMeterMeasurement();
            readingPoint.setTenantId(device.getTenantId());
            readingPoint.setMeterId(device.getId());
            readingPoint.setMetricCode(EnergyMeterConstants.METRIC_GAS_READING);
            readingPoint.setGranularity(EnergyMeterConstants.GRANULARITY_POINT);
            readingPoint.setMeasureTime(measureTime);
            readingPoint.setMetricValue(reading);
            readingPoint.setUnit(EnergyMeterConstants.UNIT_GAS);
            readingPoint.setSourceApi(EnergyMeterConstants.SOURCE_MANUAL);
            readingPoint.setSourceRecordTime(measureTime);
            readingPoint.setCollectedAt(EnergyMeterSupport.now());
            readingPoint.setRemark("模拟数据:每日表码");
            measurementMapper.upsertBatch(List.of(usagePoint, readingPoint));
            // é‡‡é›†æ—¶é—´æŒ‰å®žé™…执行时间记录,而不是数据所属日期的 00:00
            deviceMapper.markCollectSuccess(device.getId(), EnergyMeterSupport.now());
            generated++;
        }
        log.info("气表演示数据生成完成 date={} è®¾å¤‡æ•°={}", date, generated);
        return generated;
    }
    private boolean hasUsage(Long meterId, LocalDateTime measureTime) {
        LambdaQueryWrapper<EnergyMeterMeasurement> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(EnergyMeterMeasurement::getMeterId, meterId)
                .eq(EnergyMeterMeasurement::getMetricCode, EnergyMeterConstants.METRIC_GAS_USAGE)
                .eq(EnergyMeterMeasurement::getMeasureTime, measureTime);
        return measurementMapper.selectCount(wrapper) > 0;
    }
    /**
     * ä¸Šä¸€æœŸè¡¨ç ï¼šå–该日期之前的最近一条示值,没有就用设备起始表码。
     */
    private BigDecimal previousReading(EnergyMeterDevice device, LocalDateTime measureTime) {
        EnergyMeterMeasurement previous = measurementMapper.selectPreviousPoint(device.getId(),
                EnergyMeterConstants.METRIC_GAS_READING, measureTime);
        if (previous != null && previous.getMetricValue() != null) {
            return previous.getMetricValue();
        }
        return device.getInitialReading() == null ? BigDecimal.ZERO : device.getInitialReading();
    }
    /**
     * ç¡®å®šæ€§æ¨¡æ‹Ÿç”¨é‡ï¼šæŒ‰é£Ÿå“åŽ‚è’¸ç…®/灭菌的实际用气量估算。
     *
     * <p>估算口径:蒸煮锅按 4~6 m³/h、每天 6~8 å°æ—¶ï¼Œå•台约 25~35 m³/天;
     * ç­èŒé”…、小煮锅约 14~20 m³/天。基础值按设备区分,再叠加季节波动与噪点。</p>
     */
    private BigDecimal simulatedUsage(Long meterId, LocalDate date) {
        long seed = meterId * 131L + date.toEpochDay();
        double base = 14 + (meterId % 5) * 5;
        double season = base * 0.1 * Math.sin(2 * Math.PI * date.getDayOfYear() / 365);
        double noise = ((seed * 37 % 200) / 200.0 - 0.5) * 3;
        double usage = Math.max(2, base + season + noise);
        return BigDecimal.valueOf(usage).setScale(2, RoundingMode.HALF_UP);
    }
}
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterDeviceServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,692 @@
package com.ruoyi.energymeter.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.LambdaUpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.energymeter.constant.EnergyMeterConstants;
import com.ruoyi.energymeter.dto.EnergyMeterDeviceQuery;
import com.ruoyi.energymeter.dto.MeterArchive;
import com.ruoyi.energymeter.dto.MeterBindRequest;
import com.ruoyi.energymeter.dto.MeterManualBindRequest;
import com.ruoyi.energymeter.dto.MeterReadingRequest;
import com.ruoyi.energymeter.dto.MeterUpdateRequest;
import com.ruoyi.energymeter.dto.MeterUsageRow;
import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterSyncTaskMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncTask;
import com.ruoyi.energymeter.provider.MeterProviderClient;
import com.ruoyi.energymeter.service.EnergyMeterDeviceService;
import com.ruoyi.energymeter.service.EnergyMeterSyncService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import com.ruoyi.energymeter.vo.MeterArchiveVO;
import com.ruoyi.energymeter.vo.MeterReadingResultVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.stream.Collectors;
/**
 * èƒ½è€—表设备服务实现:第三方接入设备 + æ‰‹å·¥æŠ„表设备。
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class EnergyMeterDeviceServiceImpl extends ServiceImpl<EnergyMeterDeviceMapper, EnergyMeterDevice>
        implements EnergyMeterDeviceService {
    private final EnergyMeterDeviceMapper deviceMapper;
    private final EnergyMeterMeasurementMapper measurementMapper;
    private final EnergyMeterSyncTaskMapper taskMapper;
    private final MeterProviderClient provider;
    private final EnergyMeterSyncService syncService;
    @Override
    public IPage<EnergyMeterDevice> pageDevices(Page<EnergyMeterDevice> page, EnergyMeterDeviceQuery query) {
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null) {
            wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
        }
        if (query != null) {
            if (StringUtils.isNotBlank(query.getMeterType())) {
                wrapper.eq(EnergyMeterDevice::getMeterType, normalizeType(query.getMeterType()));
            }
            if (StringUtils.isNotBlank(query.getSourceType())) {
                wrapper.eq(EnergyMeterDevice::getSourceType, query.getSourceType().trim().toUpperCase());
            }
            if (StringUtils.isNotBlank(query.getMeterAddress())) {
                wrapper.eq(EnergyMeterDevice::getMeterAddress, query.getMeterAddress().trim());
            }
            if (query.getEnabled() != null) {
                wrapper.eq(EnergyMeterDevice::getEnabled, query.getEnabled());
            }
            if (query.getSimulated() != null) {
                wrapper.eq(EnergyMeterDevice::getSimulated, query.getSimulated());
            }
            if (StringUtils.isNotBlank(query.getCollectionMode())) {
                String mode = query.getCollectionMode().trim().toUpperCase();
                if ("AUTO".equals(mode)) {
                    // ç¬¬ä¸‰æ–¹æŽ¥å…¥ + ç³»ç»Ÿæ¯æ—¥è‡ªåŠ¨ç”Ÿæˆï¼Œéƒ½å±žäºŽè‡ªåŠ¨é‡‡é›†
                    wrapper.and(w -> w.eq(EnergyMeterDevice::getSourceType,
                                    EnergyMeterConstants.SOURCE_TYPE_PROVIDER)
                            .or().eq(EnergyMeterDevice::getSimulated, 1));
                } else if ("MANUAL".equals(mode)) {
                    wrapper.eq(EnergyMeterDevice::getSourceType, EnergyMeterConstants.SOURCE_TYPE_MANUAL)
                            .eq(EnergyMeterDevice::getSimulated, 0);
                }
            }
            if (StringUtils.isNotBlank(query.getBindStatus())) {
                wrapper.eq(EnergyMeterDevice::getBindStatus, query.getBindStatus());
            }
            if (StringUtils.isNotBlank(query.getKeyword())) {
                String keyword = query.getKeyword().trim();
                wrapper.and(w -> w.like(EnergyMeterDevice::getDisplayName, keyword)
                        .or().like(EnergyMeterDevice::getProviderName, keyword)
                        .or().like(EnergyMeterDevice::getMeterAddress, keyword));
            }
        }
        wrapper.orderByAsc(EnergyMeterDevice::getMeterType)
                .orderByDesc(EnergyMeterDevice::getIsSeed)
                .orderByAsc(EnergyMeterDevice::getId);
        IPage<EnergyMeterDevice> result = deviceMapper.selectPage(page, wrapper);
        enrichDevices(result.getRecords());
        return result;
    }
    @Override
    public List<EnergyMeterDevice> listDeviceOptions(String meterType) {
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null) {
            wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
        }
        if (StringUtils.isNotBlank(meterType)) {
            wrapper.eq(EnergyMeterDevice::getMeterType, normalizeType(meterType));
        }
        wrapper.eq(EnergyMeterDevice::getEnabled, 1)
                .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED)
                .orderByAsc(EnergyMeterDevice::getId);
        return deviceMapper.selectList(wrapper);
    }
    @Override
    public MeterArchiveVO verifyArchive(String meterType, String meterAddress) {
        String normalizedType = normalizeType(meterType);
        String address = StringUtils.trim(meterAddress);
        if (StringUtils.isBlank(address)) {
            throw new ServiceException("通信地址不能为空");
        }
        MeterArchive archive = provider.queryArchive(normalizedType, address);
        MeterArchiveVO vo = new MeterArchiveVO();
        vo.setMeterType(normalizedType);
        vo.setMeterAddress(address);
        vo.setProviderMeterId(archive.getProviderMeterId());
        vo.setProviderName(archive.getProviderName());
        vo.setAreaId(archive.getAreaId());
        vo.setAreaName(archive.getAreaName());
        vo.setOnlineStatus(archive.getOnlineStatus());
        vo.setParentProviderId(archive.getParentProviderId());
        vo.setMockMode(provider.mockMode());
        EnergyMeterDevice existing = findBinding(normalizedType, address);
        if (existing == null) {
            vo.setBindable(true);
            vo.setMessage("验证通过,可新增绑定");
        } else {
            vo.setExistingDeviceId(existing.getId());
            vo.setExistingEnabled(Integer.valueOf(1).equals(existing.getEnabled()));
            boolean enabled = Integer.valueOf(1).equals(existing.getEnabled());
            vo.setBindable(!enabled);
            vo.setMessage(enabled
                    ? "该通信地址已在本地绑定,请勿重复新增"
                    : "该地址已存在停用设备,确认后将重新启用并补采停用期间数据");
        }
        return vo;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public EnergyMeterDevice bind(MeterBindRequest request) {
        String normalizedType = normalizeType(request.getMeterType());
        String address = StringUtils.trim(request.getMeterAddress());
        if (StringUtils.isBlank(address)) {
            throw new ServiceException("通信地址不能为空");
        }
        MeterArchive archive = provider.queryArchive(normalizedType, address);
        EnergyMeterDevice existing = findBinding(normalizedType, address);
        if (existing != null && Integer.valueOf(1).equals(existing.getEnabled())) {
            throw new ServiceException("该通信地址已在本地绑定,请勿重复新增");
        }
        if (existing != null && EnergyMeterConstants.isManual(existing.getSourceType())) {
            throw new ServiceException("该表号已用于手工抄表设备,请先删除原设备或改用其他表号");
        }
        LocalDate yesterday = EnergyMeterSupport.today().minusDays(1);
        // é‡‡é›†èµ·ç‚¹ç”±ç³»ç»Ÿè‡ªåŠ¨ç¡®å®šï¼šå·²åœç”¨è®¾å¤‡æ²¿ç”¨åŽŸèµ·ç‚¹ï¼Œæ–°è®¾å¤‡é»˜è®¤ä¸Šä¸€è‡ªç„¶å¹´ 1 æœˆ 1 æ—¥
        LocalDate startDate = resolveStartDate(null,
                existing != null ? existing.getSyncStartDate() : null, yesterday);
        EnergyMeterDevice device = existing == null ? new EnergyMeterDevice() : existing;
        device.setMeterType(normalizedType);
        device.setSourceType(EnergyMeterConstants.SOURCE_TYPE_PROVIDER);
        device.setMeterAddress(address);
        device.setProviderMeterId(archive.getProviderMeterId());
        device.setProviderName(archive.getProviderName());
        device.setProviderAreaId(archive.getAreaId());
        device.setProviderAreaName(archive.getAreaName());
        device.setParentProviderId(archive.getParentProviderId());
        device.setOnlineStatus(archive.getOnlineStatus());
        device.setBindStatus(EnergyMeterConstants.BIND_VERIFIED);
        device.setEnabled(1);
        device.setSyncStartDate(startDate);
        device.setDisplayName(StringUtils.isNotBlank(request.getDisplayName())
                ? request.getDisplayName().trim()
                : StringUtils.defaultIfBlank(archive.getProviderName(), address));
        device.setLocation(StringUtils.defaultIfBlank(request.getLocation(), device.getLocation()));
        if (device.getIsSeed() == null) {
            device.setIsSeed(0);
        }
        if (existing == null) {
            deviceMapper.insert(device);
        } else {
            deviceMapper.updateById(device);
        }
        log.info("能耗表绑定成功 meterId={} type={} address={}", device.getId(), normalizedType, address);
        try {
            syncService.triggerBackfill(device, startDate, yesterday);
        } catch (Exception e) {
            log.warn("绑定成功但历史补采批次创建失败 meterId={}: {}", device.getId(), e.getMessage());
        }
        return device;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public EnergyMeterDevice addManual(MeterManualBindRequest request) {
        String type = normalizeType(request.getMeterType());
        String address = StringUtils.trim(request.getMeterAddress());
        if (StringUtils.isBlank(address)) {
            throw new ServiceException("表号不能为空");
        }
        if (request.getInitialReading() == null) {
            throw new ServiceException("起始表码不能为空");
        }
        if (request.getInitialReading().signum() < 0) {
            throw new ServiceException("起始表码不能为负数");
        }
        LocalDate today = EnergyMeterSupport.today();
        LocalDate initialDate = request.getInitialReadingDate() == null ? today : request.getInitialReadingDate();
        if (initialDate.isAfter(today)) {
            throw new ServiceException("起始表码日期不能晚于今天");
        }
        EnergyMeterDevice existing = findBinding(type, address);
        if (existing != null) {
            if (Integer.valueOf(1).equals(existing.getEnabled())) {
                throw new ServiceException("该表号已存在有效的" + EnergyMeterConstants.typeName(type) + "设备,请勿重复新增");
            }
            if (!EnergyMeterConstants.isManual(existing.getSourceType())) {
                throw new ServiceException("该表号已被第三方接入设备占用,请先停用原设备或改用其他表号");
            }
        }
        EnergyMeterDevice device = existing == null ? new EnergyMeterDevice() : existing;
        device.setMeterType(type);
        device.setSourceType(EnergyMeterConstants.SOURCE_TYPE_MANUAL);
        device.setMeterAddress(address);
        device.setDisplayName(StringUtils.isNotBlank(request.getDisplayName())
                ? request.getDisplayName().trim()
                : address);
        device.setLocation(StringUtils.defaultIfBlank(request.getLocation(), device.getLocation()));
        device.setBindStatus(EnergyMeterConstants.BIND_VERIFIED);
        device.setEnabled(1);
        device.setIsSeed(0);
        device.setInitialReading(request.getInitialReading());
        device.setInitialReadingDate(initialDate);
        device.setSyncStartDate(initialDate);
        device.setOnlineStatus(null);
        device.setLastErrorCode(null);
        device.setLastErrorMessage(null);
        if (existing == null) {
            deviceMapper.insert(device);
        } else {
            deviceMapper.updateById(device);
        }
        // èµ·å§‹è¡¨ç ä½œä¸ºåŸºå‡†è®°å½•写入,不计入用量
        EnergyMeterMeasurement basePoint = newManualPoint(device, initialDate.atStartOfDay(),
                request.getInitialReading(), "起始表码");
        measurementMapper.upsertBatch(List.of(basePoint));
        deviceMapper.markCollectSuccess(device.getId(), EnergyMeterSupport.now());
        log.info("新增手工抄表设备 meterId={} type={} address={} initial={}",
                device.getId(), type, address, request.getInitialReading());
        return deviceMapper.selectById(device.getId());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeDevice(Long id) {
        EnergyMeterDevice device = getOwnedDevice(id);
        if (Integer.valueOf(1).equals(device.getIsSeed())) {
            throw new ServiceException("初始化设备不允许删除,如需停用请编辑设备状态");
        }
        if (!EnergyMeterConstants.isManual(device.getSourceType())) {
            throw new ServiceException("第三方接入设备请使用“停用”,不允许删除");
        }
        measurementMapper.delete(new LambdaQueryWrapper<EnergyMeterMeasurement>()
                .eq(EnergyMeterMeasurement::getMeterId, id));
        removeById(id);
        log.info("删除手工抄表设备 meterId={} address={}", id, device.getMeterAddress());
    }
    @Override
    public IPage<EnergyMeterMeasurement> pageReadings(Long meterId, long current, long size) {
        EnergyMeterDevice device = getOwnedDevice(meterId);
        String readingMetric = EnergyMeterConstants.readingMetric(device.getMeterType());
        String usageMetric = EnergyMeterConstants.usageMetric(device.getMeterType());
        long safeSize = Math.min(Math.max(size, 1), 200);
        Page<EnergyMeterMeasurement> page = new Page<>(Math.max(current, 1), safeSize);
        LambdaQueryWrapper<EnergyMeterMeasurement> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(EnergyMeterMeasurement::getMeterId, device.getId())
                .eq(EnergyMeterMeasurement::getMetricCode, readingMetric)
                .eq(EnergyMeterMeasurement::getGranularity, EnergyMeterConstants.GRANULARITY_POINT)
                .orderByDesc(EnergyMeterMeasurement::getMeasureTime)
                .orderByDesc(EnergyMeterMeasurement::getId);
        IPage<EnergyMeterMeasurement> result = measurementMapper.selectPage(page, wrapper);
        List<EnergyMeterMeasurement> records = result.getRecords();
        if (!records.isEmpty()) {
            LocalDateTime maxTime = records.get(0).getMeasureTime();
            LocalDateTime minTime = records.get(records.size() - 1).getMeasureTime();
            List<EnergyMeterMeasurement> usages = measurementMapper.selectList(
                    new LambdaQueryWrapper<EnergyMeterMeasurement>()
                            .eq(EnergyMeterMeasurement::getMeterId, device.getId())
                            .eq(EnergyMeterMeasurement::getMetricCode, usageMetric)
                            .eq(EnergyMeterMeasurement::getGranularity, EnergyMeterConstants.GRANULARITY_DAY)
                            .ge(EnergyMeterMeasurement::getMeasureTime, minTime)
                            .le(EnergyMeterMeasurement::getMeasureTime, maxTime));
            Map<LocalDate, BigDecimal> usageMap = new HashMap<>();
            for (EnergyMeterMeasurement usage : usages) {
                usageMap.put(usage.getMeasureTime().toLocalDate(), usage.getMetricValue());
            }
            for (EnergyMeterMeasurement record : records) {
                record.setUsageValue(usageMap.get(record.getMeasureTime().toLocalDate()));
                record.setMeterName(device.getDisplayName());
                record.setMeterType(device.getMeterType());
                record.setMeterAddress(device.getMeterAddress());
            }
        }
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public MeterReadingResultVO addReading(Long meterId, MeterReadingRequest request) {
        EnergyMeterDevice device = requireManualDevice(meterId);
        if (request.getReadingDate() == null) {
            throw new ServiceException("抄表日期不能为空");
        }
        if (request.getReadingValue() == null) {
            throw new ServiceException("本次表码不能为空");
        }
        LocalDate date = request.getReadingDate();
        LocalDate today = EnergyMeterSupport.today();
        if (date.isAfter(today)) {
            throw new ServiceException("抄表日期不能晚于今天");
        }
        if (device.getInitialReadingDate() != null && date.isBefore(device.getInitialReadingDate())) {
            throw new ServiceException("抄表日期不能早于起始表码日期("
                    + device.getInitialReadingDate() + ")");
        }
        BigDecimal value = request.getReadingValue();
        if (value.signum() < 0) {
            throw new ServiceException("本次表码不能为负数");
        }
        BigDecimal previous = resolvePreviousReading(device, date.atStartOfDay());
        if (previous != null && value.compareTo(previous) < 0) {
            throw new ServiceException("本次表码不能小于上期表码(上期 " + previous.stripTrailingZeros().toPlainString() + ")");
        }
        EnergyMeterMeasurement point = newManualPoint(device, date.atStartOfDay(), value,
                StringUtils.defaultIfBlank(request.getRemark(), null));
        measurementMapper.upsertBatch(List.of(point));
        BigDecimal usage = previous == null ? null : value.subtract(previous);
        upsertUsage(device, date, usage);
        // è‹¥è¯¥æ—¥æœŸä¹‹åŽå·²æœ‰æŠ„表,重新计算下一期的用量
        EnergyMeterMeasurement next = measurementMapper.selectNextPoint(device.getId(),
                EnergyMeterConstants.readingMetric(device.getMeterType()), date.atStartOfDay());
        if (next != null) {
            recalcUsage(device, next);
        }
        deviceMapper.markCollectSuccess(device.getId(), EnergyMeterSupport.now());
        MeterReadingResultVO result = new MeterReadingResultVO();
        result.setMeterId(device.getId());
        result.setMeasureTime(date.atStartOfDay());
        result.setReadingValue(value);
        result.setPreviousReading(previous);
        result.setUsage(usage);
        result.setUnit(EnergyMeterConstants.unit(device.getMeterType()));
        return result;
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public void removeReading(Long meterId, Long measurementId) {
        EnergyMeterDevice device = requireManualDevice(meterId);
        EnergyMeterMeasurement record = measurementMapper.selectById(measurementId);
        if (record == null || !meterId.equals(record.getMeterId())) {
            throw new ServiceException("抄表记录不存在");
        }
        if (!EnergyMeterConstants.GRANULARITY_POINT.equals(record.getGranularity())) {
            throw new ServiceException("只能删除抄表记录");
        }
        if (device.getInitialReadingDate() != null
                && device.getInitialReadingDate().equals(record.getMeasureTime().toLocalDate())) {
            throw new ServiceException("起始表码不允许删除,如需修改请编辑设备");
        }
        EnergyMeterMeasurement next = measurementMapper.selectNextPoint(device.getId(),
                EnergyMeterConstants.readingMetric(device.getMeterType()), record.getMeasureTime());
        measurementMapper.deleteById(measurementId);
        deleteUsage(device, record.getMeasureTime().toLocalDate());
        if (next != null) {
            recalcUsage(device, next);
        }
        log.info("删除抄表记录 meterId={} measurementId={} date={}",
                meterId, measurementId, record.getMeasureTime().toLocalDate());
    }
    @Override
    @Transactional(rollbackFor = Exception.class)
    public EnergyMeterDevice updateLocal(Long id, MeterUpdateRequest request) {
        EnergyMeterDevice device = getOwnedDevice(id);
        LambdaUpdateWrapper<EnergyMeterDevice> wrapper = new LambdaUpdateWrapper<>();
        wrapper.eq(EnergyMeterDevice::getId, id);
        if (request.getDisplayName() != null) {
            if (StringUtils.isBlank(request.getDisplayName())) {
                throw new ServiceException("显示名称不能为空");
            }
            wrapper.set(EnergyMeterDevice::getDisplayName, request.getDisplayName().trim());
        }
        if (request.getLocation() != null) {
            wrapper.set(EnergyMeterDevice::getLocation, request.getLocation());
        }
        boolean reEnabled = false;
        if (request.getEnabled() != null) {
            if (request.getEnabled() != 0 && request.getEnabled() != 1) {
                throw new ServiceException("启停用参数不合法");
            }
            reEnabled = request.getEnabled() == 1 && !Integer.valueOf(1).equals(device.getEnabled());
            wrapper.set(EnergyMeterDevice::getEnabled, request.getEnabled());
        }
        deviceMapper.update(null, wrapper);
        EnergyMeterDevice updated = getOwnedDevice(id);
        if (reEnabled && !EnergyMeterConstants.isManual(updated.getSourceType())) {
            try {
                syncService.triggerBackfillGap(updated);
            } catch (Exception e) {
                log.warn("设备启用成功但补采批次创建失败 meterId={}: {}", id, e.getMessage());
            }
        }
        enrichDevices(List.of(updated));
        return updated;
    }
    @Override
    public EnergyMeterDevice getOwnedDevice(Long id) {
        EnergyMeterDevice device = deviceMapper.selectById(id);
        if (device == null) {
            throw new ServiceException("设备不存在或已删除");
        }
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null && device.getTenantId() != null && !tenantId.equals(device.getTenantId())) {
            throw new ServiceException("无权访问该设备");
        }
        return device;
    }
    @Override
    public List<EnergyMeterDevice> listEnabledDevices(String meterType) {
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null) {
            wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
        }
        if (StringUtils.isNotBlank(meterType)) {
            wrapper.eq(EnergyMeterDevice::getMeterType, normalizeType(meterType));
        }
        wrapper.eq(EnergyMeterDevice::getEnabled, 1)
                .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED)
                .orderByAsc(EnergyMeterDevice::getId);
        return deviceMapper.selectList(wrapper);
    }
    @Override
    public boolean mockMode() {
        return provider.mockMode();
    }
    /* ------------------------------------------------------------------ */
    /**
     * è®¡ç®—指定日期之前的上一期表码:优先取抄表记录,其次取设备起始表码。
     */
    private BigDecimal resolvePreviousReading(EnergyMeterDevice device, LocalDateTime time) {
        EnergyMeterMeasurement previous = measurementMapper.selectPreviousPoint(device.getId(),
                EnergyMeterConstants.readingMetric(device.getMeterType()), time);
        if (previous != null) {
            return previous.getMetricValue();
        }
        return device.getInitialReading();
    }
    /**
     * é‡ç®—某条抄表记录对应的日用量(在插入/删除其它抄表记录后调用)。
     */
    private void recalcUsage(EnergyMeterDevice device, EnergyMeterMeasurement point) {
        BigDecimal previous = resolvePreviousReading(device, point.getMeasureTime());
        if (previous == null) {
            deleteUsage(device, point.getMeasureTime().toLocalDate());
            return;
        }
        upsertUsage(device, point.getMeasureTime().toLocalDate(), point.getMetricValue().subtract(previous));
    }
    private void upsertUsage(EnergyMeterDevice device, LocalDate date, BigDecimal usage) {
        if (usage == null) {
            return;
        }
        EnergyMeterMeasurement record = new EnergyMeterMeasurement();
        record.setTenantId(device.getTenantId());
        record.setMeterId(device.getId());
        record.setMetricCode(EnergyMeterConstants.usageMetric(device.getMeterType()));
        record.setGranularity(EnergyMeterConstants.GRANULARITY_DAY);
        record.setMeasureTime(date.atStartOfDay());
        record.setMetricValue(usage);
        record.setUnit(EnergyMeterConstants.unit(device.getMeterType()));
        record.setSourceApi(EnergyMeterConstants.SOURCE_MANUAL);
        record.setSourceRecordTime(date.atStartOfDay());
        record.setCollectedAt(EnergyMeterSupport.now());
        measurementMapper.upsertBatch(List.of(record));
    }
    private void deleteUsage(EnergyMeterDevice device, LocalDate date) {
        measurementMapper.delete(new LambdaQueryWrapper<EnergyMeterMeasurement>()
                .eq(EnergyMeterMeasurement::getMeterId, device.getId())
                .eq(EnergyMeterMeasurement::getMetricCode, EnergyMeterConstants.usageMetric(device.getMeterType()))
                .eq(EnergyMeterMeasurement::getGranularity, EnergyMeterConstants.GRANULARITY_DAY)
                .eq(EnergyMeterMeasurement::getMeasureTime, date.atStartOfDay()));
    }
    private EnergyMeterMeasurement newManualPoint(EnergyMeterDevice device, LocalDateTime time,
                                                  BigDecimal value, String remark) {
        EnergyMeterMeasurement point = new EnergyMeterMeasurement();
        point.setTenantId(device.getTenantId());
        point.setMeterId(device.getId());
        point.setMetricCode(EnergyMeterConstants.readingMetric(device.getMeterType()));
        point.setGranularity(EnergyMeterConstants.GRANULARITY_POINT);
        point.setMeasureTime(time);
        point.setMetricValue(value);
        point.setUnit(EnergyMeterConstants.unit(device.getMeterType()));
        point.setSourceApi(EnergyMeterConstants.SOURCE_MANUAL);
        point.setSourceRecordTime(time);
        point.setCollectedAt(EnergyMeterSupport.now());
        point.setRemark(remark);
        return point;
    }
    private EnergyMeterDevice requireManualDevice(Long meterId) {
        EnergyMeterDevice device = getOwnedDevice(meterId);
        if (!EnergyMeterConstants.isManual(device.getSourceType())) {
            throw new ServiceException("该设备为第三方接入,请使用“查表”获取数据");
        }
        return device;
    }
    private void enrichDevices(List<EnergyMeterDevice> devices) {
        if (devices == null || devices.isEmpty()) {
            return;
        }
        List<Long> ids = devices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList());
        Map<Long, EnergyMeterSyncTask> latestTask = new HashMap<>();
        for (EnergyMeterSyncTask task : taskMapper.selectLatestByMeterIds(ids)) {
            latestTask.put(task.getMeterId(), task);
        }
        LocalDate yesterday = EnergyMeterSupport.today().minusDays(1);
        LocalDateTime start = yesterday.atStartOfDay();
        LocalDateTime end = yesterday.plusDays(1).atStartOfDay();
        Map<Long, BigDecimal> usageMap = new HashMap<>();
        Set<String> types = devices.stream().map(EnergyMeterDevice::getMeterType)
                .filter(StringUtils::isNotBlank).collect(Collectors.toSet());
        for (String type : types) {
            List<Long> typeIds = devices.stream()
                    .filter(d -> type.equals(d.getMeterType()))
                    .map(EnergyMeterDevice::getId).collect(Collectors.toList());
            if (typeIds.isEmpty()) {
                continue;
            }
            for (MeterUsageRow row : measurementMapper.sumUsageByMeter(typeIds,
                    EnergyMeterConstants.usageMetric(type), start, end)) {
                usageMap.put(row.getMeterId(), row.getTotal());
            }
        }
        for (EnergyMeterDevice device : devices) {
            boolean manual = EnergyMeterConstants.isManual(device.getSourceType());
            EnergyMeterSyncTask task = latestTask.get(device.getId());
            if (task != null) {
                device.setLastTaskStatus(task.getStatus());
                device.setLastTaskTime(task.getFinishedAt() != null ? task.getFinishedAt() : task.getStartedAt());
            }
            BigDecimal dayUsage = usageMap.get(device.getId());
            if (dayUsage != null) {
                device.setLastDayUsage(dayUsage);
                device.setLastDayUsageDate(yesterday);
            } else {
                // æ˜¨å¤©æ²¡æœ‰ç”¨é‡æ—¶ï¼ˆæ‰‹å·¥æŠ„表按抄表期入账、第三方设备可能只有当天数据),
                // é€€å›žæœ€è¿‘一条用量及其业务日期,避免列表长期显示 "--"
                EnergyMeterMeasurement latestUsage = measurementMapper.selectLatestPoint(device.getId(),
                        EnergyMeterConstants.usageMetric(device.getMeterType()));
                if (latestUsage != null) {
                    device.setLastDayUsage(latestUsage.getMetricValue());
                    device.setLastDayUsageDate(latestUsage.getMeasureTime().toLocalDate());
                }
            }
            EnergyMeterMeasurement latest = measurementMapper.selectLatestPoint(device.getId(),
                    EnergyMeterConstants.readingMetric(device.getMeterType()));
            if (latest != null) {
                device.setLatestReading(latest.getMetricValue());
                EnergyMeterMeasurement previous = measurementMapper.selectPreviousPoint(device.getId(),
                        EnergyMeterConstants.readingMetric(device.getMeterType()), latest.getMeasureTime());
                if (previous != null) {
                    device.setPreviousReading(previous.getMetricValue());
                } else if (device.getLastDayUsage() != null) {
                    // åªæœ‰ä¸€æ¡è¯»æ•°æ—¶ï¼ˆæ–°è£…表第一天),用“最新表码 - å½“期用量”反推上期表码
                    device.setPreviousReading(latest.getMetricValue().subtract(device.getLastDayUsage()));
                }
            }
            device.setCollectStateText(buildCollectState(device, task));
            device.setMeterTypeName(EnergyMeterConstants.typeName(device.getMeterType()));
            device.setSourceTypeName(EnergyMeterConstants.isAutoCollect(device.getSourceType(), device.getSimulated())
                    ? "自动采集" : "手工抄表");
            device.setUnit(EnergyMeterConstants.unit(device.getMeterType()));
            device.setDeletable(!Integer.valueOf(1).equals(device.getIsSeed()) && manual);
        }
    }
    private String buildCollectState(EnergyMeterDevice device, EnergyMeterSyncTask task) {
        if (EnergyMeterConstants.isSimulated(device.getSimulated())) {
            return "自动采集";
        }
        if (EnergyMeterConstants.isManual(device.getSourceType())) {
            return device.getLastSuccessAt() == null ? "尚未抄表" : "手工抄表";
        }
        if (task == null) {
            return "尚未采集";
        }
        return switch (StringUtils.defaultString(task.getStatus())) {
            case "SUCCESS" -> "采集成功";
            case "EMPTY" -> "确认无数据";
            case "FAILED" -> "采集失败";
            case "RETRY" -> "等待重试";
            case "RUNNING" -> "采集中";
            default -> "待执行";
        };
    }
    private EnergyMeterDevice findBinding(String meterType, String address) {
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(EnergyMeterDevice::getMeterType, meterType)
                .eq(EnergyMeterDevice::getMeterAddress, address);
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null) {
            wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
        }
        List<EnergyMeterDevice> list = deviceMapper.selectList(wrapper);
        return list.isEmpty() ? null : list.get(0);
    }
    private LocalDate resolveStartDate(LocalDate requested, LocalDate existingStart, LocalDate yesterday) {
        LocalDate latestAllowed = yesterday;
        LocalDate defaultStart = LocalDate.of(latestAllowed.getYear() - 1, 1, 1);
        LocalDate start = requested != null ? requested : (existingStart != null ? existingStart : defaultStart);
        if (start.isAfter(latestAllowed)) {
            throw new ServiceException("补采起始日期不能晚于昨天");
        }
        if (start.isBefore(LocalDate.of(2000, 1, 1))) {
            throw new ServiceException("补采起始日期过早,请确认供应商允许的历史范围");
        }
        return start;
    }
    private String normalizeType(String meterType) {
        String type = EnergyMeterConstants.canonicalType(meterType);
        if (type == null) {
            throw new ServiceException("能源类型只支持水表(WATER)、电表(ELECTRIC)或气表(GAS)");
        }
        return type;
    }
}
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterStatServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,389 @@
package com.ruoyi.energymeter.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.energymeter.constant.EnergyMeterConstants;
import com.ruoyi.energymeter.dto.DailyUsageRow;
import com.ruoyi.energymeter.dto.DataRangeRow;
import com.ruoyi.energymeter.dto.MonthlyUsageRow;
import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.provider.MeterProviderClient;
import com.ruoyi.energymeter.service.EnergyMeterStatService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import com.ruoyi.energymeter.vo.EnergyMeterOverviewVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;
/**
 * æ¦‚览统计实现。
 *
 * <p>只从 D ç²’度的 *_USAGE æŒ‡æ ‡æ±‡æ€»ï¼Œç»ä¸ç´¯åŠ  POINT è¡¨ç ï¼›ç¼ºå¤±æ—¥æœŸä¿æŒ null,由前端显示缺口。</p>
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class EnergyMeterStatServiceImpl implements EnergyMeterStatService {
    private static final DateTimeFormatter DATE_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private final EnergyMeterDeviceMapper deviceMapper;
    private final EnergyMeterMeasurementMapper measurementMapper;
    private final MeterProviderClient provider;
    @Override
    public EnergyMeterOverviewVO overview(String meterType, Long meterId, String month, Integer year) {
        String type = normalizeType(meterType);
        YearMonth selectedMonth = parseMonth(month);
        int selectedYear = year == null ? selectedMonth.getYear() : year;
        if (selectedYear < 2000 || selectedYear > 2100) {
            throw new ServiceException("年份不合法");
        }
        EnergyMeterOverviewVO vo = new EnergyMeterOverviewVO();
        vo.setMeterType(type);
        vo.setMetricCode(EnergyMeterConstants.usageMetric(type));
        vo.setUnit(EnergyMeterConstants.unit(type));
        vo.setMeterId(meterId);
        vo.setMockMode(provider.mockMode());
        vo.setSelectedMonth(selectedMonth.toString());
        vo.setSelectedYear(String.valueOf(selectedYear));
        vo.setMonthCost(null);
        vo.setYearCost(null);
        List<EnergyMeterDevice> devices = resolveDevices(type, meterId);
        vo.setDeviceCount(devices.size());
        vo.setScopeLabel(buildScopeLabel(devices, meterId));
        List<Long> meterIds = devices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList());
        LocalDate today = EnergyMeterSupport.today();
        LocalDate yesterday = today.minusDays(1);
        LocalDate monthStart = selectedMonth.atDay(1);
        LocalDate monthEndExclusive = selectedMonth.plusMonths(1).atDay(1);
        YearMonth previousMonth = selectedMonth.minusMonths(1);
        Map<LocalDate, BigDecimal> currentDaily = toDayMap(monthIdsEmpty(meterIds) ? List.of() : measurementMapper.sumDailyUsage(
                meterIds, vo.getMetricCode(), monthStart.atStartOfDay(), monthEndExclusive.atStartOfDay()));
        Map<LocalDate, BigDecimal> previousDaily = toDayMap(monthIdsEmpty(meterIds) ? List.of() : measurementMapper.sumDailyUsage(
                meterIds, vo.getMetricCode(), previousMonth.atDay(1).atStartOfDay(), monthStart.atStartOfDay()));
        int daysInMonth = selectedMonth.lengthOfMonth();
        int daysInPreviousMonth = previousMonth.lengthOfMonth();
        int cutDay = resolveCutDay(selectedMonth, today);
        List<EnergyMeterOverviewVO.MonthDayPoint> monthSeries = new ArrayList<>(daysInMonth);
        BigDecimal monthTotal = BigDecimal.ZERO;
        int collectedDays = 0;
        for (int day = 1; day <= daysInMonth; day++) {
            LocalDate date = monthStart.withDayOfMonth(day);
            BigDecimal currentValue = currentDaily.get(date);
            if (currentValue != null) {
                monthTotal = monthTotal.add(currentValue);
                if (day <= cutDay) {
                    collectedDays++;
                }
            }
            EnergyMeterOverviewVO.MonthDayPoint point = new EnergyMeterOverviewVO.MonthDayPoint();
            point.setDay(date.format(DATE_FORMAT));
            point.setCurrent(currentValue);
            point.setPrevious(day <= daysInPreviousMonth ? previousDaily.get(previousMonth.atDay(day)) : null);
            monthSeries.add(point);
        }
        vo.setMonthSeries(monthSeries);
        boolean monthHasData = !currentDaily.isEmpty();
        vo.setMonthHasData(monthHasData);
        vo.setMonthTotal(monthHasData ? monthTotal : null);
        BigDecimal previousTotal = BigDecimal.ZERO;
        int previousDayCount = 0;
        int compareDays = Math.min(cutDay, daysInPreviousMonth);
        for (int day = 1; day <= compareDays; day++) {
            BigDecimal value = previousDaily.get(previousMonth.atDay(day));
            if (value != null) {
                previousTotal = previousTotal.add(value);
                previousDayCount++;
            }
        }
        vo.setMonthPreviousTotal(previousDayCount > 0 ? previousTotal : null);
        int expectedDays = cutDay;
        // è®¾å¤‡å…¥ç½‘之前的日期不应计入缺口:从设备同步起点与月初的较大者开始算
        if (!devices.isEmpty()) {
            LocalDate earliestSync = devices.stream()
                    .map(EnergyMeterDevice::getSyncStartDate)
                    .filter(java.util.Objects::nonNull)
                    .min(LocalDate::compareTo)
                    .orElse(null);
            if (earliestSync != null && earliestSync.isAfter(monthStart)) {
                int days = (int) java.time.temporal.ChronoUnit.DAYS.between(earliestSync,
                        monthStart.plusDays(cutDay));
                expectedDays = Math.max(0, Math.min(days, cutDay));
            }
        }
        vo.setCollectedDays(collectedDays);
        vo.setExpectedDays(expectedDays);
        vo.setCoverageRate(expectedDays <= 0 ? BigDecimal.valueOf(100)
                : BigDecimal.valueOf(collectedDays * 100.0 / expectedDays).setScale(1, RoundingMode.HALF_UP));
        java.time.LocalDate yearStart = LocalDate.of(selectedYear, 1, 1);
        java.time.LocalDate yearEndExclusive = yearStart.plusYears(1);
        java.time.LocalDate previousYearStart = yearStart.minusYears(1);
        Map<String, BigDecimal> currentMonthly = toMonthMap(monthIdsEmpty(meterIds) ? List.of()
                : measurementMapper.sumMonthlyUsage(meterIds, vo.getMetricCode(),
                yearStart.atStartOfDay(), yearEndExclusive.atStartOfDay()));
        Map<String, BigDecimal> previousMonthly = toMonthMap(monthIdsEmpty(meterIds) ? List.of()
                : measurementMapper.sumMonthlyUsage(meterIds, vo.getMetricCode(),
                previousYearStart.atStartOfDay(), yearStart.atStartOfDay()));
        boolean yearHasData = !currentMonthly.isEmpty();
        vo.setYearHasData(yearHasData);
        int currentYear = today.getYear();
        int cutMonth = selectedYear < currentYear ? 12 : (selectedYear > currentYear ? 0 : today.getMonthValue());
        List<EnergyMeterOverviewVO.YearMonthPoint> yearSeries = new ArrayList<>(12);
        BigDecimal yearTotal = BigDecimal.ZERO;
        for (int m = 1; m <= 12; m++) {
            String key = selectedYear + "-" + String.format("%02d", m);
            String previousKey = (selectedYear - 1) + "-" + String.format("%02d", m);
            BigDecimal currentValue = currentMonthly.get(key);
            if (currentValue != null) {
                yearTotal = yearTotal.add(currentValue);
            }
            EnergyMeterOverviewVO.YearMonthPoint point = new EnergyMeterOverviewVO.YearMonthPoint();
            point.setMonth(key);
            point.setCurrent(currentValue);
            point.setPrevious(previousMonthly.get(previousKey));
            yearSeries.add(point);
        }
        vo.setYearSeries(yearSeries);
        vo.setYearTotal(yearHasData ? yearTotal : null);
        BigDecimal previousYearTotal = BigDecimal.ZERO;
        int previousMonthCount = 0;
        for (int m = 1; m <= cutMonth; m++) {
            BigDecimal value = previousMonthly.get((selectedYear - 1) + "-" + String.format("%02d", m));
            if (value != null) {
                previousYearTotal = previousYearTotal.add(value);
                previousMonthCount++;
            }
        }
        vo.setYearPreviousTotal(previousMonthCount > 0 ? previousYearTotal : null);
        DataRangeRow range = monthIdsEmpty(meterIds) ? null
                : measurementMapper.selectDataRange(meterIds, vo.getMetricCode());
        vo.setDataStart(range != null && range.getMinTime() != null ? range.getMinTime().toLocalDate().format(DATE_FORMAT) : null);
        vo.setDataEnd(range != null && range.getMaxTime() != null ? range.getMaxTime().toLocalDate().format(DATE_FORMAT) : null);
        vo.setDataThrough(yesterday.format(DATE_FORMAT));
        LocalDateTime lastSuccess = devices.stream()
                .map(EnergyMeterDevice::getLastSuccessAt)
                .filter(java.util.Objects::nonNull)
                .max(LocalDateTime::compareTo)
                .orElse(null);
        vo.setLastSuccessAt(lastSuccess == null ? null : lastSuccess.format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")));
        boolean hasData = monthHasData || yearHasData || range != null && range.getMaxTime() != null;
        vo.setHasData(hasData);
        long manualCount = devices.stream()
                .filter(d -> EnergyMeterConstants.isManual(d.getSourceType()))
                .count();
        boolean manualOnly = !devices.isEmpty() && manualCount == devices.size();
        int expectedForStatus = expectedDays;
        if (manualOnly) {
            // æ‰‹å·¥æŠ„表按“抄表期数”计,不按自然日算缺口
            vo.setExpectedDays(collectedDays);
            vo.setCoverageRate(collectedDays > 0 ? BigDecimal.valueOf(100) : BigDecimal.ZERO);
            expectedForStatus = collectedDays;
        }
        applySyncStatus(vo, hasData, lastSuccess, collectedDays, expectedForStatus, monthHasData, yearHasData, manualOnly);
        return vo;
    }
    @Override
    public IPage<EnergyMeterMeasurement> pageMeasurements(Long meterId, String metricCode, String granularity,
                                                          LocalDate start, LocalDate end, long current, long size) {
        EnergyMeterDevice device = requireDevice(meterId);
        long safeSize = Math.min(Math.max(size, 1), 200);
        Page<EnergyMeterMeasurement> page = new Page<>(Math.max(current, 1), safeSize);
        LambdaQueryWrapper<EnergyMeterMeasurement> wrapper = new LambdaQueryWrapper<>();
        wrapper.eq(EnergyMeterMeasurement::getMeterId, device.getId());
        if (StringUtils.isNotBlank(metricCode)) {
            wrapper.eq(EnergyMeterMeasurement::getMetricCode, metricCode);
        }
        if (StringUtils.isNotBlank(granularity)) {
            wrapper.eq(EnergyMeterMeasurement::getGranularity, granularity);
        }
        if (start != null) {
            wrapper.ge(EnergyMeterMeasurement::getMeasureTime, start.atStartOfDay());
        }
        if (end != null) {
            wrapper.lt(EnergyMeterMeasurement::getMeasureTime, end.plusDays(1).atStartOfDay());
        }
        wrapper.orderByDesc(EnergyMeterMeasurement::getMeasureTime)
                .orderByDesc(EnergyMeterMeasurement::getId);
        IPage<EnergyMeterMeasurement> result = measurementMapper.selectPage(page, wrapper);
        result.getRecords().forEach(item -> {
            item.setMeterName(device.getDisplayName());
            item.setMeterType(device.getMeterType());
            item.setMeterAddress(device.getMeterAddress());
        });
        return result;
    }
    /* ------------------------------------------------------------------ */
    private List<EnergyMeterDevice> resolveDevices(String meterType, Long meterId) {
        if (meterId != null) {
            EnergyMeterDevice device = requireDevice(meterId);
            if (!meterType.equals(device.getMeterType())) {
                throw new ServiceException("设备类型与当前能源类型不一致");
            }
            return List.of(device);
        }
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null) {
            wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
        }
        wrapper.eq(EnergyMeterDevice::getMeterType, meterType)
                .eq(EnergyMeterDevice::getEnabled, 1)
                .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED)
                .orderByAsc(EnergyMeterDevice::getId);
        return deviceMapper.selectList(wrapper);
    }
    private EnergyMeterDevice requireDevice(Long meterId) {
        if (meterId == null) {
            throw new ServiceException("设备ID不能为空");
        }
        EnergyMeterDevice device = deviceMapper.selectById(meterId);
        if (device == null) {
            throw new ServiceException("设备不存在或已删除");
        }
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null && device.getTenantId() != null && !tenantId.equals(device.getTenantId())) {
            throw new ServiceException("无权访问该设备");
        }
        return device;
    }
    private String buildScopeLabel(List<EnergyMeterDevice> devices, Long meterId) {
        if (meterId != null && !devices.isEmpty()) {
            EnergyMeterDevice device = devices.get(0);
            return device.getDisplayName() + "(" + device.getMeterAddress() + ")";
        }
        return "全部已绑定设备(" + devices.size() + " å°ï¼‰";
    }
    private void applySyncStatus(EnergyMeterOverviewVO vo, boolean hasData, LocalDateTime lastSuccess,
                                 int collectedDays, int expectedDays, boolean monthHasData, boolean yearHasData,
                                 boolean manualOnly) {
        String status;
        String text;
        if (manualOnly) {
            if (!monthHasData && !yearHasData) {
                status = "EMPTY";
                text = "暂无抄表记录";
            } else if (expectedDays > 0 && collectedDays < expectedDays) {
                status = "PARTIAL";
                text = "抄表数据存在缺口(" + collectedDays + "/" + expectedDays + " å¤©ï¼‰";
            } else {
                status = "OK";
                text = "手工抄表数据";
            }
            vo.setSyncStatus(status);
            vo.setSyncStatusText(text);
            return;
        }
        if (!monthHasData && !yearHasData) {
            status = "EMPTY";
            text = "暂无采集数据";
        } else if (!hasData) {
            status = "EMPTY";
            text = "暂无采集数据";
        } else if (lastSuccess == null
                || lastSuccess.isBefore(EnergyMeterSupport.now().minusHours(36))) {
            status = "STALE";
            text = "超过 36 å°æ—¶æ— æˆåŠŸé‡‡é›†";
        } else if (expectedDays > 0 && collectedDays < expectedDays) {
            status = "PARTIAL";
            text = "数据存在缺口(" + collectedDays + "/" + expectedDays + " å¤©ï¼‰";
        } else {
            status = "OK";
            text = "数据完整";
        }
        vo.setSyncStatus(status);
        vo.setSyncStatusText(text);
    }
    private int resolveCutDay(YearMonth month, LocalDate today) {
        YearMonth currentMonth = YearMonth.from(today);
        if (month.isAfter(currentMonth)) {
            return 0;
        }
        if (month.equals(currentMonth)) {
            return Math.max(today.minusDays(1).getDayOfMonth(), 0);
        }
        return month.lengthOfMonth();
    }
    private Map<LocalDate, BigDecimal> toDayMap(List<DailyUsageRow> rows) {
        Map<LocalDate, BigDecimal> map = new HashMap<>();
        for (DailyUsageRow row : rows) {
            if (row.getDay() != null && row.getTotal() != null) {
                map.put(LocalDate.parse(row.getDay()), row.getTotal());
            }
        }
        return map;
    }
    private Map<String, BigDecimal> toMonthMap(List<MonthlyUsageRow> rows) {
        Map<String, BigDecimal> map = new HashMap<>();
        for (MonthlyUsageRow row : rows) {
            if (row.getMonth() != null && row.getTotal() != null) {
                map.put(row.getMonth(), row.getTotal());
            }
        }
        return map;
    }
    private boolean monthIdsEmpty(List<Long> ids) {
        return ids == null || ids.isEmpty();
    }
    private YearMonth parseMonth(String month) {
        if (StringUtils.isBlank(month)) {
            return YearMonth.now(EnergyMeterConstants.ZONE);
        }
        try {
            return YearMonth.parse(month.trim());
        } catch (Exception e) {
            throw new ServiceException("月份格式应为 yyyy-MM");
        }
    }
    private String normalizeType(String meterType) {
        String type = EnergyMeterConstants.canonicalType(meterType);
        if (type == null) {
            throw new ServiceException("能源类型只支持水表(WATER)、电表(ELECTRIC)或气表(GAS)");
        }
        return type;
    }
}
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMeterSyncServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,768 @@
package com.ruoyi.energymeter.service.impl;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.energymeter.config.EnergyMeterProperties;
import com.ruoyi.energymeter.constant.EnergyMeterConstants;
import com.ruoyi.energymeter.dto.MeterReadingPoint;
import com.ruoyi.energymeter.dto.MeterUsagePoint;
import com.ruoyi.energymeter.dto.TaskStatusCount;
import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterSyncCheckpointMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterSyncRunMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterSyncTaskMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncCheckpoint;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncRun;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncTask;
import com.ruoyi.energymeter.provider.MeterProviderClient;
import com.ruoyi.energymeter.provider.MeterProviderException;
import com.ruoyi.energymeter.service.EnergyMeterSyncService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.dao.DuplicateKeyException;
import org.springframework.scheduling.concurrent.ThreadPoolTaskExecutor;
import org.springframework.stereotype.Service;
import org.springframework.transaction.PlatformTransactionManager;
import org.springframework.transaction.support.TransactionSynchronization;
import org.springframework.transaction.support.TransactionSynchronizationManager;
import org.springframework.transaction.support.TransactionTemplate;
import java.math.BigDecimal;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.LinkedHashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.UUID;
import java.util.concurrent.CompletableFuture;
import java.util.stream.Collectors;
/**
 * åŒæ­¥ç¼–排:批次创建、任务执行、幂等落库、进度推进与失败重试。
 *
 * <p>外部 HTTP è°ƒç”¨ä¸æ”¾åœ¨æ•°æ®åº“事务中;写入采集表、更新任务和进度在短事务内提交。</p>
 */
@Slf4j
@Service
public class EnergyMeterSyncServiceImpl implements EnergyMeterSyncService {
    private static final int UPSERT_CHUNK = 500;
    private final EnergyMeterDeviceMapper deviceMapper;
    private final EnergyMeterMeasurementMapper measurementMapper;
    private final EnergyMeterSyncRunMapper runMapper;
    private final EnergyMeterSyncTaskMapper taskMapper;
    private final EnergyMeterSyncCheckpointMapper checkpointMapper;
    private final MeterProviderClient provider;
    private final EnergyMeterProperties properties;
    private final TransactionTemplate transactionTemplate;
    private final ThreadPoolTaskExecutor taskExecutor;
    private final ThreadPoolTaskExecutor runExecutor;
    public EnergyMeterSyncServiceImpl(EnergyMeterDeviceMapper deviceMapper,
                                      EnergyMeterMeasurementMapper measurementMapper,
                                      EnergyMeterSyncRunMapper runMapper,
                                      EnergyMeterSyncTaskMapper taskMapper,
                                      EnergyMeterSyncCheckpointMapper checkpointMapper,
                                      MeterProviderClient provider,
                                      EnergyMeterProperties properties,
                                      PlatformTransactionManager transactionManager,
                                      @Qualifier("energyMeterSyncExecutor") ThreadPoolTaskExecutor taskExecutor,
                                      @Qualifier("energyMeterRunExecutor") ThreadPoolTaskExecutor runExecutor) {
        this.deviceMapper = deviceMapper;
        this.measurementMapper = measurementMapper;
        this.runMapper = runMapper;
        this.taskMapper = taskMapper;
        this.checkpointMapper = checkpointMapper;
        this.provider = provider;
        this.properties = properties;
        this.transactionTemplate = new TransactionTemplate(transactionManager);
        this.taskExecutor = taskExecutor;
        this.runExecutor = runExecutor;
    }
    /* ------------------------------------------------------------------ */
    /* æ‰¹æ¬¡åˆ›å»º                                                             */
    /* ------------------------------------------------------------------ */
    @Override
    public Long triggerScheduledSync() {
        if (!provider.mockMode() && !provider.hasApiKey()) {
            log.warn("未配置第三方授权码(NH_ENERGY_API_KEY),跳过每日自动同步");
            return null;
        }
        LocalDate today = EnergyMeterSupport.today();
        LocalDate target = today.minusDays(1);
        String runKey = EnergyMeterConstants.AUTO_RUN_KEY_PREFIX
                + today.format(DateTimeFormatter.BASIC_ISO_DATE);
        EnergyMeterSyncRun existing = runMapper.selectByRunKey(runKey);
        if (existing != null) {
            log.info("当天自动同步批次已存在,跳过重复触发 runKey={}", runKey);
            return existing.getId();
        }
        EnergyMeterSyncRun run = newRun(EnergyMeterConstants.RUN_SCHEDULED, runKey, today, target, target);
        try {
            runMapper.insert(run);
        } catch (DuplicateKeyException e) {
            EnergyMeterSyncRun duplicated = runMapper.selectByRunKey(runKey);
            log.info("并发触发自动同步,复用已有批次 runKey={}", runKey);
            return duplicated == null ? null : duplicated.getId();
        }
        List<EnergyMeterDevice> devices = deviceMapper.selectScannableDevices();
        LocalDate minStart = target;
        int taskCount = 0;
        for (EnergyMeterDevice device : devices) {
            LocalDate start = resolveAutoStart(device, target);
            if (start == null) {
                continue;
            }
            if (start.isBefore(minStart)) {
                minStart = start;
            }
            taskCount += createDeviceTasks(run.getId(), device, start, target, properties.isRawReadingEnabled());
        }
        applyRunRange(run, minStart, target, taskCount);
        log.info("自动同步批次创建完成 runKey={} è®¾å¤‡æ•°={} ä»»åŠ¡æ•°={}", runKey, devices.size(), taskCount);
        if (taskCount == 0) {
            EnergyMeterSyncRun finished = new EnergyMeterSyncRun();
            finished.setId(run.getId());
            finished.setStatus(EnergyMeterConstants.RUN_SUCCESS);
            finished.setFinishedAt(EnergyMeterSupport.now());
            runMapper.updateById(finished);
            return run.getId();
        }
        executeRunAfterCommit(run.getId());
        return run.getId();
    }
    @Override
    public void triggerBackfill(EnergyMeterDevice device, LocalDate start, LocalDate end) {
        if (device == null || start == null || end == null || start.isAfter(end)) {
            return;
        }
        if (EnergyMeterConstants.isManual(device.getSourceType())) {
            log.info("手工抄表设备不创建补采批次 meterId={}", device.getId());
            return;
        }
        if (!provider.mockMode() && !provider.hasApiKey()) {
            log.warn("未配置第三方授权码,跳过补采 meterId={}", device.getId());
            return;
        }
        EnergyMeterSyncRun run = newRun(EnergyMeterConstants.RUN_BACKFILL,
                "BACKFILL:" + UUID.randomUUID(), null, start, end);
        runMapper.insert(run);
        int taskCount = createDeviceTasks(run.getId(), device, start, end, properties.isRawReadingEnabled());
        applyRunRange(run, start, end, taskCount);
        log.info("历史补采批次创建完成 meterId={} range={}~{} tasks={}", device.getId(), start, end, taskCount);
        if (taskCount > 0) {
            executeRunAfterCommit(run.getId());
        }
    }
    @Override
    public Long triggerBackfillGap(EnergyMeterDevice device) {
        if (device == null) {
            return null;
        }
        if (EnergyMeterConstants.isManual(device.getSourceType())) {
            log.info("手工抄表设备不参与缺口补采 meterId={}", device.getId());
            return null;
        }
        if (!provider.mockMode() && !provider.hasApiKey()) {
            log.warn("未配置第三方授权码,跳过缺口补采 meterId={}", device.getId());
            return null;
        }
        LocalDate target = EnergyMeterSupport.today().minusDays(1);
        LocalDate base = device.getSyncStartDate() != null
                ? device.getSyncStartDate()
                : LocalDate.of(target.getYear() - 1, 1, 1);
        EnergyMeterSyncCheckpoint checkpoint = checkpointMapper.selectUnit(device.getId(),
                EnergyMeterConstants.usageSource(device.getMeterType()), EnergyMeterConstants.GRANULARITY_DAY);
        LocalDate start = base;
        if (checkpoint != null && checkpoint.getLastCompleteDate() != null) {
            start = checkpoint.getLastCompleteDate().plusDays(1);
        }
        if (start.isBefore(base)) {
            start = base;
        }
        if (start.isAfter(target)) {
            log.info("设备 {} æ— ç¼ºå£éœ€è¦è¡¥é‡‡", device.getId());
            return null;
        }
        EnergyMeterSyncRun run = newRun(EnergyMeterConstants.RUN_BACKFILL,
                "BACKFILL:" + UUID.randomUUID(), null, start, target);
        runMapper.insert(run);
        int taskCount = createDeviceTasks(run.getId(), device, start, target, properties.isRawReadingEnabled());
        applyRunRange(run, start, target, taskCount);
        if (taskCount > 0) {
            executeRunAfterCommit(run.getId());
        }
        return run.getId();
    }
    @Override
    public Long triggerManual(List<Long> meterIds, LocalDate start, LocalDate end, Boolean includeReadings) {
        if (!provider.mockMode() && !provider.hasApiKey()) {
            throw new ServiceException("未配置第三方授权码(环境变量 NH_ENERGY_API_KEY),无法查表");
        }
        if (meterIds == null || meterIds.isEmpty()) {
            throw new ServiceException("请选择至少一台设备");
        }
        // æ‰‹åŠ¨æŸ¥è¡¨å…è®¸æŸ¥åˆ°ä»Šå¤©ï¼ˆå½“å¤©ç”¨é‡ä¸ºæˆªè‡³æ­¤åˆ»çš„ç´¯è®¡å€¼ï¼‰ï¼ŒåªæŒ¡æœªæ¥æ—¥æœŸ
        LocalDate today = EnergyMeterSupport.today();
        LocalDate rangeEnd = end != null ? end : today;
        if (rangeEnd.isAfter(today)) {
            rangeEnd = today;
        }
        LocalDate rangeStart = start != null ? start : rangeEnd.minusDays(2);
        if (rangeStart.isAfter(rangeEnd)) {
            throw new ServiceException("开始日期不能晚于结束日期");
        }
        if (ChronoUnit.DAYS.between(rangeStart, rangeEnd) > EnergyMeterConstants.MAX_RANGE_DAYS) {
            throw new ServiceException("查询跨度超出上限,请缩小日期范围");
        }
        List<EnergyMeterDevice> devices = new ArrayList<>();
        for (Long meterId : new LinkedHashSet<>(meterIds)) {
            EnergyMeterDevice device = deviceMapper.selectById(meterId);
            if (device == null) {
                throw new ServiceException("设备不存在:" + meterId);
            }
            Long tenantId = EnergyMeterSupport.currentTenantId();
            if (tenantId != null && device.getTenantId() != null && !tenantId.equals(device.getTenantId())) {
                throw new ServiceException("无权操作设备:" + meterId);
            }
            if (!EnergyMeterConstants.BIND_VERIFIED.equals(device.getBindStatus())) {
                throw new ServiceException("设备未通过第三方档案验证,不能查表:" + meterId);
            }
            if (EnergyMeterConstants.isManual(device.getSourceType())) {
                throw new ServiceException("手工抄表设备无需查表,请在表具台账中录入表码:"
                        + StringUtils.defaultIfBlank(device.getDisplayName(), device.getMeterAddress()));
            }
            devices.add(device);
        }
        EnergyMeterSyncRun run = newRun(EnergyMeterConstants.RUN_MANUAL,
                "MANUAL:" + UUID.randomUUID(), null, rangeStart, rangeEnd);
        runMapper.insert(run);
        boolean withReadings = includeReadings == null ? properties.isRawReadingEnabled() : includeReadings;
        int taskCount = 0;
        for (EnergyMeterDevice device : devices) {
            taskCount += createDeviceTasks(run.getId(), device, rangeStart, rangeEnd, withReadings);
        }
        applyRunRange(run, rangeStart, rangeEnd, taskCount);
        log.info("手动查表批次创建完成 runId={} è®¾å¤‡æ•°={} ä»»åŠ¡æ•°={} range={}~{}",
                run.getId(), devices.size(), taskCount, rangeStart, rangeEnd);
        if (taskCount > 0) {
            executeRunAfterCommit(run.getId());
        }
        return run.getId();
    }
    /* ------------------------------------------------------------------ */
    /* æ‰¹æ¬¡æ‰§è¡Œ                                                             */
    /* ------------------------------------------------------------------ */
    @Override
    public void executeRunAsync(Long runId) {
        if (runId == null) {
            return;
        }
        runExecutor.execute(() -> {
            try {
                processRun(runId);
            } catch (Throwable e) {
                log.error("同步批次执行异常 runId=" + runId, e);
            }
        });
    }
    /**
     * åœ¨äº‹åŠ¡æäº¤åŽå†å¼€å§‹æ‰§è¡Œï¼Œé¿å…å¼‚æ­¥çº¿ç¨‹è¯»ä¸åˆ°å°šæœªæäº¤çš„ä»»åŠ¡è®°å½•ã€‚
     */
    private void executeRunAfterCommit(Long runId) {
        if (runId == null) {
            return;
        }
        if (TransactionSynchronizationManager.isSynchronizationActive()) {
            TransactionSynchronizationManager.registerSynchronization(new TransactionSynchronization() {
                @Override
                public void afterCommit() {
                    executeRunAsync(runId);
                }
            });
        } else {
            executeRunAsync(runId);
        }
    }
    private void processRun(Long runId) {
        EnergyMeterSyncRun run = runMapper.selectById(runId);
        if (run == null) {
            return;
        }
        if (EnergyMeterConstants.RUN_PENDING.equals(run.getStatus())) {
            EnergyMeterSyncRun running = new EnergyMeterSyncRun();
            running.setId(runId);
            running.setStatus(EnergyMeterConstants.RUN_RUNNING);
            running.setStartedAt(EnergyMeterSupport.now());
            runMapper.updateById(running);
        }
        LocalDateTime now = EnergyMeterSupport.now();
        // åŒä¸€è®¾å¤‡ã€åŒä¸€æŽ¥å£ã€åŒä¸€ç²’度的分片按日期顺序串行执行,保证连续进度不会被乱序推进;
        // ä¸åŒè®¾å¤‡/接口之间仍然并行,受线程池并发数限制。
        Map<String, List<EnergyMeterSyncTask>> groups = new LinkedHashMap<>();
        for (EnergyMeterSyncTask task : taskMapper.selectByRunId(runId)) {
            if (!isRunnable(task, now)) {
                continue;
            }
            String key = task.getMeterId() + "|" + task.getSourceApi() + "|" + task.getGranularity();
            groups.computeIfAbsent(key, k -> new ArrayList<>()).add(task);
        }
        List<CompletableFuture<Void>> futures = new ArrayList<>();
        for (List<EnergyMeterSyncTask> group : groups.values()) {
            group.sort(Comparator.comparing(EnergyMeterSyncTask::getRangeStart,
                    Comparator.nullsFirst(LocalDate::compareTo)));
            futures.add(CompletableFuture.runAsync(() -> {
                for (EnergyMeterSyncTask task : group) {
                    executeTask(task.getId());
                }
            }, taskExecutor));
        }
        if (!futures.isEmpty()) {
            CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();
        }
        finalizeRun(runId);
    }
    private boolean isRunnable(EnergyMeterSyncTask task, LocalDateTime now) {
        if (EnergyMeterConstants.TASK_PENDING.equals(task.getStatus())) {
            return true;
        }
        if (EnergyMeterConstants.TASK_RETRY.equals(task.getStatus())) {
            return task.getNextRetryAt() == null || !task.getNextRetryAt().isAfter(now);
        }
        return false;
    }
    private void executeTask(Long taskId) {
        LocalDateTime now = EnergyMeterSupport.now();
        if (taskMapper.claim(taskId, now) == 0) {
            return;
        }
        EnergyMeterSyncTask task = taskMapper.selectById(taskId);
        EnergyMeterDevice device = deviceMapper.selectById(task.getMeterId());
        if (device == null) {
            markTaskFailed(task, "DEVICE_MISSING", "本地设备不存在,无法采集");
            return;
        }
        try {
            FetchResult result = fetch(device, task);
            persist(device, task, result);
        } catch (MeterProviderException e) {
            markTaskFailed(task, e.getCode(), e.getMessage());
        } catch (Exception e) {
            log.error("同步任务异常 taskId=" + taskId, e);
            markTaskFailed(task, EnergyMeterConstants.ERROR_UPSTREAM, e.getMessage());
        }
    }
    private FetchResult fetch(EnergyMeterDevice device, EnergyMeterSyncTask task) {
        String meterType = device.getMeterType();
        String address = device.getMeterAddress();
        String providerMeterId = device.getProviderMeterId();
        LocalDate start = task.getRangeStart();
        LocalDate end = task.getRangeEnd();
        LocalDateTime collectedAt = EnergyMeterSupport.now();
        List<EnergyMeterMeasurement> measurements = new ArrayList<>();
        int fetched = 0;
        if (EnergyMeterConstants.GRANULARITY_DAY.equals(task.getGranularity())) {
            List<MeterUsagePoint> points = provider.queryDailyUsage(meterType, address, providerMeterId, start, end);
            fetched = points.size();
            String metric = EnergyMeterConstants.usageMetric(meterType);
            String unit = EnergyMeterConstants.unit(meterType);
            for (MeterUsagePoint point : points) {
                validateAddress(address, point.getAddress());
                if (point.getTime() == null || point.getValue() == null) {
                    throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "用量数据缺少时间或数值");
                }
                LocalDate day = point.getTime().toLocalDate();
                if (day.isBefore(start) || day.isAfter(end)) {
                    continue;
                }
                EnergyMeterMeasurement measurement = new EnergyMeterMeasurement();
                measurement.setTenantId(device.getTenantId());
                measurement.setMeterId(device.getId());
                measurement.setMetricCode(metric);
                measurement.setGranularity(EnergyMeterConstants.GRANULARITY_DAY);
                measurement.setMeasureTime(day.atStartOfDay());
                measurement.setMetricValue(point.getValue());
                measurement.setUnit(unit);
                measurement.setSourceApi(task.getSourceApi());
                measurement.setSourceRecordTime(point.getTime());
                measurement.setCollectedAt(collectedAt);
                measurements.add(measurement);
            }
        } else {
            List<MeterReadingPoint> points = provider.queryReadings(meterType, address, providerMeterId, start, end);
            fetched = points.size();
            String metric = EnergyMeterConstants.readingMetric(meterType);
            String unit = EnergyMeterConstants.unit(meterType);
            for (MeterReadingPoint point : points) {
                validateAddress(address, point.getAddress());
                if (point.getTime() == null || point.getReading() == null) {
                    throw new MeterProviderException(EnergyMeterConstants.ERROR_PARSE, "示值数据缺少时间或数值");
                }
                LocalDate day = point.getTime().toLocalDate();
                if (day.isBefore(start) || day.isAfter(end)) {
                    continue;
                }
                EnergyMeterMeasurement measurement = new EnergyMeterMeasurement();
                measurement.setTenantId(device.getTenantId());
                measurement.setMeterId(device.getId());
                measurement.setMetricCode(metric);
                measurement.setGranularity(EnergyMeterConstants.GRANULARITY_POINT);
                measurement.setMeasureTime(point.getTime());
                measurement.setMetricValue(point.getReading());
                measurement.setUnit(unit);
                measurement.setSourceApi(task.getSourceApi());
                measurement.setSourceRecordTime(point.getTime());
                measurement.setCollectedAt(collectedAt);
                measurements.add(measurement);
            }
        }
        FetchResult result = new FetchResult();
        result.fetched = fetched;
        result.measurements = measurements;
        result.metricCode = EnergyMeterConstants.GRANULARITY_DAY.equals(task.getGranularity())
                ? EnergyMeterConstants.usageMetric(meterType)
                : EnergyMeterConstants.readingMetric(meterType);
        return result;
    }
    private void validateAddress(String expected, String actual) {
        if (StringUtils.isNotBlank(actual) && !expected.equals(actual.trim())) {
            throw new MeterProviderException(EnergyMeterConstants.ERROR_ADDRESS_MISMATCH,
                    "第三方返回的通信地址与本地绑定不一致");
        }
    }
    private void persist(EnergyMeterDevice device, EnergyMeterSyncTask task, FetchResult result) {
        LocalDateTime now = EnergyMeterSupport.now();
        LocalDateTime rangeStart = task.getRangeStart().atStartOfDay();
        LocalDateTime rangeEnd = task.getRangeEnd().plusDays(1).atStartOfDay();
        int[] counters = transactionTemplate.execute(txStatus -> {
            int inserted = 0;
            int updated = 0;
            List<EnergyMeterMeasurement> writes = new ArrayList<>();
            if (!result.measurements.isEmpty()) {
                Map<LocalDateTime, BigDecimal> existing = new HashMap<>();
                for (EnergyMeterMeasurement row : measurementMapper.selectExistingPoints(device.getId(),
                        result.metricCode, task.getGranularity(), rangeStart, rangeEnd)) {
                    existing.put(row.getMeasureTime(), row.getMetricValue());
                }
                for (EnergyMeterMeasurement measurement : result.measurements) {
                    BigDecimal oldValue = existing.get(measurement.getMeasureTime());
                    if (oldValue == null) {
                        inserted++;
                        writes.add(measurement);
                    } else if (oldValue.compareTo(measurement.getMetricValue()) != 0) {
                        updated++;
                        writes.add(measurement);
                    }
                }
            }
            for (int from = 0; from < writes.size(); from += UPSERT_CHUNK) {
                int to = Math.min(from + UPSERT_CHUNK, writes.size());
                measurementMapper.upsertBatch(writes.subList(from, to));
            }
            String status = result.measurements.isEmpty()
                    ? EnergyMeterConstants.TASK_EMPTY : EnergyMeterConstants.TASK_SUCCESS;
            taskMapper.markFinished(task.getId(), status, result.fetched, inserted, updated, now);
            deviceMapper.markCollectSuccess(device.getId(), now);
            advanceCheckpoint(device, task, now);
            return new int[]{inserted, updated};
        });
        int inserted = counters == null ? 0 : counters[0];
        int updated = counters == null ? 0 : counters[1];
        log.info("同步任务完成 taskId={} meterId={} æ‹‰å–={} æ–°å¢ž={} ä¿®æ­£={}",
                task.getId(), device.getId(), result.fetched, inserted, updated);
    }
    private void advanceCheckpoint(EnergyMeterDevice device, EnergyMeterSyncTask task, LocalDateTime now) {
        LocalDate start = task.getRangeStart();
        LocalDate end = task.getRangeEnd();
        // å½“天数据仍在滚动累计(手动查表或当天补采会不断覆盖),不能把今天记为“已完成”,
        // å¦åˆ™è¡¥é‡‡ç¼ºå£åˆ¤æ–­ä¼šè¯¯ä»¥ä¸ºä»Šå¤©å·²ç»å®Œæ•´ã€‚完成进度只推进到昨天及更早。
        if (!end.isBefore(EnergyMeterSupport.today())) {
            log.debug("跳过当天进度推进 meterId={} source={} granularity={} end={}",
                    device.getId(), task.getSourceApi(), task.getGranularity(), end);
            return;
        }
        EnergyMeterSyncCheckpoint checkpoint = checkpointMapper.selectUnit(device.getId(),
                task.getSourceApi(), task.getGranularity());
        LocalDate existingDate = checkpoint == null ? null : checkpoint.getLastCompleteDate();
        boolean contiguous = existingDate == null || !start.isAfter(existingDate.plusDays(1));
        LocalDate lastComplete;
        if (contiguous) {
            lastComplete = existingDate == null || end.isAfter(existingDate) ? end : existingDate;
        } else {
            lastComplete = existingDate != null ? existingDate : start.minusDays(1);
        }
        EnergyMeterSyncCheckpoint upsert = new EnergyMeterSyncCheckpoint();
        upsert.setTenantId(device.getTenantId());
        upsert.setMeterId(device.getId());
        upsert.setSourceApi(task.getSourceApi());
        upsert.setGranularity(task.getGranularity());
        upsert.setLastCompleteDate(lastComplete);
        upsert.setLastSuccessAt(now);
        upsert.setLastRunId(task.getRunId());
        checkpointMapper.upsert(upsert);
    }
    private void markTaskFailed(EnergyMeterSyncTask task, String code, String message) {
        String safeCode = StringUtils.defaultIfBlank(code, EnergyMeterConstants.ERROR_UPSTREAM);
        String safeMessage = EnergyMeterSupport.truncate(
                StringUtils.defaultIfBlank(message, "未知错误"), 450);
        int attempts = task.getAttemptCount() == null ? 1 : task.getAttemptCount();
        // max-attempts è¡¨ç¤ºâ€œæœ€å¤šé‡è¯•次数”,首次执行之外还可重试 N æ¬¡
        int maxTotalAttempts = Math.max(1, properties.getMaxAttempts()) + 1;
        boolean retry = attempts < maxTotalAttempts;
        LocalDateTime now = EnergyMeterSupport.now();
        LocalDateTime nextRetryAt = null;
        if (retry) {
            int index = Math.min(Math.max(attempts - 1, 0), EnergyMeterConstants.RETRY_DELAY_MINUTES.length - 1);
            nextRetryAt = now.plusMinutes(EnergyMeterConstants.RETRY_DELAY_MINUTES[index]);
        }
        String status = retry ? EnergyMeterConstants.TASK_RETRY : EnergyMeterConstants.TASK_FAILED;
        LocalDateTime finalNextRetryAt = nextRetryAt;
        transactionTemplate.executeWithoutResult(state -> {
            taskMapper.markFailed(task.getId(), status, safeCode, safeMessage, finalNextRetryAt, now);
            deviceMapper.markCollectFailure(task.getMeterId(), safeCode, safeMessage);
        });
        log.warn("同步任务失败 taskId={} meterId={} code={} attempts={} å°†é‡è¯•={}",
                task.getId(), task.getMeterId(), safeCode, attempts, retry);
    }
    private void finalizeRun(Long runId) {
        TaskStatusCount count = taskMapper.countByRunId(runId);
        runMapper.refreshTotals(runId);
        int total = count == null || count.getTotal() == null ? 0 : count.getTotal();
        int success = count == null || count.getSuccess() == null ? 0 : count.getSuccess();
        int empty = count == null || count.getEmpty() == null ? 0 : count.getEmpty();
        int failed = count == null || count.getFailed() == null ? 0 : count.getFailed();
        int retry = count == null || count.getRetry() == null ? 0 : count.getRetry();
        int pending = count == null || count.getPending() == null ? 0 : count.getPending();
        int running = count == null || count.getRunning() == null ? 0 : count.getRunning();
        boolean finished = pending == 0 && running == 0 && retry == 0;
        String status;
        if (!finished) {
            status = EnergyMeterConstants.RUN_PARTIAL;
        } else if (failed == 0) {
            status = EnergyMeterConstants.RUN_SUCCESS;
        } else if (success + empty > 0) {
            status = EnergyMeterConstants.RUN_PARTIAL;
        } else {
            status = EnergyMeterConstants.RUN_FAILED;
        }
        EnergyMeterSyncRun update = new EnergyMeterSyncRun();
        update.setId(runId);
        update.setStatus(status);
        if (finished) {
            update.setFinishedAt(EnergyMeterSupport.now());
        }
        if (failed > 0) {
            update.setErrorSummary(EnergyMeterSupport.truncate(
                    failed + "/" + total + " ä¸ªä»»åŠ¡å¤±è´¥ï¼Œå¯åœ¨æ‰¹æ¬¡è¯¦æƒ…ä¸­é‡è¯•", 500));
        }
        runMapper.updateById(update);
        log.info("同步批次汇总 runId={} status={} total={} success={} empty={} failed={} retry={}",
                runId, status, total, success, empty, failed, retry);
    }
    /* ------------------------------------------------------------------ */
    /* é‡è¯•与恢复                                                           */
    /* ------------------------------------------------------------------ */
    @Override
    public int recoverDueTasks() {
        LocalDateTime now = EnergyMeterSupport.now();
        int timeoutMinutes = Math.max(1, properties.getRunningTimeoutMinutes());
        LocalDateTime deadline = now.minusMinutes(timeoutMinutes);
        int handled = 0;
        int maxTotalAttempts = Math.max(1, properties.getMaxAttempts()) + 1;
        for (EnergyMeterSyncTask task : taskMapper.selectStaleRunning(deadline, 200)) {
            int attempts = task.getAttemptCount() == null ? 0 : task.getAttemptCount();
            if (attempts >= maxTotalAttempts) {
                transactionTemplate.executeWithoutResult(state -> taskMapper.markFailed(task.getId(),
                        EnergyMeterConstants.TASK_FAILED, EnergyMeterConstants.ERROR_TIMEOUT,
                        "任务执行超时(超过 " + timeoutMinutes + " åˆ†é’Ÿï¼‰", null, now));
            } else {
                taskMapper.resetToRetry(task.getId(), now, now, "任务执行超时,自动转入重试");
            }
            handled++;
        }
        List<EnergyMeterSyncTask> dueTasks = taskMapper.selectDueRetry(now, 200);
        Set<Long> runIds = dueTasks.stream().map(EnergyMeterSyncTask::getRunId)
                .collect(Collectors.toCollection(LinkedHashSet::new));
        for (Long runId : runIds) {
            try {
                processRun(runId);
            } catch (Exception e) {
                log.error("恢复任务处理批次失败 runId=" + runId, e);
            }
        }
        handled += dueTasks.size();
        // å´©æºƒå…œåº•:批次停在 RUNNING ä½†ä»»åŠ¡éƒ½å·²ç»“æŸï¼ˆä¾‹å¦‚æ±‡æ€»é˜¶æ®µå¼‚å¸¸é€€å‡ºï¼‰æ—¶é‡æ–°æ±‡æ€»
        for (EnergyMeterSyncRun stuck : runMapper.selectStuckRuns(now.minusMinutes(5), 50)) {
            try {
                finalizeRun(stuck.getId());
                handled++;
                log.info("兜底汇总滞留批次 runId={}", stuck.getId());
            } catch (Exception e) {
                log.error("兜底汇总批次失败 runId=" + stuck.getId(), e);
            }
        }
        if (handled > 0) {
            log.info("同步恢复作业处理 {} ä¸ªä»»åŠ¡/批次", handled);
        }
        return handled;
    }
    /* ------------------------------------------------------------------ */
    /* å†…部工具                                                             */
    /* ------------------------------------------------------------------ */
    private EnergyMeterSyncRun newRun(String triggerType, String runKey, LocalDate scheduleDate,
                                      LocalDate start, LocalDate end) {
        EnergyMeterSyncRun run = new EnergyMeterSyncRun();
        run.setRunKey(runKey);
        run.setTriggerType(triggerType);
        run.setScheduleDate(scheduleDate);
        run.setRangeStart(start);
        run.setRangeEnd(end);
        run.setStatus(EnergyMeterConstants.RUN_PENDING);
        run.setTotalTasks(0);
        run.setSuccessTasks(0);
        run.setEmptyTasks(0);
        run.setFailedTasks(0);
        run.setCreateUser(EnergyMeterSupport.currentUserId());
        return run;
    }
    private void applyRunRange(EnergyMeterSyncRun run, LocalDate start, LocalDate end, int taskCount) {
        EnergyMeterSyncRun update = new EnergyMeterSyncRun();
        update.setId(run.getId());
        update.setRangeStart(start);
        update.setRangeEnd(end);
        update.setTotalTasks(taskCount);
        runMapper.updateById(update);
    }
    private int createDeviceTasks(Long runId, EnergyMeterDevice device, LocalDate start, LocalDate end,
                                  boolean includeReadings) {
        if (EnergyMeterConstants.isManual(device.getSourceType())) {
            return 0;
        }
        int created = createTasksFor(runId, device,
                EnergyMeterConstants.usageSource(device.getMeterType()),
                EnergyMeterConstants.GRANULARITY_DAY, start, end,
                Math.max(1, properties.getMaxShardDays()));
        if (includeReadings && !EnergyMeterConstants.isGas(device.getMeterType())) {
            // ç¤ºå€¼æŽ¥å£çš„æŸ¥è¯¢è·¨åº¦é™åˆ¶æ¯”用量接口更严(集社云为 15 å¤©ï¼‰ï¼Œå¿…须用更小的分片。
            // æ°”表示值接口只支持 mi15(15 åˆ†é’Ÿï¼‰ç²’度,96 ç‚¹/天,数据量过大,暂不创建示值任务。
            created += createTasksFor(runId, device,
                    EnergyMeterConstants.readingSource(device.getMeterType()),
                    EnergyMeterConstants.GRANULARITY_POINT, start, end,
                    Math.max(1, properties.getMaxReadingShardDays()));
        }
        return created;
    }
    private int createTasksFor(Long runId, EnergyMeterDevice device, String sourceApi, String granularity,
                               LocalDate start, LocalDate end, int shardDays) {
        int created = 0;
        LocalDate cursor = start;
        while (!cursor.isAfter(end)) {
            LocalDate shardEnd = cursor.plusDays(shardDays - 1L);
            if (shardEnd.isAfter(end)) {
                shardEnd = end;
            }
            created += taskMapper.insertIgnore(buildTask(runId, device, sourceApi, granularity, cursor, shardEnd));
            cursor = shardEnd.plusDays(1);
        }
        return created;
    }
    private EnergyMeterSyncTask buildTask(Long runId, EnergyMeterDevice device, String sourceApi,
                                          String granularity, LocalDate start, LocalDate end) {
        EnergyMeterSyncTask task = new EnergyMeterSyncTask();
        task.setRunId(runId);
        task.setTenantId(device.getTenantId());
        task.setMeterId(device.getId());
        task.setSourceApi(sourceApi);
        task.setGranularity(granularity);
        task.setRangeStart(start);
        task.setRangeEnd(end);
        task.setStatus(EnergyMeterConstants.TASK_PENDING);
        return task;
    }
    /**
     * è‡ªåŠ¨åŒæ­¥èµ·ç‚¹ï¼šä¼˜å…ˆè¡¥é½è¿›åº¦ç¼ºå£ï¼ŒåŒæ—¶å§‹ç»ˆå›žæŸ¥æœ€è¿‘ N å¤©ï¼ˆé»˜è®¤ 3 å¤©ï¼‰ã€‚
     */
    private LocalDate resolveAutoStart(EnergyMeterDevice device, LocalDate target) {
        LocalDate base = device.getSyncStartDate() != null
                ? device.getSyncStartDate()
                : LocalDate.of(target.getYear() - 1, 1, 1);
        LocalDate start = base;
        EnergyMeterSyncCheckpoint checkpoint = checkpointMapper.selectUnit(device.getId(),
                EnergyMeterConstants.usageSource(device.getMeterType()), EnergyMeterConstants.GRANULARITY_DAY);
        if (checkpoint != null && checkpoint.getLastCompleteDate() != null) {
            LocalDate next = checkpoint.getLastCompleteDate().plusDays(1);
            if (next.isAfter(start)) {
                start = next;
            }
        }
        int lookback = Math.max(1, properties.getLookbackDays());
        LocalDate lookbackStart = target.minusDays(lookback - 1L);
        if (start.isAfter(lookbackStart)) {
            start = lookbackStart;
        }
        if (start.isBefore(base)) {
            start = base;
        }
        if (start.isAfter(target)) {
            return null;
        }
        return start;
    }
    /** ä¸€æ¬¡ä»»åŠ¡çš„æ‹‰å–ç»“æžœã€‚ */
    private static class FetchResult {
        private int fetched;
        private String metricCode;
        private List<EnergyMeterMeasurement> measurements = new ArrayList<>();
    }
}
src/main/java/com/ruoyi/energymeter/service/impl/EnergyMonitorServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,822 @@
package com.ruoyi.energymeter.service.impl;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.energymeter.constant.EnergyMeterConstants;
import com.ruoyi.energymeter.dto.DailyUsageRow;
import com.ruoyi.energymeter.dto.DataRangeRow;
import com.ruoyi.energymeter.dto.MeterUsageRow;
import com.ruoyi.energymeter.dto.MonthlyUsageRow;
import com.ruoyi.energymeter.mapper.EnergyMeterDeviceMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper;
import com.ruoyi.energymeter.mapper.EnergyMeterSyncRunMapper;
import com.ruoyi.energymeter.pojo.EnergyMeterDevice;
import com.ruoyi.energymeter.pojo.EnergyMeterMeasurement;
import com.ruoyi.energymeter.pojo.EnergyMeterSyncRun;
import com.ruoyi.energymeter.provider.MeterProviderClient;
import com.ruoyi.energymeter.service.ElectricMeterDashboardService;
import com.ruoyi.energymeter.service.EnergyMonitorService;
import com.ruoyi.energymeter.util.EnergyMeterSupport;
import com.ruoyi.energymeter.vo.ElectricMeterDashboardVO;
import com.ruoyi.energymeter.vo.EnergyCollectionVO;
import com.ruoyi.energymeter.vo.EnergyRealtimeVO;
import com.ruoyi.energymeter.vo.EnergyReportVO;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.math.BigDecimal;
import java.math.RoundingMode;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.time.temporal.ChronoUnit;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.stream.Collectors;
/**
 * èƒ½è€—数据采集、实时监控与统计分析实现。
 *
 * <p>统计口径与用能概览保持一致:只汇总 D ç²’度的用量指标,表码(POINT)不参与累加;
 * ç¼ºå¤±æ—¥æœŸä¿æŒ null,由前端显示为缺口而不是 0。</p>
 */
@Slf4j
@Service
@RequiredArgsConstructor
public class EnergyMonitorServiceImpl implements EnergyMonitorService {
    private static final DateTimeFormatter DAY_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd");
    private static final DateTimeFormatter DATETIME_FORMAT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private static final List<String> ALL_TYPES = List.of(EnergyMeterConstants.TYPE_WATER,
            EnergyMeterConstants.TYPE_ELECTRIC, EnergyMeterConstants.TYPE_GAS);
    private static final int TREND_DAYS = 7;
    private static final int MAX_REPORT_DAYS = 366;
    private final EnergyMeterDeviceMapper deviceMapper;
    private final EnergyMeterMeasurementMapper measurementMapper;
    private final EnergyMeterSyncRunMapper runMapper;
    private final ElectricMeterDashboardService dashboardService;
    private final MeterProviderClient provider;
    /* ------------------------------------------------------------------ */
    /* æ•°æ®é‡‡é›†                                                             */
    /* ------------------------------------------------------------------ */
    @Override
    public EnergyCollectionVO collection() {
        LocalDate today = EnergyMeterSupport.today();
        EnergyCollectionVO vo = new EnergyCollectionVO();
        vo.setGeneratedAt(EnergyMeterSupport.now().format(DATETIME_FORMAT));
        vo.setToday(today.format(DAY_FORMAT));
        vo.setMockMode(provider.mockMode());
        List<EnergyMeterSyncRun> runs = runMapper.selectList(new LambdaQueryWrapper<EnergyMeterSyncRun>()
                .orderByDesc(EnergyMeterSyncRun::getId)
                .last("LIMIT 8"));
        int failed = 0;
        int pending = 0;
        for (EnergyMeterSyncRun run : runs) {
            EnergyCollectionVO.RunItem item = toRunItem(run);
            vo.getRuns().add(item);
            failed += nvl(item.getFailedTasks());
            pending += Math.max(0, nvl(item.getTotalTasks()) - nvl(item.getSuccessTasks())
                    - nvl(item.getEmptyTasks()) - nvl(item.getFailedTasks()));
        }
        vo.setFailedTaskCount(failed);
        vo.setPendingTaskCount(pending);
        for (String type : ALL_TYPES) {
            List<EnergyMeterDevice> devices = loadDevices(type);
            EnergyCollectionVO.Medium medium = new EnergyCollectionVO.Medium();
            medium.setMeterType(type);
            medium.setMeterName(mediumName(type));
            medium.setUnit(EnergyMeterConstants.unit(type));
            medium.setDeviceCount(devices.size());
            int providerCount = 0;
            int manualCount = 0;
            for (EnergyMeterDevice device : devices) {
                if (EnergyMeterConstants.isAutoCollect(device.getSourceType(), device.getSimulated())) {
                    providerCount++;
                } else {
                    manualCount++;
                }
            }
            medium.setProviderCount(providerCount);
            medium.setManualCount(manualCount);
            List<Long> ids = devices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList());
            DataRangeRow range = ids.isEmpty() ? null
                    : measurementMapper.selectDataRange(ids, EnergyMeterConstants.usageMetric(type));
            LocalDate dataStart = range == null || range.getMinTime() == null ? null : range.getMinTime().toLocalDate();
            LocalDate dataEnd = range == null || range.getMaxTime() == null ? null : range.getMaxTime().toLocalDate();
            medium.setDataStart(dataStart == null ? null : dataStart.format(DAY_FORMAT));
            medium.setDataEnd(dataEnd == null ? null : dataEnd.format(DAY_FORMAT));
            LocalDateTime lastSuccess = devices.stream()
                    .map(EnergyMeterDevice::getLastSuccessAt)
                    .filter(Objects::nonNull)
                    .max(LocalDateTime::compareTo)
                    .orElse(null);
            medium.setLastSuccessAt(lastSuccess == null ? null : lastSuccess.format(DATETIME_FORMAT));
            if (!ids.isEmpty() && dataEnd != null) {
                Integer latestDayMeters = measurementMapper.countMetersWithUsage(ids,
                        EnergyMeterConstants.usageMetric(type), dataEnd.atStartOfDay(),
                        dataEnd.plusDays(1).atStartOfDay());
                medium.setLatestDayMeters(latestDayMeters == null ? 0 : latestDayMeters);
            }
            Map<LocalDate, BigDecimal> daily = ids.isEmpty() ? Map.of() : toDayMap(measurementMapper.sumDailyUsage(ids,
                    EnergyMeterConstants.usageMetric(type), today.minusDays(TREND_DAYS - 1).atStartOfDay(),
                    today.plusDays(1).atStartOfDay()));
            medium.setCoveredDays(daily.size());
            medium.setExpectedDays(TREND_DAYS);
            medium.setCoverageRate(daily.isEmpty() ? BigDecimal.ZERO
                    : BigDecimal.valueOf(daily.size() * 100.0 / TREND_DAYS).setScale(1, RoundingMode.HALF_UP));
            // åœ¨çº¿å£å¾„:供应商标记在线,或最近 36 å°æ—¶æˆåŠŸé‡‡é›†ï¼›æ¨¡æ‹Ÿè¡¨å†çœ‹æœ€è¿‘æ•°æ®æ—¥æ˜¯å¦å·²ç”Ÿæˆ
            boolean simulatedFresh = dataEnd != null && !dataEnd.isBefore(today.minusDays(1));
            int onlineCount = 0;
            for (EnergyMeterDevice device : devices) {
                if (isOnline(device)
                        || (EnergyMeterConstants.isSimulated(device.getSimulated()) && simulatedFresh)) {
                    onlineCount++;
                }
            }
            medium.setOnlineCount(onlineCount);
            boolean manualOnly = !devices.isEmpty() && providerCount == 0;
            boolean simulatedOnly = !devices.isEmpty()
                    && devices.stream().allMatch(d -> EnergyMeterConstants.isSimulated(d.getSimulated()));
            applyCollectionStatus(medium, devices.isEmpty(), dataEnd, lastSuccess, manualOnly, simulatedOnly, today);
            vo.getMedia().add(medium);
        }
        return vo;
    }
    private EnergyCollectionVO.RunItem toRunItem(EnergyMeterSyncRun run) {
        EnergyCollectionVO.RunItem item = new EnergyCollectionVO.RunItem();
        item.setId(run.getId());
        item.setRunKey(run.getRunKey());
        item.setTriggerType(run.getTriggerType());
        item.setTriggerTypeName(switch (StringUtils.defaultString(run.getTriggerType())) {
            case EnergyMeterConstants.RUN_SCHEDULED -> "定时同步";
            case EnergyMeterConstants.RUN_BACKFILL -> "历史补采";
            default -> "手动采集";
        });
        item.setStatus(run.getStatus());
        item.setStatusText(switch (StringUtils.defaultString(run.getStatus())) {
            case EnergyMeterConstants.RUN_SUCCESS -> "全部成功";
            case EnergyMeterConstants.RUN_PARTIAL -> "部分失败";
            case EnergyMeterConstants.RUN_FAILED -> "采集失败";
            case EnergyMeterConstants.RUN_RUNNING -> "采集中";
            default -> "待执行";
        });
        item.setRangeStart(run.getRangeStart() == null ? null : run.getRangeStart().format(DAY_FORMAT));
        item.setRangeEnd(run.getRangeEnd() == null ? null : run.getRangeEnd().format(DAY_FORMAT));
        item.setTotalTasks(nvl(run.getTotalTasks()));
        item.setSuccessTasks(nvl(run.getSuccessTasks()));
        item.setEmptyTasks(nvl(run.getEmptyTasks()));
        item.setFailedTasks(nvl(run.getFailedTasks()));
        item.setStartedAt(run.getStartedAt() == null ? null : run.getStartedAt().format(DATETIME_FORMAT));
        item.setFinishedAt(run.getFinishedAt() == null ? null : run.getFinishedAt().format(DATETIME_FORMAT));
        return item;
    }
    private void applyCollectionStatus(EnergyCollectionVO.Medium medium, boolean empty, LocalDate dataEnd,
                                       LocalDateTime lastSuccess, boolean manualOnly, boolean simulatedOnly,
                                       LocalDate today) {
        if (empty) {
            medium.setStatus("NO_DEVICE");
            medium.setStatusText("未接入设备");
            return;
        }
        if (dataEnd == null) {
            medium.setStatus("EMPTY");
            medium.setStatusText("暂无采集数据");
            return;
        }
        if (simulatedOnly) {
            if (dataEnd.isBefore(today.minusDays(1))) {
                medium.setStatus("PARTIAL");
                medium.setStatusText("自动采集数据滞后 " + ChronoUnit.DAYS.between(dataEnd, today) + " å¤©");
            } else {
                medium.setStatus("OK");
                medium.setStatusText("自动采集数据");
            }
            return;
        }
        if (manualOnly) {
            if (dataEnd.isBefore(today.minusDays(1))) {
                medium.setStatus("PARTIAL");
                medium.setStatusText("数据滞后 " + ChronoUnit.DAYS.between(dataEnd, today) + " å¤©");
            } else {
                medium.setStatus("OK");
                medium.setStatusText("手工抄表数据");
            }
            return;
        }
        if (lastSuccess == null || lastSuccess.isBefore(EnergyMeterSupport.now().minusHours(36))) {
            medium.setStatus("STALE");
            medium.setStatusText("超过 36 å°æ—¶æ— æˆåŠŸé‡‡é›†");
            return;
        }
        if (dataEnd.isBefore(today.minusDays(1))) {
            medium.setStatus("PARTIAL");
            medium.setStatusText("数据滞后 " + ChronoUnit.DAYS.between(dataEnd, today) + " å¤©");
            return;
        }
        medium.setStatus("OK");
        medium.setStatusText("采集正常");
    }
    /**
     * åœ¨çº¿å£å¾„:供应商标记在线,或最近 36 å°æ—¶å†…有成功采集。
     *
     * <p>第三方档案的在线标记只在绑定/验证时落库,长期不刷新,不能单独作为“在线”依据。</p>
     */
    private boolean isOnline(EnergyMeterDevice device) {
        if (Integer.valueOf(1).equals(device.getOnlineStatus())) {
            return true;
        }
        return device.getLastSuccessAt() != null
                && device.getLastSuccessAt().isAfter(EnergyMeterSupport.now().minusHours(36));
    }
    /* ------------------------------------------------------------------ */
    /* å®žæ—¶ç›‘控                                                             */
    /* ------------------------------------------------------------------ */
    @Override
    public EnergyRealtimeVO realtime(Long meterId) {
        LocalDate today = EnergyMeterSupport.today();
        LocalDate windowStart = today.minusDays(TREND_DAYS);
        LocalDate monthStart = today.withDayOfMonth(1);
        EnergyRealtimeVO vo = new EnergyRealtimeVO();
        vo.setGeneratedAt(EnergyMeterSupport.now().format(DATETIME_FORMAT));
        vo.setToday(today.format(DAY_FORMAT));
        vo.setMockMode(provider.mockMode());
        vo.setMeterId(meterId);
        List<EnergyMeterDevice> devices;
        List<String> types;
        if (meterId != null) {
            EnergyMeterDevice device = requireDevice(meterId);
            devices = List.of(device);
            String type = EnergyMeterConstants.canonicalType(device.getMeterType());
            if (type == null) {
                throw new ServiceException("设备介质类型不合法");
            }
            types = List.of(type);
            vo.setScopeLabel(device.getDisplayName() + "(" + device.getMeterAddress() + ")");
        } else {
            devices = loadDevices(null);
            types = ALL_TYPES;
            vo.setScopeLabel("全部已接入表具(" + devices.size() + " å°ï¼‰");
        }
        Map<String, Map<LocalDate, BigDecimal>> dailyByType = new HashMap<>();
        for (String type : types) {
            List<EnergyMeterDevice> typeDevices = devices.stream()
                    .filter(d -> type.equals(d.getMeterType()))
                    .collect(Collectors.toList());
            List<Long> ids = typeDevices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList());
            Map<LocalDate, BigDecimal> windowDaily = ids.isEmpty() ? Map.of()
                    : toDayMap(measurementMapper.sumDailyUsage(ids, EnergyMeterConstants.usageMetric(type),
                    windowStart.atStartOfDay(), today.plusDays(1).atStartOfDay()));
            dailyByType.put(type, windowDaily);
            EnergyRealtimeVO.Medium medium = new EnergyRealtimeVO.Medium();
            medium.setMeterType(type);
            medium.setMeterName(mediumName(type));
            medium.setUnit(EnergyMeterConstants.unit(type));
            medium.setDeviceCount(typeDevices.size());
            LocalDate latestDate = latestDate(windowDaily, today, windowStart);
            BigDecimal latest = latestDate == null ? null : windowDaily.get(latestDate);
            LocalDate previousDate = latestDate == null ? null
                    : latestDate(windowDaily, latestDate.minusDays(1), windowStart);
            BigDecimal previous = previousDate == null ? null : windowDaily.get(previousDate);
            boolean simulatedFresh = latestDate != null && !latestDate.isBefore(today.minusDays(1));
            medium.setOnlineCount((int) typeDevices.stream()
                    .filter(d -> isOnline(d)
                            || (EnergyMeterConstants.isSimulated(d.getSimulated()) && simulatedFresh))
                    .count());
            medium.setLatestUsage(latest);
            medium.setLatestUsageDate(latestDate == null ? null : latestDate.format(DAY_FORMAT));
            medium.setPreviousUsage(previous);
            medium.setPreviousUsageDate(previousDate == null ? null : previousDate.format(DAY_FORMAT));
            medium.setCompareRate(compareRate(latest, previous));
            BigDecimal monthTotal = null;
            if (!ids.isEmpty()) {
                Map<LocalDate, BigDecimal> monthDaily = toDayMap(measurementMapper.sumDailyUsage(ids,
                        EnergyMeterConstants.usageMetric(type), monthStart.atStartOfDay(),
                        today.plusDays(1).atStartOfDay()));
                monthTotal = sumValues(monthDaily.values());
                if (monthTotal != null) {
                    monthTotal = monthTotal.setScale(3, RoundingMode.HALF_UP);
                }
            }
            medium.setMonthTotal(monthTotal);
            List<BigDecimal> trendValues = new ArrayList<>();
            for (int i = TREND_DAYS - 1; i >= 0; i--) {
                BigDecimal value = windowDaily.get(today.minusDays(i));
                if (value != null) {
                    trendValues.add(value);
                }
            }
            medium.setAvgDaily(average(trendValues));
            medium.setPeakDaily(trendValues.stream().max(BigDecimal::compareTo).orElse(null));
            EnergyMeterMeasurement reading = null;
            for (EnergyMeterDevice device : typeDevices) {
                EnergyMeterMeasurement point = measurementMapper.selectLatestPoint(device.getId(),
                        EnergyMeterConstants.readingMetric(type));
                if (point != null && (reading == null || point.getMeasureTime().isAfter(reading.getMeasureTime()))) {
                    reading = point;
                }
            }
            if (reading != null) {
                medium.setLatestReading(reading.getMetricValue());
                medium.setLatestReadingTime(reading.getMeasureTime().format(DATETIME_FORMAT));
            }
            LocalDateTime lastSuccess = typeDevices.stream()
                    .map(EnergyMeterDevice::getLastSuccessAt)
                    .filter(Objects::nonNull)
                    .max(LocalDateTime::compareTo)
                    .orElse(null);
            medium.setLastSuccessAt(lastSuccess == null ? null : lastSuccess.format(DATETIME_FORMAT));
            boolean manualOnly = !typeDevices.isEmpty()
                    && typeDevices.stream().noneMatch(d ->
                    EnergyMeterConstants.isAutoCollect(d.getSourceType(), d.getSimulated()));
            boolean simulatedOnly = !typeDevices.isEmpty()
                    && typeDevices.stream().allMatch(d -> EnergyMeterConstants.isSimulated(d.getSimulated()));
            applyRealtimeStatus(medium, typeDevices.isEmpty(), latest, latestDate, lastSuccess,
                    manualOnly, simulatedOnly, today);
            vo.getMedia().add(medium);
        }
        /* è¿‘ 7 æ—¥è¶‹åŠ¿ */
        for (int i = TREND_DAYS - 1; i >= 0; i--) {
            LocalDate day = today.minusDays(i);
            EnergyRealtimeVO.TrendPoint point = new EnergyRealtimeVO.TrendPoint();
            point.setDay(day.format(DAY_FORMAT));
            point.setWater(dailyByType.getOrDefault(EnergyMeterConstants.TYPE_WATER, Map.of()).get(day));
            point.setElectric(dailyByType.getOrDefault(EnergyMeterConstants.TYPE_ELECTRIC, Map.of()).get(day));
            point.setGas(dailyByType.getOrDefault(EnergyMeterConstants.TYPE_GAS, Map.of()).get(day));
            vo.getTrend().add(point);
        }
        /* ç”µè¡¨å®žæ—¶æ›²çº¿ */
        if (types.contains(EnergyMeterConstants.TYPE_ELECTRIC)) {
            EnergyRealtimeVO.Electric electric = buildElectric(meterId);
            vo.setElectric(electric);
            if (electric != null) {
                for (EnergyRealtimeVO.Medium medium : vo.getMedia()) {
                    if (EnergyMeterConstants.TYPE_ELECTRIC.equals(medium.getMeterType())) {
                        medium.setCurrentPower(electric.getCurrentPower());
                        medium.setPowerFactor(electric.getPowerFactor());
                    }
                }
            }
        }
        buildAlerts(vo, devices, today);
        return vo;
    }
    private EnergyRealtimeVO.Electric buildElectric(Long meterId) {
        ElectricMeterDashboardVO dashboard;
        try {
            dashboard = dashboardService.dashboard(meterId, 0);
        } catch (Exception e) {
            log.warn("实时监控查询电表 15 åˆ†é’Ÿæ•°æ®å¤±è´¥ï¼š{}", e.getMessage());
            return null;
        }
        EnergyRealtimeVO.Electric electric = new EnergyRealtimeVO.Electric();
        electric.setRangeStart(dashboard.getToday() == null ? null : dashboard.getToday().getRangeStart());
        electric.setRangeEnd(dashboard.getToday() == null ? null : dashboard.getToday().getRangeEnd());
        electric.setTodayTotal(dashboard.getToday() == null ? null : dashboard.getToday().getTotal());
        electric.setYesterdaySamePeriodTotal(dashboard.getToday() == null ? null
                : dashboard.getToday().getYesterdaySamePeriodTotal());
        List<ElectricMeterDashboardVO.QuarterPoint> points = dashboard.getToday() == null
                ? List.of() : dashboard.getToday().getSeries();
        BigDecimal lastPower = null;
        BigDecimal lastFactor = null;
        for (ElectricMeterDashboardVO.QuarterPoint point : points) {
            EnergyRealtimeVO.CurvePoint curvePoint = new EnergyRealtimeVO.CurvePoint();
            curvePoint.setTime(point.getTime());
            curvePoint.setActive(point.getActive());
            if (point.getActive() == null) {
                electric.getSeries().add(curvePoint);
                continue;
            }
            // 15 åˆ†é’Ÿç”µé‡æŠ˜ç®—功率:kWh / 0.25h = kW
            BigDecimal power = point.getActive().multiply(BigDecimal.valueOf(4)).setScale(3, RoundingMode.HALF_UP);
            curvePoint.setPower(power);
            electric.getSeries().add(curvePoint);
            lastPower = power;
            lastFactor = powerFactor(power, point.getReactive());
        }
        electric.setCurrentPower(lastPower);
        electric.setPowerFactor(lastFactor);
        return electric;
    }
    private BigDecimal powerFactor(BigDecimal activePower, BigDecimal reactiveEnergy) {
        if (activePower == null) {
            return null;
        }
        BigDecimal reactivePower = reactiveEnergy == null ? BigDecimal.ZERO
                : reactiveEnergy.multiply(BigDecimal.valueOf(4));
        double p = activePower.doubleValue();
        double q = reactivePower.doubleValue();
        if (p <= 0 && q <= 0) {
            return null;
        }
        double factor = p / Math.sqrt(p * p + q * q);
        return BigDecimal.valueOf(Math.min(factor, 1.0)).setScale(3, RoundingMode.HALF_UP);
    }
    private void applyRealtimeStatus(EnergyRealtimeVO.Medium medium, boolean noDevice, BigDecimal latest,
                                     LocalDate latestDate, LocalDateTime lastSuccess, boolean manualOnly,
                                     boolean simulatedOnly, LocalDate today) {
        if (noDevice) {
            medium.setStatus("NO_DEVICE");
            medium.setStatusText("未接入设备");
            return;
        }
        if (latest == null && medium.getLatestReading() == null) {
            medium.setStatus("EMPTY");
            medium.setStatusText("暂无数据");
            return;
        }
        if (simulatedOnly) {
            if (latestDate != null && latestDate.isBefore(today.minusDays(1))) {
                medium.setStatus("STALE");
                medium.setStatusText("自动采集数据滞后");
            } else {
                medium.setStatus("OK");
                medium.setStatusText("自动采集数据");
            }
            return;
        }
        if (manualOnly) {
            if (latestDate != null && latestDate.isBefore(today.minusDays(1))) {
                medium.setStatus("STALE");
                medium.setStatusText("抄表数据滞后");
            } else {
                medium.setStatus("OK");
                medium.setStatusText("手工抄表数据");
            }
            return;
        }
        if (lastSuccess == null || lastSuccess.isBefore(EnergyMeterSupport.now().minusHours(36))) {
            medium.setStatus("STALE");
            medium.setStatusText("采集滞后");
            return;
        }
        medium.setStatus("OK");
        medium.setStatusText("运行正常");
    }
    private void buildAlerts(EnergyRealtimeVO vo, List<EnergyMeterDevice> devices, LocalDate today) {
        List<EnergyRealtimeVO.Alert> alerts = vo.getAlerts();
        for (EnergyRealtimeVO.Medium medium : vo.getMedia()) {
            if ("STALE".equals(medium.getStatus()) || "EMPTY".equals(medium.getStatus())) {
                alerts.add(alert("warning", medium.getMeterName() + ":" + medium.getStatusText()
                        + ",请检查设备或手动采集"));
            }
        }
        int deviceAlerts = 0;
        for (EnergyMeterDevice device : devices) {
            if (StringUtils.isNotBlank(device.getLastErrorCode()) && deviceAlerts < 3) {
                String message = StringUtils.isBlank(device.getLastErrorMessage())
                        ? device.getLastErrorCode() : device.getLastErrorMessage();
                alerts.add(alert("warning", "「" + device.getDisplayName() + "」最近一次采集失败:" + message));
                deviceAlerts++;
            }
        }
        if (Boolean.TRUE.equals(vo.getMockMode())) {
            alerts.add(alert("info", "当前为演示数据模式,采集结果由本地模拟生成,仅用于联调与页面演示。"));
        }
        if (alerts.size() > 6) {
            vo.setAlerts(new ArrayList<>(alerts.subList(0, 6)));
        }
    }
    private EnergyRealtimeVO.Alert alert(String level, String text) {
        EnergyRealtimeVO.Alert alert = new EnergyRealtimeVO.Alert();
        alert.setLevel(level);
        alert.setText(text);
        return alert;
    }
    /* ------------------------------------------------------------------ */
    /* ç»Ÿè®¡åˆ†æž                                                             */
    /* ------------------------------------------------------------------ */
    @Override
    public EnergyReportVO report(String startDate, String endDate, String granularity, String meterType,
                                 String dimension) {
        LocalDate today = EnergyMeterSupport.today();
        LocalDate start = StringUtils.isBlank(startDate) ? today.withDayOfMonth(1) : parseDate(startDate, "开始日期");
        LocalDate end = StringUtils.isBlank(endDate) ? today : parseDate(endDate, "结束日期");
        if (end.isBefore(start)) {
            throw new ServiceException("结束日期不能早于开始日期");
        }
        if (ChronoUnit.DAYS.between(start, end) > MAX_REPORT_DAYS) {
            throw new ServiceException("统计跨度不能超过 " + MAX_REPORT_DAYS + " å¤©ï¼Œè¯·ç¼©å°æ—¶é—´èŒƒå›´");
        }
        String gran = "MONTH".equalsIgnoreCase(StringUtils.trim(granularity)) ? "MONTH" : "DAY";
        String dim = "DEVICE".equalsIgnoreCase(StringUtils.trim(dimension)) ? "DEVICE" : "MEDIUM";
        String filterType = StringUtils.isBlank(meterType) || "ALL".equalsIgnoreCase(meterType.trim())
                ? "ALL" : EnergyMeterConstants.canonicalType(meterType);
        if (filterType == null) {
            throw new ServiceException("能源类型只支持水表(WATER)、电表(ELECTRIC)或蒸汽表(GAS)");
        }
        List<String> types = "ALL".equals(filterType) ? ALL_TYPES : List.of(filterType);
        EnergyReportVO vo = new EnergyReportVO();
        vo.setGeneratedAt(EnergyMeterSupport.now().format(DATETIME_FORMAT));
        vo.setMockMode(provider.mockMode());
        vo.setStart(start.format(DAY_FORMAT));
        vo.setEnd(end.format(DAY_FORMAT));
        vo.setGranularity(gran);
        vo.setDimension(dim);
        vo.setMeterType(filterType);
        List<String> periods = buildPeriods(start, end, gran);
        vo.setPeriods(periods);
        vo.setTotalPeriods(periods.size());
        vo.setNote("统计口径:仅汇总日粒度用量,表码不参与累加;缺失周期按空值展示,不按 0 è®¡ç®—。");
        List<EnergyMeterDevice> devices = loadDevices(null).stream()
                .filter(d -> types.contains(d.getMeterType()))
                .collect(Collectors.toList());
        Map<String, Map<String, BigDecimal>> valuesByType = new HashMap<>();
        for (String type : types) {
            List<Long> ids = devices.stream()
                    .filter(d -> type.equals(d.getMeterType()))
                    .map(EnergyMeterDevice::getId)
                    .collect(Collectors.toList());
            Map<String, BigDecimal> values = new HashMap<>();
            if (!ids.isEmpty()) {
                if ("MONTH".equals(gran)) {
                    for (MonthlyUsageRow row : measurementMapper.sumMonthlyUsage(ids,
                            EnergyMeterConstants.usageMetric(type), start.atStartOfDay(),
                            end.plusDays(1).atStartOfDay())) {
                        if (row.getMonth() != null && row.getTotal() != null) {
                            values.put(row.getMonth(), row.getTotal());
                        }
                    }
                } else {
                    for (DailyUsageRow row : measurementMapper.sumDailyUsage(ids,
                            EnergyMeterConstants.usageMetric(type), start.atStartOfDay(),
                            end.plusDays(1).atStartOfDay())) {
                        if (row.getDay() != null && row.getTotal() != null) {
                            values.put(row.getDay(), row.getTotal());
                        }
                    }
                }
            }
            valuesByType.put(type, values);
        }
        int coveredPeriods = 0;
        for (String period : periods) {
            EnergyReportVO.PeriodRow row = new EnergyReportVO.PeriodRow();
            row.setPeriod(period);
            boolean hasValue = false;
            for (String type : types) {
                BigDecimal value = valuesByType.get(type).get(period);
                if (value != null) {
                    hasValue = true;
                }
                if (EnergyMeterConstants.isWater(type)) {
                    row.setWater(value);
                } else if (EnergyMeterConstants.isElectric(type)) {
                    row.setElectric(value);
                } else {
                    row.setGas(value);
                }
            }
            if (hasValue) {
                coveredPeriods++;
            }
            vo.getRows().add(row);
        }
        vo.setCoveredPeriods(coveredPeriods);
        Map<String, BigDecimal> mediumTotals = new HashMap<>();
        for (String type : types) {
            BigDecimal total = sumValues(valuesByType.get(type).values());
            mediumTotals.put(type, total);
            EnergyReportVO.MediumTotal mediumTotal = new EnergyReportVO.MediumTotal();
            mediumTotal.setMeterType(type);
            mediumTotal.setMeterName(mediumName(type));
            mediumTotal.setUnit(EnergyMeterConstants.unit(type));
            mediumTotal.setTotal(total == null ? null : total.setScale(3, RoundingMode.HALF_UP));
            mediumTotal.setDeviceCount((int) devices.stream()
                    .filter(d -> type.equals(d.getMeterType())).count());
            vo.getTotals().add(mediumTotal);
        }
        for (String type : types) {
            List<EnergyMeterDevice> typeDevices = devices.stream()
                    .filter(d -> type.equals(d.getMeterType()))
                    .collect(Collectors.toList());
            if (typeDevices.isEmpty()) {
                continue;
            }
            List<Long> ids = typeDevices.stream().map(EnergyMeterDevice::getId).collect(Collectors.toList());
            Map<Long, BigDecimal> usageMap = new HashMap<>();
            for (MeterUsageRow row : measurementMapper.sumUsageByMeter(ids, EnergyMeterConstants.usageMetric(type),
                    start.atStartOfDay(), end.plusDays(1).atStartOfDay())) {
                if (row.getMeterId() != null && row.getTotal() != null) {
                    usageMap.put(row.getMeterId(), row.getTotal());
                }
            }
            BigDecimal mediumTotal = mediumTotals.get(type);
            for (EnergyMeterDevice device : typeDevices) {
                EnergyReportVO.DeviceRow row = new EnergyReportVO.DeviceRow();
                row.setMeterId(device.getId());
                row.setMeterName(device.getDisplayName());
                row.setMeterAddress(device.getMeterAddress());
                row.setMeterType(type);
                row.setMeterTypeName(mediumName(type));
                row.setSourceTypeName(EnergyMeterConstants.isAutoCollect(device.getSourceType(), device.getSimulated())
                        ? "自动采集" : "手工抄表");
                row.setUnit(EnergyMeterConstants.unit(type));
                BigDecimal total = usageMap.get(device.getId());
                row.setTotal(total == null ? null : total.setScale(3, RoundingMode.HALF_UP));
                if (total != null && mediumTotal != null && mediumTotal.compareTo(BigDecimal.ZERO) > 0) {
                    row.setShare(total.multiply(BigDecimal.valueOf(100))
                            .divide(mediumTotal, 1, RoundingMode.HALF_UP));
                }
                vo.getDevices().add(row);
            }
        }
        vo.getDevices().sort(Comparator
                .comparingInt((EnergyReportVO.DeviceRow row) -> typeOrder(row.getMeterType()))
                .thenComparing(EnergyReportVO.DeviceRow::getTotal, Comparator.nullsLast(Comparator.reverseOrder()))
                .thenComparing(EnergyReportVO.DeviceRow::getMeterId));
        return vo;
    }
    private int typeOrder(String type) {
        if (EnergyMeterConstants.isWater(type)) {
            return 0;
        }
        if (EnergyMeterConstants.isElectric(type)) {
            return 1;
        }
        return 2;
    }
    private List<String> buildPeriods(LocalDate start, LocalDate end, String gran) {
        List<String> periods = new ArrayList<>();
        if ("MONTH".equals(gran)) {
            YearMonth month = YearMonth.from(start);
            YearMonth last = YearMonth.from(end);
            while (!month.isAfter(last)) {
                periods.add(month.toString());
                month = month.plusMonths(1);
            }
        } else {
            for (LocalDate day = start; !day.isAfter(end); day = day.plusDays(1)) {
                periods.add(day.format(DAY_FORMAT));
            }
        }
        return periods;
    }
    /* ------------------------------------------------------------------ */
    /* å·¥å…·                                                                 */
    /* ------------------------------------------------------------------ */
    private List<EnergyMeterDevice> loadDevices(String meterType) {
        LambdaQueryWrapper<EnergyMeterDevice> wrapper = new LambdaQueryWrapper<>();
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null) {
            wrapper.eq(EnergyMeterDevice::getTenantId, tenantId);
        }
        if (StringUtils.isNotBlank(meterType)) {
            wrapper.eq(EnergyMeterDevice::getMeterType, meterType);
        }
        wrapper.eq(EnergyMeterDevice::getEnabled, 1)
                .eq(EnergyMeterDevice::getBindStatus, EnergyMeterConstants.BIND_VERIFIED)
                .orderByAsc(EnergyMeterDevice::getId);
        return deviceMapper.selectList(wrapper);
    }
    private EnergyMeterDevice requireDevice(Long meterId) {
        EnergyMeterDevice device = deviceMapper.selectById(meterId);
        if (device == null) {
            throw new ServiceException("设备不存在或已删除");
        }
        Long tenantId = EnergyMeterSupport.currentTenantId();
        if (tenantId != null && device.getTenantId() != null && !tenantId.equals(device.getTenantId())) {
            throw new ServiceException("无权访问该设备");
        }
        return device;
    }
    private String mediumName(String meterType) {
        if (EnergyMeterConstants.isWater(meterType)) {
            return "水表";
        }
        if (EnergyMeterConstants.isElectric(meterType)) {
            return "电表";
        }
        if (EnergyMeterConstants.isGas(meterType)) {
            return "蒸汽表";
        }
        return meterType;
    }
    private Map<LocalDate, BigDecimal> toDayMap(List<DailyUsageRow> rows) {
        Map<LocalDate, BigDecimal> map = new HashMap<>();
        for (DailyUsageRow row : rows) {
            if (row.getDay() != null && row.getTotal() != null) {
                map.put(LocalDate.parse(row.getDay()), row.getTotal());
            }
        }
        return map;
    }
    private LocalDate latestDate(Map<LocalDate, BigDecimal> daily, LocalDate from, LocalDate floor) {
        LocalDate day = from;
        while (!day.isBefore(floor)) {
            BigDecimal value = daily.get(day);
            if (value != null) {
                return day;
            }
            day = day.minusDays(1);
        }
        return null;
    }
    private BigDecimal compareRate(BigDecimal latest, BigDecimal previous) {
        if (latest == null || previous == null || previous.compareTo(BigDecimal.ZERO) <= 0) {
            return null;
        }
        return latest.subtract(previous)
                .multiply(BigDecimal.valueOf(100))
                .divide(previous, 1, RoundingMode.HALF_UP);
    }
    private BigDecimal sumValues(java.util.Collection<BigDecimal> values) {
        BigDecimal sum = BigDecimal.ZERO;
        boolean hasValue = false;
        for (BigDecimal value : values) {
            if (value != null) {
                sum = sum.add(value);
                hasValue = true;
            }
        }
        return hasValue ? sum : null;
    }
    private BigDecimal average(List<BigDecimal> values) {
        if (values == null || values.isEmpty()) {
            return null;
        }
        BigDecimal sum = BigDecimal.ZERO;
        for (BigDecimal value : values) {
            sum = sum.add(value);
        }
        return sum.divide(BigDecimal.valueOf(values.size()), 3, RoundingMode.HALF_UP);
    }
    private int nvl(Integer value) {
        return value == null ? 0 : value;
    }
    private LocalDate parseDate(String value, String label) {
        try {
            return LocalDate.parse(value.trim());
        } catch (Exception e) {
            throw new ServiceException(label + "格式应为 yyyy-MM-dd");
        }
    }
}
src/main/java/com/ruoyi/energymeter/util/EnergyMeterSupport.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,55 @@
package com.ruoyi.energymeter.util;
import com.ruoyi.common.utils.SecurityUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.ZoneId;
/**
 * ç™»å½•态与时间工具:系统级定时任务不会携带登录态,这里统一兜底。
 */
public final class EnergyMeterSupport {
    private EnergyMeterSupport() {
    }
    public static Long currentTenantId() {
        try {
            return SecurityUtils.getLoginUser().getTenantId();
        } catch (Exception e) {
            return null;
        }
    }
    public static Long currentUserId() {
        try {
            return SecurityUtils.getUserId();
        } catch (Exception e) {
            return null;
        }
    }
    public static boolean isAdmin() {
        try {
            return SecurityUtils.getLoginUser().getUser().isAdmin();
        } catch (Exception e) {
            return false;
        }
    }
    public static LocalDate today() {
        return LocalDate.now(ZoneId.of("Asia/Shanghai"));
    }
    public static LocalDateTime now() {
        return LocalDateTime.now(ZoneId.of("Asia/Shanghai"));
    }
    public static String truncate(String text, int maxLength) {
        if (text == null) {
            return null;
        }
        return text.length() <= maxLength ? text : text.substring(0, maxLength);
    }
}
src/main/java/com/ruoyi/energymeter/vo/ElectricMeterDashboardVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,124 @@
package com.ruoyi.energymeter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * ç”µè¡¨å®žæ—¶çœ‹æ¿ï¼šä»Šæ—¥ 15 åˆ†é’Ÿæ›²çº¿ + è¿‘一周电能质量。
 *
 * <p>只包含电量、功率、功率因数,不包含任何费用字段。</p>
 */
@Data
@Schema(description = "电表实时看板")
public class ElectricMeterDashboardVO implements Serializable {
    private static final long serialVersionUID = 1L;
    private String meterType = "ELECTRIC";
    @Schema(description = "电量单位")
    private String unit;
    private Long meterId;
    private String scopeLabel;
    private Integer deviceCount;
    private Boolean mockMode;
    @Schema(description = "今日数据")
    private Today today;
    @Schema(description = "电能质量")
    private Quality quality;
    @Data
    public static class Today implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "业务日期 yyyy-MM-dd")
        private String date;
        @Schema(description = "当日数据起始,如 00:00")
        private String rangeStart;
        @Schema(description = "当日数据截止,如 17:58")
        private String rangeEnd;
        @Schema(description = "当日累计用电量 kWh")
        private BigDecimal total;
        @Schema(description = "昨日全天累计用电量 kWh")
        private BigDecimal yesterdayTotal;
        @Schema(description = "昨日同一时刻累计用电量 kWh")
        private BigDecimal yesterdaySamePeriodTotal;
        @Schema(description = "较昨日同时段变化百分比,昨日无数据时为 null")
        private BigDecimal compareRate;
        @Schema(description = "今日 96 ä¸ªæ—¶é—´ç‰‡")
        private List<QuarterPoint> series = new ArrayList<>();
        @Schema(description = "昨日 96 ä¸ªæ—¶é—´ç‰‡")
        private List<QuarterPoint> yesterdaySeries = new ArrayList<>();
    }
    @Data
    public static class QuarterPoint implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "时间片结束时间 HH:mm,如 17:45")
        private String time;
        @Schema(description = "有功电量 kWh")
        private BigDecimal active;
        @Schema(description = "无功电量 kvarh")
        private BigDecimal reactive;
    }
    @Data
    public static class Quality implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "统计开始日期")
        private String weekStart;
        @Schema(description = "统计结束日期")
        private String weekEnd;
        @Schema(description = "近一周 / ä¸Šä¸€å‘¨")
        private String label;
        @Schema(description = "按小时聚合的功率点")
        private List<PowerPoint> series = new ArrayList<>();
    }
    @Data
    public static class PowerPoint implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "时间,MM-dd HH:mm")
        private String time;
        @Schema(description = "有功功率 kW")
        private BigDecimal activePower;
        @Schema(description = "无功功率 kvar")
        private BigDecimal reactivePower;
        @Schema(description = "功率因数 0-1")
        private BigDecimal powerFactor;
    }
}
src/main/java/com/ruoyi/energymeter/vo/EnergyCollectionVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,133 @@
package com.ruoyi.energymeter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * èƒ½è€—数据采集概览:水、电、蒸汽三类介质的接入设备、归集进度与最近采集批次。
 */
@Data
@Schema(description = "能耗数据采集概览")
public class EnergyCollectionVO implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "生成时间")
    private String generatedAt;
    @Schema(description = "业务日期 yyyy-MM-dd")
    private String today;
    @Schema(description = "是否演示数据模式")
    private Boolean mockMode;
    @Schema(description = "三类介质采集概览")
    private List<Medium> media = new ArrayList<>();
    @Schema(description = "最近采集批次")
    private List<RunItem> runs = new ArrayList<>();
    @Schema(description = "最近批次失败任务数")
    private Integer failedTaskCount = 0;
    @Schema(description = "最近批次待执行任务数")
    private Integer pendingTaskCount = 0;
    @Data
    @Schema(description = "单类介质采集概览")
    public static class Medium implements Serializable {
        private static final long serialVersionUID = 1L;
        private String meterType;
        @Schema(description = "介质名称:水表 / ç”µè¡¨ / è’¸æ±½è¡¨")
        private String meterName;
        @Schema(description = "单位 m3 / kWh")
        private String unit;
        @Schema(description = "启用设备数")
        private Integer deviceCount = 0;
        @Schema(description = "第三方自动采集设备数")
        private Integer providerCount = 0;
        @Schema(description = "手工抄表设备数")
        private Integer manualCount = 0;
        @Schema(description = "供应商侧在线设备数")
        private Integer onlineCount = 0;
        @Schema(description = "最近成功采集时间")
        private String lastSuccessAt;
        @Schema(description = "已归集数据起始日期")
        private String dataStart;
        @Schema(description = "已归集数据结束日期")
        private String dataEnd;
        @Schema(description = "数据截止日已归集到用量的设备数")
        private Integer latestDayMeters = 0;
        @Schema(description = "近 7 æ—¥æœ‰æ•°æ®çš„天数")
        private Integer coveredDays = 0;
        @Schema(description = "统计天数")
        private Integer expectedDays = 7;
        @Schema(description = "近 7 æ—¥æ•°æ®è¦†ç›–率 0-100")
        private BigDecimal coverageRate;
        @Schema(description = "采集状态 OK/PARTIAL/STALE/EMPTY/NO_DEVICE")
        private String status;
        @Schema(description = "采集状态文案")
        private String statusText;
    }
    @Data
    @Schema(description = "最近采集批次")
    public static class RunItem implements Serializable {
        private static final long serialVersionUID = 1L;
        private Long id;
        private String runKey;
        @Schema(description = "SCHEDULED/MANUAL/BACKFILL")
        private String triggerType;
        private String triggerTypeName;
        @Schema(description = "批次状态")
        private String status;
        private String statusText;
        @Schema(description = "同步开始日期")
        private String rangeStart;
        @Schema(description = "同步结束日期")
        private String rangeEnd;
        private Integer totalTasks;
        private Integer successTasks;
        private Integer emptyTasks;
        private Integer failedTasks;
        private String startedAt;
        private String finishedAt;
    }
}
src/main/java/com/ruoyi/energymeter/vo/EnergyMeterOverviewVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,137 @@
package com.ruoyi.energymeter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * ç”¨èƒ½æ¦‚览:卡片、月度按日、年度按月、采集状态。
 *
 * <p>费用在口径未确认前保持 null,前端显示 {@code --},不会绘制成 0。</p>
 */
@Data
@Schema(description = "水电表用能概览")
public class EnergyMeterOverviewVO implements Serializable {
    private static final long serialVersionUID = 1L;
    private String meterType;
    @Schema(description = "统计指标编码")
    private String metricCode;
    @Schema(description = "单位 m3 / kWh")
    private String unit;
    @Schema(description = "选中的设备ID,全部设备时为空")
    private Long meterId;
    @Schema(description = "数据范围说明")
    private String scopeLabel;
    @Schema(description = "参与统计的设备数量")
    private Integer deviceCount;
    @Schema(description = "是否演示数据模式")
    private Boolean mockMode;
    @Schema(description = "选中月份 yyyy-MM")
    private String selectedMonth;
    @Schema(description = "选中年份 yyyy")
    private String selectedYear;
    @Schema(description = "选中月累计用量")
    private BigDecimal monthTotal;
    @Schema(description = "上月同期累计用量")
    private BigDecimal monthPreviousTotal;
    @Schema(description = "选中年累计用量")
    private BigDecimal yearTotal;
    @Schema(description = "去年同月累计用量(年度对比)")
    private BigDecimal yearPreviousTotal;
    @Schema(description = "选中月费用,口径未确认时为 null")
    private BigDecimal monthCost;
    @Schema(description = "选中年费用,口径未确认时为 null")
    private BigDecimal yearCost;
    @Schema(description = "按日对比序列")
    private List<MonthDayPoint> monthSeries = new ArrayList<>();
    @Schema(description = "按月对比序列")
    private List<YearMonthPoint> yearSeries = new ArrayList<>();
    @Schema(description = "最近成功采集时间")
    private String lastSuccessAt;
    @Schema(description = "已采集数据起始日期")
    private String dataStart;
    @Schema(description = "已采集数据结束日期")
    private String dataEnd;
    @Schema(description = "数据截至说明")
    private String dataThrough;
    @Schema(description = "本月已采集天数")
    private Integer collectedDays;
    @Schema(description = "本月应采集天数")
    private Integer expectedDays;
    @Schema(description = "本月数据覆盖率 0-100")
    private BigDecimal coverageRate;
    @Schema(description = "采集状态 OK/PARTIAL/STALE/EMPTY")
    private String syncStatus;
    @Schema(description = "采集状态文案")
    private String syncStatusText;
    @Schema(description = "是否有数据")
    private Boolean hasData;
    @Schema(description = "所选月份是否有数据")
    private Boolean monthHasData;
    @Schema(description = "所选年份是否有数据")
    private Boolean yearHasData;
    @Data
    public static class MonthDayPoint implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "日,yyyy-MM-dd")
        private String day;
        @Schema(description = "当月当日用量")
        private BigDecimal current;
        @Schema(description = "上月同日用量")
        private BigDecimal previous;
    }
    @Data
    public static class YearMonthPoint implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "月,yyyy-MM")
        private String month;
        @Schema(description = "当年当月用量")
        private BigDecimal current;
        @Schema(description = "去年同月用量")
        private BigDecimal previous;
    }
}
src/main/java/com/ruoyi/energymeter/vo/EnergyRealtimeVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,180 @@
package com.ruoyi.energymeter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * èƒ½è€—实时监控:三类介质最新用量、负荷状态、近 7 æ—¥è¶‹åŠ¿ä¸Žç”µè¡¨ 15 åˆ†é’Ÿå®žæ—¶æ›²çº¿ã€‚
 *
 * <p>水表、蒸汽表只有日冻结数据,展示的是最近一个有数据的业务日;电表额外提供当日 15 åˆ†é’Ÿå®žæ—¶ç”µé‡ä¸ŽåŠŸçŽ‡ã€‚</p>
 */
@Data
@Schema(description = "能耗实时监控")
public class EnergyRealtimeVO implements Serializable {
    private static final long serialVersionUID = 1L;
    private String generatedAt;
    @Schema(description = "业务日期 yyyy-MM-dd")
    private String today;
    private Boolean mockMode;
    @Schema(description = "数据范围说明")
    private String scopeLabel;
    @Schema(description = "单台设备ID,全部设备时为空")
    private Long meterId;
    @Schema(description = "三类介质实时概览")
    private List<Medium> media = new ArrayList<>();
    @Schema(description = "近 7 æ—¥ä¸‰ç±»ä»‹è´¨ç”¨é‡è¶‹åŠ¿")
    private List<TrendPoint> trend = new ArrayList<>();
    @Schema(description = "电表实时数据")
    private Electric electric;
    @Schema(description = "运行提示")
    private List<Alert> alerts = new ArrayList<>();
    @Data
    @Schema(description = "单类介质实时概览")
    public static class Medium implements Serializable {
        private static final long serialVersionUID = 1L;
        private String meterType;
        @Schema(description = "介质名称:水表 / ç”µè¡¨ / è’¸æ±½è¡¨")
        private String meterName;
        @Schema(description = "单位 m3 / kWh")
        private String unit;
        private Integer deviceCount = 0;
        private Integer onlineCount = 0;
        @Schema(description = "最近一个有数据的业务日用量")
        private BigDecimal latestUsage;
        @Schema(description = "最近用量业务日期 yyyy-MM-dd")
        private String latestUsageDate;
        @Schema(description = "上一个有数据的业务日用量")
        private BigDecimal previousUsage;
        @Schema(description = "上一用量业务日期 yyyy-MM-dd")
        private String previousUsageDate;
        @Schema(description = "较上一业务日变化百分比")
        private BigDecimal compareRate;
        @Schema(description = "本月累计用量")
        private BigDecimal monthTotal;
        @Schema(description = "近 7 æ—¥æ—¥å‡ç”¨é‡")
        private BigDecimal avgDaily;
        @Schema(description = "近 7 æ—¥å³°å€¼ç”¨é‡")
        private BigDecimal peakDaily;
        @Schema(description = "最近表码")
        private BigDecimal latestReading;
        @Schema(description = "最近表码时间")
        private String latestReadingTime;
        @Schema(description = "最近成功采集时间")
        private String lastSuccessAt;
        @Schema(description = "当前有功功率 kW,仅电表")
        private BigDecimal currentPower;
        @Schema(description = "功率因数 0-1,仅电表")
        private BigDecimal powerFactor;
        @Schema(description = "状态 OK/STALE/EMPTY/NO_DEVICE")
        private String status;
        private String statusText;
    }
    @Data
    @Schema(description = "按日趋势")
    public static class TrendPoint implements Serializable {
        private static final long serialVersionUID = 1L;
        private String day;
        private BigDecimal water;
        private BigDecimal electric;
        private BigDecimal gas;
    }
    @Data
    @Schema(description = "电表实时数据")
    public static class Electric implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "当日数据起始,如 00:00")
        private String rangeStart;
        @Schema(description = "当日数据截止,如 17:58")
        private String rangeEnd;
        @Schema(description = "今日累计用电量 kWh")
        private BigDecimal todayTotal;
        @Schema(description = "昨日同期累计用电量 kWh")
        private BigDecimal yesterdaySamePeriodTotal;
        @Schema(description = "当前有功功率 kW")
        private BigDecimal currentPower;
        @Schema(description = "当前功率因数")
        private BigDecimal powerFactor;
        @Schema(description = "今日 15 åˆ†é’Ÿæ›²çº¿")
        private List<CurvePoint> series = new ArrayList<>();
    }
    @Data
    @Schema(description = "电表 15 åˆ†é’Ÿç‚¹")
    public static class CurvePoint implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "时间片结束时间 HH:mm")
        private String time;
        @Schema(description = "时间片有功电量 kWh")
        private BigDecimal active;
        @Schema(description = "折算有功功率 kW")
        private BigDecimal power;
    }
    @Data
    @Schema(description = "运行提示")
    public static class Alert implements Serializable {
        private static final long serialVersionUID = 1L;
        @Schema(description = "info/warning")
        private String level;
        private String text;
    }
}
src/main/java/com/ruoyi/energymeter/vo/EnergyReportVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,118 @@
package com.ruoyi.energymeter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;
/**
 * èƒ½è€—统计分析报表:按日/月与按介质/设备两个维度汇总水、电、蒸汽用量。
 */
@Data
@Schema(description = "能耗统计报表")
public class EnergyReportVO implements Serializable {
    private static final long serialVersionUID = 1L;
    private String generatedAt;
    @Schema(description = "是否演示数据模式")
    private Boolean mockMode;
    private String start;
    private String end;
    @Schema(description = "DAY / MONTH")
    private String granularity;
    @Schema(description = "MEDIUM / DEVICE")
    private String dimension;
    @Schema(description = "ALL / WATER / ELECTRIC / GAS")
    private String meterType;
    @Schema(description = "统计周期列表")
    private List<String> periods = new ArrayList<>();
    @Schema(description = "按介质汇总的周期数据")
    private List<PeriodRow> rows = new ArrayList<>();
    @Schema(description = "按设备汇总的数据")
    private List<DeviceRow> devices = new ArrayList<>();
    @Schema(description = "各介质合计")
    private List<MediumTotal> totals = new ArrayList<>();
    @Schema(description = "有数据的周期数")
    private Integer coveredPeriods = 0;
    @Schema(description = "统计周期总数")
    private Integer totalPeriods = 0;
    @Schema(description = "统计口径说明")
    private String note;
    @Data
    @Schema(description = "单个统计周期的三类介质用量")
    public static class PeriodRow implements Serializable {
        private static final long serialVersionUID = 1L;
        private String period;
        private BigDecimal water;
        private BigDecimal electric;
        private BigDecimal gas;
    }
    @Data
    @Schema(description = "单台设备期间用量")
    public static class DeviceRow implements Serializable {
        private static final long serialVersionUID = 1L;
        private Long meterId;
        private String meterName;
        private String meterAddress;
        private String meterType;
        @Schema(description = "介质名称:水表 / ç”µè¡¨ / è’¸æ±½è¡¨")
        private String meterTypeName;
        private String sourceTypeName;
        private String unit;
        @Schema(description = "期间用量")
        private BigDecimal total;
        @Schema(description = "占本介质比例 0-100")
        private BigDecimal share;
    }
    @Data
    @Schema(description = "单类介质合计")
    public static class MediumTotal implements Serializable {
        private static final long serialVersionUID = 1L;
        private String meterType;
        private String meterName;
        private String unit;
        private BigDecimal total;
        private Integer deviceCount = 0;
    }
}
src/main/java/com/ruoyi/energymeter/vo/MeterArchiveVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,47 @@
package com.ruoyi.energymeter.vo;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
/**
 * æ¡£æ¡ˆéªŒè¯ç»“果。
 */
@Data
@Schema(description = "第三方档案验证结果")
public class MeterArchiveVO implements Serializable {
    private static final long serialVersionUID = 1L;
    private String meterType;
    private String meterAddress;
    private String providerMeterId;
    private String providerName;
    private Long areaId;
    private String areaName;
    private Integer onlineStatus;
    private String parentProviderId;
    @Schema(description = "是否可以绑定")
    private Boolean bindable;
    @Schema(description = "已存在的本地设备ID(重复绑定时返回)")
    private Long existingDeviceId;
    @Schema(description = "已存在设备是否启用")
    private Boolean existingEnabled;
    @Schema(description = "提示信息")
    private String message;
    @Schema(description = "是否处于演示数据模式")
    private Boolean mockMode;
}
src/main/java/com/ruoyi/energymeter/vo/MeterReadingResultVO.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,41 @@
package com.ruoyi.energymeter.vo;
import com.fasterxml.jackson.annotation.JsonFormat;
import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;
/**
 * æ‰‹å·¥å½•表结果。
 */
@Data
@Schema(description = "手工录表结果")
public class MeterReadingResultVO implements Serializable {
    private static final long serialVersionUID = 1L;
    @Schema(description = "表码记录ID")
    private Long id;
    @Schema(description = "设备ID")
    private Long meterId;
    @Schema(description = "抄表时间")
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime measureTime;
    @Schema(description = "本次表码")
    private BigDecimal readingValue;
    @Schema(description = "上期表码")
    private BigDecimal previousReading;
    @Schema(description = "本期用量")
    private BigDecimal usage;
    @Schema(description = "单位")
    private String unit;
}
src/main/resources/application-dev.yml
@@ -264,3 +264,33 @@
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
# ==================== æ°´ç”µè¡¨æŽ¥å…¥ ====================
energy-meter:
  provider:
    # åŸºç¡€åœ°å€
    base-url: http://nhapi.yunjichaobiao.com/WebApi
    # æŽˆæƒç 
    api-key: 62F19BF685484594B57FE97FC7444AF3
    mock-enabled: false
    # ä¸¤æ¬¡è¯·æ±‚的最小间隔(毫秒)
    min-interval-ms: 500
    connect-timeout-seconds: 5
    read-timeout-seconds: 30
  # æ¯å¤© 08:00 åŒæ­¥æ—¶å§‹ç»ˆå›žæŸ¥æœ€è¿‘ 3 å¤©
  lookback-days: 3
  # åŽ†å²è¡¥é‡‡å•ä¸ªåˆ†ç‰‡æœ€å¤§è‡ªç„¶æ—¥ï¼ˆç”¨é‡æŽ¥å£é™åˆ¶ 31 å¤©è·¨åº¦ï¼‰
  max-shard-days: 30
  # ç¤ºå€¼æŽ¥å£å•个分片最大自然日(集社云 GetWaterRawData é™åˆ¶ 15 å¤©è·¨åº¦ï¼‰
  max-reading-shard-days: 15
  # ç¬¬ä¸‰æ–¹æœ€å¤§å¹¶å‘数与失败重试次数(1、5、15 åˆ†é’Ÿï¼‰
  concurrency: 2
  max-attempts: 3
  # RUNNING ä»»åŠ¡è¶…æ—¶é˜ˆå€¼ï¼ˆåˆ†é’Ÿï¼‰
  running-timeout-minutes: 30
  # æ°´è¡¨åŽŸå§‹ç¤ºå€¼ä»»åŠ¡å¼€å…³ï¼ˆåˆ†é¡µèƒ½åŠ›ç¡®è®¤åŽå¯å…³é—­ï¼‰
  raw-reading-enabled: true
  # æ˜¯å¦æ³¨å†Œ 08:00 è‡ªåŠ¨åŒæ­¥ä¸Ž 10 åˆ†é’Ÿæ¢å¤ä½œä¸š
  scheduler-enabled: true
  sync-cron: "0 0 8 * * ?"
  recovery-cron: "0 0/10 * * * ?"
src/main/resources/application-xswh.yml
@@ -264,3 +264,33 @@
  compress: true # æ˜¯å¦åŽ‹ç¼©
  needCompressSize: 10MB # åŽ‹ç¼©é˜ˆå€¼
  compressQuality: 0.5 # åŽ‹ç¼©è´¨é‡(0.0-1.0)
# ==================== æ°´ç”µè¡¨æŽ¥å…¥ ====================
energy-meter:
  provider:
    # åŸºç¡€åœ°å€
    base-url: http://nhapi.yunjichaobiao.com/WebApi
    # æŽˆæƒç 
    api-key: 62F19BF685484594B57FE97FC7444AF3
    mock-enabled: false
    # ä¸¤æ¬¡è¯·æ±‚的最小间隔(毫秒)
    min-interval-ms: 500
    connect-timeout-seconds: 5
    read-timeout-seconds: 30
  # æ¯å¤© 08:00 åŒæ­¥æ—¶å§‹ç»ˆå›žæŸ¥æœ€è¿‘ 3 å¤©
  lookback-days: 3
  # åŽ†å²è¡¥é‡‡å•ä¸ªåˆ†ç‰‡æœ€å¤§è‡ªç„¶æ—¥ï¼ˆç”¨é‡æŽ¥å£é™åˆ¶ 31 å¤©è·¨åº¦ï¼‰
  max-shard-days: 30
  # ç¤ºå€¼æŽ¥å£å•个分片最大自然日(集社云 GetWaterRawData é™åˆ¶ 15 å¤©è·¨åº¦ï¼‰
  max-reading-shard-days: 15
  # ç¬¬ä¸‰æ–¹æœ€å¤§å¹¶å‘数与失败重试次数(1、5、15 åˆ†é’Ÿï¼‰
  concurrency: 2
  max-attempts: 3
  # RUNNING ä»»åŠ¡è¶…æ—¶é˜ˆå€¼ï¼ˆåˆ†é’Ÿï¼‰
  running-timeout-minutes: 30
  # æ°´è¡¨åŽŸå§‹ç¤ºå€¼ä»»åŠ¡å¼€å…³ï¼ˆåˆ†é¡µèƒ½åŠ›ç¡®è®¤åŽå¯å…³é—­ï¼‰
  raw-reading-enabled: true
  # æ˜¯å¦æ³¨å†Œ 08:00 è‡ªåŠ¨åŒæ­¥ä¸Ž 10 åˆ†é’Ÿæ¢å¤ä½œä¸š
  scheduler-enabled: true
  sync-cron: "0 0 8 * * ?"
  recovery-cron: "0 0/10 * * * ?"
src/main/resources/application.yml
@@ -38,5 +38,3 @@
      model-name: "deepseek-r1:1.5b"
      log-requests: true
      log-responses: true
knowledge:
  one: D:\新疆大罗素企业产品体系说明文档.md
src/main/resources/mapper/energymeter/EnergyMeterMeasurementMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,111 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.energymeter.mapper.EnergyMeterMeasurementMapper">
    <sql id="meterIdIn">
        <if test="meterIds != null and meterIds.size() > 0">
            AND meter_id IN
            <foreach collection="meterIds" item="id" open="(" separator="," close=")">
                #{id}
            </foreach>
        </if>
        <if test="meterIds == null or meterIds.size() == 0">
            AND 1 = 0
        </if>
    </sql>
    <select id="sumDailyUsage" resultType="com.ruoyi.energymeter.dto.DailyUsageRow">
        SELECT DATE_FORMAT(measure_time, '%Y-%m-%d') AS day,
               COALESCE(SUM(metric_value), 0) AS total
        FROM energy_meter_measurement
        WHERE metric_code = #{metricCode}
          AND granularity = 'D'
          AND measure_time &gt;= #{start}
          AND measure_time &lt; #{end}
        <include refid="meterIdIn"/>
        GROUP BY DATE_FORMAT(measure_time, '%Y-%m-%d')
        ORDER BY day
    </select>
    <select id="sumUsageByMeter" resultType="com.ruoyi.energymeter.dto.MeterUsageRow">
        SELECT meter_id AS meter_id,
               COALESCE(SUM(metric_value), 0) AS total
        FROM energy_meter_measurement
        WHERE metric_code = #{metricCode}
          AND granularity = 'D'
          AND measure_time &gt;= #{start}
          AND measure_time &lt; #{end}
        <include refid="meterIdIn"/>
        GROUP BY meter_id
    </select>
    <select id="sumMonthlyUsage" resultType="com.ruoyi.energymeter.dto.MonthlyUsageRow">
        SELECT DATE_FORMAT(measure_time, '%Y-%m') AS month,
               COALESCE(SUM(metric_value), 0) AS total
        FROM energy_meter_measurement
        WHERE metric_code = #{metricCode}
          AND granularity = 'D'
          AND measure_time &gt;= #{start}
          AND measure_time &lt; #{end}
        <include refid="meterIdIn"/>
        GROUP BY DATE_FORMAT(measure_time, '%Y-%m')
        ORDER BY month
    </select>
    <select id="selectExistingPoints" resultType="com.ruoyi.energymeter.pojo.EnergyMeterMeasurement">
        SELECT id, tenant_id, meter_id, metric_code, granularity, measure_time, metric_value, unit,
               source_api, source_record_time, collected_at
        FROM energy_meter_measurement
        WHERE meter_id = #{meterId}
          AND metric_code = #{metricCode}
          AND granularity = #{granularity}
          AND measure_time &gt;= #{start}
          AND measure_time &lt; #{end}
    </select>
    <insert id="upsertBatch">
        INSERT INTO energy_meter_measurement
        (tenant_id, meter_id, metric_code, granularity, measure_time, metric_value, unit,
         source_api, source_record_time, collected_at)
        VALUES
        <foreach collection="list" item="it" separator=",">
            (#{it.tenantId}, #{it.meterId}, #{it.metricCode}, #{it.granularity}, #{it.measureTime},
             #{it.metricValue}, #{it.unit}, #{it.sourceApi}, #{it.sourceRecordTime}, #{it.collectedAt})
        </foreach>
        ON DUPLICATE KEY UPDATE
            metric_value = VALUES(metric_value),
            unit = VALUES(unit),
            source_api = VALUES(source_api),
            source_record_time = VALUES(source_record_time),
            collected_at = VALUES(collected_at)
    </insert>
    <select id="selectDataRange" resultType="com.ruoyi.energymeter.dto.DataRangeRow">
        SELECT MIN(measure_time) AS min_time, MAX(measure_time) AS max_time
        FROM energy_meter_measurement
        WHERE metric_code = #{metricCode}
          AND granularity = 'D'
        <include refid="meterIdIn"/>
    </select>
    <select id="countMetersWithUsage" resultType="java.lang.Integer">
        SELECT COUNT(DISTINCT meter_id)
        FROM energy_meter_measurement
        WHERE metric_code = #{metricCode}
          AND granularity = 'D'
          AND measure_time &gt;= #{start}
          AND measure_time &lt; #{end}
        <include refid="meterIdIn"/>
    </select>
    <select id="sumTotal" resultType="java.math.BigDecimal">
        SELECT COALESCE(SUM(metric_value), 0)
        FROM energy_meter_measurement
        WHERE metric_code = #{metricCode}
          AND granularity = 'D'
          AND measure_time &gt;= #{start}
          AND measure_time &lt; #{end}
        <include refid="meterIdIn"/>
    </select>
</mapper>
src/main/resources/mapper/energymeter/EnergyMeterSyncCheckpointMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,18 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.energymeter.mapper.EnergyMeterSyncCheckpointMapper">
    <insert id="upsert">
        INSERT INTO energy_meter_sync_checkpoint
        (tenant_id, meter_id, source_api, granularity, last_complete_date, last_success_at, last_run_id, create_time, update_time)
        VALUES
        (#{cp.tenantId}, #{cp.meterId}, #{cp.sourceApi}, #{cp.granularity}, #{cp.lastCompleteDate},
         #{cp.lastSuccessAt}, #{cp.lastRunId}, NOW(3), NOW(3))
        ON DUPLICATE KEY UPDATE
            last_complete_date = GREATEST(COALESCE(last_complete_date, VALUES(last_complete_date)), VALUES(last_complete_date)),
            last_success_at = VALUES(last_success_at),
            last_run_id = VALUES(last_run_id),
            update_time = NOW(3)
    </insert>
</mapper>
src/main/resources/mapper/energymeter/EnergyMeterSyncRunMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,24 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.energymeter.mapper.EnergyMeterSyncRunMapper">
    <update id="refreshTotals">
        UPDATE energy_meter_sync_run r
        JOIN (
            SELECT run_id,
                   COUNT(*) AS total_cnt,
                   COALESCE(SUM(status = 'SUCCESS'), 0) AS success_cnt,
                   COALESCE(SUM(status = 'EMPTY'), 0) AS empty_cnt,
                   COALESCE(SUM(status = 'FAILED'), 0) AS failed_cnt
            FROM energy_meter_sync_task
            WHERE run_id = #{runId}
            GROUP BY run_id
        ) t ON t.run_id = r.id
        SET r.total_tasks = t.total_cnt,
            r.success_tasks = t.success_cnt,
            r.empty_tasks = t.empty_cnt,
            r.failed_tasks = t.failed_cnt
        WHERE r.id = #{runId}
    </update>
</mapper>
src/main/resources/mapper/energymeter/EnergyMeterSyncTaskMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,88 @@
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.ruoyi.energymeter.mapper.EnergyMeterSyncTaskMapper">
    <insert id="insertIgnore">
        INSERT IGNORE INTO energy_meter_sync_task
        (run_id, tenant_id, meter_id, source_api, granularity, range_start, range_end, status,
         attempt_count, fetched_count, inserted_count, updated_count, create_time, update_time)
        VALUES
        (#{task.runId}, #{task.tenantId}, #{task.meterId}, #{task.sourceApi}, #{task.granularity},
         #{task.rangeStart}, #{task.rangeEnd}, #{task.status}, 0, 0, 0, 0, NOW(3), NOW(3))
    </insert>
    <update id="claim">
        UPDATE energy_meter_sync_task
        SET status = 'RUNNING',
            attempt_count = attempt_count + 1,
            started_at = #{now},
            finished_at = NULL,
            next_retry_at = NULL,
            error_code = NULL,
            error_message = NULL
        WHERE id = #{id}
          AND status IN ('PENDING', 'RETRY')
    </update>
    <select id="selectDueRetry" resultType="com.ruoyi.energymeter.pojo.EnergyMeterSyncTask">
        SELECT * FROM energy_meter_sync_task
        WHERE status = 'RETRY'
          AND next_retry_at IS NOT NULL
          AND next_retry_at &lt;= #{now}
        ORDER BY next_retry_at
        LIMIT #{limit}
    </select>
    <select id="selectStaleRunning" resultType="com.ruoyi.energymeter.pojo.EnergyMeterSyncTask">
        SELECT * FROM energy_meter_sync_task
        WHERE status = 'RUNNING'
          AND started_at IS NOT NULL
          AND started_at &lt; #{deadline}
        ORDER BY started_at
        LIMIT #{limit}
    </select>
    <update id="resetToRetry">
        UPDATE energy_meter_sync_task
        SET status = 'RETRY',
            next_retry_at = #{nextRetryAt},
            error_code = 'UPSTREAM_TIMEOUT',
            error_message = #{reason},
            update_time = #{now}
        WHERE id = #{id}
          AND status = 'RUNNING'
    </update>
    <select id="selectByRunId" resultType="com.ruoyi.energymeter.pojo.EnergyMeterSyncTask">
        SELECT t.*, d.display_name AS meter_name, d.meter_type, d.meter_address
        FROM energy_meter_sync_task t
        LEFT JOIN energy_meter_device d ON d.id = t.meter_id
        WHERE t.run_id = #{runId}
        ORDER BY t.id
    </select>
    <select id="selectLatestByMeterIds" resultType="com.ruoyi.energymeter.pojo.EnergyMeterSyncTask">
        SELECT * FROM (
            SELECT t.*, ROW_NUMBER() OVER (PARTITION BY t.meter_id ORDER BY t.id DESC) AS rn
            FROM energy_meter_sync_task t
            WHERE t.meter_id IN
            <foreach collection="meterIds" item="id" open="(" separator="," close=")">
                #{id}
            </foreach>
        ) x
        WHERE x.rn = 1
    </select>
    <select id="countByRunId" resultType="com.ruoyi.energymeter.dto.TaskStatusCount">
        SELECT COUNT(*) AS `total`,
               COALESCE(SUM(status = 'SUCCESS'), 0) AS `success`,
               COALESCE(SUM(status = 'EMPTY'), 0) AS `empty`,
               COALESCE(SUM(status = 'FAILED'), 0) AS `failed`,
               COALESCE(SUM(status = 'RETRY'), 0) AS `retry`,
               COALESCE(SUM(status = 'PENDING'), 0) AS `pending`,
               COALESCE(SUM(status = 'RUNNING'), 0) AS `running`
        FROM energy_meter_sync_task
        WHERE run_id = #{runId}
    </select>
</mapper>