package com.battery.business.report;
|
|
import java.math.BigDecimal;
|
import java.math.MathContext;
|
import java.util.ArrayList;
|
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 java.util.stream.Stream;
|
import com.alibaba.fastjson2.JSON;
|
import com.alibaba.fastjson2.JSONObject;
|
import com.alibaba.fastjson2.JSONWriter;
|
import com.battery.business.domain.CurrentBatteryField;
|
import com.battery.business.domain.TBatteryGroupField;
|
import com.battery.business.domain.TTest;
|
import com.battery.business.domain.TTestBattery;
|
import com.battery.business.report.FormationReportRequest.Selection;
|
import com.battery.business.report.FormationReportRequest.Settings;
|
import com.battery.business.service.ITBatteryGroupFieldService;
|
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 FormationReportService
|
{
|
private final ITTestService tests;
|
private final ITestDataQueryService data;
|
private final FormationReportRepository reports;
|
private final ITBatteryGroupFieldService definitions;
|
|
public FormationReportService(ITTestService tests, ITestDataQueryService data,
|
FormationReportRepository reports, ITBatteryGroupFieldService definitions)
|
{ this.tests = tests; this.data = data; this.reports = reports; this.definitions = definitions; }
|
|
public record Candidate(Selection selection, String name, String type, long groupId, String batteryGroup, String batteryNo) { }
|
public record Item(Candidate source, List<Integer> cycleNos, List<Map<String, Object>> cycles,
|
Map<String, Map<String, BigDecimal>> metrics, List<CurrentBatteryField> businessFields) { }
|
public record Matrix(List<Item> items, List<Integer> cycleNos) { }
|
public record Summary(long id, String name, String creator, long createdAt, String testName, String batteryGroup) { }
|
|
/** 列表来源名称取自已保存版本,测试删除后仍保留筛选与识别信息。 */
|
public List<Summary> list()
|
{
|
return reports.list().stream().map(summary -> {
|
var sources = items(JSON.parseObject(reports.get(summary.id()).snapshotJson()));
|
return new Summary(summary.id(), summary.name(), summary.creator(), summary.createdAt(),
|
String.join("、", sources.stream().map(item -> item.source().name()).distinct().toList()),
|
String.join("、", sources.stream().map(item -> item.source().batteryGroup()).distinct().toList()));
|
}).toList();
|
}
|
|
/** 只提供类型名称精确为“化成”的检测完成测试及其真实参与电池。 */
|
public List<Candidate> sources()
|
{
|
TTest query = new TTest(); query.setTestStatus("COMPLETED");
|
List<Candidate> result = new ArrayList<>();
|
for (TTest summary : tests.selectTestList(query))
|
{
|
if (!"化成".equals(summary.getTestTypeName())) continue;
|
TTest test = tests.selectTestById(summary.getTestId());
|
if (eligible(test)) for (TTestBattery battery : test.getBatteries()) result.add(candidate(test, battery));
|
}
|
return result;
|
}
|
|
/** 校验电池属于所选测试,返回三类持久化记录实际出现的循环号与循环统计。 */
|
public Matrix preview(List<Selection> selections)
|
{
|
if (selections == null || selections.isEmpty() || selections.contains(null)
|
|| new HashSet<>(selections).size() != selections.size()) throw new ServiceException("请选择不重复的化成测试电池");
|
Map<Long, TTest> loaded = new HashMap<>();
|
Map<Long, List<Map<String, Object>>> cycles = new HashMap<>(), records = new HashMap<>(), steps = new HashMap<>();
|
List<Item> items = new ArrayList<>();
|
for (Selection selection : selections)
|
{
|
if (selection.testId() == null || selection.testBatteryId() == null) throw new ServiceException("测试和电池身份不能为空");
|
TTest test = loaded.computeIfAbsent(selection.testId(), tests::selectTestById);
|
if (!eligible(test)) throw new ServiceException("仅可选择类型为化成的检测完成测试");
|
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),
|
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()));
|
}
|
return new Matrix(items, items.stream().flatMap(item -> item.cycleNos().stream()).distinct().sorted().toList());
|
}
|
|
/** 从当前来源冻结所选字段;首版和报表身份在同一事务提交。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> create(FormationReportRequest request, long userId, String userName)
|
{
|
Settings settings = new Settings(validateFields(request.settings().fields(), true), request.settings().values());
|
Matrix matrix = freeze(preview(request.selections()), settings.fields());
|
validateValues(settings, matrix.items());
|
var snapshot = new LinkedHashMap<String, Object>();
|
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description());
|
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");
|
return get(reports.insert(request.name().trim(), json(snapshot), userId, userName, System.currentTimeMillis()), null);
|
}
|
|
/** 当前或历史版本只读取独立快照,来源删除后仍能重开。 */
|
public Map<String, Object> get(long id, Integer versionNo)
|
{
|
var version = reports.get(id, versionNo);
|
var 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, FormationReportRequest.Edit request, long userId, String userName)
|
{
|
var snapshot = current(id, request.baseVersionNo());
|
validateFields(request.settings().fields(), false);
|
validateValues(request.settings(), items(snapshot));
|
snapshot.put("name", request.name().trim()); snapshot.put("description", request.description()); snapshot.put("settings", request.settings());
|
reports.append(id, request.baseVersionNo() + 1, request.name().trim(), json(snapshot), userId, userName, request.changeSummary().trim());
|
return get(id, null);
|
}
|
|
/** 主动重读已保存范围,保留仍属于当前循环的人工值;不存在的来源使整次提交失败。 */
|
@Transactional(rollbackFor = Exception.class)
|
public Map<String, Object> refresh(long id, CompareReportEditRequest.Refresh request, long userId, String userName)
|
{
|
var snapshot = current(id, request.baseVersionNo());
|
var settings = JSON.parseObject(json(snapshot.get("settings")), Settings.class);
|
Matrix matrix = freeze(preview(items(snapshot).stream().map(item -> item.source().selection()).toList()), settings.fields());
|
Set<String> allowed = valueKeys(settings.fields(), matrix.items());
|
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));
|
reports.append(id, request.baseVersionNo() + 1, snapshot.getString("name"), json(snapshot), userId, userName, "按最新来源生成:" + request.changeSummary().trim());
|
return get(id, null);
|
}
|
|
/** 删除全部化成版本,与最小删除日志在同一事务提交,不删除测试事实。 */
|
@Transactional(rollbackFor = Exception.class)
|
public void delete(long id, long userId, String userName) { reports.lock(id); reports.delete(id, userId, userName); }
|
|
/** 校验有序结构;首次保存及模板解析共享身份,普通编辑沿用冻结身份。 */
|
public List<FormationField> validateFields(List<FormationField> fields, boolean resolveDefinitions)
|
{
|
if (fields == null || fields.isEmpty() || fields.size() > 100 || fields.contains(null)) throw new ServiceException("化成字段不能为空");
|
var names = new HashSet<String>(); var ids = new HashSet<Long>();
|
var current = resolveDefinitions ? definitions.selectBatteryGroupFieldList(new TBatteryGroupField()) : List.<TBatteryGroupField>of();
|
return fields.stream().map(field -> {
|
if (!names.add(field.scope() + "|" + field.key())) throw new ServiceException("化成字段不能重复");
|
if (!"battery".equals(field.kind())) return field;
|
Long id = field.fieldDefinitionId();
|
if (resolveDefinitions)
|
{
|
var definition = current.stream().filter(value -> field.fieldDefinitionId() == null
|
? value.getFieldName().equals(field.key()) : value.getFieldId().equals(field.fieldDefinitionId())).findFirst();
|
if (definition.isEmpty() && !Set.of("Z", "H", "F", "A", "B").contains(field.key()))
|
throw new ServiceException("共享字段不存在,请重新选择:" + field.key());
|
if (definition.isPresent()) id = definition.get().getFieldId();
|
}
|
if (id != null && !ids.add(id)) throw new ServiceException("共享字段不能重复");
|
return new FormationField(field.key(), field.scope(), field.kind(), field.displayName(), field.unit(), id);
|
}).toList();
|
}
|
|
private JSONObject current(long id, int baseVersionNo)
|
{
|
// 与删除共用报表身份锁;旧页面只能重开,不能从陈旧基准覆盖新版本。
|
reports.lock(id);
|
var version = reports.get(id);
|
if (version.versionNo() != baseVersionNo) throw new ServiceException("报表已有新版本,请重新打开后编辑");
|
return JSON.parseObject(version.snapshotJson());
|
}
|
|
private Matrix freeze(Matrix matrix, List<FormationField> fields)
|
{
|
Set<String> selected = new HashSet<>(fields.stream().filter(field -> "metric".equals(field.kind())).map(FormationField::key).toList());
|
// 能量效率依赖两项原始能量,即使未展示它们也须冻结,保证快照可复算。
|
if (selected.contains("energyEfficiency")) selected.addAll(List.of("chargeEnergy", "dischargeEnergy"));
|
var frozen = matrix.items().stream().map(item -> {
|
var rows = item.cycles().stream().map(row -> {
|
Map<String, Object> copy = new LinkedHashMap<>(row);
|
FormationField.METRIC_UNITS.keySet().stream().filter(key -> !selected.contains(key)).forEach(copy::remove);
|
return copy;
|
}).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());
|
}).toList();
|
return new Matrix(frozen, matrix.cycleNos());
|
}
|
|
private void validateValues(Settings settings, List<Item> items)
|
{
|
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 Set<String> valueKeys(List<FormationField> fields, List<Item> items)
|
{
|
Set<String> keys = new HashSet<>();
|
for (Item item : items) for (FormationField field : fields)
|
{
|
String prefix = item.source().selection().testId() + "|" + item.source().selection().testBatteryId() + "|";
|
for (int cycle : "detail".equals(field.scope()) ? List.of(0) : item.cycleNos()) keys.add(prefix + cycle + "|" + field.key());
|
}
|
return keys;
|
}
|
|
private Map<String, Map<String, BigDecimal>> metrics(List<Map<String, Object>> rows, List<Integer> cycleNos)
|
{
|
Map<String, Map<String, BigDecimal>> result = new LinkedHashMap<>();
|
for (int no : cycleNos)
|
{
|
var matches = rows.stream().filter(row -> cycleNo(row) == no).toList();
|
Map<String, BigDecimal> values = new LinkedHashMap<>();
|
// 多条统计记录没有已确认的合并口径,保持缺失,不挑任意一条或借用其他循环。
|
if (matches.size() == 1) for (String key : FormationField.METRIC_UNITS.keySet())
|
if (matches.get(0).get(key) instanceof Number number) values.put(key, new BigDecimal(number.toString()));
|
BigDecimal charge = values.get("chargeEnergy"), discharge = values.get("dischargeEnergy");
|
// 分母必须为正;允许结果超过 100%,不截断计算结果。
|
if (charge != null && discharge != null && charge.signum() > 0)
|
values.put("energyEfficiency", discharge.multiply(BigDecimal.valueOf(100)).divide(charge, MathContext.DECIMAL128));
|
result.put(Integer.toString(no), values);
|
}
|
return result;
|
}
|
|
private boolean eligible(TTest test) { return "COMPLETED".equals(test.getTestStatus()) && "化成".equals(test.getTestTypeName()); }
|
private Candidate candidate(TTest test, TTestBattery battery)
|
{ return new Candidate(new Selection(test.getTestId(), battery.getTestBatteryId()), test.getTestName(), test.getTestTypeName(), battery.getGroupId(), battery.getGroupName(), battery.getBatteryNo()); }
|
private List<Map<String, Object>> batteryRows(List<Map<String, Object>> rows, TTestBattery battery)
|
{ 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); }
|
}
|