yuan
3 天以前 1a21433e0babfa7cafc5a4d86609442ec9f150a4
feat: 添加电表和采集器同步服务及相关数据模型
已添加37个文件
2251 ■■■■■ 文件已修改
src/main/java/com/ruoyi/http/config/TqdianbiaoConfig.java 34 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/controller/StatisticEleController.java 79 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/controller/TqdianbiaoCollectorController.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/controller/TqdianbiaoEleRecordController.java 59 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/controller/TqdianbiaoMeterController.java 66 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/mapper/TqdianbiaoCollectorMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/mapper/TqdianbiaoEleRecordMapper.java 20 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/mapper/TqdianbiaoMeterMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/mapper/TqdianbiaoSyncLogMapper.java 7 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/pojo/TqdianbiaoCollector.java 40 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/pojo/TqdianbiaoEleRecord.java 70 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/pojo/TqdianbiaoMeter.java 50 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/pojo/TqdianbiaoSyncLog.java 32 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/StatisticEleService.java 26 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/TqdianbiaoCollectorManageService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/TqdianbiaoCollectorSyncService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/TqdianbiaoEleRecordManageService.java 22 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/TqdianbiaoEleSyncService.java 15 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/TqdianbiaoMeterManageService.java 21 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/TqdianbiaoMeterSyncService.java 6 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/TqdianbiaoSyncLogService.java 8 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/StatisticEleServiceImpl.java 179 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoCollectorManageServiceImpl.java 91 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoCollectorSyncServiceImpl.java 90 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoEleRecordManageServiceImpl.java 140 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoEleSyncServiceImpl.java 150 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoMeterManageServiceImpl.java 120 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoMeterSyncServiceImpl.java 112 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoSyncLogServiceImpl.java 39 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/task/TqdianbiaoSyncTask.java 58 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java 252 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/util/StatisticEleParseUtil.java 122 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/util/StatisticEleReadingUtil.java 72 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/vo/StatisticEleRecordVo.java 67 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/vo/StatisticEleSummaryVo.java 33 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/vo/StatisticEleSyncStatusVo.java 19 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/resources/mapper/http/TqdianbiaoEleRecordMapper.xml 45 ●●●●● 补丁 | 查看 | 原始文档 | blame | 历史
src/main/java/com/ruoyi/http/config/TqdianbiaoConfig.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,34 @@
package com.ruoyi.http.config;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
/**
 * å¤©å¯ç”µè¡¨å¹³å°é…ç½®
 */
@Data
@Component
@ConfigurationProperties(prefix = "tqdianbiao")
public class TqdianbiaoConfig {
    /** API åŸºç¡€åœ°å€ */
    private String baseUrl = "https://168.tqdianbiao.com";
    /** è®¤è¯ token */
    private String auth = "b6229401590539d5def7f2bf897b33de";
    /** æ˜¯å¦å¿½ç•¥äº’感器变比:0-否,1-是 */
    private Integer ignoreRadio = 1;
    /** åŒæ­¥é…ç½® */
    private Sync sync = new Sync();
    @Data
    public static class Sync {
        /** æ˜¯å¦å¯ç”¨å®šæ—¶åŒæ­¥ */
        private Boolean enabled = true;
        /** å°æ—¶åŒæ­¥å›žçœ‹çª—口(小时数) */
        private Integer hourWindow = 2;
    }
}
src/main/java/com/ruoyi/http/controller/StatisticEleController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,79 @@
package com.ruoyi.http.controller;
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 com.ruoyi.http.service.StatisticEleService;
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.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
@RestController
@Tag(name = "能耗电表统计")
@RequestMapping("/statisticEle")
@RequiredArgsConstructor
public class StatisticEleController extends BaseController {
    private final StatisticEleService statisticEleService;
    @GetMapping("/list")
    @Operation(summary = "能耗数据-列表查询(本地库)")
    @Log(title = "能耗数据-列表查询", businessType = BusinessType.OTHER)
    public AjaxResult list(
            @RequestParam(defaultValue = "hour") String dimension,
            @RequestParam String startTime,
            @RequestParam String endTime,
            @RequestParam(defaultValue = "1") Integer ignoreRadio) {
        return AjaxResult.success(statisticEleService.listRecords(dimension, startTime, endTime, ignoreRadio));
    }
    @GetMapping("/summary")
    @Operation(summary = "能耗数据-汇总统计(本地库)")
    @Log(title = "能耗数据-汇总统计", businessType = BusinessType.OTHER)
    public AjaxResult summary(
            @RequestParam(defaultValue = "hour") String dimension,
            @RequestParam String startTime,
            @RequestParam String endTime) {
        return AjaxResult.success(statisticEleService.getSummary(dimension, startTime, endTime));
    }
    @GetMapping("/yesterday")
    @Operation(summary = "昨日用电量汇总")
    public AjaxResult yesterday() {
        return AjaxResult.success(statisticEleService.getYesterdaySummary());
    }
    @GetMapping("/syncStatus")
    @Operation(summary = "能耗数据-同步状态")
    public AjaxResult syncStatus() {
        return AjaxResult.success(statisticEleService.getSyncStatus());
    }
    @GetMapping("/raw")
    @Operation(summary = "能耗数据-原始接口(调试用,慎用)")
    @Log(title = "能耗数据-原始接口", businessType = BusinessType.OTHER)
    public AjaxResult raw(
            @RequestParam(defaultValue = "hour") String dimension,
            @RequestParam String startTime,
            @RequestParam String endTime) {
        return AjaxResult.success(statisticEleService.fetchRawData(dimension, startTime, endTime));
    }
    @PostMapping("/export")
    @Operation(summary = "能耗数据-导出")
    @Log(title = "能耗数据-导出", businessType = BusinessType.EXPORT)
    public void export(
            @RequestParam(defaultValue = "hour") String dimension,
            @RequestParam String startTime,
            @RequestParam String endTime,
            HttpServletResponse response) {
        statisticEleService.exportRecords(dimension, startTime, endTime, response);
    }
}
src/main/java/com/ruoyi/http/controller/TqdianbiaoCollectorController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.http.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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 com.ruoyi.http.pojo.TqdianbiaoCollector;
import com.ruoyi.http.service.TqdianbiaoCollectorManageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@Tag(name = "采集器档案")
@RequestMapping("/tqdianbiao/collector")
@RequiredArgsConstructor
public class TqdianbiaoCollectorController extends BaseController {
    private final TqdianbiaoCollectorManageService collectorManageService;
    @GetMapping("/listPage")
    @Operation(summary = "采集器档案-分页查询")
    public AjaxResult listPage(Page page, TqdianbiaoCollector query) {
        return AjaxResult.success(collectorManageService.listPage(page, query));
    }
    @GetMapping("/listAll")
    @Operation(summary = "采集器档案-全部列表")
    public AjaxResult listAll() {
        return AjaxResult.success(collectorManageService.list());
    }
    @PostMapping("/add")
    @Log(title = "采集器档案-新增", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody TqdianbiaoCollector collector) {
        return collectorManageService.addCollector(collector) ? AjaxResult.success() : AjaxResult.error();
    }
    @PostMapping("/update")
    @Log(title = "采集器档案-修改", businessType = BusinessType.UPDATE)
    public AjaxResult update(@RequestBody TqdianbiaoCollector collector) {
        return collectorManageService.updateCollector(collector) ? AjaxResult.success() : AjaxResult.error();
    }
    @DeleteMapping("/delete")
    @Log(title = "采集器档案-删除", businessType = BusinessType.DELETE)
    public AjaxResult delete(@RequestBody List<Long> ids) {
        return collectorManageService.deleteByIds(ids) ? AjaxResult.success() : AjaxResult.error();
    }
    @PostMapping("/sync")
    @Log(title = "采集器档案-远程同步", businessType = BusinessType.OTHER)
    public AjaxResult sync() {
        int count = collectorManageService.syncFromRemote();
        return AjaxResult.success("同步成功,共同步 " + count + " æ¡");
    }
}
src/main/java/com/ruoyi/http/controller/TqdianbiaoEleRecordController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,59 @@
package com.ruoyi.http.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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 com.ruoyi.http.pojo.TqdianbiaoEleRecord;
import com.ruoyi.http.service.TqdianbiaoEleRecordManageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@Tag(name = "电量记录管理")
@RequestMapping("/tqdianbiao/eleRecord")
@RequiredArgsConstructor
public class TqdianbiaoEleRecordController extends BaseController {
    private final TqdianbiaoEleRecordManageService eleRecordManageService;
    @GetMapping("/listPage")
    @Operation(summary = "电量记录-分页查询")
    public AjaxResult listPage(Page page, TqdianbiaoEleRecord query) {
        return AjaxResult.success(eleRecordManageService.listPage(page, query));
    }
    @PostMapping("/add")
    @Log(title = "电量记录-新增", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody TqdianbiaoEleRecord record) {
        return eleRecordManageService.addRecord(record) ? AjaxResult.success() : AjaxResult.error();
    }
    @PostMapping("/update")
    @Log(title = "电量记录-修改", businessType = BusinessType.UPDATE)
    public AjaxResult update(@RequestBody TqdianbiaoEleRecord record) {
        return eleRecordManageService.updateRecord(record) ? AjaxResult.success() : AjaxResult.error();
    }
    @DeleteMapping("/delete")
    @Log(title = "电量记录-删除", businessType = BusinessType.DELETE)
    public AjaxResult delete(@RequestBody List<Long> ids) {
        return eleRecordManageService.deleteByIds(ids) ? AjaxResult.success() : AjaxResult.error();
    }
    @GetMapping("/prevReading")
    @Operation(summary = "获取上次电量读数")
    public AjaxResult prevReading(Long meterId, String timeKey) {
        return AjaxResult.success(eleRecordManageService.getPrevReading(meterId, timeKey));
    }
}
src/main/java/com/ruoyi/http/controller/TqdianbiaoMeterController.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,66 @@
package com.ruoyi.http.controller;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
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 com.ruoyi.http.pojo.TqdianbiaoMeter;
import com.ruoyi.http.service.TqdianbiaoMeterManageService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;
@RestController
@Tag(name = "电表档案")
@RequestMapping("/tqdianbiao/meter")
@RequiredArgsConstructor
public class TqdianbiaoMeterController extends BaseController {
    private final TqdianbiaoMeterManageService meterManageService;
    @GetMapping("/listPage")
    @Operation(summary = "电表档案-分页查询")
    public AjaxResult listPage(Page page, TqdianbiaoMeter query) {
        return AjaxResult.success(meterManageService.listPage(page, query));
    }
    @GetMapping("/listAll")
    @Operation(summary = "电表档案-全部列表")
    public AjaxResult listAll() {
        return AjaxResult.success(meterManageService.list());
    }
    @PostMapping("/add")
    @Log(title = "电表档案-新增", businessType = BusinessType.INSERT)
    public AjaxResult add(@RequestBody TqdianbiaoMeter meter) {
        return meterManageService.addMeter(meter) ? AjaxResult.success() : AjaxResult.error();
    }
    @PostMapping("/update")
    @Log(title = "电表档案-修改", businessType = BusinessType.UPDATE)
    public AjaxResult update(@RequestBody TqdianbiaoMeter meter) {
        return meterManageService.updateMeter(meter) ? AjaxResult.success() : AjaxResult.error();
    }
    @DeleteMapping("/delete")
    @Log(title = "电表档案-删除", businessType = BusinessType.DELETE)
    public AjaxResult delete(@RequestBody List<Long> ids) {
        return meterManageService.deleteByIds(ids) ? AjaxResult.success() : AjaxResult.error();
    }
    @PostMapping("/sync")
    @Log(title = "电表档案-远程同步", businessType = BusinessType.OTHER)
    public AjaxResult sync() {
        int count = meterManageService.syncFromRemote();
        return AjaxResult.success("同步成功,共同步 " + count + " æ¡");
    }
}
src/main/java/com/ruoyi/http/mapper/TqdianbiaoCollectorMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package com.ruoyi.http.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.http.pojo.TqdianbiaoCollector;
public interface TqdianbiaoCollectorMapper extends BaseMapper<TqdianbiaoCollector> {
}
src/main/java/com/ruoyi/http/mapper/TqdianbiaoEleRecordMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,20 @@
package com.ruoyi.http.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.http.pojo.TqdianbiaoEleRecord;
import com.ruoyi.http.vo.StatisticEleRecordVo;
import org.apache.ibatis.annotations.Param;
import java.util.List;
public interface TqdianbiaoEleRecordMapper extends BaseMapper<TqdianbiaoEleRecord> {
    List<StatisticEleRecordVo> selectRecordList(
            @Param("dimensions") List<String> dimensions,
            @Param("startTime") String startTime,
            @Param("endTime") String endTime);
    TqdianbiaoEleRecord selectPrevReading(
            @Param("meterId") Long meterId,
            @Param("timeKey") String timeKey);
}
src/main/java/com/ruoyi/http/mapper/TqdianbiaoMeterMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package com.ruoyi.http.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.http.pojo.TqdianbiaoMeter;
public interface TqdianbiaoMeterMapper extends BaseMapper<TqdianbiaoMeter> {
}
src/main/java/com/ruoyi/http/mapper/TqdianbiaoSyncLogMapper.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,7 @@
package com.ruoyi.http.mapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ruoyi.http.pojo.TqdianbiaoSyncLog;
public interface TqdianbiaoSyncLogMapper extends BaseMapper<TqdianbiaoSyncLog> {
}
src/main/java/com/ruoyi/http/pojo/TqdianbiaoCollector.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,40 @@
package com.ruoyi.http.pojo;
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 lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("tqdianbiao_collector")
public class TqdianbiaoCollector {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String collectorId;
    private String collectorNo;
    private Boolean online;
    private Integer csq;
    private String connectTime;
    private String disconnectTime;
    private String description;
    private LocalDateTime syncTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    @TableField(exist = false)
    private String keyword;
}
src/main/java/com/ruoyi/http/pojo/TqdianbiaoEleRecord.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,70 @@
package com.ruoyi.http.pojo;
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 lombok.Data;
import java.math.BigDecimal;
import java.time.LocalDateTime;
@Data
@TableName("tqdianbiao_ele_record")
public class TqdianbiaoEleRecord {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long meterId;
    /** ç”µè¡¨åç§°(冗余) */
    private String meterName;
    private String dimension;
    /** æŠ„表方式 sync/manual */
    private String readingMethod;
    private String timeKey;
    private String startTime;
    private String endTime;
    private BigDecimal totalConsumption;
    private BigDecimal sharpConsumption;
    private BigDecimal peakConsumption;
    private BigDecimal flatConsumption;
    private BigDecimal valleyConsumption;
    private BigDecimal deepValleyConsumption;
    private String startReading;
    private String endReading;
    /** ä¸Šæ¬¡ç”µé‡(总读数) */
    private BigDecimal prevReading;
    /** æœ¬æ¬¡ç”µé‡(总读数) */
    private BigDecimal currReading;
    private Integer ratio;
    private LocalDateTime syncTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    @TableField(exist = false)
    private String startTimeKey;
    @TableField(exist = false)
    private String endTimeKey;
}
src/main/java/com/ruoyi/http/pojo/TqdianbiaoMeter.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,50 @@
package com.ruoyi.http.pojo;
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 lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("tqdianbiao_meter")
public class TqdianbiaoMeter {
    @TableId(type = IdType.AUTO)
    private Long id;
    private Long meterId;
    /** ç”µè¡¨åç§° */
    private String meterName;
    private String collectorId;
    private String collectorNo;
    private String address;
    private Integer rate;
    private String relayState;
    private String meterType;
    private Integer csq;
    private String description;
    /** æ¥æº sync-平台同步 manual-手动添加 */
    private String source;
    private LocalDateTime syncTime;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
    @TableField(exist = false)
    private String keyword;
}
src/main/java/com/ruoyi/http/pojo/TqdianbiaoSyncLog.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,32 @@
package com.ruoyi.http.pojo;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
@Data
@TableName("tqdianbiao_sync_log")
public class TqdianbiaoSyncLog {
    @TableId(type = IdType.AUTO)
    private Long id;
    private String syncType;
    private String windowStart;
    private String windowEnd;
    private String status;
    private Integer recordCount;
    private Integer apiCallCount;
    private String errorMsg;
    private LocalDateTime createTime;
}
src/main/java/com/ruoyi/http/service/StatisticEleService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,26 @@
package com.ruoyi.http.service;
import com.ruoyi.http.vo.StatisticEleRecordVo;
import com.ruoyi.http.vo.StatisticEleSummaryVo;
import com.ruoyi.http.vo.StatisticEleSyncStatusVo;
import jakarta.servlet.http.HttpServletResponse;
import java.util.List;
/**
 * å¤©å¯ç”µè¡¨ç»Ÿè®¡æŽ¥å£
 */
