yeg
2026-09-07 a971e41526802625338a42885da3501af75bb79b
battery-business/src/main/java/com/battery/business/report/SpliceReportService.java
@@ -1,29 +1,247 @@
package com.battery.business.report;
import java.math.BigDecimal; import java.math.MathContext; import java.util.*;
import com.alibaba.fastjson2.*; import com.battery.business.domain.*; import com.battery.business.service.*;
import com.ruoyi.common.exception.ServiceException; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional;
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;
 public SpliceReportService(ITTestService t, ITestDataQueryService d, SpliceReportRepository r){tests=t;data=d;reports=r;}
/** 拼接报表按测试顺序接续循环,工步与循环指标分别读取对应的当前有效记录。 */
@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 q=new TTest(); q.setTestStatus("COMPLETED"); List<Candidate> out=new ArrayList<>(); for(TTest s:tests.selectTestList(q)){TTest t=tests.selectTestById(s.getTestId()); for(TTestBattery b:t.getBatteries()) out.add(candidate(t,b));} return out; }
 public Matrix preview(List<SpliceReportRequest.Selection> selections){
  if(selections==null||selections.size()<2||new HashSet<>(selections).size()!=selections.size()) throw new ServiceException("请选择至少两个不重复的检测完成测试电池");
  List<Map<String,Object>> segments=new ArrayList<>(); Set<Long> groups=new HashSet<>(); Set<String> batteries=new HashSet<>(); int offset=0; long group=0; String commonBattery=null;
  for(var sel:selections){TTest t=tests.selectTestById(sel.testId()); if(t==null||!"COMPLETED".equals(t.getTestStatus())) throw new ServiceException("仅可选择检测完成测试"); TTestBattery b=t.getBatteries().stream().filter(x->sel.testBatteryId().equals(x.getTestBatteryId())).findFirst().orElseThrow(()->new ServiceException("电池不属于所选测试")); if(group==0)group=b.getGroupId(); if(group!=b.getGroupId()) throw new ServiceException("拼接测试必须属于同一电池组"); if(commonBattery==null)commonBattery=b.getBatteryNo(); if(!commonBattery.equals(b.getBatteryNo())) throw new ServiceException("拼接测试必须选择共同参与的电池号"); groups.add(b.getGroupId()); batteries.add(b.getBatteryNo());
   List<Map<String,Object>> rows=data.cycles(sel.testId()).stream().filter(r->Objects.equals(r.get("batteryNo"),b.getBatteryNo())).toList(); List<Integer> nos=rows.stream().map(this::cycle).filter(x->x>0).distinct().sorted().toList(); List<Map<String,Object>> stepRows=data.steps(sel.testId()).stream().filter(r->Objects.equals(r.get("batteryNo"),b.getBatteryNo())).toList(); Map<String,Object> seg=new LinkedHashMap<>(); seg.put("source",candidate(t,b)); seg.put("offset",offset); seg.put("cycles",rows); seg.put("originalCycleNos",nos); seg.put("statistics",statistics(rows,stepRows,nos)); segments.add(seg); offset += nos.isEmpty()?0:nos.get(nos.size()-1); }
  if(batteries.size()==0) throw new ServiceException("未找到共同参与电池"); return new Matrix(segments,java.util.stream.IntStream.range(1,offset+1).boxed().toList()); }
 private Map<String,Object> statistics(List<Map<String,Object>> rows,List<Map<String,Object>> stepRows,List<Integer> nos){Map<String,Object> out=new LinkedHashMap<>(); for(int no:nos){var m=rows.stream().filter(r->cycle(r)==no).toList(); Map<String,Object> v=new LinkedHashMap<>(); if(m.size()==1){Map<String,Object> row=m.get(0); for(String k:List.of("chargeEnergy","dischargeEnergy","chargeCapacity","dischargeCapacity")) if(row.get(k)!=null)v.put(k,row.get(k)); List<Map<String,Object>> steps=stepRows.stream().filter(r->cycle(r)==no).toList(); Object end=steps.stream().map(r->r.get("endVoltage")).filter(Objects::nonNull).reduce((a,b)->b).orElse(null); if(end!=null)v.put("finalVoltage1",end); Number c=num(v.get("chargeEnergy")),d=num(v.get("dischargeEnergy")); if(c!=null&&d!=null&&c.doubleValue()>0)v.put("energyEfficiency",BigDecimal.valueOf(d.doubleValue()).multiply(BigDecimal.valueOf(100)).divide(BigDecimal.valueOf(c.doubleValue()),MathContext.DECIMAL128));} out.put(Integer.toString(no),v);} return out;}
 private Number num(Object x){return x instanceof Number?n((Number)x):null;} private Number n(Number x){return x;}
 @Transactional public Map<String,Object> create(SpliceReportRequest r,long uid,String user){Matrix m=preview(r.selections()); Map<String,Object> snap=new LinkedHashMap<>(); snap.put("name",r.name().trim());snap.put("description",r.description());snap.put("selections",r.selections());snap.put("segments",m.segments());snap.put("cycleNos",m.cycleNos());snap.put("settings",r.settings()); return get(reports.insert(r.name().trim(),JSON.toJSONString(snap),uid,user,System.currentTimeMillis()),null);}
 public Map<String,Object> get(long id,Integer version){var v=reports.get(id,version);Map<String,Object> x=JSON.parseObject(v.snapshotJson());x.put("id",id);x.put("versionNo",v.versionNo());x.put("creator",v.report().creator());x.put("createdAt",v.report().createdAt());x.put("operatorName",v.operatorName());x.put("versionCreatedAt",v.versionCreatedAt());x.put("changeSummary",v.changeSummary());return x;}
 @Transactional public Map<String,Object> edit(long id,SpliceReportRequest.Edit r,long uid,String user){reports.lock(id);var v=reports.get(id);if(v.versionNo()!=r.baseVersionNo())throw new ServiceException("报表已有新版本,请重新打开后编辑");var x=JSON.parseObject(v.snapshotJson());x.put("name",r.name().trim());x.put("description",r.description());x.put("settings",r.settings());reports.append(id,r.baseVersionNo()+1,r.name().trim(),x.toJSONString(),uid,user,r.changeSummary().trim());return get(id,null);}
 @Transactional public Map<String,Object> refresh(long id,int base,String summary,long uid,String user){reports.lock(id);var v=reports.get(id);if(v.versionNo()!=base)throw new ServiceException("报表已有新版本,请重新打开后编辑");var x=JSON.parseObject(v.snapshotJson());var selections=JSON.parseArray(x.getString("selections"),SpliceReportRequest.Selection.class);Matrix m=preview(selections);x.put("segments",m.segments());x.put("cycleNos",m.cycleNos());reports.append(id,base+1,x.getString("name"),x.toJSONString(),uid,user,"按最新来源生成:"+summary);return get(id,null);}
 /** 仅返回检测完成测试的真实参与电池,默认按开始时间及测试身份排序。 */
 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 Candidate candidate(TTest t,TTestBattery b){return new Candidate(new SpliceReportRequest.Selection(t.getTestId(),b.getTestBatteryId()),t.getTestName(),t.getTestTypeName(),b.getGroupId(),b.getGroupName(),b.getBatteryNo(),t.getStartedAtUtc()==null?0:t.getStartedAtUtc().getTime());} private int cycle(Map<String,Object> r){return r.get("cycleNo") instanceof Number n?n.intValue():0;}
 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; }
}