package com.battery.business.service; import java.math.BigDecimal; import java.time.*; import java.util.*; import javax.sql.DataSource; import com.battery.business.domain.*; import com.battery.business.mapper.TTestMapper; import org.springframework.jdbc.core.JdbcTemplate; import org.springframework.stereotype.Service; /** 首页只读聚合已有事实,不发起设备控制或为展示新增采样。 */ @Service public class DashboardService { private static final ZoneId BUSINESS_ZONE = ZoneId.of("Asia/Hong_Kong"); private final ITTestService tests; private final ITDetectionCabinetService cabinets; private final TTestMapper testMapper; private final JdbcTemplate jdbc; public DashboardService(ITTestService tests, ITDetectionCabinetService cabinets, TTestMapper testMapper, DataSource source) { this.tests = tests; this.cabinets = cabinets; this.testMapper = testMapper; jdbc = new JdbcTemplate(source); } public record Summary(long testCount, long batteryGroupCount, long todayCount, long monthCount) {} public record Trend(List labels, List values) {} public record TestRow(long id, String name, String type, String status, String receptionStatus, String step, long warning) {} public record CabinetRow(long id, String name, BigDecimal capacity, int channelCount, long usedChannels, long testCount, long batteryCount, String onlineStatus) {} public record Warning(long id, long testId, String testName, String item, long occurredAt) {} public record Snapshot(Summary summary, Map trends, List tests, List cabinets, List warnings) {} /** 使用全部未删除测试计算总量及香港自然日/月新增数,统计不受页面分页限制。 */ public Snapshot read(Instant now) { var allTests = tests.selectTestList(new TTest()); var today = now.atZone(BUSINESS_ZONE).toLocalDate(); Map dates = new HashMap<>(); allTests.stream().filter(test -> test.getCreateTime() != null).forEach(test -> dates.merge(test.getCreateTime().toInstant().atZone(BUSINESS_ZONE).toLocalDate(), 1L, Long::sum)); var summary = new Summary(allTests.size(), jdbc.queryForObject("select count(*) from t_battery_group where del_flag=0", Long.class), dates.getOrDefault(today, 0L), dates.entrySet().stream().filter(entry -> YearMonth.from(entry.getKey()).equals(YearMonth.from(today))).mapToLong(Map.Entry::getValue).sum()); var trends = new LinkedHashMap(); trends.put("week", days(today.minusDays(6), today, dates)); trends.put("month", days(today.withDayOfMonth(1), today, dates)); List months = new ArrayList<>(); List counts = new ArrayList<>(); for (int month = 1; month <= today.getMonthValue(); month++) { final var key = YearMonth.of(today.getYear(), month); months.add(month + "月"); counts.add(dates.entrySet().stream().filter(entry -> YearMonth.from(entry.getKey()).equals(key)).mapToLong(Map.Entry::getValue).sum()); } trends.put("year", new Trend(months, counts)); var open = testMapper.selectWarningExceptions(null, "OPEN"); Map warningCounts = new HashMap<>(); List warnings = open.stream().map(item -> { warningCounts.merge(item.getTestId(), 1L, Long::sum); return new Warning(item.getExceptionId(), item.getTestId(), item.getTestName(), "DEVICE_OFFLINE".equals(item.getExceptionType()) ? "设备离线" : metricName(item.getMetricCode()), item.getLastOccurredAtUtc() == null ? 0 : item.getLastOccurredAtUtc().getTime()); }).sorted(Comparator.comparingLong(Warning::occurredAt).reversed()).toList(); // 首页明确展示“最近记录”,不把持久化历史冒充设备当前工步;多电池分别保留记录身份。 Map> latest = new HashMap<>(); jdbc.query("select r.test_id,b.battery_no,r.step_no,r.cycle_no from t_test_realtime_record r join t_test_battery b on b.test_battery_id=r.test_battery_id and b.del_flag=0 join (select test_battery_id,max(record_id) record_id from t_test_realtime_record group by test_battery_id) last_record on last_record.record_id=r.record_id", (org.springframework.jdbc.core.RowCallbackHandler) rs -> { if (rs.getObject(3) != null && rs.getObject(4) != null) latest.computeIfAbsent(rs.getLong(1), id -> new ArrayList<>()).add(rs.getString(2) + ":工步" + rs.getInt(3) + " / 循环" + rs.getInt(4)); }); var rows = allTests.stream().map(test -> new TestRow(test.getTestId(), test.getTestName(), test.getTestTypeName(), test.getTestStatus(), test.getReceptionStatus(), String.join(";", latest.getOrDefault(test.getTestId(), List.of())), warningCounts.getOrDefault(test.getTestId(), 0L))).toList(); Map usage = new HashMap<>(); // 端口、电池和关联测试按未完成测试去重统计;在线事实仍由设备服务独立提供。 jdbc.query("select c.cabinet_id,count(distinct binding.channel_id),count(distinct t.test_id),count(distinct b.test_battery_id) from t_test_channel_binding binding join t_detection_channel c on c.channel_id=binding.channel_id and c.del_flag=0 join t_test t on t.test_id=binding.test_id and t.del_flag=0 and t.test_status<>'COMPLETED' join t_test_battery b on b.test_battery_id=binding.test_battery_id and b.del_flag=0 group by c.cabinet_id", (org.springframework.jdbc.core.RowCallbackHandler) rs -> usage.put(rs.getLong(1), new long[] { rs.getLong(2), rs.getLong(3), rs.getLong(4) })); var cabinetRows = cabinets.selectDetectionCabinetList(new TDetectionCabinet()).stream().map(cabinet -> { var values = usage.getOrDefault(cabinet.getCabinetId(), new long[3]); return new CabinetRow(cabinet.getCabinetId(), cabinet.getCabinetName(), cabinet.getCapacity(), cabinet.getChannelCount() == null ? 0 : cabinet.getChannelCount(), values[0], values[1], values[2], "0".equals(cabinet.getOnlineStatus()) ? "在线" : "离线"); }).toList(); return new Snapshot(summary, trends, rows, cabinetRows, warnings); } private Trend days(LocalDate begin, LocalDate end, Map dates) { List labels = new ArrayList<>(); List values = new ArrayList<>(); for (LocalDate day = begin; !day.isAfter(end); day = day.plusDays(1)) { labels.add(day.getMonthValue() + "/" + day.getDayOfMonth()); values.add(dates.getOrDefault(day, 0L)); } return new Trend(labels, values); } private String metricName(String code) { if (code == null) return "阈值异常"; return Map.of("VOLTAGE", "电压", "CURRENT", "电流", "TEMPERATURE", "温度", "POSITIVE_POTENTIAL", "正电位", "NEGATIVE_POTENTIAL", "负电位").getOrDefault(code, code); } }