public interface StatisticEleService {
    String fetchRawData(String dimension, String startTime, String endTime);
    List<StatisticEleRecordVo> listRecords(String dimension, String startTime, String endTime, Integer ignoreRadio);
    StatisticEleSummaryVo getSummary(String dimension, String startTime, String endTime);
    StatisticEleSummaryVo getYesterdaySummary();
    void exportRecords(String dimension, String startTime, String endTime, HttpServletResponse response);
    StatisticEleSyncStatusVo getSyncStatus();
}
src/main/java/com/ruoyi/http/service/TqdianbiaoCollectorManageService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.http.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.http.pojo.TqdianbiaoCollector;
import java.util.List;
public interface TqdianbiaoCollectorManageService extends IService<TqdianbiaoCollector> {
    IPage<TqdianbiaoCollector> listPage(Page page, TqdianbiaoCollector query);
    boolean addCollector(TqdianbiaoCollector collector);
    boolean updateCollector(TqdianbiaoCollector collector);
    boolean deleteByIds(List<Long> ids);
    int syncFromRemote();
}
src/main/java/com/ruoyi/http/service/TqdianbiaoCollectorSyncService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
package com.ruoyi.http.service;
public interface TqdianbiaoCollectorSyncService {
    int syncCollectors();
}
src/main/java/com/ruoyi/http/service/TqdianbiaoEleRecordManageService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,22 @@
package com.ruoyi.http.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.http.pojo.TqdianbiaoEleRecord;
import java.math.BigDecimal;
import java.util.List;
public interface TqdianbiaoEleRecordManageService extends IService<TqdianbiaoEleRecord> {
    IPage<TqdianbiaoEleRecord> listPage(Page page, TqdianbiaoEleRecord query);
    boolean addRecord(TqdianbiaoEleRecord record);
    boolean updateRecord(TqdianbiaoEleRecord record);
    boolean deleteByIds(List<Long> ids);
    BigDecimal getPrevReading(Long meterId, String timeKey);
}
src/main/java/com/ruoyi/http/service/TqdianbiaoEleSyncService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,15 @@
package com.ruoyi.http.service;
public interface TqdianbiaoEleSyncService {
    int syncHourData();
    /** ä¸€æ¬¡æ€§è¡¥æ•°ï¼šåŒæ­¥å‰ä¸‰å¤© 00:00 è‡³å½“前小时,用完可删 */
    int syncLast3DaysHourData();
    int syncDayData();
    int syncMonthData();
    int syncYearData();
}
src/main/java/com/ruoyi/http/service/TqdianbiaoMeterManageService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,21 @@
package com.ruoyi.http.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.http.pojo.TqdianbiaoMeter;
import java.util.List;
public interface TqdianbiaoMeterManageService extends IService<TqdianbiaoMeter> {
    IPage<TqdianbiaoMeter> listPage(Page page, TqdianbiaoMeter query);
    boolean addMeter(TqdianbiaoMeter meter);
    boolean updateMeter(TqdianbiaoMeter meter);
    boolean deleteByIds(List<Long> ids);
    int syncFromRemote();
}
src/main/java/com/ruoyi/http/service/TqdianbiaoMeterSyncService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,6 @@
package com.ruoyi.http.service;
public interface TqdianbiaoMeterSyncService {
    int syncMeters();
}
src/main/java/com/ruoyi/http/service/TqdianbiaoSyncLogService.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,8 @@
package com.ruoyi.http.service;
public interface TqdianbiaoSyncLogService {
    void logSuccess(String syncType, String windowStart, String windowEnd, int recordCount);
    void logFailure(String syncType, String windowStart, String windowEnd, String errorMsg);
}
src/main/java/com/ruoyi/http/service/impl/StatisticEleServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,179 @@
package com.ruoyi.http.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.common.utils.poi.ExcelUtil;
import com.ruoyi.http.config.TqdianbiaoConfig;
import com.ruoyi.http.mapper.TqdianbiaoCollectorMapper;
import com.ruoyi.http.mapper.TqdianbiaoEleRecordMapper;
import com.ruoyi.http.mapper.TqdianbiaoMeterMapper;
import com.ruoyi.http.mapper.TqdianbiaoSyncLogMapper;
import com.ruoyi.http.pojo.TqdianbiaoCollector;
import com.ruoyi.http.pojo.TqdianbiaoEleRecord;
import com.ruoyi.http.pojo.TqdianbiaoSyncLog;
import com.ruoyi.http.service.StatisticEleService;
import com.ruoyi.http.util.StatisticEleAggregateUtil;
import com.ruoyi.http.util.StatisticEleAggregateUtil.HourRange;
import com.ruoyi.http.vo.StatisticEleRecordVo;
import com.ruoyi.http.vo.StatisticEleSummaryVo;
import com.ruoyi.http.vo.StatisticEleSyncStatusVo;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.function.Function;
import java.util.stream.Collectors;
@Service
@Slf4j
@RequiredArgsConstructor
public class StatisticEleServiceImpl implements StatisticEleService {
    private static final Set<String> STAT_DIMENSIONS = Set.of("day", "month", "quarter", "year");
    private static final List<String> DATA_DIMENSIONS = List.of("hour", "manual");
    private static final DateTimeFormatter LOG_TIME_FMT = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
    private final TqdianbiaoConfig config;
    private final TqdianbiaoEleRecordMapper eleRecordMapper;
    private final TqdianbiaoMeterMapper meterMapper;
    private final TqdianbiaoCollectorMapper collectorMapper;
    private final TqdianbiaoSyncLogMapper syncLogMapper;
    @Override
    public String fetchRawData(String dimension, String startTime, String endTime) {
        if (!"hour".equals(dimension)) {
            throw new ServiceException("仅支持拉取小时原始数据");
        }
        String url = config.getBaseUrl() + "/Api/StatisticEle/hour";
        String param = String.format(
                "auth=%s&start_time=%s&end_time=%s&ignore_radio=%d",
                config.getAuth(), startTime, endTime, config.getIgnoreRadio()
        );
        log.warn("调用远程电表接口(调试): {}?{}", url, param);
        return HttpUtils.sendGet(url, param);
    }
    @Override
    public List<StatisticEleRecordVo> listRecords(String dimension, String startTime, String endTime, Integer ignoreRadio) {
        if ("hour".equals(dimension) || "collection".equals(dimension)) {
            return queryHourRecords(startTime, endTime);
        }
        return aggregateFromHour(dimension, startTime, endTime, true);
    }
    @Override
    public StatisticEleSummaryVo getSummary(String dimension, String startTime, String endTime) {
        if (!StringUtils.hasText(startTime) || !StringUtils.hasText(endTime)) {
            throw new ServiceException("开始时间和结束时间不能为空");
        }
        if ("hour".equals(dimension)) {
            List<StatisticEleRecordVo> detailRecords = queryHourRecords(startTime, endTime);
            List<StatisticEleRecordVo> chartRecords = StatisticEleAggregateUtil.aggregateHourToBuckets(
                    detailRecords, StatisticEleAggregateUtil.HOUR_TO_HOUR);
            return buildSummary(detailRecords, chartRecords);
        }
        if (!STAT_DIMENSIONS.contains(dimension)) {
            throw new ServiceException("统计维度无效,支持 hour/day/month/quarter/year");
        }
        List<StatisticEleRecordVo> detailRecords = aggregateFromHour(dimension, startTime, endTime, true);
        List<StatisticEleRecordVo> chartRecords = aggregateFromHour(dimension, startTime, endTime, false);
        return buildSummary(detailRecords, chartRecords);
    }
    private StatisticEleSummaryVo buildSummary(
            List<StatisticEleRecordVo> detailRecords, List<StatisticEleRecordVo> chartRecords) {
        StatisticEleAggregateUtil.StatisticEleSummaryMetrics metrics =
                StatisticEleAggregateUtil.calcMetrics(chartRecords);
        StatisticEleSummaryVo summary = new StatisticEleSummaryVo();
        summary.setRecords(detailRecords);
        summary.setChartRecords(chartRecords);
        summary.setRecordCount(metrics.getRecordCount());
        summary.setTotalConsumption(metrics.getTotalConsumption());
        summary.setAvgConsumption(metrics.getAvgConsumption());
        summary.setMaxConsumption(metrics.getMaxConsumption());
        summary.setMinConsumption(metrics.getMinConsumption());
        return summary;
    }
    @Override
    public StatisticEleSummaryVo getYesterdaySummary() {
        HourRange range = StatisticEleAggregateUtil.yesterdayHourRange();
        List<StatisticEleRecordVo> records = queryHourRecords(range.startTime(), range.endTime());
        List<StatisticEleRecordVo> chartRecords = StatisticEleAggregateUtil.aggregateHourToBuckets(
                records, StatisticEleAggregateUtil.HOUR_TO_HOUR);
        StatisticEleSummaryVo summary = buildSummary(records, chartRecords);
        summary.setTotalConsumption(round(StatisticEleAggregateUtil.sumRecordsTotal(records)));
        return summary;
    }
    private static double round(double value) {
        return Math.round(value * 100.0) / 100.0;
    }
    @Override
    public void exportRecords(String dimension, String startTime, String endTime, HttpServletResponse response) {
        List<StatisticEleRecordVo> records;
        if ("hour".equals(dimension)) {
            records = queryHourRecords(startTime, endTime);
        } else {
            records = aggregateFromHour(dimension, startTime, endTime, true);
        }
        ExcelUtil<StatisticEleRecordVo> util = new ExcelUtil<>(StatisticEleRecordVo.class);
        util.exportExcel(response, records, "能耗统计数据");
    }
    @Override
    public StatisticEleSyncStatusVo getSyncStatus() {
        StatisticEleSyncStatusVo status = new StatisticEleSyncStatusVo();
        status.setMeterCount(Math.toIntExact(meterMapper.selectCount(null)));
        status.setCollectorCount(Math.toIntExact(collectorMapper.selectCount(null)));
        status.setOnlineCollectorCount(Math.toIntExact(collectorMapper.selectCount(
                Wrappers.<TqdianbiaoCollector>lambdaQuery().eq(TqdianbiaoCollector::getOnline, true))));
        Map<String, Long> recordCountByDimension = new HashMap<>();
        recordCountByDimension.put("hour", eleRecordMapper.selectCount(
                Wrappers.<TqdianbiaoEleRecord>lambdaQuery().eq(TqdianbiaoEleRecord::getDimension, "hour")));
        status.setRecordCountByDimension(recordCountByDimension);
        Map<String, String> lastSyncTimeByType = new HashMap<>();
        for (String syncType : List.of("collector", "meter", "hour")) {
            TqdianbiaoSyncLog latest = syncLogMapper.selectOne(
                    Wrappers.<TqdianbiaoSyncLog>lambdaQuery()
                            .eq(TqdianbiaoSyncLog::getSyncType, syncType)
                            .eq(TqdianbiaoSyncLog::getStatus, "success")
                            .orderByDesc(TqdianbiaoSyncLog::getCreateTime)
                            .last("LIMIT 1"));
            if (latest != null && latest.getCreateTime() != null) {
                lastSyncTimeByType.put(syncType, latest.getCreateTime().format(LOG_TIME_FMT));
            }
        }
        status.setLastSyncTimeByType(lastSyncTimeByType);
        return status;
    }
    private List<StatisticEleRecordVo> queryHourRecords(String startTime, String endTime) {
        String normalizedStart = StatisticEleAggregateUtil.normalizeQueryStartTimeKey(startTime);
        String normalizedEnd = StatisticEleAggregateUtil.normalizeQueryEndTimeKey(endTime);
        return eleRecordMapper.selectRecordList(DATA_DIMENSIONS, normalizedStart, normalizedEnd);
    }
    private List<StatisticEleRecordVo> aggregateFromHour(
            String dimension, String startTime, String endTime, boolean perMeter) {
        HourRange range = StatisticEleAggregateUtil.toHourQueryRange(dimension, startTime, endTime);
        List<StatisticEleRecordVo> hourRecords = queryHourRecords(range.startTime(), range.endTime());
        Function<String, String> bucketFn = StatisticEleAggregateUtil.bucketFn(dimension);
        if (perMeter) {
            return StatisticEleAggregateUtil.aggregateHourPerMeter(hourRecords, bucketFn);
        }
        return StatisticEleAggregateUtil.aggregateHourToBuckets(hourRecords, bucketFn);
    }
}
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoCollectorManageServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,91 @@
package com.ruoyi.http.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.http.mapper.TqdianbiaoCollectorMapper;
import com.ruoyi.http.pojo.TqdianbiaoCollector;
import com.ruoyi.http.service.TqdianbiaoCollectorManageService;
import com.ruoyi.http.service.TqdianbiaoCollectorSyncService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TqdianbiaoCollectorManageServiceImpl
        extends ServiceImpl<TqdianbiaoCollectorMapper, TqdianbiaoCollector>
        implements TqdianbiaoCollectorManageService {
    private final TqdianbiaoCollectorSyncService collectorSyncService;
    @Override
    public IPage<TqdianbiaoCollector> listPage(Page page, TqdianbiaoCollector query) {
        return page(page, Wrappers.<TqdianbiaoCollector>lambdaQuery()
                .and(StringUtils.isNotEmpty(query.getKeyword()), w -> w
                        .like(TqdianbiaoCollector::getCollectorNo, query.getKeyword())
                        .or()
                        .like(TqdianbiaoCollector::getCollectorId, query.getKeyword())
                        .or()
                        .like(TqdianbiaoCollector::getDescription, query.getKeyword()))
                .eq(StringUtils.isNotEmpty(query.getCollectorId()), TqdianbiaoCollector::getCollectorId, query.getCollectorId())
                .orderByDesc(TqdianbiaoCollector::getUpdateTime));
    }
    @Override
    public boolean addCollector(TqdianbiaoCollector collector) {
        validateCollector(collector);
        if (existsCollectorId(collector.getCollectorId(), null)) {
            throw new ServiceException("采集器档案ID已存在");
        }
        collector.setSyncTime(LocalDateTime.now());
        return save(collector);
    }
    @Override
    public boolean updateCollector(TqdianbiaoCollector collector) {
        if (collector.getId() == null) {
            throw new ServiceException("ID不能为空");
        }
        validateCollector(collector);
        if (existsCollectorId(collector.getCollectorId(), collector.getId())) {
            throw new ServiceException("采集器档案ID已存在");
        }
        collector.setSyncTime(LocalDateTime.now());
        return updateById(collector);
    }
    @Override
    public boolean deleteByIds(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            throw new ServiceException("请选择至少一条数据");
        }
        return removeBatchByIds(ids);
    }
    @Override
    public int syncFromRemote() {
        return collectorSyncService.syncCollectors();
    }
    private void validateCollector(TqdianbiaoCollector collector) {
        if (StringUtils.isEmpty(collector.getCollectorId())) {
            throw new ServiceException("采集器档案ID不能为空");
        }
        if (StringUtils.isEmpty(collector.getCollectorNo())) {
            throw new ServiceException("采集器号不能为空");
        }
    }
    private boolean existsCollectorId(String collectorId, Long excludeId) {
        return count(Wrappers.<TqdianbiaoCollector>lambdaQuery()
                .eq(TqdianbiaoCollector::getCollectorId, collectorId)
                .ne(excludeId != null, TqdianbiaoCollector::getId, excludeId)) > 0;
    }
}
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoCollectorSyncServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,90 @@
package com.ruoyi.http.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.http.config.TqdianbiaoConfig;
import com.ruoyi.http.mapper.TqdianbiaoCollectorMapper;
import com.ruoyi.http.pojo.TqdianbiaoCollector;
import com.ruoyi.http.service.TqdianbiaoCollectorSyncService;
import com.ruoyi.http.service.TqdianbiaoSyncLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
@Service
@Slf4j
@RequiredArgsConstructor
public class TqdianbiaoCollectorSyncServiceImpl implements TqdianbiaoCollectorSyncService {
    private final TqdianbiaoConfig config;
    private final TqdianbiaoCollectorMapper collectorMapper;
    private final TqdianbiaoSyncLogService syncLogService;
    @Override
    public int syncCollectors() {
        String syncType = "collector";
        try {
            String url = config.getBaseUrl() + "/Api/Collector";
            String param = "auth=" + config.getAuth();
            String raw = HttpUtils.sendGet(url, param);
            JSONArray list = parseList(raw);
            LocalDateTime now = LocalDateTime.now();
            int count = 0;
            for (int i = 0; i < list.size(); i++) {
                JSONObject item = list.getJSONObject(i);
                String collectorId = item.getString("id");
                if (collectorId == null) {
                    continue;
                }
                TqdianbiaoCollector entity = collectorMapper.selectOne(
                        Wrappers.<TqdianbiaoCollector>lambdaQuery()
                                .eq(TqdianbiaoCollector::getCollectorId, collectorId)
                                .last("LIMIT 1"));
                if (entity == null) {
                    entity = new TqdianbiaoCollector();
                    entity.setCollectorId(collectorId);
                }
                entity.setCollectorNo(item.getString("collectorid"));
                entity.setOnline(item.getBoolean("online"));
                entity.setCsq(item.getInteger("csq"));
                entity.setConnectTime(item.getString("connect_time"));
                entity.setDisconnectTime(item.getString("disconnect_time"));
                entity.setDescription(item.getString("description"));
                entity.setSyncTime(now);
                if (entity.getId() == null) {
                    collectorMapper.insert(entity);
                } else {
                    collectorMapper.updateById(entity);
                }
                count++;
            }
            syncLogService.logSuccess(syncType, null, null, count);
            return count;
        } catch (Exception e) {
            log.error("采集器同步失败", e);
            syncLogService.logFailure(syncType, null, null, e.getMessage());
            throw e;
        }
    }
    private JSONArray parseList(String raw) {
        Object parsed = JSON.parse(raw);
        if (parsed instanceof JSONArray) {
            return (JSONArray) parsed;
        }
        JSONObject root = (JSONObject) parsed;
        if (root.getIntValue("status") != 1) {
            throw new IllegalStateException("采集器接口返回异常: " + raw);
        }
        Object data = root.get("data");
        if (data instanceof JSONArray) {
            return (JSONArray) data;
        }
        return new JSONArray();
    }
}
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoEleRecordManageServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,140 @@
package com.ruoyi.http.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.http.mapper.TqdianbiaoEleRecordMapper;
import com.ruoyi.http.mapper.TqdianbiaoMeterMapper;
import com.ruoyi.http.pojo.TqdianbiaoEleRecord;
import com.ruoyi.http.pojo.TqdianbiaoMeter;
import com.ruoyi.http.service.TqdianbiaoEleRecordManageService;
import com.ruoyi.http.util.StatisticEleReadingUtil;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Set;
@Service
public class TqdianbiaoEleRecordManageServiceImpl
        extends ServiceImpl<TqdianbiaoEleRecordMapper, TqdianbiaoEleRecord>
        implements TqdianbiaoEleRecordManageService {
    private static final Set<String> MANUAL_DIMENSIONS = Set.of("manual");
    private final TqdianbiaoMeterMapper meterMapper;
    public TqdianbiaoEleRecordManageServiceImpl(TqdianbiaoMeterMapper meterMapper) {
        this.meterMapper = meterMapper;
    }
    @Override
    public IPage<TqdianbiaoEleRecord> listPage(Page page, TqdianbiaoEleRecord query) {
        return page(page, Wrappers.<TqdianbiaoEleRecord>lambdaQuery()
                .eq(StringUtils.isNotEmpty(query.getDimension()), TqdianbiaoEleRecord::getDimension, query.getDimension())
                .eq(query.getMeterId() != null, TqdianbiaoEleRecord::getMeterId, query.getMeterId())
                .ge(StringUtils.isNotEmpty(query.getStartTimeKey()), TqdianbiaoEleRecord::getTimeKey, query.getStartTimeKey())
                .le(StringUtils.isNotEmpty(query.getEndTimeKey()), TqdianbiaoEleRecord::getTimeKey, query.getEndTimeKey())
                .orderByDesc(TqdianbiaoEleRecord::getTimeKey));
    }
    @Override
    public boolean addRecord(TqdianbiaoEleRecord record) {
        validateRecord(record);
        enrichFromMeter(record);
        if (existsUnique(record, null)) {
            throw new ServiceException("该电表在该时间点已存在记录");
        }
        record.setSyncTime(LocalDateTime.now());
        return save(record);
    }
    @Override
    public boolean updateRecord(TqdianbiaoEleRecord record) {
        if (record.getId() == null) {
            throw new ServiceException("ID不能为空");
        }
        validateRecord(record);
        enrichFromMeter(record);
        if (existsUnique(record, record.getId())) {
            throw new ServiceException("该电表在该时间点已存在记录");
        }
        record.setSyncTime(LocalDateTime.now());
        return updateById(record);
    }
    @Override
    public boolean deleteByIds(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            throw new ServiceException("请选择至少一条数据");
        }
        return removeBatchByIds(ids);
    }
    @Override
    public BigDecimal getPrevReading(Long meterId, String timeKey) {
        if (meterId == null || StringUtils.isEmpty(timeKey)) {
            return null;
        }
        TqdianbiaoEleRecord prev = baseMapper.selectPrevReading(meterId, timeKey);
        if (prev == null) {
            return null;
        }
        if (prev.getCurrReading() != null) {
            return prev.getCurrReading();
        }
        return StatisticEleReadingUtil.parseFirstReading(prev.getEndReading());
    }
    private void enrichFromMeter(TqdianbiaoEleRecord record) {
        TqdianbiaoMeter meter = meterMapper.selectOne(
                Wrappers.<TqdianbiaoMeter>lambdaQuery()
                        .eq(TqdianbiaoMeter::getMeterId, record.getMeterId())
                        .last("LIMIT 1"));
        if (meter != null) {
            record.setMeterName(meter.getMeterName());
            if (record.getRatio() == null && meter.getRate() != null) {
                record.setRatio(meter.getRate());
            }
        }
        if (record.getPrevReading() != null && record.getCurrReading() != null) {
            record.setTotalConsumption(
                    StatisticEleReadingUtil.calcConsumption(record.getPrevReading(), record.getCurrReading(), record.getRatio()));
        }
    }
    private void validateRecord(TqdianbiaoEleRecord record) {
        if (record.getMeterId() == null) {
            throw new ServiceException("电表不能为空");
        }
        if (StringUtils.isEmpty(record.getDimension()) || !MANUAL_DIMENSIONS.contains(record.getDimension())) {
            throw new ServiceException("仅支持手动录入(manual)维度数据");
        }
        if (StringUtils.isEmpty(record.getTimeKey())) {
            throw new ServiceException("时间标识不能为空");
        }
        if (record.getTimeKey().length() != 12) {
            throw new ServiceException("手动录入时间标识格式应为 YYYYMMDDHHmm");
        }
        if (record.getCurrReading() == null) {
            throw new ServiceException("本次电量不能为空");
        }
        if (record.getPrevReading() == null) {
            throw new ServiceException("上次电量不能为空");
        }
        record.setReadingMethod("manual");
    }
    private boolean existsUnique(TqdianbiaoEleRecord record, Long excludeId) {
        return count(Wrappers.<TqdianbiaoEleRecord>lambdaQuery()
                .eq(TqdianbiaoEleRecord::getMeterId, record.getMeterId())
                .eq(TqdianbiaoEleRecord::getDimension, record.getDimension())
                .eq(TqdianbiaoEleRecord::getTimeKey, record.getTimeKey())
                .ne(excludeId != null, TqdianbiaoEleRecord::getId, excludeId)) > 0;
    }
}
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoEleSyncServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,150 @@
package com.ruoyi.http.service.impl;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.http.config.TqdianbiaoConfig;
import com.ruoyi.http.mapper.TqdianbiaoEleRecordMapper;
import com.ruoyi.http.mapper.TqdianbiaoMeterMapper;
import com.ruoyi.http.pojo.TqdianbiaoEleRecord;
import com.ruoyi.http.pojo.TqdianbiaoMeter;
import com.ruoyi.http.service.TqdianbiaoEleSyncService;
import com.ruoyi.http.service.TqdianbiaoSyncLogService;
import com.ruoyi.http.util.StatisticEleParseUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
@Service
@Slf4j
@RequiredArgsConstructor
public class TqdianbiaoEleSyncServiceImpl implements TqdianbiaoEleSyncService {
    private static final DateTimeFormatter HOUR_FMT = DateTimeFormatter.ofPattern("yyyyMMddHH");
    private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
    private static final DateTimeFormatter MONTH_FMT = DateTimeFormatter.ofPattern("yyyyMM");
    private static final DateTimeFormatter YEAR_FMT = DateTimeFormatter.ofPattern("yyyy");
    private final TqdianbiaoConfig config;
    private final TqdianbiaoEleRecordMapper eleRecordMapper;
    private final TqdianbiaoMeterMapper meterMapper;
    private final TqdianbiaoSyncLogService syncLogService;
    @Override
    public int syncHourData() {
        int window = config.getSync().getHourWindow() != null ? config.getSync().getHourWindow() : 2;
        LocalDateTime end = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
        LocalDateTime start = end.minusHours(window);
        String startTime = start.format(HOUR_FMT);
        String endTime = end.format(HOUR_FMT);
        return syncDimension("hour", startTime, endTime);
    }
    @Override
    public int syncLast3DaysHourData() {
        LocalDateTime start = LocalDate.now().minusDays(3).atStartOfDay();
        LocalDateTime end = LocalDateTime.now().withMinute(0).withSecond(0).withNano(0);
        return syncHourRangeInChunks(start, end);
    }
    /** æŒ‰æœ€å¤š 24 å°æ—¶ä¸€æ®µåˆ†æ‰¹æ‹‰å–(平台接口限制) */
    private int syncHourRangeInChunks(LocalDateTime start, LocalDateTime end) {
        int total = 0;
        LocalDateTime windowStart = start;
        while (!windowStart.isAfter(end)) {
            LocalDateTime windowEnd = windowStart.plusHours(23);
            if (windowEnd.isAfter(end)) {
                windowEnd = end;
            }
            total += syncDimension("hour", windowStart.format(HOUR_FMT), windowEnd.format(HOUR_FMT));
            windowStart = windowEnd.plusHours(1);
        }
        return total;
    }
    @Override
    public int syncDayData() {
        LocalDate yesterday = LocalDate.now().minusDays(1);
        String day = yesterday.format(DAY_FMT);
        return syncDimension("day", day, day);
    }
    @Override
    public int syncMonthData() {
        LocalDate now = LocalDate.now();
        String month = now.format(MONTH_FMT);
        return syncDimension("month", month, month);
    }
    @Override
    public int syncYearData() {
        LocalDate now = LocalDate.now();
        String year = now.format(YEAR_FMT);
        return syncDimension("year", year, year);
    }
    private int syncDimension(String dimension, String startTime, String endTime) {
        return syncDimension(dimension, startTime, endTime, config.getIgnoreRadio());
    }
    private int syncDimension(String dimension, String startTime, String endTime, int ignoreRadio) {
        try {
            String url = config.getBaseUrl() + "/Api/StatisticEle/" + dimension;
            String param = String.format(
                    "auth=%s&start_time=%s&end_time=%s&ignore_radio=%d",
                    config.getAuth(), startTime, endTime, ignoreRadio
            );
            log.info("同步电量数据: {} {}-{} ignore_radio={}", dimension, startTime, endTime, ignoreRadio);
            String raw = HttpUtils.sendGet(url, param);
            List<TqdianbiaoEleRecord> records = StatisticEleParseUtil.parseToEntities(raw, dimension);
            enrichMeterInfo(records);
            int count = upsertRecords(records);
            syncLogService.logSuccess(dimension, startTime, endTime, count);
            return count;
        } catch (Exception e) {
            log.error("电量同步失败 dimension={} {}-{}", dimension, startTime, endTime, e);
            syncLogService.logFailure(dimension, startTime, endTime, e.getMessage());
            throw e;
        }
    }
    private void enrichMeterInfo(List<TqdianbiaoEleRecord> records) {
        for (TqdianbiaoEleRecord record : records) {
            if (record.getMeterId() == null) {
                continue;
            }
            TqdianbiaoMeter meter = meterMapper.selectOne(
                    Wrappers.<TqdianbiaoMeter>lambdaQuery()
                            .eq(TqdianbiaoMeter::getMeterId, record.getMeterId())
                            .last("LIMIT 1"));
            if (meter != null && StringUtils.hasText(meter.getMeterName())) {
                record.setMeterName(meter.getMeterName());
            }
        }
    }
    private int upsertRecords(List<TqdianbiaoEleRecord> records) {
        int count = 0;
        for (TqdianbiaoEleRecord record : records) {
            TqdianbiaoEleRecord existing = eleRecordMapper.selectOne(
                    Wrappers.<TqdianbiaoEleRecord>lambdaQuery()
                            .eq(TqdianbiaoEleRecord::getMeterId, record.getMeterId())
                            .eq(TqdianbiaoEleRecord::getDimension, record.getDimension())
                            .eq(TqdianbiaoEleRecord::getTimeKey, record.getTimeKey())
                            .last("LIMIT 1"));
            if (existing == null) {
                eleRecordMapper.insert(record);
            } else {
                record.setId(existing.getId());
                eleRecordMapper.updateById(record);
            }
            count++;
        }
        return count;
    }
}
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoMeterManageServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,120 @@
package com.ruoyi.http.service.impl;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
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.http.mapper.TqdianbiaoMeterMapper;
import com.ruoyi.http.pojo.TqdianbiaoMeter;
import com.ruoyi.http.service.TqdianbiaoMeterManageService;
import com.ruoyi.http.service.TqdianbiaoMeterSyncService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.util.CollectionUtils;
import java.time.LocalDateTime;
import java.util.List;
@Service
@RequiredArgsConstructor
public class TqdianbiaoMeterManageServiceImpl
        extends ServiceImpl<TqdianbiaoMeterMapper, TqdianbiaoMeter>
        implements TqdianbiaoMeterManageService {
    private static final long MANUAL_METER_ID_BASE = 900_000_000L;
    private final TqdianbiaoMeterSyncService meterSyncService;
    @Override
    public IPage<TqdianbiaoMeter> listPage(Page page, TqdianbiaoMeter query) {
        return page(page, Wrappers.<TqdianbiaoMeter>lambdaQuery()
                .and(StringUtils.isNotEmpty(query.getKeyword()), w -> w
                        .like(TqdianbiaoMeter::getMeterName, query.getKeyword())
                        .or()
                        .like(TqdianbiaoMeter::getAddress, query.getKeyword())
                        .or()
                        .like(TqdianbiaoMeter::getDescription, query.getKeyword()))
                .eq(query.getMeterId() != null, TqdianbiaoMeter::getMeterId, query.getMeterId())
                .eq(StringUtils.isNotEmpty(query.getSource()), TqdianbiaoMeter::getSource, query.getSource())
                .orderByDesc(TqdianbiaoMeter::getUpdateTime));
    }
    @Override
    public boolean addMeter(TqdianbiaoMeter meter) {
        validateManualMeter(meter);
        meter.setSource("manual");
        meter.setMeterId(generateManualMeterId());
        if (meter.getRate() == null) {
            meter.setRate(1);
        }
        if (StringUtils.isEmpty(meter.getRelayState())) {
            meter.setRelayState("1");
        }
        meter.setSyncTime(LocalDateTime.now());
        return save(meter);
    }
    @Override
    public boolean updateMeter(TqdianbiaoMeter meter) {
        if (meter.getId() == null) {
            throw new ServiceException("ID不能为空");
        }
        TqdianbiaoMeter existing = getById(meter.getId());
        if (existing == null) {
            throw new ServiceException("电表不存在");
        }
        if (StringUtils.isEmpty(meter.getAddress())) {
            throw new ServiceException("表地址不能为空");
        }
        String meterName = StringUtils.isNotEmpty(meter.getMeterName()) ? meter.getMeterName() : meter.getAddress();
        existing.setMeterName(meterName);
        existing.setAddress(meter.getAddress());
        existing.setDescription(meter.getDescription());
        existing.setRelayState(meter.getRelayState());
        if (existing.getSource() == null) {
            existing.setSource("sync");
        }
        if ("manual".equals(existing.getSource())) {
            existing.setRate(meter.getRate());
        }
        existing.setSyncTime(LocalDateTime.now());
        return updateById(existing);
    }
    @Override
    public boolean deleteByIds(List<Long> ids) {
        if (CollectionUtils.isEmpty(ids)) {
            throw new ServiceException("请选择至少一条数据");
        }
        return removeBatchByIds(ids);
    }
    @Override
    public int syncFromRemote() {
        return meterSyncService.syncMeters();
    }
    private void validateManualMeter(TqdianbiaoMeter meter) {
        if (StringUtils.isEmpty(meter.getMeterName())) {
            throw new ServiceException("电表名称不能为空");
        }
        if (StringUtils.isEmpty(meter.getAddress())) {
            throw new ServiceException("表地址不能为空");
        }
    }
    private Long generateManualMeterId() {
        TqdianbiaoMeter maxManual = getOne(
                Wrappers.<TqdianbiaoMeter>lambdaQuery()
                        .eq(TqdianbiaoMeter::getSource, "manual")
                        .orderByDesc(TqdianbiaoMeter::getMeterId)
                        .last("LIMIT 1"),
                false);
        if (maxManual == null || maxManual.getMeterId() == null) {
            return MANUAL_METER_ID_BASE + 1;
        }
        return maxManual.getMeterId() + 1;
    }
}
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoMeterSyncServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,112 @@
package com.ruoyi.http.service.impl;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.ruoyi.common.utils.http.HttpUtils;
import com.ruoyi.http.config.TqdianbiaoConfig;
import com.ruoyi.http.mapper.TqdianbiaoMeterMapper;
import com.ruoyi.http.pojo.TqdianbiaoMeter;
import com.ruoyi.http.service.TqdianbiaoMeterSyncService;
import com.ruoyi.http.service.TqdianbiaoSyncLogService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;
import java.time.LocalDateTime;
@Service
@Slf4j
@RequiredArgsConstructor
public class TqdianbiaoMeterSyncServiceImpl implements TqdianbiaoMeterSyncService {
    private final TqdianbiaoConfig config;
    private final TqdianbiaoMeterMapper meterMapper;
    private final TqdianbiaoSyncLogService syncLogService;
    @Override
    public int syncMeters() {
        String syncType = "meter";
        try {
            String url = config.getBaseUrl() + "/Api/Meter";
            String param = "auth=" + config.getAuth();
            String raw = HttpUtils.sendGet(url, param);
            JSONArray list = parseList(raw);
            LocalDateTime now = LocalDateTime.now();
            int count = 0;
            for (int i = 0; i < list.size(); i++) {
                JSONObject item = list.getJSONObject(i);
                if (!"0".equals(String.valueOf(item.get("device_type")))) {
                    continue;
                }
                Long meterId = item.getLong("id");
                if (meterId == null) {
                    continue;
                }
                TqdianbiaoMeter entity = meterMapper.selectOne(
                        Wrappers.<TqdianbiaoMeter>lambdaQuery()
                                .eq(TqdianbiaoMeter::getMeterId, meterId)
                                .last("LIMIT 1"));
                boolean isNew = entity == null;
                if (isNew) {
                    entity = new TqdianbiaoMeter();
                    entity.setMeterId(meterId);
                    entity.setSource("sync");
                }
                String savedName = entity.getMeterName();
                String savedAddress = entity.getAddress();
                String savedDesc = entity.getDescription();
                String savedRelay = entity.getRelayState();
                entity.setCollectorId(item.getString("cid"));
                entity.setCollectorNo(item.getString("collectorid"));
                entity.setMeterType(item.getString("type"));
                entity.setCsq(item.getInteger("csq"));
                entity.setRate(item.getInteger("rate"));
                entity.setSyncTime(now);
                if (isNew) {
                    entity.setAddress(item.getString("address"));
                    entity.setRelayState(item.getString("relay_state"));
                    entity.setDescription(item.getString("description"));
                    entity.setMeterName(StringUtils.hasText(savedName) ? savedName : item.getString("address"));
                } else {
                    entity.setMeterName(savedName);
                    entity.setAddress(savedAddress);
                    entity.setDescription(savedDesc);
                    entity.setRelayState(savedRelay);
                }
                if (entity.getId() == null) {
                    meterMapper.insert(entity);
                } else {
                    meterMapper.updateById(entity);
                }
                count++;
            }
            syncLogService.logSuccess(syncType, null, null, count);
            return count;
        } catch (Exception e) {
            log.error("电表同步失败", e);
            syncLogService.logFailure(syncType, null, null, e.getMessage());
            throw e;
        }
    }
    private JSONArray parseList(String raw) {
        Object parsed = JSON.parse(raw);
        if (parsed instanceof JSONArray) {
            return (JSONArray) parsed;
        }
        JSONObject root = (JSONObject) parsed;
        if (root.getIntValue("status") != 1) {
            throw new IllegalStateException("电表接口返回异常: " + raw);
        }
        Object data = root.get("data");
        if (data instanceof JSONArray) {
            return (JSONArray) data;
        }
        return new JSONArray();
    }
}
src/main/java/com/ruoyi/http/service/impl/TqdianbiaoSyncLogServiceImpl.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,39 @@
package com.ruoyi.http.service.impl;
import com.ruoyi.http.mapper.TqdianbiaoSyncLogMapper;
import com.ruoyi.http.pojo.TqdianbiaoSyncLog;
import com.ruoyi.http.service.TqdianbiaoSyncLogService;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
@Service
@RequiredArgsConstructor
public class TqdianbiaoSyncLogServiceImpl implements TqdianbiaoSyncLogService {
    private final TqdianbiaoSyncLogMapper syncLogMapper;
    @Override
    public void logSuccess(String syncType, String windowStart, String windowEnd, int recordCount) {
        TqdianbiaoSyncLog log = new TqdianbiaoSyncLog();
        log.setSyncType(syncType);
        log.setWindowStart(windowStart);
        log.setWindowEnd(windowEnd);
        log.setStatus("success");
        log.setRecordCount(recordCount);
        log.setApiCallCount(1);
        syncLogMapper.insert(log);
    }
    @Override
    public void logFailure(String syncType, String windowStart, String windowEnd, String errorMsg) {
        TqdianbiaoSyncLog log = new TqdianbiaoSyncLog();
        log.setSyncType(syncType);
        log.setWindowStart(windowStart);
        log.setWindowEnd(windowEnd);
        log.setStatus("fail");
        log.setRecordCount(0);
        log.setApiCallCount(1);
        log.setErrorMsg(errorMsg != null && errorMsg.length() > 500 ? errorMsg.substring(0, 500) : errorMsg);
        syncLogMapper.insert(log);
    }
}
src/main/java/com/ruoyi/http/task/TqdianbiaoSyncTask.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,58 @@
package com.ruoyi.http.task;
import com.ruoyi.http.config.TqdianbiaoConfig;
import com.ruoyi.http.service.TqdianbiaoCollectorSyncService;
import com.ruoyi.http.service.TqdianbiaoEleSyncService;
import com.ruoyi.http.service.TqdianbiaoMeterSyncService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
/**
 * å¤©å¯ç”µè¡¨å®šæ—¶åŒæ­¥ï¼šä»…同步采集器、电表、小时电量(统计由小时累积计算)
 */
