yeg
2026-09-08 c19dbe36f39315c5bc4d2b17f2969517b442d0ad
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
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<SpliceReportRequest.Selection, TTestBattery> selected = new LinkedHashMap<>();
  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("电池不属于所选测试"));
   selected.put(selection, battery);
  }
  List<Map<String, Object>> segments = new ArrayList<>();
  Set<Integer> mappedCycles = new TreeSet<>();
  Integer previousEnd = null;
  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);
   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(testId), battery));
    segment.put("cycles", rows); segment.put("steps", stepRows);
    segment.put("businessFields", battery.getFields());
    var detail = loaded.get(testId).getConsoleDetail();
    if (detail != null) {
     // 只冻结所选测试电池的填写值,字段名称不推断共享字段身份或单位。
     Map<String,Object> frozen = new LinkedHashMap<>();
     frozen.put("experimentName", detail.getExperimentName());
     frozen.put("batteryFields", detail.getBatteryFields()); frozen.put("specialFields", detail.getSpecialFields());
     Map<String,String> values = new LinkedHashMap<>();
     detail.getFieldValues().forEach((name, byBattery) -> {
      if (byBattery.containsKey(battery.getBatteryNo())) values.put(name, byBattery.get(battery.getBatteryNo()));
     });
     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;
   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) {
  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("spliceMode", "battery-segments");
  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());
  snapshot.put("spliceMode", "battery-segments");
  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; }
}