package com.battery.business.report;
|
|
import java.util.ArrayList;
|
import java.util.Comparator;
|
import java.util.HashMap;
|
import java.util.HashSet;
|
import java.util.LinkedHashMap;
|
import java.util.List;
|
import java.util.Map;
|
import java.util.Objects;
|
import java.util.Set;
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONWriter;
|
import com.battery.business.domain.CurrentBatteryField;
|
import com.battery.business.domain.TTest;
|
import com.battery.business.domain.TTestBattery;
|
import com.battery.business.domain.TTestLogicStepMapping;
|
import com.battery.business.report.CompareReportRequest.Selection;
|
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 CompareReportService
|
{
|
private final ITTestService tests;
|
private final ITestDataQueryService data;
|
private final CompareReportRepository reports;
|
|
public CompareReportService(ITTestService tests, ITestDataQueryService data, CompareReportRepository reports)
|
{ this.tests = tests; this.data = data; this.reports = reports; }
|
|
public record Candidate(Selection selection, String name, String type, long groupId,
|
String batteryGroup, String batteryNo, String process) { }
|
public record Item(Candidate source, Long zeroTime, List<Map<String, Object>> records,
|
TTestLogicStepMapping logicalMapping, List<CurrentBatteryField> businessFields) { }
|
|
/** 查询检测完成测试中实际存在的比对来源,不借用测试页面动作权限。 */
|
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());
|
if (!"COMPLETED".equals(test.getTestStatus())) continue;
|
result.addAll(candidates(test, data.records(test.getTestId())));
|
}
|
return result;
|
}
|
|
/** 预览所选当前有效记录;相同电池的不同循环或工步是独立比对项。 */
|
public List<Item> preview(List<Selection> selections)
|
{
|
if (selections == null || selections.isEmpty() || selections.contains(null)
|
|| new HashSet<>(selections).size() != selections.size())
|
throw new ServiceException("请选择不重复的比对项");
|
Map<Long, TTest> loadedTests = new HashMap<>();
|
Map<Long, List<Map<String, Object>>> loadedRecords = new HashMap<>();
|
List<Item> result = new ArrayList<>();
|
for (Selection selection : selections)
|
{
|
if (selection.testId() == null) throw new ServiceException("测试身份不能为空");
|
TTest test = loadedTests.computeIfAbsent(selection.testId(), tests::selectTestById);
|
if (!"COMPLETED".equals(test.getTestStatus())) throw new ServiceException("仅可选择检测完成测试");
|
List<Map<String, Object>> records = loadedRecords.computeIfAbsent(selection.testId(), data::records);
|
Candidate source = candidates(test, records).stream().filter(item -> item.selection().equals(selection)).findFirst()
|
.orElseThrow(() -> new ServiceException("比对来源已失效,请重新选择电池、循环和工步"));
|
TTestBattery battery = test.getBatteries().stream().filter(item -> item.getTestBatteryId().equals(selection.testBatteryId())).findFirst().orElseThrow();
|
TTestLogicStepMapping mapping = selection.logicalNo() == null ? null : test.getLogicStepMappings().stream()
|
.filter(item -> item.getLogicalNo().equals(selection.logicalNo())).findFirst().orElseThrow();
|
int firstStep = firstStep(records, selection.cycleNo());
|
List<Map<String, Object>> selected = records.stream().filter(row -> Objects.equals(source.batteryNo(), row.get("batteryNo"))
|
&& number(row, "cycleNo") == selection.cycleNo()
|
&& (mapping == null ? number(row, "stepNo") == selection.stepNo()
|
: inMapping(number(row, "stepNo"), firstStep, mapping))).toList();
|
// 柜与采集器使用各自设备时间,共享所选范围零点;隐藏指标不改变范围或零点。
|
Long zero = selected.stream().flatMap(row -> List.of("deviceTime", "collectorDeviceTime").stream().map(row::get))
|
.filter(Number.class::isInstance).map(value -> ((Number) value).longValue()).min(Long::compareTo).orElse(null);
|
result.add(new Item(source, zero, selected, mapping, battery.getFields()));
|
}
|
return result;
|
}
|
|
/** 从后端重新读取来源并原子保存身份与首版,任一失败不留下半份报表。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> create(CompareReportRequest request, long operatorId, String operatorName)
|
{
|
if (request == null || request.selections() == null || request.selections().size() < 2)
|
throw new ServiceException("请至少添加两条比对项");
|
validateSettings(request.settings());
|
List<Item> items = preview(request.selections()).stream().map(item -> new Item(item.source(), item.zeroTime(), item.records(), item.logicalMapping(),
|
item.businessFields().stream().filter(field -> request.settings().batteryTemplateFields().contains(field.getFieldName())).toList())).toList();
|
Map<String, Object> snapshot = new LinkedHashMap<>();
|
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description());
|
snapshot.put("experimentName", request.experimentName()); snapshot.put("items", items); snapshot.put("settings", request.settings());
|
snapshot.put("units", Map.of("voltage", "V", "current", "A", "temperature", "℃", "positiveVoltage", "V", "negativeVoltage", "V", "capacity", "Ah", "energy", "Wh"));
|
snapshot.put("displayUnits", Map.of("voltage", "V", "current", "mA", "temperature", "℃", "positiveVoltage", "V", "negativeVoltage", "V", "capacity", "mAh", "energy", "mWh"));
|
snapshot.put("timeUnit", "epoch-milliseconds");
|
long id = reports.insert(request.name().trim(), JSON.toJSONString(snapshot, JSONWriter.Feature.WriteMapNullValue), operatorId, operatorName, System.currentTimeMillis());
|
return get(id);
|
}
|
|
/** 重开只读取保存版本,来源测试、字段或映射已删除也不重新解析。 */
|
public Map<String, Object> get(long id)
|
{ return get(id, null); }
|
|
/** 指定版本按其冻结内容还原,操作信息来自正式版本记录。 */
|
public Map<String, Object> get(long id, Integer versionNo)
|
{
|
var version = reports.get(id, versionNo);
|
Map<String, Object> result = JSON.parseObject(version.snapshotJson());
|
result.put("id", id); result.put("versionNo", version.versionNo());
|
result.put("creator", version.report().creator()); result.put("createdAt", version.report().createdAt());
|
result.put("operatorName", version.operatorName()); result.put("versionCreatedAt", version.versionCreatedAt());
|
result.put("changeSummary", version.changeSummary());
|
return result;
|
}
|
|
/** 以最新版本为基准追加展示版本,测量、来源身份和单位完全沿用快照。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> edit(long id, CompareReportEditRequest request, long operatorId, String operatorName)
|
{
|
var snapshot = currentSnapshot(id, request.baseVersionNo());
|
validateSettings(request.settings());
|
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description());
|
snapshot.put("experimentName", request.experimentName()); snapshot.put("settings", request.settings());
|
reports.append(id, request.baseVersionNo() + 1, request.name().trim(), JSON.toJSONString(snapshot, JSONWriter.Feature.WriteMapNullValue),
|
operatorId, operatorName, request.changeSummary().trim());
|
return get(id);
|
}
|
|
/** 基准必须为最新版本且来源仍有效;重读来源生成新快照,保留独立的人工表格值。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> refresh(long id, CompareReportEditRequest.Refresh request, long operatorId, String operatorName)
|
{
|
var snapshot = currentSnapshot(id, request.baseVersionNo());
|
var items = JSON.parseArray(JSON.toJSONString(snapshot.get("items")), Item.class);
|
var settings = JSON.parseObject(JSON.toJSONString(snapshot.get("settings")), CompareReportRequest.Settings.class);
|
var updated = preview(items.stream().map(item -> item.source().selection()).toList()).stream()
|
.map(item -> new Item(item.source(), item.zeroTime(), item.records(), item.logicalMapping(),
|
item.businessFields().stream().filter(field -> settings.batteryTemplateFields().contains(field.getFieldName())).toList())).toList();
|
snapshot.put("items", updated);
|
reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), JSON.toJSONString(snapshot, JSONWriter.Feature.WriteMapNullValue),
|
operatorId, operatorName, "按最新来源生成:" + request.changeSummary().trim());
|
return get(id);
|
}
|
|
private com.alibaba.fastjson2.JSONObject currentSnapshot(long id, int baseVersionNo)
|
{
|
// 同一报表内串行追加;陈旧页面需重新打开,不从历史版生成回滚版本。
|
reports.lock(id);
|
var current = reports.get(id);
|
if (current.versionNo() != baseVersionNo) throw new ServiceException("报表已有新版本,请重新打开后编辑");
|
return JSON.parseObject(current.snapshotJson());
|
}
|
|
/** 授权用户删除整份报表及全部版本,不影响来源测试;删除与最小日志同成同败。 */
|
@Transactional(rollbackFor = Exception.class)
|
public void delete(long id, long operatorId, String operatorName)
|
{
|
// 与版本追加共用身份锁,避免删除期间再写入孤立版本;重复删除不另记日志。
|
reports.lock(id);
|
reports.delete(id, operatorId, operatorName);
|
}
|
|
private void validateSettings(CompareReportRequest.Settings settings)
|
{
|
Set<String> metrics = Set.of("voltage", "current", "temperature", "positiveVoltage", "negativeVoltage");
|
if (settings == null || !metrics.containsAll(settings.metrics())) throw new ServiceException("报表指标无效");
|
if (settings.seriesColors().stream().anyMatch(color -> color == null || !color.matches("#[0-9a-fA-F]{6}")))
|
throw new ServiceException("曲线颜色无效");
|
List<String> fields = new ArrayList<>(settings.batteryTemplateFields()); fields.addAll(settings.specialFields());
|
if (fields.stream().anyMatch(field -> field == null || field.isBlank() || field.length() > 100)
|
|| new HashSet<>(fields).size() != fields.size()) throw new ServiceException("报表字段无效或重复");
|
}
|
|
private List<Candidate> candidates(TTest test, List<Map<String, Object>> records)
|
{
|
List<Candidate> result = new ArrayList<>();
|
for (TTestBattery battery : test.getBatteries())
|
{
|
var batteryRows = records.stream().filter(row -> battery.getBatteryNo().equals(row.get("batteryNo"))).toList();
|
for (int cycle : batteryRows.stream().map(row -> number(row, "cycleNo")).filter(value -> value > 0).distinct().sorted().toList())
|
{
|
var cycleRows = batteryRows.stream().filter(row -> number(row, "cycleNo") == cycle).toList();
|
var steps = cycleRows.stream().filter(row -> number(row, "stepNo") > 0)
|
.sorted(Comparator.comparingInt(row -> number(row, "stepNo"))).toList();
|
Set<Integer> seen = new HashSet<>();
|
for (var row : steps) if (seen.add(number(row, "stepNo")))
|
result.add(candidate(test, battery, cycle, number(row, "stepNo"), null, number(row, "stepNo") + "-" + Objects.toString(row.get("stepName"), "")));
|
for (var mapping : test.getLogicStepMappings())
|
if (steps.stream().anyMatch(row -> inMapping(number(row, "stepNo"), firstStep(records, cycle), mapping)))
|
result.add(candidate(test, battery, cycle, null, mapping.getLogicalNo(), "逻辑工步 " + mapping.getLogicalNo() + "-" + mapping.getLogicalName()));
|
}
|
}
|
return result;
|
}
|
|
private Candidate candidate(TTest test, TTestBattery battery, int cycle, Integer step, Integer logical, String process)
|
{
|
return new Candidate(new Selection(test.getTestId(), battery.getTestBatteryId(), cycle, step, logical),
|
test.getTestName(), test.getTestTypeName(), battery.getGroupId(), battery.getGroupName(), battery.getBatteryNo(), process);
|
}
|
|
private int number(Map<String, Object> row, String key) { return row.get(key) instanceof Number value ? value.intValue() : 0; }
|
private int firstStep(List<Map<String, Object>> records, int cycle)
|
{
|
return records.stream().filter(row -> number(row, "cycleNo") == cycle).mapToInt(row -> number(row, "stepNo")).filter(value -> value > 0).min().orElse(1);
|
}
|
private boolean inMapping(int step, int first, TTestLogicStepMapping mapping)
|
{
|
// 已完成测试不依赖临时设备方案,沿用控制台按循环首个已保存工步推算位置的口径,保留跳号。
|
int position = step - first + 1;
|
return position >= mapping.getStartStepNo() && position <= mapping.getEndStepNo();
|
}
|
}
|