@Slf4j
@Component
@RequiredArgsConstructor
public class TqdianbiaoSyncTask {
    private final TqdianbiaoConfig config;
    private final TqdianbiaoCollectorSyncService collectorSyncService;
    private final TqdianbiaoMeterSyncService meterSyncService;
    private final TqdianbiaoEleSyncService eleSyncService;
    @Scheduled(cron = "30 5 * * * ?")
    public void syncCollectors() {
        if (!isEnabled()) return;
        try {
            collectorSyncService.syncCollectors();
        } catch (Exception e) {
            log.error("采集器定时同步异常", e);
        }
    }
    @Scheduled(cron = "30 7 * * * ?")
    public void syncMeters() {
        if (!isEnabled()) return;
        try {
            meterSyncService.syncMeters();
        } catch (Exception e) {
            log.error("电表定时同步异常", e);
        }
    }
    @Scheduled(cron = "30 10 * * * ?")
    public void syncHourEle() {
        if (!isEnabled()) return;
        try {
            eleSyncService.syncHourData();
        } catch (Exception e) {
            log.error("小时电量定时同步异常", e);
        }
    }
    private boolean isEnabled() {
        return config.getSync() != null && Boolean.TRUE.equals(config.getSync().getEnabled());
    }
}
src/main/java/com/ruoyi/http/util/StatisticEleAggregateUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,252 @@
package com.ruoyi.http.util;
import com.ruoyi.http.vo.StatisticEleRecordVo;
import java.time.LocalDate;
import java.time.YearMonth;
import java.time.format.DateTimeFormatter;
import java.util.Comparator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
import java.util.stream.Collectors;
/**
 * ç”µé‡ç»Ÿè®¡èšåˆå·¥å…·ï¼ˆåŸºäºŽå°æ—¶æ•°æ®å‘上汇总)
 */
