实现化成循环曲线快照与指定版本打印授权
按测试、电池和循环冻结全部实时记录及设备时间零点,保存曲线显隐与来源颜色设置。
复用独立版本存储,补充化成导出权限并保留早期版本缺失内容,修复循环键序列化。
后端全量测试通过,隔离浏览器验证保存、修订、来源删除与打印权限。
| | |
| | | /** 值按测试、电池、循环、字段归属,只属于报表展示,不反写测量快照。 */ |
| | | public record Settings(@NotNull @Size(min = 1, max = 100) List<@NotNull @Valid FormationField> fields, |
| | | @NotNull Map<String, @NotNull String> values, |
| | | @NotNull Map<String, @NotNull Integer> chargeStepSelections) |
| | | @NotNull Map<String, @NotNull Integer> chargeStepSelections, |
| | | List<@NotNull String> metrics, Map<String, @NotNull String> seriesColors) |
| | | { |
| | | /** 旧版缺少显隐设置时显示五类指标,不补造测量快照。 */ |
| | | public List<String> metrics() { return metrics == null ? List.of("voltage", "current", "positiveVoltage", "negativeVoltage", "temperature") : metrics; } |
| | | |
| | | /** 未自定义颜色时由页面使用默认色板。 */ |
| | | public Map<String, String> seriesColors() { return seriesColors == null ? Map.of() : seriesColors; } |
| | | /** 展示设置拒绝来源和测量快照。 */ |
| | | @JsonAnySetter |
| | | public void rejectUnknown(String field, Object value) { throw new ServiceException("化成设置不允许提交属性:" + field); } |
| | |
| | | |
| | | public record Candidate(Selection selection, String name, String type, long groupId, String batteryGroup, String batteryNo) { } |
| | | public record ChargeStep(int stepNo, String name, String stepType, BigDecimal endVoltage) { } |
| | | public record Curve(Long zeroTime, List<Map<String, Object>> records) { } |
| | | public record Item(Candidate source, List<Integer> cycleNos, List<Map<String, Object>> cycles, |
| | | Map<String, Map<String, BigDecimal>> metrics, List<CurrentBatteryField> businessFields, |
| | | Map<Integer, List<ChargeStep>> chargeSteps) { } |
| | | Map<Integer, List<ChargeStep>> chargeSteps, Map<Integer, Curve> curves) { } |
| | | public record Matrix(List<Item> items, List<Integer> cycleNos) { } |
| | | public record Summary(long id, String name, String creator, long createdAt, String testName, String batteryGroup) { } |
| | | |
| | |
| | | TTestBattery battery = test.getBatteries().stream().filter(row -> selection.testBatteryId().equals(row.getTestBatteryId())) |
| | | .findFirst().orElseThrow(() -> new ServiceException("电池不属于所选测试")); |
| | | List<Map<String, Object>> rows = batteryRows(cycles.computeIfAbsent(selection.testId(), data::cycles), battery); |
| | | var actual = Stream.of(rows, batteryRows(records.computeIfAbsent(selection.testId(), data::records), battery), |
| | | var realtime = batteryRows(records.computeIfAbsent(selection.testId(), data::records), battery); |
| | | var actual = Stream.of(rows, realtime, |
| | | batteryRows(steps.computeIfAbsent(selection.testId(), data::steps), battery)).flatMap(List::stream) |
| | | .map(this::cycleNo).filter(no -> no > 0).distinct().sorted().toList(); |
| | | items.add(new Item(candidate(test, battery), actual, rows, metrics(rows, actual), battery.getFields(), chargeSteps(steps.get(selection.testId()), battery, actual))); |
| | | items.add(new Item(candidate(test, battery), actual, rows, metrics(rows, actual), battery.getFields(), chargeSteps(steps.get(selection.testId()), battery, actual), curves(realtime, actual))); |
| | | } |
| | | return new Matrix(items, items.stream().flatMap(item -> item.cycleNos().stream()).distinct().sorted().toList()); |
| | | } |
| | |
| | | public Map<String, Object> create(FormationReportRequest request, long userId, String userName) |
| | | { |
| | | Settings settings = new Settings(validateFields(request.settings().fields(), true), request.settings().values(), |
| | | request.settings().chargeStepSelections() == null ? Map.of() : request.settings().chargeStepSelections()); |
| | | request.settings().chargeStepSelections() == null ? Map.of() : request.settings().chargeStepSelections(), |
| | | request.settings().metrics(), request.settings().seriesColors()); |
| | | Matrix matrix = freeze(preview(request.selections()), settings.fields()); |
| | | validateValues(settings, matrix.items()); validateChargeSelections(settings, matrix.items()); |
| | | var snapshot = new LinkedHashMap<String, Object>(); |
| | |
| | | snapshot.put("items", matrix.items()); snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", settings); |
| | | snapshot.put("units", Map.of("chargeCapacity", "Ah", "dischargeCapacity", "Ah", "chargeEnergy", "Wh", "dischargeEnergy", "Wh", "efficiency", "%", "energyEfficiency", "%")); |
| | | snapshot.put("displayUnits", FormationField.METRIC_UNITS); snapshot.put("timeUnit", "epoch-milliseconds"); |
| | | curveUnits(snapshot); |
| | | return get(reports.insert(request.name().trim(), json(snapshot), userId, userName, System.currentTimeMillis()), null); |
| | | } |
| | | |
| | |
| | | Map<String, String> values = new LinkedHashMap<>(); |
| | | settings.values().forEach((key, value) -> { if (allowed.contains(key)) values.put(key, value); }); |
| | | snapshot.put("items", matrix.items()); snapshot.put("cycleNos", matrix.cycleNos()); snapshot.put("settings", new Settings(settings.fields(), values, |
| | | settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections())); |
| | | settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections(), settings.metrics(), settings.seriesColors())); |
| | | curveUnits(snapshot); |
| | | reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), json(snapshot), userId, userName, "按最新来源生成:" + request.changeSummary().trim()); |
| | | return get(id, null); |
| | | } |
| | |
| | | }).toList(); |
| | | return new Item(item.source(), item.cycleNos(), rows, metrics(rows, item.cycleNos()), item.businessFields().stream() |
| | | .filter(value -> fields.stream().anyMatch(field -> "battery".equals(field.kind()) && (field.fieldDefinitionId() == null |
| | | ? field.key().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList(), item.chargeSteps()); |
| | | ? field.key().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList(), item.chargeSteps(), item.curves()); |
| | | }).toList(); |
| | | return new Matrix(frozen, matrix.cycleNos()); |
| | | } |
| | | |
| | | private void validateValues(Settings settings, List<Item> items) |
| | | { |
| | | if (settings.metrics().stream().anyMatch(value -> value == null || !Set.of("voltage", "current", "positiveVoltage", "negativeVoltage", "temperature").contains(value)) |
| | | || new HashSet<>(settings.metrics()).size() != settings.metrics().size()) throw new ServiceException("化成曲线指标无效或重复"); |
| | | Set<String> sources = new HashSet<>(items.stream().map(item -> item.source().selection().testId() + "|" + item.source().selection().testBatteryId()).toList()); |
| | | if (!sources.containsAll(settings.seriesColors().keySet()) || settings.seriesColors().values().stream() |
| | | .anyMatch(color -> color == null || !color.matches("#[0-9a-fA-F]{6}"))) throw new ServiceException("化成曲线颜色或来源无效"); |
| | | if (settings.values() == null || !valueKeys(settings.fields(), items).containsAll(settings.values().keySet()) |
| | | || settings.values().values().stream().anyMatch(value -> value == null || value.length() > 500)) |
| | | throw new ServiceException("人工值不属于当前字段、测试电池或实际循环"); |
| | | } |
| | | |
| | | private void curveUnits(Map<String, Object> snapshot) |
| | | { |
| | | for (String key : List.of("units", "displayUnits")) |
| | | { |
| | | var units = new LinkedHashMap<>(JSON.parseObject(json(snapshot.get(key)))); |
| | | units.putAll(Map.of("voltage", "V", "current", "units".equals(key) ? "A" : "mA", |
| | | "positiveVoltage", "V", "negativeVoltage", "V", "temperature", "℃")); |
| | | snapshot.put(key, units); |
| | | } |
| | | } |
| | | |
| | | private Map<Integer, Curve> curves(List<Map<String, Object>> rows, List<Integer> cycles) |
| | | { |
| | | Map<Integer, Curve> result = new LinkedHashMap<>(); |
| | | for (int cycle : cycles) |
| | | { |
| | | var selected = rows.stream().filter(row -> cycleNo(row) == cycle).toList(); |
| | | // 每测试、电池、循环共享最早设备时间零点;柜与采集器仍保留各自时间,显隐不裁剪记录。 |
| | | Long zero = selected.stream().flatMap(row -> Stream.of(row.get("deviceTime"), row.get("collectorDeviceTime"))) |
| | | .filter(Number.class::isInstance).map(value -> ((Number) value).longValue()).min(Long::compareTo).orElse(null); |
| | | result.put(cycle, new Curve(zero, selected)); |
| | | } |
| | | return result; |
| | | } |
| | | |
| | | private void validateChargeSelections(Settings settings, List<Item> items) |
| | |
| | | Map<String, Integer> selections = settings.chargeStepSelections() == null ? Map.of() : settings.chargeStepSelections(); |
| | | for (Item item : items) for (int cycle : item.cycleNos()) { |
| | | String key = item.source().selection().testId() + "|" + item.source().selection().testBatteryId() + "|" + cycle; |
| | | List<ChargeStep> choices = item.chargeSteps().getOrDefault(cycle, List.of()); |
| | | // 早期版本未冻结工步时保持缺失,普通编辑不能重新读取设备事实。 |
| | | List<ChargeStep> choices = item.chargeSteps() == null ? List.of() : item.chargeSteps().getOrDefault(cycle, List.of()); |
| | | Integer selected = selections.get(key); |
| | | if (choices.stream().filter(step -> isReliableCharge(step.stepType())).count() == 1) { |
| | | int expected = choices.stream().filter(step -> isReliableCharge(step.stepType())).findFirst().get().stepNo(); |
| | |
| | | { return rows.stream().filter(row -> Objects.equals(battery.getBatteryNo(), row.get("batteryNo"))).toList(); } |
| | | private int cycleNo(Map<String, Object> row) { return row.get("cycleNo") instanceof Number number ? number.intValue() : 0; } |
| | | private List<Item> items(JSONObject snapshot) { return JSON.parseArray(json(snapshot.get("items")), Item.class); } |
| | | private String json(Object value) { return JSON.toJSONString(value, JSONWriter.Feature.WriteMapNullValue); } |
| | | // 循环号是整数键,存入数据库 JSON 时必须写成带引号的成员名。 |
| | | private String json(Object value) { return JSON.toJSONString(value, JSONWriter.Feature.WriteMapNullValue, JSONWriter.Feature.WriteNonStringKeyAsString); } |
| | | } |
| | |
| | | @GetMapping("/{id}/versions/{versionNo}") @PreAuthorize("@ss.hasPermi('system:formation-report:query')") |
| | | public AjaxResult version(@PathVariable long id, @PathVariable int versionNo) { return success(service.get(id, versionNo)); } |
| | | |
| | | /** 导出权限独立读取指定快照,打开打印不记录下载成功。 */ |
| | | @GetMapping("/{id}/versions/{versionNo}/print") @PreAuthorize("@ss.hasPermi('system:formation-report:export')") |
| | | public AjaxResult print(@PathVariable long id, @PathVariable int versionNo) { return success(service.get(id, versionNo)); } |
| | | |
| | | /** 普通编辑不接受来源或测量快照。 */ |
| | | @PostMapping("/{id}/versions") @PreAuthorize("@ss.hasPermi('system:formation-report:edit')") |
| | | public AjaxResult edit(@PathVariable long id, @Valid @RequestBody FormationReportRequest.Edit request) |
| New file |
| | |
| | | -- 化成指定版本打印独立授权,不建立下载历史。 |
| | | INSERT INTO sys_menu(menu_name,parent_id,order_num,path,menu_type,visible,status,perms,create_by,create_time) |
| | | SELECT '导出',parent_id,5,'#','F','0','0','system:formation-report:export','system',UTC_TIMESTAMP(3) |
| | | FROM sys_menu WHERE perms='system:formation-report:query'; |