yeg
2026-09-06 37600a85b79387ab6afa7bf342eedbf938331e1d
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
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());
    }
 
    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();
    }
}