public final class StatisticEleAggregateUtil {
    private static final DateTimeFormatter DAY_FMT = DateTimeFormatter.ofPattern("yyyyMMdd");
    private StatisticEleAggregateUtil() {
    }
    public static final Function<String, String> HOUR_TO_DAY = tk -> tk != null && tk.length() >= 8 ? tk.substring(0, 8) : null;
    public static final Function<String, String> HOUR_TO_HOUR = tk -> normalizeHourKey(tk);
    public static final Function<String, String> HOUR_TO_MONTH = tk -> tk != null && tk.length() >= 6 ? tk.substring(0, 6) : null;
    public static final Function<String, String> HOUR_TO_YEAR = tk -> tk != null && tk.length() >= 4 ? tk.substring(0, 4) : null;
    public static final Function<String, String> HOUR_TO_QUARTER = tk -> {
        String monthKey = HOUR_TO_MONTH.apply(tk);
        return monthKey != null ? toQuarterKey(monthKey) : null;
    };
    /**
     * æŒ‰æ—¶é—´æ¡¶æ±‡æ€»ï¼ˆå¤šç”µè¡¨åˆå¹¶ï¼Œç”¨äºŽå›¾è¡¨ï¼‰
     */
    public static List<StatisticEleRecordVo> aggregateHourToBuckets(
            List<StatisticEleRecordVo> hourRecords, Function<String, String> bucketFn) {
        Map<String, StatisticEleRecordVo> map = new LinkedHashMap<>();
        for (StatisticEleRecordVo record : hourRecords) {
            String bucket = bucketFn.apply(record.getTimeKey());
            if (bucket == null) {
                continue;
            }
            mergeInto(map, bucket, null, record);
        }
        return sorted(map);
    }
    /**
     * æŒ‰æ—¶é—´æ¡¶+电表汇总(用于明细)
     */
    public static List<StatisticEleRecordVo> aggregateHourPerMeter(
            List<StatisticEleRecordVo> hourRecords, Function<String, String> bucketFn) {
        Map<String, StatisticEleRecordVo> map = new LinkedHashMap<>();
        for (StatisticEleRecordVo record : hourRecords) {
            String bucket = bucketFn.apply(record.getTimeKey());
            if (bucket == null || record.getMeterId() == null) {
                continue;
            }
            String key = bucket + "_" + record.getMeterId();
            mergeInto(map, key, bucket, record);
            StatisticEleRecordVo agg = map.get(key);
            agg.setMeterId(record.getMeterId());
            agg.setAddress(record.getAddress());
            agg.setCollectorNo(record.getCollectorNo());
        }
        return sorted(map);
    }
    /**
     * å…¼å®¹ï¼šæŒ‰ timeKey ç›´æŽ¥æ±‡æ€»
     */
    public static List<StatisticEleRecordVo> aggregateByTimeKey(List<StatisticEleRecordVo> records) {
        Map<String, StatisticEleRecordVo> map = new LinkedHashMap<>();
        for (StatisticEleRecordVo record : records) {
            String key = record.getTimeKey();
            if (key == null) {
                continue;
            }
            mergeInto(map, key, key, record);
        }
        return sorted(map);
    }
    public static String toQuarterKey(String monthOrDayKey) {
        if (monthOrDayKey == null || monthOrDayKey.length() < 6) {
            return null;
        }
        int year = Integer.parseInt(monthOrDayKey.substring(0, 4));
        int month = Integer.parseInt(monthOrDayKey.substring(4, 6));
        int quarter = (month - 1) / 3 + 1;
        return year + "Q" + quarter;
    }
    /**
     * ç»Ÿè®¡ç»´åº¦ -> å°æ—¶ time_key æŸ¥è¯¢èŒƒå›´
     */
    public static HourRange toHourQueryRange(String dimension, String startTime, String endTime) {
        return switch (dimension) {
            case "hour" -> new HourRange(startTime, endTime);
            case "day" -> new HourRange(startTime + "00", endTime + "23");
            case "month" -> new HourRange(startTime + "0100", endTime + lastDayOfMonth(endTime) + "23");
            case "year" -> new HourRange(startTime + "010100", endTime + "123123");
            case "quarter" -> new HourRange(
                    startTime.length() >= 8 ? startTime.substring(0, 8) + "00" : startTime + "00",
                    endTime.length() >= 8 ? endTime.substring(0, 8) + "23" : endTime + "23");
            default -> throw new IllegalArgumentException("不支持的维度: " + dimension);
        };
    }
    public static HourRange yesterdayHourRange() {
        String day = LocalDate.now().minusDays(1).format(DAY_FMT);
        return new HourRange(normalizeQueryStartTimeKey(day + "00"), normalizeQueryEndTimeKey(day + "23"));
    }
    /** æŸ¥è¯¢èµ·å§‹ time_key(统一 12 ä½ï¼Œå…¼å®¹ 10 ä½å°æ—¶é”®ï¼‰ */
    public static String normalizeQueryStartTimeKey(String timeKey) {
        if (timeKey == null || timeKey.isBlank()) {
            return timeKey;
        }
        if (timeKey.length() == 8) {
            return timeKey + "0000";
        }
        if (timeKey.length() == 10) {
            return timeKey + "00";
        }
        return timeKey.length() > 12 ? timeKey.substring(0, 12) : timeKey;
    }
    /** æŸ¥è¯¢ç»“束 time_key(统一 12 ä½ï¼Œå…¼å®¹ 10 ä½å°æ—¶é”®ï¼‰ */
    public static String normalizeQueryEndTimeKey(String timeKey) {
        if (timeKey == null || timeKey.isBlank()) {
            return timeKey;
        }
        if (timeKey.length() == 8) {
            return timeKey + "2359";
        }
        if (timeKey.length() == 10) {
            return timeKey + "59";
        }
        return timeKey.length() > 12 ? timeKey.substring(0, 12) : timeKey;
    }
    /** æ˜Žç»†è®°å½•总用电量(与数据采集页求和方式一致) */
    public static double sumRecordsTotal(List<StatisticEleRecordVo> records) {
        return records.stream()
                .map(StatisticEleRecordVo::getTotalConsumption)
                .filter(v -> v != null)
                .mapToDouble(Double::doubleValue)
                .sum();
    }
    public static Function<String, String> bucketFn(String dimension) {
        return switch (dimension) {
            case "hour" -> HOUR_TO_HOUR;
            case "day" -> HOUR_TO_DAY;
            case "month" -> HOUR_TO_MONTH;
            case "quarter" -> HOUR_TO_QUARTER;
            case "year" -> HOUR_TO_YEAR;
            default -> StatisticEleAggregateUtil::normalizeHourKey;
        };
    }
    public static String normalizeHourKey(String timeKey) {
        if (timeKey == null) {
            return null;
        }
        return timeKey.length() >= 10 ? timeKey.substring(0, 10) : timeKey;
    }
    public static StatisticEleSummaryMetrics calcMetrics(List<StatisticEleRecordVo> buckets) {
        StatisticEleSummaryMetrics metrics = new StatisticEleSummaryMetrics();
        metrics.setRecordCount(buckets.size());
        if (buckets.isEmpty()) {
            metrics.setTotalConsumption(0.0);
            metrics.setAvgConsumption(0.0);
            metrics.setMaxConsumption(0.0);
            metrics.setMinConsumption(0.0);
            return metrics;
        }
        List<Double> values = buckets.stream()
                .map(StatisticEleRecordVo::getTotalConsumption)
                .filter(v -> v != null)
                .collect(Collectors.toList());
        double total = values.stream().mapToDouble(Double::doubleValue).sum();
        metrics.setTotalConsumption(round(total));
        metrics.setAvgConsumption(round(total / values.size()));
        metrics.setMaxConsumption(round(values.stream().mapToDouble(Double::doubleValue).max().orElse(0)));
        metrics.setMinConsumption(round(values.stream().mapToDouble(Double::doubleValue).min().orElse(0)));
        return metrics;
    }
    private static void mergeInto(Map<String, StatisticEleRecordVo> map, String mapKey,
                                  String timeKey, StatisticEleRecordVo record) {
        StatisticEleRecordVo agg = map.computeIfAbsent(mapKey, k -> {
            StatisticEleRecordVo vo = new StatisticEleRecordVo();
            vo.setTimeKey(timeKey != null ? timeKey : k);
            vo.setTotalConsumption(0.0);
            vo.setSharpConsumption(0.0);
            vo.setPeakConsumption(0.0);
            vo.setFlatConsumption(0.0);
            vo.setValleyConsumption(0.0);
            return vo;
        });
        agg.setTotalConsumption(add(agg.getTotalConsumption(), record.getTotalConsumption()));
        agg.setSharpConsumption(add(agg.getSharpConsumption(), record.getSharpConsumption()));
        agg.setPeakConsumption(add(agg.getPeakConsumption(), record.getPeakConsumption()));
        agg.setFlatConsumption(add(agg.getFlatConsumption(), record.getFlatConsumption()));
        agg.setValleyConsumption(add(agg.getValleyConsumption(), record.getValleyConsumption()));
    }
    private static List<StatisticEleRecordVo> sorted(Map<String, StatisticEleRecordVo> map) {
        return map.values().stream()
                .sorted(Comparator.comparing(StatisticEleRecordVo::getTimeKey))
                .collect(Collectors.toList());
    }
    private static String lastDayOfMonth(String yyyyMM) {
        YearMonth ym = YearMonth.parse(yyyyMM, DateTimeFormatter.ofPattern("yyyyMM"));
        return yyyyMM + String.format("%02d", ym.lengthOfMonth());
    }
    private static Double add(Double a, Double b) {
        return (a == null ? 0.0 : a) + (b == null ? 0.0 : b);
    }
    private static double round(double value) {
        return Math.round(value * 100.0) / 100.0;
    }
    public record HourRange(String startTime, String endTime) {}
    public static class StatisticEleSummaryMetrics {
        private Double totalConsumption;
        private Double avgConsumption;
        private Double maxConsumption;
        private Double minConsumption;
        private Integer recordCount;
        public Double getTotalConsumption() { return totalConsumption; }
        public void setTotalConsumption(Double v) { this.totalConsumption = v; }
        public Double getAvgConsumption() { return avgConsumption; }
        public void setAvgConsumption(Double v) { this.avgConsumption = v; }
        public Double getMaxConsumption() { return maxConsumption; }
        public void setMaxConsumption(Double v) { this.maxConsumption = v; }
        public Double getMinConsumption() { return minConsumption; }
        public void setMinConsumption(Double v) { this.minConsumption = v; }
        public Integer getRecordCount() { return recordCount; }
        public void setRecordCount(Integer v) { this.recordCount = v; }
    }
}
src/main/java/com/ruoyi/http/util/StatisticEleParseUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,122 @@
package com.ruoyi.http.util;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONArray;
import com.alibaba.fastjson2.JSONObject;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.http.pojo.TqdianbiaoEleRecord;
import com.ruoyi.http.vo.StatisticEleRecordVo;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.Comparator;
import java.util.List;
public final class StatisticEleParseUtil {
    private StatisticEleParseUtil() {
    }
    public static List<TqdianbiaoEleRecord> parseToEntities(String raw, String dimension) {
        JSONObject root = JSON.parseObject(raw);
        if (root == null) {
            throw new ServiceException("电表接口返回为空");
        }
        if (root.getIntValue("status") != 1) {
            String msg = root.getString("msg");
            throw new ServiceException("电表接口返回异常: " + (msg != null ? msg : raw));
        }
        JSONObject data = root.getJSONObject("data");
        if (data == null || data.isEmpty()) {
            return new ArrayList<>();
        }
        LocalDateTime now = LocalDateTime.now();
        List<TqdianbiaoEleRecord> list = new ArrayList<>();
        for (String timeKey : data.keySet()) {
            JSONArray records = data.getJSONArray(timeKey);
            if (records == null) {
                continue;
            }
            for (int i = 0; i < records.size(); i++) {
                JSONObject rec = records.getJSONObject(i);
                TqdianbiaoEleRecord entity = new TqdianbiaoEleRecord();
                entity.setMeterId(rec.getLong("mid"));
                entity.setDimension(dimension);
                entity.setTimeKey(timeKey);
                entity.setReadingMethod("sync");
                Integer ratio = rec.getInteger("r");
                entity.setRatio(ratio);
                entity.setStartTime(rec.getString("st"));
                entity.setEndTime(rec.getString("et"));
                JSONArray sArr = rec.getJSONArray("s");
                JSONArray eArr = rec.getJSONArray("e");
                JSONArray dArr = rec.getJSONArray("d");
                entity.setStartReading(StatisticEleReadingUtil.formatReadingArray(sArr));
                entity.setEndReading(StatisticEleReadingUtil.formatReadingArray(eArr));
                entity.setPrevReading(StatisticEleReadingUtil.firstReading(sArr));
                entity.setCurrReading(StatisticEleReadingUtil.firstReading(eArr));
                fillConsumption(entity, dArr, ratio, sArr, eArr);
                entity.setSyncTime(now);
                list.add(entity);
            }
        }
        list.sort(Comparator.comparing(TqdianbiaoEleRecord::getTimeKey));
        return list;
    }
    public static List<StatisticEleRecordVo> toVoList(List<TqdianbiaoEleRecord> entities) {
        List<StatisticEleRecordVo> list = new ArrayList<>();
        for (TqdianbiaoEleRecord entity : entities) {
            list.add(toVo(entity));
        }
        return list;
    }
    public static StatisticEleRecordVo toVo(TqdianbiaoEleRecord entity) {
        StatisticEleRecordVo vo = new StatisticEleRecordVo();
        vo.setId(entity.getId());
        vo.setTimeKey(entity.getTimeKey());
        vo.setMeterId(entity.getMeterId());
        vo.setMeterName(entity.getMeterName());
        vo.setRatio(entity.getRatio());
        vo.setReadingMethod(entity.getReadingMethod());
        vo.setPrevReading(toDouble(entity.getPrevReading()));
        vo.setCurrReading(toDouble(entity.getCurrReading()));
        vo.setStartTime(entity.getStartTime());
        vo.setEndTime(entity.getEndTime());
        vo.setTotalConsumption(toDouble(entity.getTotalConsumption()));
        vo.setSharpConsumption(toDouble(entity.getSharpConsumption()));
        vo.setPeakConsumption(toDouble(entity.getPeakConsumption()));
        vo.setFlatConsumption(toDouble(entity.getFlatConsumption()));
        vo.setValleyConsumption(toDouble(entity.getValleyConsumption()));
        vo.setStartReading(entity.getStartReading());
        vo.setEndReading(entity.getEndReading());
        return vo;
    }
    private static void fillConsumption(TqdianbiaoEleRecord entity, JSONArray d, Integer ratio,
                                        JSONArray sArr, JSONArray eArr) {
        BigDecimal rawTotal = null;
        if (d != null && !d.isEmpty()) {
            rawTotal = d.getBigDecimal(0);
            entity.setSharpConsumption(d.size() >= 2 ? d.getBigDecimal(1) : null);
            entity.setPeakConsumption(d.size() >= 3 ? d.getBigDecimal(2) : null);
            entity.setFlatConsumption(d.size() >= 4 ? d.getBigDecimal(3) : null);
            entity.setValleyConsumption(d.size() >= 5 ? d.getBigDecimal(4) : null);
            entity.setDeepValleyConsumption(d.size() >= 6 ? d.getBigDecimal(5) : null);
        } else {
            rawTotal = StatisticEleReadingUtil.calcConsumption(
                    StatisticEleReadingUtil.firstReading(sArr),
                    StatisticEleReadingUtil.firstReading(eArr),
                    1);
        }
        entity.setTotalConsumption(StatisticEleReadingUtil.calcConsumptionFromRaw(rawTotal, ratio));
    }
    private static Double toDouble(BigDecimal value) {
        return value == null ? null : value.doubleValue();
    }
}
src/main/java/com/ruoyi/http/util/StatisticEleReadingUtil.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,72 @@
package com.ruoyi.http.util;
import com.alibaba.fastjson2.JSONArray;
import java.math.BigDecimal;
import java.math.RoundingMode;
/**
 * ç”µé‡è¯»æ•°ä¸Žå€çŽ‡è®¡ç®—
 */
