yeg
2026-08-27 5a18d863778ffabcc45da185f7f77e7462289bb9
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
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
package com.ruoyi.system.service.impl;
 
import java.util.ArrayList;
import java.util.Date;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import com.alibaba.fastjson2.JSON;
import com.alibaba.fastjson2.JSONException;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import com.ruoyi.common.exception.ServiceException;
import com.ruoyi.common.utils.StringUtils;
import com.ruoyi.system.domain.TTest;
import com.ruoyi.system.domain.TTestBattery;
import com.ruoyi.system.domain.TTestBatteryField;
import com.ruoyi.system.domain.TTestChannelBinding;
import com.ruoyi.system.domain.TTestPlanStep;
import com.ruoyi.system.domain.TTestResult;
import com.ruoyi.system.domain.TTestWarningThreshold;
import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo;
import com.ruoyi.system.mapper.TTestMapper;
import com.ruoyi.system.service.ITTestService;
 
/** 测试配置服务,负责状态边界、关系校验、事务写入和只追加审计。 */
@Service
public class TTestServiceImpl implements ITTestService
{
    private static final String WAITING = "WAITING";
    private static final String NOT_MONITORED = "NOT_MONITORED";
    private static final Set<String> TEST_STATUSES = Set.of(
            WAITING, "RUNNING", "PAUSED", "RUNNING_ABNORMAL", "COMPLETION_PENDING", "COMPLETED", "TERMINATED");
    private static final Set<String> MONITORING_STATUSES = Set.of(
            NOT_MONITORED, "WAITING_DEVICE_START", "MONITORING", "BACKFILLING", "MONITORING_ENDED", "CANCELLED");
    private static final Set<String> THRESHOLD_UNITS = Set.of("A", "V", "Ah", "Wh", "℃");
 
    @Autowired
    private TTestMapper testMapper;
 
    @Override
    public TTest selectTestById(Long testId)
    {
        if (testId == null) throw new ServiceException("测试ID不能为空");
        TTest test = testMapper.selectTestById(testId);
        if (test == null) throw new ServiceException("测试不存在");
        loadConfiguration(test);
        return test;
    }
 
    @Override
    public List<TTest> selectTestList(TTest test)
    {
        if (test == null) test = new TTest();
        test.setTestName(trimToNull(test.getTestName()));
        test.setBatteryNo(trimToNull(test.getBatteryNo()));
        validateQueryStatus(test.getTestStatus(), TEST_STATUSES, "测试状态无效");
        validateQueryStatus(test.getMonitoringStatus(), MONITORING_STATUSES, "监测状态无效");
        return testMapper.selectTestList(test);
    }
 
