package com.battery.business.report;
|
|
import java.math.BigDecimal;
|
import java.math.MathContext;
|
import java.util.*;
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.battery.business.domain.CurrentBatteryField;
|
import com.battery.business.domain.TTest;
|
import com.battery.business.domain.TTestBattery;
|
import com.battery.business.service.ITTestService;
|
import com.battery.business.service.ITestDataQueryService;
|
import com.ruoyi.common.exception.ServiceException;
|
import org.springframework.stereotype.Service;
|
import org.springframework.transaction.annotation.Transactional;
|
|
/** 拼接报表按测试顺序接续循环,工步与循环指标分别读取对应的当前有效记录。 */
|
@Service
|
public class SpliceReportService {
|
private final ITTestService tests;
|
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;
|
}
|
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 List<Candidate> sources() {
|
TTest query = new TTest(); query.setTestStatus("COMPLETED");
|
List<Candidate> result = new ArrayList<>();
|
for (TTest summary : tests.selectTestList(query)) {
|
TTest test = tests.selectTestById(summary.getTestId());
|
for (TTestBattery battery : test.getBatteries()) result.add(candidate(test, battery));
|
}
|
return result.stream().sorted(Comparator.comparingLong(Candidate::startedAt)
|
.thenComparingLong(value -> value.selection().testId())).toList();
|
}
|
/** 校验至少两个同组测试选择相同的参与电池集合,按提交顺序接续真实循环。 */
|
public Matrix preview(List<SpliceReportRequest.Selection> selections) {
|
if (selections == null || selections.size() < 2 || selections.contains(null)
|
|| new HashSet<>(selections).size() != selections.size())
|
throw new ServiceException("请选择至少两个不重复的检测完成测试电池");
|
Map<Long, TTest> loaded = new LinkedHashMap<>();
|
Map<Long, List<TTestBattery>> selected = new LinkedHashMap<>();
|
Long group = null;
|
for (var selection : selections) {
|
if (selection.testId() == null || selection.testBatteryId() == null) throw new ServiceException("测试和电池身份不能为空");
|
TTest test = loaded.computeIfAbsent(selection.testId(), tests::selectTestById);
|
if (test == null || !"COMPLETED".equals(test.getTestStatus())) throw new ServiceException("仅可选择检测完成测试");
|
TTestBattery battery = test.getBatteries().stream().filter(row -> selection.testBatteryId().equals(row.getTestBatteryId()))
|
.findFirst().orElseThrow(() -> new ServiceException("电池不属于所选测试"));
|
if (group == null) group = battery.getGroupId();
|
if (!Objects.equals(group, battery.getGroupId())) throw new ServiceException("拼接测试必须属于同一电池组");
|
selected.computeIfAbsent(selection.testId(), key -> new ArrayList<>()).add(battery);
|
}
|
if (selected.size() < 2) throw new ServiceException("请至少选择两个不同测试");
|
Set<String> common = null;
|
for (var batteries : selected.values()) {
|
Set<String> names = new HashSet<>(batteries.stream().map(TTestBattery::getBatteryNo).toList());
|
if (common == null) common = names;
|
if (names.size() != batteries.size() || !common.equals(names)) throw new ServiceException("拼接测试必须选择相同的共同参与电池集合");
|
}
|
List<Map<String, Object>> segments = new ArrayList<>();
|
Set<Integer> mappedCycles = new TreeSet<>();
|
Integer previousEnd = null;
|
for (var entry : selected.entrySet()) {
|
List<Map<String, Object>> cycles = data.cycles(entry.getKey()), steps = data.steps(entry.getKey()), records = data.records(entry.getKey());
|
List<Map<String, Object>> current = new ArrayList<>();
|
Set<Integer> sourceCycles = new TreeSet<>();
|
for (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)
|
.map(this::cycle).filter(no -> no > 0).distinct().sorted().toList();
|
Map<String, Object> segment = new LinkedHashMap<>();
|
segment.put("source", candidate(loaded.get(entry.getKey()), battery));
|
segment.put("cycles", rows); segment.put("steps", stepRows);
|
segment.put("businessFields", battery.getFields());
|
segment.put("originalCycleNos", nos); segment.put("statistics", statistics(rows, stepRows, nos));
|
current.add(segment); sourceCycles.addAll(nos);
|
}
|
// 同一测试的全部电池共用偏移;首个非空段保留原号,后段首循环紧接前段末循环。
|
int offset = previousEnd == null || sourceCycles.isEmpty() ? 0 : previousEnd + 1 - sourceCycles.iterator().next();
|
current.forEach(segment -> segment.put("offset", offset));
|
for (int no : sourceCycles) mappedCycles.add(no + offset);
|
if (!sourceCycles.isEmpty()) previousEnd = Collections.max(sourceCycles) + offset;
|
segments.addAll(current);
|
}
|
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) {
|
Map<String,Object> result = new LinkedHashMap<>();
|
for (int no : nos) {
|
Map<String,Object> values = new LinkedHashMap<>();
|
var cycleRows = rows.stream().filter(row -> cycle(row) == no).toList();
|
if (cycleRows.size() == 1) {
|
for (String key : List.of("chargeEnergy", "dischargeEnergy", "chargeCapacity", "dischargeCapacity"))
|
if (cycleRows.get(0).get(key) != null) values.put(key, cycleRows.get(0).get(key));
|
Number charge = num(values.get("chargeEnergy")), discharge = num(values.get("dischargeEnergy"));
|
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));
|
}
|
// 查询接口按实际发生顺序返回工步;先选末记录再取值,空值不得向前回退。
|
var actualSteps = stepRows.stream().filter(row -> cycle(row) == no).toList();
|
if (!actualSteps.isEmpty()) values.put("finalVoltage1", actualSteps.get(actualSteps.size() - 1).get("endVoltage"));
|
result.put(Integer.toString(no), values);
|
}
|
return result;
|
}
|
private Number num(Object value) { return value instanceof Number number ? number : null; }
|
|
/** 首次保存时重新读取来源并冻结字段与计算原值,人工调整只写入显示设置。 */
|
@Transactional
|
public Map<String,Object> create(SpliceReportRequest request, long uid, String user) {
|
var settings = settings(request.settings(), true);
|
Matrix matrix = freeze(preview(request.selections()), fields(settings));
|
validateStatistics(settings, matrix.segments());
|
Map<String,Object> snapshot = new LinkedHashMap<>();
|
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);
|
return get(reports.insert(request.name().trim(), JSON.toJSONString(snapshot), uid, user, System.currentTimeMillis()), null);
|
}
|
|
/** 当前和历史版本只读独立快照,来源删除后仍可重开。 */
|
public Map<String,Object> get(long id, Integer version) {
|
var saved = reports.get(id, version);
|
Map<String,Object> result = JSON.parseObject(saved.snapshotJson());
|
result.put("id", id); result.put("versionNo", saved.versionNo());
|
result.put("creator", saved.report().creator()); result.put("createdAt", saved.report().createdAt());
|
result.put("operatorName", saved.operatorName()); result.put("versionCreatedAt", saved.versionCreatedAt());
|
result.put("changeSummary", saved.changeSummary());
|
return result;
|
}
|
|
/** 普通编辑只追加展示设置,保留原版本来源、计算原值及字段事实。 */
|
@Transactional
|
public Map<String,Object> edit(long id, SpliceReportRequest.Edit request, long uid, String user) {
|
var snapshot = current(id, request.baseVersionNo());
|
var settings = settings(request.settings(), false);
|
validateStatistics(settings, segments(snapshot));
|
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); snapshot.put("settings", settings);
|
reports.append(id, request.baseVersionNo() + 1, request.name().trim(), snapshot.toJSONString(), uid, user, request.changeSummary().trim());
|
return get(id, null);
|
}
|
|
/** 主动按最新来源生成版本,沿用已保存选择及字段结构;来源失效则整次回滚。 */
|
@Transactional
|
public Map<String,Object> refresh(long id, int base, String summary, long uid, String user) {
|
if (summary == null || summary.isBlank() || summary.length() > 500) throw new ServiceException("请填写有效修改说明");
|
var snapshot = current(id, base);
|
var selections = JSON.parseArray(snapshot.getString("selections"), SpliceReportRequest.Selection.class);
|
var settings = settings(snapshot.getJSONObject("settings"), false);
|
Matrix matrix = freeze(preview(selections), fields(settings));
|
var overrides = JSON.parseObject(JSON.toJSONString(settings.getOrDefault("statisticValues", Map.of())));
|
overrides.keySet().retainAll(statisticKeys(matrix.segments()));
|
settings.put("statisticValues", overrides);
|
snapshot.put("settings", settings); snapshot.put("segments", matrix.segments()); snapshot.put("cycleNos", matrix.cycleNos());
|
reports.append(id, base + 1, snapshot.getString("name"), snapshot.toJSONString(), uid, user, "按最新来源生成:" + summary.trim());
|
return get(id, null);
|
}
|
|
/** 整份报表及全部版本与最小删除日志在同一事务中提交,日志失败时保留报表。 */
|
@Transactional
|
public void delete(long id, long uid, String user) { reports.lock(id); reports.delete(id, uid, user); }
|
|
private JSONObject current(long id, int base) {
|
// 与删除共用身份锁,拒绝从陈旧基准覆盖较新的版本。
|
reports.lock(id);
|
var saved = reports.get(id);
|
if (saved.versionNo() != base) throw new ServiceException("报表已有新版本,请重新打开后编辑");
|
return JSON.parseObject(saved.snapshotJson());
|
}
|
|
private Map<String,Object> settings(Map<String,Object> input, boolean resolveDefinitions) {
|
var result = new LinkedHashMap<>(input);
|
List<SpliceField> structure;
|
if (input.containsKey("fieldStructure")) structure = fields(input);
|
else {
|
structure = new ArrayList<>();
|
for (String key : List.of("batteryFields", "specialFields")) {
|
var names = JSON.parseArray(JSON.toJSONString(input.getOrDefault(key, List.of())), String.class);
|
for (String name : names) structure.add(new SpliceField(name, key.equals("batteryFields") ? "battery" : "special", "", null, name));
|
}
|
}
|
structure = templates.validate(structure, resolveDefinitions);
|
result.put("fieldStructure", structure);
|
result.put("batteryFields", structure.stream().filter(field -> field.category().equals("battery")).map(SpliceField::name).toList());
|
result.put("specialFields", structure.stream().filter(field -> field.category().equals("special")).map(SpliceField::name).toList());
|
return result;
|
}
|
|
private List<SpliceField> fields(Map<String,Object> settings) {
|
return JSON.parseArray(JSON.toJSONString(settings.get("fieldStructure")), SpliceField.class);
|
}
|
|
private List<Map<String,Object>> segments(JSONObject snapshot) {
|
return snapshot.getJSONArray("segments").stream().map(value -> (Map<String,Object>) value).toList();
|
}
|
|
private Matrix freeze(Matrix matrix, List<SpliceField> fields) {
|
for (var segment : matrix.segments()) {
|
var business = JSON.parseArray(JSON.toJSONString(segment.get("businessFields")), CurrentBatteryField.class);
|
segment.put("businessFields", business.stream().filter(value -> fields.stream().anyMatch(field ->
|
field.category().equals("battery") && (field.fieldDefinitionId() == null
|
? field.name().equals(value.getFieldName()) : field.fieldDefinitionId().equals(value.getFieldDefinitionId())))).toList());
|
}
|
return matrix;
|
}
|
|
private Set<String> statisticKeys(List<Map<String,Object>> segments) {
|
Set<String> allowed = new HashSet<>();
|
for (var segment : segments) {
|
var source = JSON.parseObject(JSON.toJSONString(segment.get("source")), Candidate.class);
|
var cycles = JSON.parseArray(JSON.toJSONString(segment.get("originalCycleNos")), Integer.class);
|
for (int no : cycles) for (String metric : List.of("finalVoltage1", "energyEfficiency"))
|
allowed.add(source.selection().testId() + "|" + source.selection().testBatteryId() + "|" + no + "|" + metric);
|
}
|
return allowed;
|
}
|
|
private void validateStatistics(Map<String,Object> settings, List<Map<String,Object>> segments) {
|
Object submitted = settings.get("statisticValues");
|
if (submitted == null) return;
|
if (!(submitted instanceof Map<?,?> values)) throw new ServiceException("人工指标格式无效");
|
Set<String> allowed = statisticKeys(segments);
|
for (var entry : values.entrySet()) {
|
if (!allowed.contains(entry.getKey())) throw new ServiceException("人工指标不属于当前报表循环");
|
if (entry.getValue() == null || entry.getValue().toString().isBlank()) continue;
|
try { new BigDecimal(entry.getValue().toString()); }
|
catch (NumberFormatException error) { throw new ServiceException("人工指标必须为数值或留空"); }
|
}
|
}
|
|
private Candidate candidate(TTest test, TTestBattery battery) {
|
return new Candidate(new SpliceReportRequest.Selection(test.getTestId(), battery.getTestBatteryId()),
|
test.getTestName(), test.getTestTypeName(), battery.getGroupId(), battery.getGroupName(), battery.getBatteryNo(),
|
test.getStartedAtUtc() == null ? 0 : test.getStartedAtUtc().getTime());
|
}
|
private int cycle(Map<String,Object> row) { return row.get("cycleNo") instanceof Number number ? number.intValue() : 0; }
|
}
|