Commit 015d5bc611b7f2864e9cca86a4c1c58d91f236f4

Authored by 杨鸣坤
1 parent 3b9df84d

feat: 新增eq_kwh综合统计查询接口

@@ -193,4 +193,19 @@ public class HealthController { @@ -193,4 +193,19 @@ public class HealthController {
193 @RequestParam(defaultValue = "12") Integer pageSize) { 193 @RequestParam(defaultValue = "12") Integer pageSize) {
194 return energySearchService.queryEnergyTimelineStatus(date, pageNo, pageSize); 194 return energySearchService.queryEnergyTimelineStatus(date, pageNo, pageSize);
195 } 195 }
  196 +
  197 + /**
  198 + * eq_kwh综合统计查询
  199 + * 查询指定日期范围内所有设备的稼动率、各状态时长、异常排名等
  200 + *
  201 + * @param startDate 开始日期 yyyy-MM-dd
  202 + * @param endDate 结束日期 yyyy-MM-dd
  203 + * @return 包含: 总稼动率、各状态时间(xxx.xx时)、当前运行状态、异常机台排名、每设备0/1/2/3状态时间
  204 + */
  205 + @GetMapping("/energy/eqKwhStatistics")
  206 + public Map<String, Object> eqKwhStatistics(
  207 + @RequestParam String startDate,
  208 + @RequestParam String endDate) {
  209 + return energySearchService.queryEqKwhStatistics(startDate, endDate);
  210 + }