public final class StatisticEleReadingUtil {
    private StatisticEleReadingUtil() {
    }
    public static BigDecimal firstReading(JSONArray arr) {
        if (arr == null || arr.isEmpty()) {
            return null;
        }
        return arr.getBigDecimal(0);
    }
    public static BigDecimal calcConsumption(BigDecimal prev, BigDecimal curr, Integer ratio) {
        if (prev == null || curr == null) {
            return null;
        }
        BigDecimal diff = curr.subtract(prev);
        return applyRatio(diff, ratio);
    }
    public static BigDecimal calcConsumptionFromRaw(BigDecimal raw, Integer ratio) {
        if (raw == null) {
            return null;
        }
        return applyRatio(raw, ratio);
    }
    public static BigDecimal applyRatio(BigDecimal value, Integer ratio) {
        if (value == null) {
            return null;
        }
        int r = ratio == null || ratio <= 0 ? 1 : ratio;
        return value.multiply(BigDecimal.valueOf(r)).setScale(4, RoundingMode.HALF_UP);
    }
    public static String formatReadingArray(JSONArray arr) {
        if (arr == null || arr.isEmpty()) {
            return "";
        }
        StringBuilder sb = new StringBuilder();
        for (int i = 0; i < arr.size(); i++) {
            if (i > 0) {
                sb.append("/");
            }
            sb.append(arr.getDouble(i));
        }
        return sb.toString();
    }
    public static BigDecimal parseFirstReading(String reading) {
        if (reading == null || reading.isBlank()) {
            return null;
        }
        int idx = reading.indexOf('/');
        String first = idx > 0 ? reading.substring(0, idx) : reading;
        try {
            return new BigDecimal(first.trim());
        } catch (NumberFormatException e) {
            return null;
        }
    }
}
src/main/java/com/ruoyi/http/vo/StatisticEleRecordVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,67 @@
package com.ruoyi.http.vo;
import com.ruoyi.framework.aspectj.lang.annotation.Excel;
import lombok.Data;
/**
 * ç”µè¡¨ç»Ÿè®¡æ•°æ®è®°å½•
 */
