From 5a18d863778ffabcc45da185f7f77e7462289bb9 Mon Sep 17 00:00:00 2001
From: yeg <1347747024@qq.com>
Date: 星期四, 27 八月 2026 20:45:31 +0800
Subject: [PATCH] feat(test): implement test configuration and channel binding
---
ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java | 321 +++++++++++++++++++++++++++++++++++++---------------
1 files changed, 227 insertions(+), 94 deletions(-)
diff --git a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
index 33cfd05..de2f8b9 100644
--- a/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
+++ b/ruoyi-system/src/main/java/com/ruoyi/system/service/impl/TTestServiceImpl.java
@@ -1,10 +1,14 @@
package com.ruoyi.system.service.impl;
-import java.math.BigDecimal;
+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;
@@ -13,22 +17,25 @@
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 = "0";
- private static final String TESTING = "1";
- private static final String ABNORMAL = "2";
- private static final String FINISHED = "3";
+ 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;
@@ -39,14 +46,7 @@
if (testId == null) throw new ServiceException("测试ID不能为空");
TTest test = testMapper.selectTestById(testId);
if (test == null) throw new ServiceException("测试不存在");
- List<TTestBattery> batteries = testMapper.selectTestBatteries(testId);
- List<TTestBatteryField> fields = testMapper.selectTestBatteryFields(testId);
- for (TTestBattery battery : batteries)
- {
- battery.setFields(fields.stream().filter(field -> battery.getRelationId().equals(field.getRelationId())).toList());
- }
- test.setBatteries(batteries);
- test.setThresholds(testMapper.selectTestThresholds(testId));
+ loadConfiguration(test);
return test;
}
@@ -56,49 +56,81 @@
if (test == null) test = new TTest();
test.setTestName(trimToNull(test.getTestName()));
test.setBatteryNo(trimToNull(test.getBatteryNo()));
- validateStatus(test.getStatus());
+ validateQueryStatus(test.getTestStatus(), TEST_STATUSES, "测试状态无效");
+ validateQueryStatus(test.getMonitoringStatus(), MONITORING_STATUSES, "监测状态无效");
return testMapper.selectTestList(test);
}
@Override
- @Transactional(rollbackFor = Exception.class)
- public int insertTest(TTest test)
+ 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);
- validateRelations(test);
- test.setStatus(WAITING);
+ validateConfiguration(test);
+ test.setTestStatus(WAITING);
+ test.setMonitoringStatus(NOT_MONITORED);
test.setDelFlag("0");
test.setCreateTime(new Date());
testMapper.insertTest(test);
- saveRelations(test);
- return 1;
+ 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);
- TTest current = selectTestById(test.getTestId());
- if (!WAITING.equals(current.getStatus())) throw new ServiceException("仅待检测状态允许编辑草稿");
- validateRelations(test);
+ validateConfiguration(test);
test.setUpdateTime(new Date());
int rows = testMapper.updateTest(test);
- testMapper.deleteTestFieldsByTestId(test.getTestId(), test.getUpdateBy());
- testMapper.deleteTestBatteriesByTestId(test.getTestId(), test.getUpdateBy());
- testMapper.deleteTestThresholdsByTestId(test.getTestId(), test.getUpdateBy());
- saveRelations(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
- @Transactional(rollbackFor = Exception.class)
public int startTest(Long testId, String updateBy)
{
- TTest current = selectTestById(testId);
- if (!WAITING.equals(current.getStatus())) throw new ServiceException("只有待检测状态可以启动");
- validateRelations(current);
- return testMapper.updateTestStatus(testId, TESTING, updateBy);
+ throw new ServiceException("开始监测由后续监测动作处理");
}
@Override
@@ -106,14 +138,14 @@
public int deleteTestByIds(Long[] testIds, String updateBy)
{
if (testIds == null || testIds.length == 0) throw new ServiceException("请选择要删除的测试");
+ int rows = 0;
for (Long testId : testIds)
{
- TTest test = selectTestById(testId);
- if (TESTING.equals(test.getStatus()) || ABNORMAL.equals(test.getStatus()))
- throw new ServiceException("检测中或异常测试不能删除");
+ 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);
}
- int rows = 0;
- for (Long testId : testIds) rows += testMapper.deleteTestById(testId, updateBy);
return rows;
}
@@ -125,9 +157,41 @@
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)
{
- if (test == null) throw new ServiceException("测试信息不能为空");
test.setTestName(trimToNull(test.getTestName()));
test.setTestPurpose(trimToNull(test.getTestPurpose()));
test.setRemark(trimToNull(test.getRemark()));
@@ -135,69 +199,138 @@
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.getPrincipalId() == null || testMapper.countUserEnabled(test.getPrincipalId()) == 0) throw new ServiceException("负责人不存在或已停用");
- if (test.getTestTypeId() == null || testMapper.countTestTypeEnabled(test.getTestTypeId()) == 0) throw new ServiceException("测试类型不存在或未启用");
+ 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("测试名称已存在");
+ if (same != null && (!update || !test.getTestId().equals(same.getTestId())))
+ throw new ServiceException("测试名称已存在");
}
- private void validateRelations(TTest test)
+ private void validateConfiguration(TTest test)
{
- if (test.getBatteries() == null || test.getBatteries().isEmpty()) throw new ServiceException("至少配置一个测试电池");
- if (test.getThresholds() == null || test.getThresholds().isEmpty()) throw new ServiceException("至少配置一个预警阈值");
+ 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<String> resources = new HashSet<>();
- for (TTestBattery battery : test.getBatteries())
+ Set<Long> channelIds = new HashSet<>();
+ Long groupId = null;
+ for (TTestBattery battery : batteries)
{
- if (battery == null || battery.getGroupId() == null || StringUtils.isEmpty(trimToNull(battery.getBatteryNo()))
- || battery.getCabinetId() == null || battery.getCollectorId() == null) throw new ServiceException("测试电池关系资料不完整");
+ 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()));
- validateChannel(battery.getTestChannelNo(), "测试通道");
- validateChannel(battery.getTemperatureChannelNo(), "温度通道");
- validateChannel(battery.getPositiveChannelNo(), "正电位通道");
- validateChannel(battery.getNegativeChannelNo(), "负电位通道");
- if (battery.getTemperatureChannelNo().equals(battery.getPositiveChannelNo())
- || battery.getTemperatureChannelNo().equals(battery.getNegativeChannelNo())
- || battery.getPositiveChannelNo().equals(battery.getNegativeChannelNo())) throw new ServiceException("温度及正负电位通道不能重复");
- if (!batteryNos.add(battery.getBatteryNo())) throw new ServiceException("测试电池号不能重复");
- String resource = battery.getCabinetId() + ":" + battery.getTestChannelNo();
- if (!resources.add(resource)) throw new ServiceException("检测柜测试通道不能重复");
+ 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("电池号不属于指定的启用电池组");
- if (testMapper.countCabinetEnabled(battery.getCabinetId()) == 0)
- throw new ServiceException("检测柜不存在或未启用");
- if (testMapper.countCollectorEnabled(battery.getCollectorId(), battery.getCabinetId()) == 0)
- throw new ServiceException("采集器不存在、未启用或不属于指定检测柜");
- if (testMapper.countTestingByBatteryNo(battery.getBatteryNo(), test.getTestId()) > 0
- || testMapper.countTestingByCabinetChannel(battery.getCabinetId(), battery.getTestChannelNo(), test.getTestId()) > 0)
- throw new ServiceException("电池号或检测柜测试通道已被检测中的任务占用");
- if (battery.getFields() == null || battery.getFields().isEmpty()) throw new ServiceException("每个测试电池必须配置扩展属性");
- Set<String> fieldNames = new HashSet<>();
- for (TTestBatteryField field : battery.getFields())
- if (field == null || StringUtils.isEmpty(trimToNull(field.getFieldName())) || !fieldNames.add(field.getFieldName().trim())) throw new ServiceException("扩展属性名称不能为空且不能重复");
- }
- Set<String> thresholdTypes = new HashSet<>();
- for (TTestWarningThreshold threshold : test.getThresholds())
- {
- if (threshold == null || StringUtils.isEmpty(trimToNull(threshold.getThresholdType())) || threshold.getMinValue() == null || threshold.getMaxValue() == null) throw new ServiceException("阈值配置不完整");
- threshold.setThresholdType(threshold.getThresholdType().trim());
- if (!thresholdTypes.add(threshold.getThresholdType())) throw new ServiceException("阈值类型不能重复");
- if (threshold.getMinValue().compareTo(threshold.getMaxValue()) > 0) throw new ServiceException("阈值最小值不能大于最大值");
- if (!threshold.getThresholdType().contains(":")) throw new ServiceException("阈值类型必须包含通道号");
+ 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 saveRelations(TTest test)
+ 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);
- for (TTestBatteryField field : battery.getFields())
+ if (battery.getFields() != null)
{
- field.setRelationId(battery.getRelationId());
- testMapper.insertTestBatteryField(field);
+ 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())
{
@@ -206,14 +339,9 @@
}
}
- private void validateChannel(Integer channel, String name)
+ private void validateQueryStatus(String status, Set<String> allowed, String message)
{
- if (channel == null || channel < 0) throw new ServiceException(name + "必须为非负整数");
- }
-
- private void validateStatus(String status)
- {
- if (StringUtils.isNotEmpty(status) && !WAITING.equals(status) && !TESTING.equals(status) && !ABNORMAL.equals(status) && !FINISHED.equals(status)) throw new ServiceException("测试状态无效");
+ if (StringUtils.isNotEmpty(status) && !allowed.contains(status)) throw new ServiceException(message);
}
private String trimToNull(String value)
@@ -221,4 +349,9 @@
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);
+ }
}
--
Gitblit v1.7.1