196 } 211 }
@@ -545,4 +545,334 @@ public class EnergySearchService { @@ -545,4 +545,334 @@ public class EnergySearchService {
545 sb.append(s).append("秒"); 545 sb.append(s).append("秒");
546 return sb.toString(); 546 return sb.toString();
547 } 547 }
  548 +
  549 + /**
  550 + * 格式化时长为 xxx.xx 时
  551 + */
  552 + private static String formatDurationToHours(long totalSeconds) {
  553 + double hours = totalSeconds / 3600.0;
  554 + return String.format("%.2f时", hours);
  555 + }
  556 +
  557 + // ==================== eq_kwh 综合统计查询 ====================
  558 +
  559 + /**
  560 + * 查询 eq_kwh 表的综合统计数据
  561 + * 返回:
  562 + * 1. 所有设备的总的稼动率 + 每个状态的时间(xxx.xx时)
  563 + * 2. 当前设备的运行状态
  564 + * 3. 异常机台排名(待机+停机时间最长往下排, xx时xx分xx秒)
  565 + * 4. 每个设备统计时间内的总的0/1/2/3的时间(xx时xx分xx秒)
  566 + *
  567 + * @param startDate 开始日期 yyyy-MM-dd
  568 + * @param endDate 结束日期 yyyy-MM-dd
  569 + */
  570 + public Map<String, Object> queryEqKwhStatistics(String startDate, String endDate) {
  571 + log.info("========== [eq_kwh综合统计] startDate={}, endDate={} ==========", startDate, endDate);
  572 +
  573 + if (!StringUtils.hasText(startDate) || !StringUtils.hasText(endDate)) {
  574 + return Map.of(
  575 + "code", 400, "msg", "参数错误: startDate和endDate必填",
  576 + "data", Map.of()
  577 + );
  578 + }
  579 +
  580 + // 1. 构建日期列表
  581 + List<String> dateList = buildDateList(startDate, endDate);
  582 + if (dateList.isEmpty()) {
  583 + return buildEmptyEqKwhStats();
  584 + }
  585 + log.info("日期范围共 {} 天: {} ~ {}", dateList.size(), dateList.get(0), dateList.get(dateList.size() - 1));
  586 +
  587 + // 2. 获取所有设备列表及名称
  588 + Map<String, String> deviceNameMap = queryAllEnergyDeviceNames();
  589 +
  590 + // 3. 从 eq_kwh 表批量查询所有设备在指定日期范围内的数据
  591 + List<Map<String, Object>> allRawData = queryEqKwhBatch(dateList);
  592 + log.info("eq_kwh 批量查询返回 {} 条原始记录", allRawData.size());
  593 +
  594 + // 4. 按 dtuSn 分组聚合数据,计算每个设备的各状态时长
  595 + Map<String, DeviceStatResult> deviceStatMap = aggregateByDevice(allRawData, deviceNameMap);
  596 +
  597 + // 确保所有设备都在结果中(无数据的设为0)
  598 + for (String sn : deviceNameMap.keySet()) {
  599 + if (!deviceStatMap.containsKey(sn)) {
  600 + deviceStatMap.put(sn, new DeviceStatResult(sn, deviceNameMap.getOrDefault(sn, "")));
  601 + }
  602 + }
  603 +
  604 + // 5. 计算全局汇总
  605 + return buildEqKwhStatisticsResult(deviceStatMap, deviceNameMap);
  606 + }
  607 +
  608 + /** 设备统计内部结构 */
  609 + static class DeviceStatResult {
  610 + String dtuSn;
  611 + String deviceName;
  612 + long status0; // 离线时长(秒)
  613 + long status1; // 停机时长(秒)
  614 + long status2; // 待机时长(秒)
  615 + long status3; // 运行时长(秒)
  616 + double totalKwh; // 总用电量
  617 +
  618 + DeviceStatResult(String dtuSn, String deviceName) {
  619 + this.dtuSn = dtuSn;
  620 + this.deviceName = deviceName;
  621 + }
  622 +
  623 + /** 总时长(秒) */
  624 + long totalDuration() { return status0 + status1 + status2 + status3; }
  625 + /** 异常时长 = 停机 + 待机 */
  626 + long abnormalDuration() { return status1 + status2; }
  627 + /** 稼动分母 = 停机 + 待机 + 运行 (排除离线) */
  628 + long activeDuration() { return status1 + status2 + status3; }
  629 + }
  630 +
  631 + /** 构建空结果 */
  632 + private Map<String, Object> buildEmptyEqKwhStats() {
  633 + return Map.of(
  634 + "code", 200, "msg", "请求成功", "data",
  635 + Map.of(
  636 + "summary", Map.of(
  637 + "availabilityRate", "0.00%",
  638 + "totalStatusDuration", Map.of(
  639 + "status0", Map.of("durationFormatted", "0时0分0秒", "durationSeconds", 0L, "durationHours", "0.00时"),
  640 + "status1", Map.of("durationFormatted", "0时0分0秒", "durationSeconds", 0L, "durationHours", "0.00时"),
  641 + "status2", Map.of("durationFormatted", "0时0分0秒", "durationSeconds", 0L, "durationHours", "0.00时"),
  642 + "status3", Map.of("durationFormatted", "0时0分0秒", "durationSeconds", 0L, "durationHours", "0.00时")
  643 + )
  644 + ),
  645 + "currentStatus", Map.of("0", 0, "1", 0, "2", 0, "3", 0),
  646 + "abnormalRanking", List.of(),
  647 + "deviceList", List.of()
  648 + )
  649 + );
  650 + }
  651 +
  652 + /** 获取能耗设备表所有dtuSn及名称 */
  653 + private Map<String, String> queryAllEnergyDeviceNames() {
  654 + String sql = "SELECT dtuSn, deviceName FROM " + energyTableName + " WHERE corp_code = ? ORDER BY dtuSn";
  655 + List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, energyCorpCode);
  656 + Map<String, String> result = new LinkedHashMap<>(rows.size());
  657 + for (Map<String, Object> row : rows) {
  658 + result.put((String) row.get("dtuSn"), (String) row.get("deviceName"));
  659 + }
  660 + return result;
  661 + }
  662 +
  663 + /** 构建连续日期列表 */
  664 + private List<String> buildDateList(String startDate, String endDate) {
  665 + List<String> result = new ArrayList<>();
  666 + try {
  667 + SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd");
  668 + Calendar cur = Calendar.getInstance();
  669 + cur.setTime(sdf.parse(startDate));
  670 + Calendar endCal = Calendar.getInstance();
  671 + endCal.setTime(sdf.parse(endDate));
  672 + while (!cur.after(endCal)) {
  673 + result.add(sdf.format(cur.getTime()));
  674 + cur.add(Calendar.DAY_OF_MONTH, 1);
  675 + }
  676 + } catch (Exception e) {
  677 + log.error("日期解析失败: {} ~ {}", startDate, endDate, e);
  678 + }
  679 + return result;
  680 + }
  681 +
  682 + /** 从 eq_kwh 表批量查询指定日期范围的所有记录 */
  683 + private List<Map<String, Object>> queryEqKwhBatch(List<String> dateList) {
  684 + if (dateList.isEmpty()) return Collections.emptyList();
  685 +
  686 + StringBuilder sql = new StringBuilder(
  687 + "SELECT dtuSn, use_date, description FROM " + eqKwhTableName +
  688 + " WHERE corp_code = ? AND use_date IN (");
  689 + List<Object> params = new ArrayList<>();
  690 + params.add(energyCorpCode);
  691 +
  692 + for (String d : dateList) { sql.append("?,"); params.add(d + " 00:00:00"); }
  693 + sql.deleteCharAt(sql.length() - 1).append(")");
  694 + sql.append(" ORDER BY dtuSn, use_date");
  695 +
  696 + return jdbcTemplate.queryForList(sql.toString(), params.toArray());
  697 + }
  698 +
  699 + /** 按 dtuSn 分组聚合各状态时长 */
  700 + private Map<String, DeviceStatResult> aggregateByDevice(List<Map<String, Object>> rawData,
  701 + Map<String, String> deviceNameMap) {
  702 + Map<String, DeviceStatResult> resultMap = new LinkedHashMap<>();
  703 +
  704 + for (Map<String, Object> row : rawData) {
  705 + String sn = (String) row.get("dtuSn");
  706 + String desc = row.get("description") != null ? String.valueOf(row.get("description")) : "";
  707 +
  708 + DeviceStatResult dsr = resultMap.computeIfAbsent(sn,
  709 + k -> new DeviceStatResult(k, deviceNameMap.getOrDefault(k, "")));
  710 +
  711 + if (!StringUtils.hasText(desc)) continue;
  712 +
  713 + try {
  714 + JSONArray dataArray = JSON.parseArray(desc);
  715 + if (dataArray == null) continue;
  716 +
  717 + for (int i = 0; i < dataArray.size(); i++) {
  718 + JSONObject item = dataArray.getJSONObject(i);
  719 + if (item == null) continue;
  720 +
  721 + // 用电量
  722 + Double value = item.getDouble("value");
  723 + if (value != null) dsr.totalKwh += value;
  724 +
  725 + // 各状态时长: 0-离线, 1-停机, 2-待机, 3-运行
  726 + for (int sk = 0; sk <= 3; sk++) {
  727 + Long dur = item.getLong(String.valueOf(sk));
  728 + if (dur != null && dur > 0) {
  729 + switch (sk) {
  730 + case 0 -> dsr.status0 += dur;
  731 + case 1 -> dsr.status1 += dur;
  732 + case 2 -> dsr.status2 += dur;
  733 + case 3 -> dsr.status3 += dur;
  734 + }
  735 + }
  736 + }
  737 + }
  738 + } catch (Exception e) {
  739 + log.warn("解析eq_kwh description异常 - dtuSn:{}", sn, e);
  740 + }
  741 + }
  742 +
  743 + return resultMap;
  744 + }
  745 +
  746 + /** 构建最终统计结果 */
  747 + private Map<String, Object> buildEqKwhStatisticsResult(Map<String, DeviceStatResult> deviceStatMap,
  748 + Map<String, String> deviceNameMap) {
  749 + // ---- 全局汇总 ----
  750 + long totalS0 = 0, totalS1 = 0, totalS2 = 0, totalS3 = 0;
  751 + double totalKwhSum = 0;
  752 +
  753 + // ---- 设备详情列表 ----
  754 + List<Map<String, Object>> deviceList = new ArrayList<>(deviceStatMap.size());
  755 +
  756 + for (Map.Entry<String, DeviceStatResult> entry : deviceStatMap.entrySet()) {
  757 + DeviceStatResult dsr = entry.getValue();
  758 +
  759 + totalS0 += dsr.status0; totalS1 += dsr.status1;
  760 + totalS2 += dsr.status2; totalS3 += dsr.status3;
  761 + totalKwhSum += dsr.totalKwh;
  762 +
  763 + // 单设备稼动率 = 运行 / (停机+待机+运行)
  764 + long active = dsr.activeDuration();
  765 + double devRate = active > 0 ? Math.round(dsr.status3 * 10000.0 / active) / 100.0 : 0.0;
  766 +
  767 + Map<String, Object> devItem = new LinkedHashMap<>();
  768 + devItem.put("dtuSn", dsr.dtuSn);
  769 + devItem.put("deviceName", dsr.deviceName);
  770 + // 各状态时长 - 格式化为 xx时xx分xx秒
  771 + devItem.put("status0", Map.of(
  772 + "durationFormatted", formatDuration(dsr.status0),
  773 + "durationSeconds", dsr.status0,
  774 + "durationHours", formatDurationToHours(dsr.status0)
  775 + ));
  776 + devItem.put("status1", Map.of(
  777 + "durationFormatted", formatDuration(dsr.status1),
  778 + "durationSeconds", dsr.status1,
  779 + "durationHours", formatDurationToHours(dsr.status1)
  780 + ));
  781 + devItem.put("status2", Map.of(
  782 + "durationFormatted", formatDuration(dsr.status2),
  783 + "durationSeconds", dsr.status2,
  784 + "durationHours", formatDurationToHours(dsr.status2)
  785 + ));
  786 + devItem.put("status3", Map.of(
  787 + "durationFormatted", formatDuration(dsr.status3),
  788 + "durationSeconds", dsr.status3,
  789 + "durationHours", formatDurationToHours(dsr.status3)
  790 + ));
  791 + devItem.put("totalDurationFormatted", formatDuration(dsr.totalDuration()));
  792 + devItem.put("totalDurationSeconds", dsr.totalDuration());
  793 + devItem.put("totalDurationHours", formatDurationToHours(dsr.totalDuration()));
  794 + devItem.put("totalKwh", Math.round(dsr.totalKwh * 100.0) / 100.0);
  795 + devItem.put("availabilityRate", String.format("%.2f%%", devRate));
  796 + devItem.put("availabilityRateValue", devRate);
  797 + // 异常时长用于排序(内部字段,后面移除)
  798 + devItem.put("_abnormalDur", dsr.abnormalDuration());
  799 +
  800 + deviceList.add(devItem);
  801 + }
  802 +
  803 + // ---- ① 总稼动率 ----
  804 + long totalActive = totalS1 + totalS2 + totalS3;
  805 + double overallAvailabilityRate = totalActive > 0
  806 + ? Math.round(totalS3 * 10000.0 / totalActive) / 100.0 : 0.0;
  807 +
  808 + // ---- ② 当前设备运行状态(从energy表查) ----
  809 + Map<String, Integer> currentStatus = queryCurrentRunStatus();
  810 +
  811 + // ---- ③ 异常机台排名(按 停机+待机 降序) ----
  812 + deviceList.sort((a, b) -> Long.compare(
  813 + ((Number) b.get("_abnormalDur")).longValue(),
  814 + ((Number) a.get("_abnormalDur")).longValue()
  815 + ));
  816 + // 移除辅助字段
  817 + for (Map<String, Object> d : deviceList) {
  818 + d.remove("_abnormalDur");
  819 + }
  820 +
  821 + return Map.of(
  822 + "code", 200, "msg", "请求成功", "data",
  823 + Map.of(
  824 + "summary", Map.of(
  825 + "availabilityRate", String.format("%.2f%%", overallAvailabilityRate),
  826 + "availabilityRateValue", overallAvailabilityRate,
  827 + "totalDevices", deviceNameMap.size(),
  828 + "totalKwh", Math.round(totalKwhSum * 100.0) / 100.0,
  829 + "totalStatusDuration", Map.of(
  830 + "status0", Map.of(
  831 + "durationFormatted", formatDuration(totalS0),
  832 + "durationSeconds", totalS0,
  833 + "durationHours", formatDurationToHours(totalS0)
  834 + ),
  835 + "status1", Map.of(
  836 + "durationFormatted", formatDuration(totalS1),
  837 + "durationSeconds", totalS1,
  838 + "durationHours", formatDurationToHours(totalS1)
  839 + ),
  840 + "status2", Map.of(
  841 + "durationFormatted", formatDuration(totalS2),
  842 + "durationSeconds", totalS2,
  843 + "durationHours", formatDurationToHours(totalS2)
  844 + ),
  845 + "status3", Map.of(
  846 + "durationFormatted", formatDuration(totalS3),
  847 + "durationSeconds", totalS3,
  848 + "durationHours", formatDurationToHours(totalS3)
  849 + )
  850 + )
  851 + ),
  852 + "currentStatus", currentStatus,
  853 + "abnormalRanking", deviceList,
  854 + "deviceList", deviceList
  855 + )
  856 + );
  857 + }
  858 +
  859 + /** 查询当前设备运行状态分布(从energy表) runStatus: 0-离线, 1-停机, 2-待机, 3-运行 */
  860 + private Map<String, Integer> queryCurrentRunStatus() {
  861 + String sql = "SELECT runStatus, COUNT(*) as cnt FROM " + energyTableName +
  862 + " WHERE corp_code = ? GROUP BY runStatus";
  863 + List<Map<String, Object>> rows = jdbcTemplate.queryForList(sql, energyCorpCode);
  864 +
  865 + int s0 = 0, s1 = 0, s2 = 0, s3 = 0;
  866 + for (Map<String, Object> row : rows) {
  867 + String key = String.valueOf(row.get("runStatus"));
  868 + int cnt = ((Number) row.get("cnt")).intValue();
  869 + switch (key) {
  870 + case "0" -> s0 = cnt;
  871 + case "1" -> s1 = cnt;
  872 + case "2" -> s2 = cnt;
  873 + case "3" -> s3 = cnt;
  874 + }
  875 + }
  876 + return Map.of("0", s0, "1", s1, "2", s2, "3", s3);
  877 + }
548 } 878 }