@Data
public class StatisticEleRecordVo {
    private Long id;
    @Excel(name = "时间标识")
    private String timeKey;
    @Excel(name = "电表ID")
    private Long meterId;
    @Excel(name = "电表名称")
    private String meterName;
    @Excel(name = "表地址")
    private String address;
    @Excel(name = "倍率")
    private Integer ratio;
    @Excel(name = "上次电量")
    private Double prevReading;
    @Excel(name = "本次电量")
    private Double currReading;
    @Excel(name = "本次用电量(kWh)")
    private Double totalConsumption;
    @Excel(name = "抄表方式")
    private String readingMethod;
    @Excel(name = "开始时间")
    private String startTime;
    @Excel(name = "结束时间")
    private String endTime;
    @Excel(name = "尖峰(kWh)")
    private Double sharpConsumption;
    @Excel(name = "å³°(kWh)")
    private Double peakConsumption;
    @Excel(name = "å¹³(kWh)")
    private Double flatConsumption;
    @Excel(name = "è°·(kWh)")
    private Double valleyConsumption;
    @Excel(name = "起始读数")
    private String startReading;
    @Excel(name = "结束读数")
    private String endReading;
    /** å…¼å®¹å­—段,页面不展示 */
    private String collectorNo;
}
src/main/java/com/ruoyi/http/vo/StatisticEleSummaryVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,33 @@
package com.ruoyi.http.vo;
import lombok.Data;
import java.util.List;
/**
 * ç”µè¡¨ç»Ÿè®¡æ±‡æ€»
 */