    @Override
    public TestConfigurationOptionsVo selectConfigurationOptions()
    {
        TestConfigurationOptionsVo options = new TestConfigurationOptionsVo();
        options.setUsers(testMapper.selectEnabledUserOptions());
        options.setTestTypes(testMapper.selectEnabledTestTypeOptions());
        options.setBatteries(testMapper.selectEnabledBatteryOptions());
        options.setChannels(testMapper.selectRegisteredChannelOptions());
        options.setCollectors(testMapper.selectEnabledCollectorOptions());
        return options;
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public Long createTest(TTest test)
    {
        if (test == null) throw new ServiceException("测试信息不能为空");
        test.setCreatorId(test.getOperatorId());
        if (test.getMonitorTimeoutMinutes() == null) test.setMonitorTimeoutMinutes(30);
        validateBase(test, false);
        validateConfiguration(test);
        test.setTestStatus(WAITING);
        test.setMonitoringStatus(NOT_MONITORED);
        test.setDelFlag("0");
        test.setCreateTime(new Date());
        testMapper.insertTest(test);
        saveConfiguration(test);
        testMapper.insertTestAudit(test.getTestId(), "TEST_CREATED", test.getOperatorId(), test.getCreateBy(),
                null, JSON.toJSONString(test));
        return test.getTestId();
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int updateTest(TTest test)
    {
        if (test == null || test.getTestId() == null) throw new ServiceException("测试ID不能为空");
        TTest current = testMapper.selectTestByIdForUpdate(test.getTestId());
        if (current == null) throw new ServiceException("测试不存在");
        loadConfiguration(current);
        if (!NOT_MONITORED.equals(current.getMonitoringStatus()))
            throw new ServiceException("开始监测后不能修改测试配置");
 
        test.setCreatorId(current.getCreatorId());
        test.setTestStatus(current.getTestStatus());
        test.setMonitoringStatus(current.getMonitoringStatus());
        if (test.getMonitorTimeoutMinutes() == null)
            test.setMonitorTimeoutMinutes(current.getMonitorTimeoutMinutes());
        validateBase(test, true);
        validateConfiguration(test);
        test.setUpdateTime(new Date());
        int rows = testMapper.updateTest(test);
        if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试");
 
        // 监测开始前没有原始采集事实,配置关系可在同一事务内整体替换,审计记录仍只追加。
        testMapper.deleteTestBindingsByTestId(test.getTestId());
        testMapper.deleteTestPlanStepsByTestId(test.getTestId());
        testMapper.deleteTestFieldsByTestId(test.getTestId());
        testMapper.deleteTestBatteriesByTestId(test.getTestId());
        testMapper.deleteTestThresholdsByTestId(test.getTestId());
        saveConfiguration(test);
        testMapper.insertTestAudit(test.getTestId(), "TEST_CONFIGURATION_UPDATED", test.getOperatorId(),
                test.getUpdateBy(), JSON.toJSONString(current), JSON.toJSONString(test));
        return rows;
    }
 
    @Override
    public int startTest(Long testId, String updateBy)
    {
        throw new ServiceException("开始监测由后续监测动作处理");
    }
 
    @Override
    @Transactional(rollbackFor = Exception.class)
    public int deleteTestByIds(Long[] testIds, String updateBy)
    {
        if (testIds == null || testIds.length == 0) throw new ServiceException("请选择要删除的测试");
        int rows = 0;
        for (Long testId : testIds)
        {
            TTest current = testMapper.selectTestByIdForUpdate(testId);
            if (current == null) throw new ServiceException("测试不存在");
            if (!NOT_MONITORED.equals(current.getMonitoringStatus())) throw new ServiceException("开始监测后不能删除测试");
            rows += testMapper.deleteTestById(testId, updateBy);
        }
        return rows;
    }
 
    @Override
    public List<TTestResult> selectTestResults(TTestResult result)
    {
        if (result == null || result.getTestId() == null) throw new ServiceException("测试ID不能为空");
        selectTestById(result.getTestId());
        return testMapper.selectTestResults(result);
    }
 
    private void loadConfiguration(TTest test)
    {
        Long testId = test.getTestId();
        List<TTestPlanStep> planSteps = mutable(testMapper.selectTestPlanSteps(testId));
        for (TTestPlanStep step : planSteps)
        {
            try
            {
                step.setParameters(JSON.parseObject(step.getParametersJson()));
            }
            catch (JSONException exception)
            {
                throw new ServiceException("计划工步参数无法解析");
            }
        }
 
        List<TTestBattery> batteries = mutable(testMapper.selectTestBatteries(testId));
        List<TTestBatteryField> fields = mutable(testMapper.selectTestBatteryFields(testId));
        Map<Long, TTestChannelBinding> bindings = new HashMap<>();
        for (TTestChannelBinding binding : mutable(testMapper.selectTestChannelBindings(testId)))
            bindings.put(binding.getBatteryRelationId(), binding);
        for (TTestBattery battery : batteries)
        {
            battery.setBinding(bindings.get(battery.getRelationId()));
            battery.setFields(fields.stream()
                    .filter(field -> battery.getRelationId().equals(field.getRelationId()))
                    .toList());
        }
        test.setPlanSteps(planSteps);
        test.setBatteries(batteries);
        test.setThresholds(mutable(testMapper.selectTestThresholds(testId)));
    }
 
    private void validateBase(TTest test, boolean update)
    {
        test.setTestName(trimToNull(test.getTestName()));
        test.setTestPurpose(trimToNull(test.getTestPurpose()));
        test.setRemark(trimToNull(test.getRemark()));
        if (StringUtils.isEmpty(test.getTestName())) throw new ServiceException("测试名称不能为空");
        if (test.getTestName().length() > 100) throw new ServiceException("测试名称长度不能超过100个字符");
        if (StringUtils.isEmpty(test.getTestPurpose())) throw new ServiceException("测试目的不能为空");
        if (test.getRemark() != null && test.getRemark().length() > 500) throw new ServiceException("备注长度不能超过500个字符");
        if (test.getMonitorTimeoutMinutes() == null || test.getMonitorTimeoutMinutes() < 1
                || test.getMonitorTimeoutMinutes() > 1440)
            throw new ServiceException("监测超时分钟数必须在1到1440之间");
        if (test.getCreatorId() == null || testMapper.countUserEnabled(test.getCreatorId()) == 0)
            throw new ServiceException("创建人不存在或已停用");
        if (test.getPrincipalId() == null || testMapper.countUserEnabled(test.getPrincipalId()) == 0)
            throw new ServiceException("测试负责人不存在或已停用");
        if (test.getTestTypeId() == null || testMapper.countTestTypeEnabled(test.getTestTypeId()) == 0)
            throw new ServiceException("测试类型不存在或未启用");
        TTest same = testMapper.selectTestByName(test.getTestName());
        if (same != null && (!update || !test.getTestId().equals(same.getTestId())))
            throw new ServiceException("测试名称已存在");
    }
 
    private void validateConfiguration(TTest test)
    {
        validateBatteries(test.getBatteries());
        validatePlanSteps(test.getPlanSteps());
        validateThresholds(test.getThresholds());
    }
 
    private void validateBatteries(List<TTestBattery> batteries)
    {
        if (batteries == null || batteries.isEmpty()) throw new ServiceException("测试至少绑定一个电池号");
        Set<String> batteryNos = new HashSet<>();
        Set<Long> channelIds = new HashSet<>();
        Long groupId = null;
        for (TTestBattery battery : batteries)
        {
            if (battery == null || battery.getGroupId() == null) throw new ServiceException("电池组不能为空");
            if (groupId == null) groupId = battery.getGroupId();
            else if (!groupId.equals(battery.getGroupId())) throw new ServiceException("同一测试只能选择一个电池组");
            battery.setBatteryNo(trimToNull(battery.getBatteryNo()));
            if (battery.getBatteryNo() == null) throw new ServiceException("电池号不能为空");
            if (!batteryNos.add(battery.getBatteryNo())) throw new ServiceException("测试内电池号不可重复");
            if (testMapper.countBatteryInEnabledGroup(battery.getGroupId(), battery.getBatteryNo()) == 0)
                throw new ServiceException("电池号不属于所选启用电池组");
 
            TTestChannelBinding binding = battery.getBinding();
            if (binding == null || binding.getChannelId() == null) throw new ServiceException("主要检测柜通道不能为空");
            if (!channelIds.add(binding.getChannelId())) throw new ServiceException("同一检测柜通道不可重复绑定");
            if (testMapper.countRegisteredChannel(binding.getChannelId()) == 0)
                throw new ServiceException("检测柜通道不存在、未登记或未启用");
            binding.setExpectedBarcode(trimToNull(binding.getExpectedBarcode()));
            if (binding.getExpectedBarcode() != null && binding.getExpectedBarcode().length() > 100)
                throw new ServiceException("预期条码长度不能超过100个字符");
            if (battery.getCollectorId() != null && testMapper.countCollectorEnabled(battery.getCollectorId()) == 0)
                throw new ServiceException("采集器不存在或未启用");
            validateBatteryFields(battery.getFields());
        }
    }
 
    private void validateBatteryFields(List<TTestBatteryField> fields)
    {
        if (fields == null) return;
        Set<String> fieldNames = new HashSet<>();
        for (TTestBatteryField field : fields)
        {
            if (field == null) throw new ServiceException("扩展属性不能为空");
            field.setFieldName(trimToNull(field.getFieldName()));
            if (field.getFieldName() == null || !fieldNames.add(field.getFieldName()))
                throw new ServiceException("扩展属性名称不能为空且不可重复");
        }
    }
 
    private void validatePlanSteps(List<TTestPlanStep> planSteps)
    {
        if (planSteps == null || planSteps.isEmpty()) throw new ServiceException("计划工步不能为空");
        Set<String> identities = new HashSet<>();
        Set<Integer> orders = new HashSet<>();
        for (TTestPlanStep step : planSteps)
        {
            if (step == null || step.getCycleNo() == null || step.getCycleNo() < 1
                    || step.getStepNo() == null || step.getStepNo() < 1
                    || step.getSortOrder() == null || step.getSortOrder() < 1)
                throw new ServiceException("计划工步序号必须为正整数");
            step.setStepType(trimToNull(step.getStepType()));
            step.setStepName(trimToNull(step.getStepName()));
            if (step.getStepType() == null || step.getStepName() == null || step.getParameters() == null)
                throw new ServiceException("计划工步配置不完整");
            if (!identities.add(step.getCycleNo() + ":" + step.getStepNo()) || !orders.add(step.getSortOrder()))
                throw new ServiceException("计划工步序号不可重复");
            step.setParametersJson(JSON.toJSONString(step.getParameters()));
        }
    }
 
    private void validateThresholds(List<TTestWarningThreshold> thresholds)
    {
        if (thresholds == null || thresholds.isEmpty()) throw new ServiceException("阈值配置不能为空");
        Set<String> metricCodes = new HashSet<>();
        for (TTestWarningThreshold threshold : thresholds)
        {
            if (threshold == null) throw new ServiceException("阈值配置不完整");
            threshold.setMetricCode(trimToNull(threshold.getMetricCode()));
            threshold.setUnit(trimToNull(threshold.getUnit()));
            threshold.setSeverity(trimToNull(threshold.getSeverity()));
            if (threshold.getMetricCode() == null || !metricCodes.add(threshold.getMetricCode()))
                throw new ServiceException("阈值指标编码不能为空且不可重复");
            if (threshold.getMinValue() == null && threshold.getMaxValue() == null)
                throw new ServiceException("阈值最小值和最大值至少填写一项");
            if (threshold.getMinValue() != null && threshold.getMaxValue() != null
                    && threshold.getMinValue().compareTo(threshold.getMaxValue()) > 0)
                throw new ServiceException("阈值最小值不能大于最大值");
            if (!THRESHOLD_UNITS.contains(threshold.getUnit())) throw new ServiceException("阈值单位无效");
            if (threshold.getSeverity() == null) threshold.setSeverity("WARNING");
        }
    }
 
    private void saveConfiguration(TTest test)
    {
        for (TTestPlanStep step : test.getPlanSteps())
        {
            step.setTestId(test.getTestId());
            testMapper.insertTestPlanStep(step);
        }
        for (TTestBattery battery : test.getBatteries())
        {
            battery.setTestId(test.getTestId());
            testMapper.insertTestBattery(battery);
            if (battery.getFields() != null)
            {
                for (TTestBatteryField field : battery.getFields())
                {
                    field.setRelationId(battery.getRelationId());
                    testMapper.insertTestBatteryField(field);
                }
            }
            TTestChannelBinding binding = battery.getBinding();
            binding.setTestId(test.getTestId());
            binding.setBatteryRelationId(battery.getRelationId());
            binding.setClaimStatus("PENDING");
            testMapper.insertTestChannelBinding(binding);
        }
        for (TTestWarningThreshold threshold : test.getThresholds())
        {
            threshold.setTestId(test.getTestId());
            testMapper.insertTestThreshold(threshold);
        }
    }
 
    private void validateQueryStatus(String status, Set<String> allowed, String message)
    {
        if (StringUtils.isNotEmpty(status) && !allowed.contains(status)) throw new ServiceException(message);
    }
 
    private String trimToNull(String value)
    {
        String trimmed = StringUtils.trim(value);
        return StringUtils.isEmpty(trimmed) ? null : trimmed;
    }
 
    private <T> List<T> mutable(List<T> values)
    {
        return values == null ? new ArrayList<>() : new ArrayList<>(values);
    }
}