| | |
| | | { |
| | | TTest test = tests.selectTestById(summary.getTestId()); |
| | | if (!"COMPLETED".equals(test.getTestStatus())) continue; |
| | | result.addAll(candidates(test, data.records(test.getTestId()))); |
| | | var available = candidates(test, data.records(test.getTestId())); |
| | | Set<String> mapped = new HashSet<>(); |
| | | for (var item : available) if (item.selection().logicalNo() != null) |
| | | mapped.add(item.selection().testBatteryId() + "|" + item.selection().cycleNo()); |
| | | // 与控制台一致:有保存的逻辑工步时,新选入口展示合并结果;旧快照仍保留原选择。 |
| | | result.addAll(available.stream().filter(item -> item.selection().logicalNo() != null |
| | | || !mapped.contains(item.selection().testBatteryId() + "|" + item.selection().cycleNo())).toList()); |
| | | } |
| | | return result; |
| | | } |
| | |
| | | /** 预览所选当前有效记录;相同电池的不同循环或工步是独立比对项。 */ |
| | | public List<Item> preview(List<Selection> selections) |
| | | { |
| | | if (selections == null || selections.isEmpty() || selections.contains(null) |
| | | if (selections == null || selections.isEmpty() || selections.stream().anyMatch(Objects::isNull) |
| | | || new HashSet<>(selections).size() != selections.size()) |
| | | throw new ServiceException("请选择不重复的比对项"); |
| | | Map<Long, TTest> loadedTests = new HashMap<>(); |
| | |
| | | import com.battery.business.domain.TTest; |
| | | import com.battery.business.domain.TTestBattery; |
| | | import com.battery.business.service.ITTestService; |
| | | import com.battery.business.service.ITBatteryGroupService; |
| | | import com.battery.business.service.ITestDataQueryService; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import org.springframework.stereotype.Service; |
| | |
| | | private final ITestDataQueryService data; |
| | | private final SpliceReportRepository reports; |
| | | private final SpliceFieldTemplateService templates; |
| | | public SpliceReportService(ITTestService tests, ITestDataQueryService data, SpliceReportRepository reports, SpliceFieldTemplateService templates) { |
| | | this.tests = tests; this.data = data; this.reports = reports; this.templates = templates; |
| | | private final ITBatteryGroupService groups; |
| | | public SpliceReportService(ITTestService tests, ITestDataQueryService data, SpliceReportRepository reports, SpliceFieldTemplateService templates, ITBatteryGroupService groups) { |
| | | this.tests = tests; this.data = data; this.reports = reports; this.templates = templates; this.groups = groups; |
| | | } |
| | | public record Candidate(SpliceReportRequest.Selection selection,String name,String type,long groupId,String group,String batteryNo,long startedAt){} |
| | | public record Matrix(List<Map<String,Object>> segments,List<Integer> cycleNos){} |
| | |
| | | } |
| | | /** 校验每个测试电池来源并按提交顺序逐段接续真实循环。 */ |
| | | public Matrix preview(List<SpliceReportRequest.Selection> selections) { |
| | | if (selections == null || selections.size() < 2 || selections.contains(null) |
| | | if (selections == null || selections.size() < 2 || selections.stream().anyMatch(Objects::isNull) |
| | | || new HashSet<>(selections).size() != selections.size()) |
| | | throw new ServiceException("请选择至少两个不重复的检测完成测试电池"); |
| | | Map<Long, TTest> loaded = new LinkedHashMap<>(); |
| | |
| | | } |
| | | List<Map<String, Object>> segments = new ArrayList<>(); |
| | | Set<Integer> mappedCycles = new TreeSet<>(); |
| | | Integer previousEnd = null; |
| | | int position = 0; |
| | | for (var entry : selected.entrySet()) { |
| | | long testId = entry.getKey().testId(); |
| | | List<Map<String, Object>> cycles = data.cycles(testId), steps = data.steps(testId), records = data.records(testId); |
| | | List<Map<String, Object>> cycles = data.cycles(testId), steps = data.steps(testId); |
| | | TTestBattery battery = entry.getValue(); |
| | | var rows = batteryRows(cycles, battery); |
| | | var stepRows = batteryRows(steps, battery); |
| | | var nos = java.util.stream.Stream.of(rows, stepRows, batteryRows(records, battery)).flatMap(List::stream) |
| | | var nos = java.util.stream.Stream.of(rows, stepRows).flatMap(List::stream) |
| | | .map(this::cycle).filter(no -> no > 0).distinct().sorted().toList(); |
| | | Map<String, Object> segment = new LinkedHashMap<>(); |
| | | segment.put("source", candidate(loaded.get(testId), battery)); |
| | |
| | | }); |
| | | frozen.put("fieldValues", values); segment.put("consoleDetail", frozen); |
| | | } |
| | | segment.put("originalCycleNos", nos); segment.put("statistics", statistics(rows, stepRows, nos)); |
| | | // 每个测试电池独立计算偏移;空段不占用循环,各指标共用同一定位。 |
| | | int offset = previousEnd == null || nos.isEmpty() ? 0 : previousEnd + 1 - nos.get(0); |
| | | segment.put("offset", offset); |
| | | for (int no : nos) mappedCycles.add(no + offset); |
| | | if (!nos.isEmpty()) previousEnd = nos.get(nos.size() - 1) + offset; |
| | | BigDecimal factor = capacityFactor(battery); |
| | | segment.put("capacityFactor", factor); |
| | | segment.put("originalCycleNos", nos); segment.put("statistics", statistics(rows, stepRows, nos, factor)); |
| | | // 累计位置只计实际循环点;原循环号单独冻结,不能用数值偏移保留缺号空位。 |
| | | Map<String,Integer> positions = new LinkedHashMap<>(); |
| | | segment.put("offset", position); |
| | | for (int no : nos) { positions.put(Integer.toString(no), ++position); mappedCycles.add(position); } |
| | | segment.put("cyclePositions", positions); |
| | | segments.add(segment); |
| | | } |
| | | return new Matrix(segments, new ArrayList<>(mappedCycles)); |
| | |
| | | private List<Map<String, Object>> batteryRows(List<Map<String, Object>> rows, TTestBattery battery) { |
| | | return rows.stream().filter(row -> Objects.equals(row.get("batteryNo"), battery.getBatteryNo())).toList(); |
| | | } |
| | | private Map<String,Object> statistics(List<Map<String,Object>> rows, List<Map<String,Object>> stepRows, List<Integer> nos) { |
| | | private BigDecimal capacityFactor(TTestBattery battery) { |
| | | var group = groups.selectBatteryGroupById(battery.getGroupId()); |
| | | if (group == null || group.getF() == null) return null; |
| | | try { var value = new BigDecimal(group.getF().trim()); return value.signum() == 0 ? null : value; } |
| | | catch (NumberFormatException error) { return null; } |
| | | } |
| | | private Map<String,Object> statistics(List<Map<String,Object>> rows, List<Map<String,Object>> stepRows, List<Integer> nos, BigDecimal factor) { |
| | | Map<String,Object> result = new LinkedHashMap<>(); |
| | | for (int no : nos) { |
| | | Map<String,Object> values = new LinkedHashMap<>(); |
| | |
| | | if (charge != null && discharge != null && charge.doubleValue() > 0) |
| | | values.put("energyEfficiency", new BigDecimal(discharge.toString()).multiply(BigDecimal.valueOf(100)) |
| | | .divide(new BigDecimal(charge.toString()), MathContext.DECIMAL128)); |
| | | // 与项目图表一致:Ah 转 mAh 后除以来源电池组 F,缺失或零分母不补值。 |
| | | Number capacity = num(values.get("dischargeCapacity")); |
| | | if (capacity != null && factor != null) |
| | | values.put("dischargeCapacityF", new BigDecimal(capacity.toString()).multiply(BigDecimal.valueOf(1000)).divide(factor, MathContext.DECIMAL128)); |
| | | } |
| | | // 查询接口按实际发生顺序返回工步;先选末记录再取值,空值不得向前回退。 |
| | | var actualSteps = stepRows.stream().filter(row -> cycle(row) == no).toList(); |
| | | var actualSteps = stepRows.stream().filter(row -> cycle(row) == no) |
| | | .sorted(Comparator.comparingInt(row -> row.get("stepNo") instanceof Number value ? value.intValue() : 0)).toList(); |
| | | if (!actualSteps.isEmpty()) values.put("finalVoltage1", actualSteps.get(actualSteps.size() - 1).get("endVoltage")); |
| | | result.put(Integer.toString(no), values); |
| | | } |
| | |
| | | validateStatistics(settings, matrix.segments()); |
| | | validateSourceDetails(settings, matrix.segments()); |
| | | Map<String,Object> snapshot = new LinkedHashMap<>(); |
| | | snapshot.put("spliceMode", "battery-segments"); |
| | | snapshot.put("spliceMode", "project-cycles"); |
| | | snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); |
| | | snapshot.put("selections", request.selections()); snapshot.put("segments", matrix.segments()); |
| | | snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", settings); |
| | |
| | | overrides.keySet().retainAll(statisticKeys(matrix.segments())); |
| | | settings.put("statisticValues", overrides); |
| | | snapshot.put("settings", settings); snapshot.put("segments", matrix.segments()); snapshot.put("cycleNos", matrix.cycleNos()); |
| | | snapshot.put("spliceMode", "battery-segments"); |
| | | snapshot.put("spliceMode", "project-cycles"); |
| | | reports.append(id, base + 1, snapshot.getString("name"), snapshot.toJSONString(), uid, user, "按最新来源生成:" + summary.trim()); |
| | | return get(id, null); |
| | | } |
| New file |
| | |
| | | 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<String> labels, List<Long> 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<String, Trend> trends, List<TestRow> tests, List<CabinetRow> cabinets, List<Warning> warnings) {} |
| | | |
| | | /** 使用全部未删除测试计算总量及香港自然日/月新增数,统计不受页面分页限制。 */ |
| | | public Snapshot read(Instant now) { |
| | | var allTests = tests.selectTestList(new TTest()); |
| | | var today = now.atZone(BUSINESS_ZONE).toLocalDate(); |
| | | Map<LocalDate, Long> 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<String, Trend>(); |
| | | trends.put("week", days(today.minusDays(6), today, dates)); |
| | | trends.put("month", days(today.withDayOfMonth(1), today, dates)); |
| | | List<String> months = new ArrayList<>(); List<Long> 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<Long, Long> warningCounts = new HashMap<>(); |
| | | List<Warning> 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<Long, List<String>> 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<Long, long[]> 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<LocalDate, Long> dates) { |
| | | List<String> labels = new ArrayList<>(); List<Long> 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); |
| | | } |
| | | } |
| | |
| | | package com.battery.business.service; |
| | | |
| | | import java.sql.Timestamp; |
| | | import java.sql.Statement; |
| | | import java.util.List; |
| | | import javax.sql.DataSource; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | import org.springframework.jdbc.core.RowMapper; |
| | | import org.springframework.jdbc.support.GeneratedKeyHolder; |
| | | import org.springframework.stereotype.Service; |
| | | import org.springframework.transaction.annotation.Transactional; |
| | | |
| | | /** 待办真实存储;所有查询和写操作都按当前负责人身份约束。 */ |
| | | /** 独立待办持久化,负责人限制始终使用登录用户 ID,不依赖姓名或管理员身份。 */ |
| | | @Service |
| | | public class TodoService { |
| | | private final JdbcTemplate jdbc; |
| | | public TodoService(DataSource source) { this.jdbc = new JdbcTemplate(source); } |
| | | public record Todo(long id, String content, long ownerId, String owner, String status, String creator, long createdAt, long operatedAt, String handleRemark) {} |
| | | public TodoService(DataSource source) { jdbc = new JdbcTemplate(source); } |
| | | public record Todo(long id, String content, long ownerId, String owner, String status, |
| | | String creator, long createdAt, long operatedAt, String handleRemark) {} |
| | | public record UserOption(long id, String name) {} |
| | | private static final String COLUMNS = "select todo_id,content,owner_id,owner_name,status,creator_name,created_at_utc,operated_at_utc,handle_remark from t_todo"; |
| | | private static final RowMapper<Todo> ROW = (rs, index) -> new Todo(rs.getLong(1), rs.getString(2), rs.getLong(3), rs.getString(4), |
| | | rs.getString(5), rs.getString(6), rs.getTimestamp(7).getTime(), rs.getTimestamp(8).getTime(), rs.getString(9)); |
| | | |
| | | /** 返回当前负责人自己的全部待办;包括已处理项,不向创建人或管理员额外开放。 */ |
| | | public List<Todo> list(long ownerId) { |
| | | return jdbc.query("select todo_id,content,owner_id,owner_name,status,creator_name,created_at_utc,operated_at_utc,coalesce(handle_remark,'') from t_todo where owner_id=? order by operated_at_utc desc,todo_id desc", (rs, i) -> new Todo(rs.getLong(1),rs.getString(2),rs.getLong(3),rs.getString(4),rs.getString(5),rs.getString(6),rs.getTimestamp(7).getTime(),rs.getTimestamp(8).getTime(),rs.getString(9)), ownerId); |
| | | return jdbc.query(COLUMNS + " where owner_id=? order by operated_at_utc desc,todo_id desc", ROW, ownerId); |
| | | } |
| | | public Todo create(String content, long ownerId, String owner, long creatorId, String creator) { |
| | | Timestamp now = new Timestamp(System.currentTimeMillis()); |
| | | jdbc.update("insert into t_todo(content,owner_id,owner_name,creator_id,creator_name,status,created_at_utc,operated_at_utc,handle_remark) values(?,?,?,?,?,'待处理',?,?,null)", content.trim(), ownerId, owner, creatorId, creator, now, now); |
| | | return jdbc.queryForObject("select todo_id,content,owner_id,owner_name,status,creator_name,created_at_utc,operated_at_utc,coalesce(handle_remark,'') from t_todo where todo_id=last_insert_id()", (rs,i)->new Todo(rs.getLong(1),rs.getString(2),rs.getLong(3),rs.getString(4),rs.getString(5),rs.getString(6),rs.getTimestamp(7).getTime(),rs.getTimestamp(8).getTime(),rs.getString(9))); |
| | | |
| | | /** 供新增表单选择启用用户,不要求用户管理模块权限。 */ |
| | | public List<UserOption> owners() { |
| | | return jdbc.query("select user_id,coalesce(nullif(nick_name,''),user_name) from sys_user where status='0' and del_flag='0' order by user_id", |
| | | (rs, index) -> new UserOption(rs.getLong(1), rs.getString(2))); |
| | | } |
| | | public boolean handle(long id,long ownerId,String remark) { return jdbc.update("update t_todo set status='已处理',handle_remark=?,operated_at_utc=? where todo_id=? and owner_id=? and status='待处理'",remark.trim(),new Timestamp(System.currentTimeMillis()),id,ownerId)==1; } |
| | | public boolean remove(long id,long ownerId) { return jdbc.update("delete from t_todo where todo_id=? and owner_id=?",id,ownerId)==1; } |
| | | |
| | | /** 新增后仅返回确认主键,分配给别人时不把该待办回传给非负责人。 */ |
| | | @Transactional |
| | | public long create(String content, long ownerId, long creatorId, String creator) { |
| | | String value = required(content, 200, "待办内容"); |
| | | var owner = owners().stream().filter(user -> user.id() == ownerId).findFirst() |
| | | .orElseThrow(() -> new ServiceException("请选择启用的负责人")); |
| | | var now = new Timestamp(System.currentTimeMillis()); |
| | | var key = new GeneratedKeyHolder(); |
| | | // 主键从本次 INSERT 的 JDBC 结果取得,不跨连接使用会话相关的 last_insert_id。 |
| | | jdbc.update(connection -> { |
| | | var statement = connection.prepareStatement("insert into t_todo(content,owner_id,owner_name,creator_id,creator_name,status,created_at_utc,operated_at_utc,handle_remark) values(?,?,?,?,?,'待处理',?,?,'')", Statement.RETURN_GENERATED_KEYS); |
| | | statement.setString(1, value); statement.setLong(2, owner.id()); statement.setString(3, owner.name()); |
| | | statement.setLong(4, creatorId); statement.setString(5, creator); statement.setTimestamp(6, now); statement.setTimestamp(7, now); |
| | | return statement; |
| | | }, key); |
| | | return key.getKey().longValue(); |
| | | } |
| | | |
| | | /** 负责人只能将自己的待处理事项完成一次;写入条件同时校验身份和当前状态。 */ |
| | | public void handle(long id, long ownerId, String remark) { |
| | | String value = required(remark, 500, "处理说明"); |
| | | if (jdbc.update("update t_todo set status='已处理',handle_remark=?,operated_at_utc=? where todo_id=? and owner_id=? and status='待处理'", |
| | | value, new Timestamp(System.currentTimeMillis()), id, ownerId) != 1) |
| | | throw new ServiceException("待办不存在、已处理或无权处理"); |
| | | } |
| | | |
| | | /** 按现有删除交互物理删除自己的待办,不涉及测试或异常事实。 */ |
| | | public void remove(long id, long ownerId) { |
| | | if (jdbc.update("delete from t_todo where todo_id=? and owner_id=?", id, ownerId) != 1) |
| | | throw new ServiceException("待办不存在或无权删除"); |
| | | } |
| | | |
| | | private String required(String text, int max, String name) { |
| | | if (text == null || text.trim().isEmpty() || text.trim().length() > max) |
| | | throw new ServiceException(name + "不能为空且长度不能超过" + max); |
| | | return text.trim(); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.battery.business.web; |
| | | |
| | | import java.time.Instant; |
| | | import com.battery.business.service.DashboardService; |
| | | import com.ruoyi.common.core.controller.BaseController; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | /** 首页查询使用独立查看权限,不借用用户管理和测试编辑权限。 */ |
| | | @RestController |
| | | @RequestMapping("/system/dashboard") |
| | | public class DashboardController extends BaseController { |
| | | private final DashboardService service; |
| | | public DashboardController(DashboardService service) { this.service = service; } |
| | | /** 汇总当前测试、检测柜和开放异常;个人待办通过自己的负责人接口读取。 */ |
| | | @GetMapping @PreAuthorize("@ss.hasAnyPermi('dashboard:workbench:list,dashboard:workbench:query')") |
| | | public AjaxResult get() { return success(service.read(Instant.now())); } |
| | | } |
| | |
| | | package com.battery.business.web; |
| | | |
| | | import java.util.List; |
| | | import com.battery.business.service.TodoService; |
| | | import com.ruoyi.common.core.controller.BaseController; |
| | | import com.ruoyi.common.core.domain.AjaxResult; |
| | | import org.springframework.security.access.prepost.PreAuthorize; |
| | | import org.springframework.web.bind.annotation.*; |
| | | |
| | | /** 待办列表接口;负责人身份由服务端从当前登录会话取得。 */ |
| | | /** 待办动作权限与负责人身份分别校验,创建身份只取当前登录会话。 */ |
| | | @RestController |
| | | @RequestMapping("/system/todo") |
| | | public class TodoController extends BaseController { |
| | | private final TodoService service; |
| | | public TodoController(TodoService service) { this.service = service; } |
| | | @GetMapping("/list") @PreAuthorize("@ss.hasPermi('todo:list:query')") |
| | | /** 仅查询自己的待办,不开放按其他负责人 ID 查询。 */ |
| | | @GetMapping("/list") @PreAuthorize("@ss.hasAnyPermi('todo:list:list,todo:list:query')") |
| | | public AjaxResult list() { return success().put("rows", service.list(getUserId())); } |
| | | /** 返回新增待办可选的启用用户。 */ |
| | | @GetMapping("/owners") @PreAuthorize("@ss.hasPermi('todo:list:add')") |
| | | public AjaxResult owners() { return success(service.owners()); } |
| | | /** 校验负责人后创建,返回主键确认,不泄露别人待办的详情。 */ |
| | | @PostMapping @PreAuthorize("@ss.hasPermi('todo:list:add')") |
| | | public AjaxResult create(@RequestBody CreateRequest request) { if (request == null || request.content() == null || request.content().trim().isEmpty() || request.ownerId() == null || request.ownerName() == null) return error("待办内容和负责人不能为空"); return success(service.create(request.content(), request.ownerId(), request.ownerName(), getUserId(), getUsername())); } |
| | | public AjaxResult create(@RequestBody CreateRequest request) { |
| | | if (request == null || request.ownerId() == null) return error("负责人不能为空"); |
| | | return success(service.create(request.content(), request.ownerId(), getUserId(), getUsername())); |
| | | } |
| | | /** 仅负责人可完成自己的待办,已处理不能再次修改。 */ |
| | | @PutMapping("/{id}/handle") @PreAuthorize("@ss.hasPermi('todo:list:handle')") |
| | | public AjaxResult handle(@PathVariable long id,@RequestBody HandleRequest request) { if (request == null || request.remark() == null || request.remark().trim().isEmpty()) return error("处理说明不能为空"); return service.handle(id,getUserId(),request.remark()) ? success() : error("待办不存在、已处理或无权处理"); } |
| | | public AjaxResult handle(@PathVariable long id, @RequestBody HandleRequest request) { |
| | | service.handle(id, getUserId(), request == null ? null : request.remark()); return success(); |
| | | } |
| | | /** 删除只作用于当前负责人自己的记录。 */ |
| | | @DeleteMapping("/{id}") @PreAuthorize("@ss.hasPermi('todo:list:remove')") |
| | | public AjaxResult remove(@PathVariable long id) { return service.remove(id,getUserId()) ? success() : error("待办不存在或无权删除"); } |
| | | public record CreateRequest(String content, Long ownerId, String ownerName) {} |
| | | public AjaxResult remove(@PathVariable long id) { service.remove(id, getUserId()); return success(); } |
| | | public record CreateRequest(String content, Long ownerId) {} |
| | | public record HandleRequest(String remark) {} |
| | | } |
| | |
| | | @Autowired |
| | | private ISysOperLogService operLogService; |
| | | |
| | | @PreAuthorize("@ss.hasPermi('monitor:operlog:list')") |
| | | @PreAuthorize("@ss.hasAnyPermi('monitor:operlog:list,monitor:operlog:query')") |
| | | @GetMapping("/list") |
| | | public TableDataInfo list(SysOperLog operLog) |
| | | { |
| New file |
| | |
| | | -- 最终库补齐首页及待办权限;只登记权限,不为任何角色自动增加授权。 |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '首页',0,0,'dashboard','M','0','0','','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE path='dashboard' AND menu_type='M'); |
| | | SET @dashboard_module = (SELECT menu_id FROM sys_menu WHERE path='dashboard' AND menu_type='M' LIMIT 1); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '工作台',@dashboard_module,1,'workbench','C','0','0','dashboard:workbench:list','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='dashboard:workbench:list'); |
| | | SET @dashboard_page = (SELECT menu_id FROM sys_menu WHERE perms='dashboard:workbench:list' LIMIT 1); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '查看',@dashboard_page,1,'#','F','0','0','dashboard:workbench:query','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='dashboard:workbench:query'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '待办管理',0,5,'todo','M','0','0','','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE path='todo' AND menu_type='M'); |
| | | SET @todo_module = (SELECT menu_id FROM sys_menu WHERE path='todo' AND menu_type='M' LIMIT 1); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '待办列表',@todo_module,1,'todo-list','C','0','0','todo:list:list','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:list'); |
| | | SET @todo_menu = (SELECT menu_id FROM sys_menu WHERE perms='todo:list:list' LIMIT 1); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '查看',@todo_menu,1,'#','F','0','0','todo:list:query','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:query'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '新增待办',@todo_menu,1,'#','F','0','0','todo:list:add','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:add'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '处理待办',@todo_menu,2,'#','F','0','0','todo:list:handle','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:handle'); |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '删除待办',@todo_menu,3,'#','F','0','0','todo:list:remove','system',UTC_TIMESTAMP(3) |
| | | WHERE NOT EXISTS (SELECT 1 FROM sys_menu WHERE perms='todo:list:remove'); |
| | | |
| | | -- 权限表仅展示三级结构,将既有操作日志放到系统管理页面层,不新增日志修改能力。 |
| | | UPDATE sys_menu SET parent_id=1 WHERE menu_id=500 AND parent_id=108 AND perms='monitor:operlog:list'; |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | import java.util.*; |
| | | import com.battery.business.domain.*; |
| | | import com.battery.business.service.*; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | class CompareMergedSourcesTest { |
| | | @Test |
| | | void selectingSourcesUsesSavedMergeAndPreviewIncludesItsActualSteps() { |
| | | var tests = mock(ITTestService.class); var data = mock(ITestDataQueryService.class); |
| | | var battery = new TTestBattery(); battery.setTestBatteryId(5L); battery.setGroupId(9L); battery.setBatteryNo("1号"); battery.setFields(List.of()); |
| | | var test = new TTest(); test.setTestId(1L); test.setTestStatus("COMPLETED"); test.setBatteries(List.of(battery)); test.setLogicStepMappings(List.of()); |
| | | when(tests.selectTestList(any())).thenReturn(List.of(test)); when(tests.selectTestById(1L)).thenReturn(test); |
| | | var records = List.of(Map.<String,Object>of("batteryNo", "1号", "cycleNo", 3, "stepNo", 10, "deviceTime", 1000L), |
| | | Map.<String,Object>of("batteryNo", "1号", "cycleNo", 3, "stepNo", 11, "deviceTime", 2000L)); |
| | | when(data.records(1L)).thenReturn(records); |
| | | var service = new CompareReportService(tests, data, null); |
| | | var actual = service.sources(); assertEquals(2, actual.size()); |
| | | var mapping = new TTestLogicStepMapping(); mapping.setLogicalNo(1); mapping.setLogicalName("合并充电"); mapping.setStartStepNo(1); mapping.setEndStepNo(2); |
| | | test.setLogicStepMappings(List.of(mapping)); |
| | | var merged = service.sources(); assertEquals(1, merged.size()); |
| | | assertEquals(1, merged.get(0).selection().logicalNo()); |
| | | assertNull(merged.get(0).selection().stepNo()); |
| | | assertTrue(merged.get(0).process().contains("合并充电")); |
| | | var preview = service.preview(List.of(merged.get(0).selection())); |
| | | assertEquals(records, preview.get(0).records()); |
| | | assertEquals(1000, preview.get(0).zeroTime()); |
| | | assertEquals(1, service.preview(List.of(actual.get(0).selection())).get(0).records().size(), "旧版来源仍可明确刷新原实际工步"); |
| | | } |
| | | } |
| | |
| | | } |
| | | var compare = new CompareReportService(tests, data, new CompareReportRepository(database)); |
| | | var formation = new FormationReportService(tests, data, new FormationReportRepository(database), definitions, value -> StepDirectionResolver.Direction.UNKNOWN); |
| | | var splice = new SpliceReportService(tests, data, new SpliceReportRepository(database), new SpliceFieldTemplateService(database, definitions)); |
| | | var splice = new SpliceReportService(tests, data, new SpliceReportRepository(database), new SpliceFieldTemplateService(database, definitions), mock(ITBatteryGroupService.class)); |
| | | var settings = JSON.parseObject(switch (type) { |
| | | case "compare" -> "{\"metrics\":[],\"seriesColors\":[],\"batteryTemplateFields\":[],\"specialFields\":[],\"fieldStructure\":[],\"detailFieldValues\":{}}"; |
| | | case "formation" -> "{\"fields\":[{\"key\":\"chargeCapacity\",\"scope\":\"cycle\",\"kind\":\"metric\",\"displayName\":\"充电容量\",\"unit\":\"mAh\"}],\"values\":{},\"chargeStepSelections\":{},\"metrics\":[],\"seriesColors\":{}}"; |
| New file |
| | |
| | | package com.battery.business.report; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | import java.math.BigDecimal; |
| | | import java.util.*; |
| | | import com.alibaba.fastjson2.JSON; |
| | | import com.battery.business.domain.*; |
| | | import com.battery.business.service.*; |
| | | import org.junit.jupiter.api.Test; |
| | | |
| | | class SpliceProjectCyclesTest { |
| | | @Test |
| | | void actualCyclesBecomeConsecutivePositionsWithFrozenProjectStatistics() { |
| | | var tests = mock(ITTestService.class); |
| | | var data = mock(ITestDataQueryService.class); |
| | | var groups = mock(ITBatteryGroupService.class); |
| | | var group = new TBatteryGroup(); group.setF("2"); |
| | | when(groups.selectBatteryGroupById(9L)).thenReturn(group); |
| | | for (long id : List.of(1L, 2L)) { |
| | | var battery = new TTestBattery(); battery.setTestBatteryId(id); battery.setGroupId(9L); |
| | | battery.setBatteryNo("电池1"); battery.setFields(List.of()); |
| | | var test = new TTest(); test.setTestId(id); test.setTestStatus("COMPLETED"); test.setBatteries(List.of(battery)); |
| | | when(tests.selectTestById(id)).thenReturn(test); |
| | | var nos = id == 1 ? List.of(1, 3) : List.of(10, 11); |
| | | when(data.cycles(id)).thenReturn(nos.stream().map(no -> Map.<String,Object>of("batteryNo", "电池1", "cycleNo", no, "dischargeCapacity", new BigDecimal("0.004"))).toList()); |
| | | when(data.steps(id)).thenReturn(nos.stream().flatMap(no -> List.of( |
| | | Map.<String,Object>of("batteryNo", "电池1", "cycleNo", no, "stepNo", 8), |
| | | Map.<String,Object>of("batteryNo", "电池1", "cycleNo", no, "stepNo", 1, "endVoltage", 4)).stream()).toList()); |
| | | } |
| | | var service = new SpliceReportService(tests, data, null, null, groups); |
| | | var result = service.preview(List.of(new SpliceReportRequest.Selection(1L, 1L), new SpliceReportRequest.Selection(2L, 2L))); |
| | | assertEquals(List.of(1, 2, 3, 4), result.cycleNos()); |
| | | assertEquals(Map.of("1", 1, "3", 2), result.segments().get(0).get("cyclePositions")); |
| | | assertEquals(Map.of("10", 3, "11", 4), result.segments().get(1).get("cyclePositions")); |
| | | assertEquals(List.of(10, 11), result.segments().get(1).get("originalCycleNos")); |
| | | var stat = JSON.parseObject(JSON.toJSONString(result.segments().get(0).get("statistics"))).getJSONObject("1"); |
| | | assertEquals(0, new BigDecimal("2").compareTo(stat.getBigDecimal("dischargeCapacityF"))); |
| | | assertNull(stat.get("finalVoltage1"), "末工步缺失不能回退到前一工步电压"); |
| | | assertNull(stat.get("finalVoltage2"), "未定义公式不能补值"); |
| | | verify(data, never()).records(anyLong()); |
| | | group.setF("0"); |
| | | var unavailable = service.preview(List.of(new SpliceReportRequest.Selection(1L, 1L), new SpliceReportRequest.Selection(2L, 2L))); |
| | | assertNull(JSON.parseObject(JSON.toJSONString(unavailable.segments().get(0).get("statistics"))).getJSONObject("1").get("dischargeCapacityF")); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.battery.business.service; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import static org.mockito.Mockito.*; |
| | | import java.time.Instant; |
| | | import java.util.*; |
| | | import com.battery.business.domain.*; |
| | | import com.battery.business.mapper.TTestMapper; |
| | | import org.h2.jdbcx.JdbcDataSource; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | |
| | | class DashboardServiceTest { |
| | | @Test |
| | | void aggregatesAllTestsAtHongKongMidnightAndKeepsDeviceFactsSeparate() { |
| | | var database = new JdbcDataSource(); database.setURL("jdbc:h2:mem:dashboard_" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"); |
| | | var jdbc = new JdbcTemplate(database); |
| | | jdbc.execute("create table t_battery_group(group_id bigint, del_flag int)"); |
| | | jdbc.execute("insert into t_battery_group values(1,0),(2,1)"); |
| | | jdbc.execute("create table t_test_realtime_record(record_id bigint,test_id bigint,test_battery_id bigint,step_no int,cycle_no int)"); |
| | | jdbc.execute("create table t_test_battery(test_battery_id bigint,battery_no varchar(20),del_flag int)"); |
| | | jdbc.execute("create table t_test_channel_binding(channel_id bigint,test_id bigint,test_battery_id bigint)"); |
| | | jdbc.execute("create table t_detection_channel(channel_id bigint,cabinet_id bigint,del_flag int)"); |
| | | jdbc.execute("create table t_test(test_id bigint,test_status varchar(20),del_flag int)"); |
| | | jdbc.execute("insert into t_test values(1,'RUNNING',0),(2,'COMPLETED',0)"); |
| | | jdbc.execute("insert into t_test_battery values(1,'电池1',0),(2,'电池2',0)"); |
| | | jdbc.execute("insert into t_detection_channel values(1,7,0)"); |
| | | jdbc.execute("insert into t_test_channel_binding values(1,1,1),(1,2,2)"); |
| | | var tests = mock(ITTestService.class); |
| | | var cabinets = mock(ITDetectionCabinetService.class); |
| | | var mapper = mock(TTestMapper.class); |
| | | var rows = new ArrayList<TTest>(); |
| | | for (int index = 0; index < 1001; index++) { |
| | | var test = new TTest(); test.setTestId((long) index + 1); test.setTestStatus("RUNNING"); test.setReceptionStatus("PAUSED"); |
| | | test.setCreateTime(Date.from(Instant.parse(index == 0 ? "2026-08-31T15:59:59Z" : "2026-08-31T16:00:00Z"))); rows.add(test); |
| | | } |
| | | when(tests.selectTestList(any())).thenReturn(rows); |
| | | var cabinet = new TDetectionCabinet(); cabinet.setCabinetId(7L); cabinet.setChannelCount(8); cabinet.setOnlineStatus("0"); |
| | | when(cabinets.selectDetectionCabinetList(any())).thenReturn(List.of(cabinet)); |
| | | var warning = new TTestWarningException(); warning.setExceptionId(3L); warning.setTestId(1L); warning.setExceptionType("DEVICE_OFFLINE"); |
| | | when(mapper.selectWarningExceptions(null, "OPEN")).thenReturn(List.of(warning)); |
| | | var result = new DashboardService(tests, cabinets, mapper, database).read(Instant.parse("2026-09-01T00:00:00Z")); |
| | | assertEquals(1001, result.summary().testCount()); |
| | | assertEquals(1, result.summary().batteryGroupCount()); |
| | | assertEquals(1000, result.summary().todayCount()); |
| | | assertEquals(1000, result.summary().monthCount()); |
| | | assertEquals(List.of(1000L), result.trends().get("month").values()); |
| | | assertEquals(1, result.trends().get("year").values().get(7)); |
| | | assertEquals(1000, result.trends().get("year").values().get(8)); |
| | | assertEquals("在线", result.cabinets().get(0).onlineStatus()); |
| | | assertEquals(1, result.cabinets().get(0).usedChannels()); |
| | | assertEquals(1, result.cabinets().get(0).testCount()); |
| | | assertEquals("RUNNING", result.tests().get(0).status()); |
| | | assertEquals("PAUSED", result.tests().get(0).receptionStatus()); |
| | | assertEquals("设备离线", result.warnings().get(0).item()); |
| | | } |
| | | } |
| New file |
| | |
| | | package com.battery.business.service; |
| | | |
| | | import static org.junit.jupiter.api.Assertions.*; |
| | | import java.nio.file.*; |
| | | import java.util.UUID; |
| | | import com.ruoyi.common.exception.ServiceException; |
| | | import org.h2.jdbcx.JdbcDataSource; |
| | | import org.junit.jupiter.api.Test; |
| | | import org.springframework.jdbc.core.JdbcTemplate; |
| | | |
| | | class TodoServiceTest { |
| | | @Test |
| | | void onlyOwnerCanReadHandleAndRemoveEvenWhenNamesMatch() throws Exception { |
| | | var database = new JdbcDataSource(); |
| | | database.setURL("jdbc:h2:mem:todo_" + UUID.randomUUID() + ";MODE=MySQL;DB_CLOSE_DELAY=-1"); |
| | | var jdbc = new JdbcTemplate(database); |
| | | jdbc.execute("CREATE TABLE sys_user(user_id BIGINT, user_name VARCHAR(100), nick_name VARCHAR(100), status CHAR(1), del_flag CHAR(1))"); |
| | | jdbc.update("INSERT INTO sys_user VALUES(1,'creator','同名','0','0'),(2,'owner','同名','0','0'),(3,'disabled','停用','1','0')"); |
| | | var ddl = Files.readString(Path.of("src/main/resources/db/migration-final/V20__todo_records.sql")); |
| | | jdbc.execute(ddl.substring(0, ddl.indexOf("ENGINE=InnoDB"))); |
| | | var service = new TodoService(database); |
| | | long id = service.create(" 工作事项 ", 2, 1, "创建人"); |
| | | assertTrue(service.list(1).isEmpty()); |
| | | assertEquals("工作事项", service.list(2).get(0).content()); |
| | | assertEquals(id, service.list(2).get(0).id()); |
| | | assertEquals(2, service.owners().size()); |
| | | assertThrows(ServiceException.class, () -> service.create("事项", 3, 1, "创建人")); |
| | | assertThrows(ServiceException.class, () -> service.handle(id, 1, "管理员或创建人不可越权")); |
| | | assertThrows(ServiceException.class, () -> service.remove(id, 1)); |
| | | assertThrows(ServiceException.class, () -> service.handle(id, 2, " ")); |
| | | service.handle(id, 2, "已完成"); |
| | | var reopened = new TodoService(database).list(2).get(0); |
| | | assertEquals("已处理", reopened.status()); |
| | | assertEquals("已完成", reopened.handleRemark()); |
| | | assertThrows(ServiceException.class, () -> service.handle(id, 2, "再次处理")); |
| | | service.remove(id, 2); |
| | | assertTrue(service.list(2).isEmpty()); |
| | | } |
| | | } |