@Data
public class StatisticEleSummaryVo {
    /** æ€»ç”¨ç”µé‡(kWh) */
    private Double totalConsumption;
    /** å¹³å‡ç”¨ç”µé‡(kWh) */
    private Double avgConsumption;
    /** æœ€å¤§ç”¨ç”µé‡(kWh) */
    private Double maxConsumption;
    /** æœ€å°ç”¨ç”µé‡(kWh) */
    private Double minConsumption;
    /** æ•°æ®æ¡æ•° */
    private Integer recordCount;
    /** å›¾è¡¨æ•°æ®ï¼ˆæŒ‰æ—¶é—´æ±‡æ€»ï¼‰ */
    private List<StatisticEleRecordVo> chartRecords;
    /** æ˜Žç»†è®°å½•(按电表) */
    private List<StatisticEleRecordVo> records;
}
src/main/java/com/ruoyi/http/vo/StatisticEleSyncStatusVo.java
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,19 @@
package com.ruoyi.http.vo;
import lombok.Data;
import java.util.Map;
@Data
public class StatisticEleSyncStatusVo {
    private Integer meterCount;
    private Integer collectorCount;
    private Integer onlineCollectorCount;
    private Map<String, Long> recordCountByDimension;
    private Map<String, String> lastSyncTimeByType;
}
src/main/resources/mapper/http/TqdianbiaoEleRecordMapper.xml
¶Ô±ÈÐÂÎļþ
@@ -0,0 +1,45 @@
<?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.http.mapper.TqdianbiaoEleRecordMapper">
    <select id="selectRecordList" resultType="com.ruoyi.http.vo.StatisticEleRecordVo">
        SELECT
            r.id AS id,
            r.time_key AS timeKey,
            r.meter_id AS meterId,
            COALESCE(r.meter_name, m.meter_name) AS meterName,
            m.address AS address,
            r.ratio AS ratio,
            r.reading_method AS readingMethod,
            r.prev_reading AS prevReading,
            r.curr_reading AS currReading,
            r.start_time AS startTime,
            r.end_time AS endTime,
            r.total_consumption AS totalConsumption,
            r.sharp_consumption AS sharpConsumption,
            r.peak_consumption AS peakConsumption,
            r.flat_consumption AS flatConsumption,
            r.valley_consumption AS valleyConsumption,
            r.start_reading AS startReading,
            r.end_reading AS endReading
        FROM tqdianbiao_ele_record r
        LEFT JOIN tqdianbiao_meter m ON r.meter_id = m.meter_id
        WHERE r.dimension IN
        <foreach collection="dimensions" item="dim" open="(" separator="," close=")">
            #{dim}
        </foreach>
          AND RPAD(r.time_key, 12, '0') &gt;= #{startTime}
          AND RPAD(r.time_key, 12, '0') &lt;= #{endTime}
        ORDER BY r.time_key DESC, r.meter_id ASC
    </select>
    <select id="selectPrevReading" resultType="com.ruoyi.http.pojo.TqdianbiaoEleRecord">
        SELECT id, meter_id, time_key, curr_reading, end_reading, ratio
        FROM tqdianbiao_ele_record
        WHERE meter_id = #{meterId}
          AND time_key &lt; #{timeKey}
        ORDER BY time_key DESC
        LIMIT 1
    </select>
</mapper>