yeg
2026-09-06 b2cf27e4e124608820062ceddea1803d2c2e4b6e
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
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)
    {
        var version = reports.get(id);
        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());
        return result;
    }
 
    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();
    }
}