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.dao.DuplicateKeyException;
|
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.TTestCollectorChannel;
|
import com.ruoyi.system.domain.TTestPlanStep;
|
import com.ruoyi.system.domain.TTestResult;
|
import com.ruoyi.system.domain.TTestFieldSnapshot;
|
import com.ruoyi.system.domain.TTestWarningThreshold;
|
import com.ruoyi.system.domain.TDetectionChannel;
|
import com.ruoyi.system.domain.TTestReceptionInterval;
|
import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo;
|
import com.ruoyi.device.bts.BtsDevelopmentSimulator;
|
import com.ruoyi.device.simulator.BtsSimulatorStateStore.CollectorRealtimeMeasurement;
|
import com.ruoyi.system.mapper.TTestMapper;
|
import com.ruoyi.system.service.ITTestService;
|
import com.ruoyi.system.service.ISharedFieldSnapshotService;
|
|
/** 测试配置服务,负责状态边界、关系校验、事务写入和只追加审计。 */
|
@Service
|
public class TTestServiceImpl implements ITTestService
|
{
|
private static final String WAITING = "WAITING";
|
private static final String NOT_STARTED = "NOT_STARTED";
|
private static final Set<String> TEST_STATUSES = Set.of(
|
WAITING, "RUNNING", "PAUSED", "RUNNING_ABNORMAL", "COMPLETION_PENDING", "COMPLETED", "TERMINATED");
|
private static final Set<String> RECEPTION_STATUSES = Set.of(NOT_STARTED, "RECEIVING", "PAUSED", "ENDED");
|
private static final Set<String> THRESHOLD_UNITS = Set.of("A", "V", "Ah", "Wh", "℃");
|
private static final Set<String> REQUIRED_COLLECTOR_MEASUREMENTS = Set.of(
|
"PRIMARY_TEMPERATURE", "PRIMARY_POSITIVE_POTENTIAL", "PRIMARY_NEGATIVE_POTENTIAL");
|
|
@Autowired
|
private TTestMapper testMapper;
|
|
@Autowired
|
private ISharedFieldSnapshotService sharedFieldSnapshotService;
|
|
@Autowired(required = false)
|
private BtsDevelopmentSimulator collectorSimulator;
|
|
@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.getReceptionStatus(), RECEPTION_STATUSES, "接收状态无效");
|
List<TTest> tests = mutable(testMapper.selectTestList(test));
|
populateBatteryNos(tests);
|
return tests;
|
}
|
|
@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.setReceptionStatus(NOT_STARTED);
|
test.setFieldSnapshots(sharedFieldSnapshotService.createForBatteryGroup(
|
test.getBatteries().get(0).getGroupId()));
|
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_STARTED.equals(current.getReceptionStatus()))
|
throw new ServiceException("开始接收后不能修改测试配置");
|
|
test.setCreatorId(current.getCreatorId());
|
test.setTestStatus(current.getTestStatus());
|
test.setReceptionStatus(current.getReceptionStatus());
|
if (test.getMonitorTimeoutMinutes() == null)
|
test.setMonitorTimeoutMinutes(current.getMonitorTimeoutMinutes());
|
validateBase(test, true);
|
validateConfiguration(test);
|
Long currentGroupId = current.getBatteries().isEmpty() ? null : current.getBatteries().get(0).getGroupId();
|
Long nextGroupId = test.getBatteries().get(0).getGroupId();
|
test.setFieldSnapshots(sharedFieldSnapshotService.resolveForUpdate(
|
currentGroupId, nextGroupId, current.getFieldSnapshots()));
|
test.setUpdateTime(new Date());
|
int rows = testMapper.updateTest(test);
|
if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试");
|
|
// 开始接收前没有数据归属事实,配置关系可在同一事务内整体替换,审计记录仍只追加。
|
testMapper.deleteTestCollectorChannelsByTestId(test.getTestId());
|
testMapper.deleteTestBindingsByTestId(test.getTestId());
|
testMapper.deleteTestPlanStepsByTestId(test.getTestId());
|
testMapper.deleteTestFieldsByTestId(test.getTestId());
|
testMapper.deleteTestBatteriesByTestId(test.getTestId());
|
testMapper.deleteTestThresholdsByTestId(test.getTestId());
|
sharedFieldSnapshotService.deleteByTestId(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 startReception(Long testId, Long operatorId, String updateBy)
|
{
|
if (testId == null) throw new ServiceException("测试ID不能为空");
|
if (operatorId == null) throw new ServiceException("操作人不能为空");
|
TTest current = testMapper.selectTestByIdForUpdate(testId);
|
if (current == null) throw new ServiceException("测试不存在");
|
if (!NOT_STARTED.equals(current.getReceptionStatus()))
|
throw new ServiceException("当前测试不是未开始接收状态");
|
loadConfiguration(current);
|
if (current.getBatteries().isEmpty()) throw new ServiceException("测试未配置电池号");
|
|
Date actionAt = new Date();
|
List<TTestReceptionInterval> intervals = new ArrayList<>();
|
Set<Long> channelIds = new HashSet<>();
|
Set<String> collectorChannels = new HashSet<>();
|
boolean runtimeAttach = false;
|
for (TTestBattery battery : current.getBatteries())
|
{
|
TTestChannelBinding binding = battery.getBinding();
|
if (binding == null || binding.getChannelId() == null)
|
throw new ServiceException("电池号 " + battery.getBatteryNo() + " 未绑定检测柜通道");
|
if (!channelIds.add(binding.getChannelId()))
|
throw new ServiceException("检测柜通道重复绑定,无法开始接收");
|
TDetectionChannel channel = testMapper.selectReceptionChannelForUpdate(binding.getChannelId());
|
if (channel == null || !isEnabled(channel.getStatus()) || !isOnline(channel.getOnlineStatus()))
|
throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 当前离线或不可观测");
|
if (channel.getLastSeenAtUtc() == null)
|
throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 尚未取得成功观测边界");
|
runtimeAttach |= "WORKING".equalsIgnoreCase(channel.getWorkStatus())
|
|| "PAUSED".equalsIgnoreCase(channel.getWorkStatus());
|
if (testMapper.selectChannelLeaseForUpdate(binding.getChannelId()) != null)
|
throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 已被其他测试占用");
|
if (battery.getCollectorId() == null || testMapper.countCollectorEnabled(battery.getCollectorId()) == 0)
|
throw new ServiceException("电池号 " + battery.getBatteryNo() + " 的采集器不存在或未启用");
|
if (!testMapper.collectorOnline(battery.getCollectorId()))
|
throw new ServiceException("采集器 " + battery.getCollectorId() + " 当前离线或不可观测");
|
Integer collectorChannelCount = testMapper.selectCollectorChannelCount(battery.getCollectorId());
|
validateCollectorChannels(battery.getCollectorId(), collectorChannelCount == null ? 0 : collectorChannelCount,
|
battery.getCollectorChannels(), collectorChannels);
|
|
intervals.add(interval(current, binding.getBindingId(), "CABINET_CHANNEL", binding.getChannelId(), null,
|
actionAt, Date.from(channel.getLastSeenAtUtc()), runtimeAttach
|
? "运行中接入:采用检测柜最近成功观测边界" : "采用检测柜最近成功观测边界"));
|
for (TTestCollectorChannel mapping : mutable(battery.getCollectorChannels()))
|
{
|
intervals.add(interval(current, binding.getBindingId(), "COLLECTOR_CHANNEL", battery.getCollectorId(),
|
mapping.getChannelCode(), actionAt, actionAt, "采集器开始接收确认边界"));
|
}
|
}
|
|
try
|
{
|
for (TTestBattery battery : current.getBatteries())
|
{
|
TTestChannelBinding binding = battery.getBinding();
|
testMapper.insertChannelLease(binding.getChannelId(), testId, binding.getBindingId(), operatorId, actionAt);
|
for (TTestCollectorChannel mapping : mutable(battery.getCollectorChannels()))
|
testMapper.insertCollectorChannelLease(battery.getCollectorId(), mapping.getChannelCode(), testId,
|
binding.getBindingId(), operatorId, actionAt);
|
}
|
for (TTestReceptionInterval interval : intervals) testMapper.insertReceptionInterval(interval);
|
if (testMapper.updateReceptionStarted(testId, actionAt, updateBy) != 1)
|
throw new ServiceException("测试状态已变更,请刷新后重试");
|
testMapper.insertTestAudit(testId, "RECEPTION_STARTED", operatorId, updateBy, null,
|
JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "intervalCount", intervals.size(),
|
"runtimeAttach", runtimeAttach, "intervals", intervals)));
|
return 1;
|
}
|
catch (DuplicateKeyException exception)
|
{
|
throw new ServiceException("数据来源已被其他测试占用,请刷新后重试");
|
}
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int pauseReception(Long testId, Long operatorId, String updateBy)
|
{
|
if (testId == null) throw new ServiceException("测试ID不能为空");
|
if (operatorId == null) throw new ServiceException("操作人不能为空");
|
TTest current = testMapper.selectTestByIdForUpdate(testId);
|
if (current == null) throw new ServiceException("测试不存在");
|
if (!"RECEIVING".equals(current.getReceptionStatus()))
|
throw new ServiceException("当前测试不是接收中状态");
|
|
List<TTestReceptionInterval> openIntervals = mutable(testMapper.selectOpenReceptionIntervalsForUpdate(testId));
|
if (openIntervals.isEmpty()) throw new ServiceException("当前测试没有开放的接收区间");
|
Date actionAt = new Date();
|
Map<Long, Date> boundaries = new HashMap<>();
|
// 先确认所有来源都能取得边界,再写入任何一条关闭记录,保证整项动作原子失败。
|
for (TTestReceptionInterval open : openIntervals)
|
{
|
Date boundary = confirmBoundary(open, actionAt);
|
boundaries.put(open.getIntervalId(), boundary);
|
open.setEndBoundaryAtUtc(boundary);
|
open.setIntervalStatus("CLOSED");
|
}
|
for (TTestReceptionInterval open : openIntervals)
|
{
|
if (testMapper.closeReceptionInterval(open.getIntervalId(), boundaries.get(open.getIntervalId()),
|
"暂停接收:采用来源最近成功观测边界") != 1)
|
throw new ServiceException("接收区间已变更,请刷新后重试");
|
}
|
if (testMapper.updateReceptionPaused(testId, updateBy, actionAt) != 1)
|
throw new ServiceException("测试状态已变更,请刷新后重试");
|
testMapper.insertTestAudit(testId, "RECEPTION_PAUSED", operatorId, updateBy, null,
|
JSON.toJSONString(Map.of("receptionStatus", "PAUSED", "intervalCount", openIntervals.size(),
|
"intervals", openIntervals)));
|
return 1;
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int resumeReception(Long testId, Long operatorId, String updateBy)
|
{
|
if (testId == null) throw new ServiceException("测试ID不能为空");
|
if (operatorId == null) throw new ServiceException("操作人不能为空");
|
TTest current = testMapper.selectTestByIdForUpdate(testId);
|
if (current == null) throw new ServiceException("测试不存在");
|
if (!"PAUSED".equals(current.getReceptionStatus()))
|
throw new ServiceException("当前测试不是暂停接收状态");
|
loadConfiguration(current);
|
if (current.getBatteries().isEmpty()) throw new ServiceException("测试未配置电池号");
|
|
Date actionAt = new Date();
|
List<TTestReceptionInterval> intervals = new ArrayList<>();
|
Set<Long> channelIds = new HashSet<>();
|
Set<String> collectorChannels = new HashSet<>();
|
for (TTestBattery battery : current.getBatteries())
|
{
|
TTestChannelBinding binding = battery.getBinding();
|
if (binding == null || binding.getChannelId() == null)
|
throw new ServiceException("电池号 " + battery.getBatteryNo() + " 未绑定检测柜通道");
|
if (!channelIds.add(binding.getChannelId())) throw new ServiceException("检测柜通道重复绑定,无法继续接收");
|
TDetectionChannel channel = testMapper.selectReceptionChannelForUpdate(binding.getChannelId());
|
if (channel == null || !isEnabled(channel.getStatus()) || !isOnline(channel.getOnlineStatus())
|
|| channel.getLastSeenAtUtc() == null)
|
throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 当前无法确认继续接收边界");
|
if (isEndedWorkStatus(channel.getWorkStatus()))
|
throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 已结束,不能继续接收");
|
if (battery.getCollectorId() == null || testMapper.countCollectorEnabled(battery.getCollectorId()) == 0
|
|| !testMapper.collectorOnline(battery.getCollectorId()))
|
throw new ServiceException("采集器 " + battery.getCollectorId() + " 当前无法确认继续接收边界");
|
Integer count = testMapper.selectCollectorChannelCount(battery.getCollectorId());
|
validateCollectorChannels(battery.getCollectorId(), count == null ? 0 : count,
|
battery.getCollectorChannels(), collectorChannels);
|
Date cabinetBoundary = Date.from(channel.getLastSeenAtUtc());
|
ensureAfterPreviousBoundary(current.getTestId(), "CABINET_CHANNEL", binding.getChannelId(), null,
|
cabinetBoundary);
|
intervals.add(interval(current, binding.getBindingId(), "CABINET_CHANNEL", binding.getChannelId(), null,
|
actionAt, cabinetBoundary, "继续接收:采用检测柜最近成功观测边界"));
|
for (TTestCollectorChannel mapping : mutable(battery.getCollectorChannels()))
|
{
|
Date collectorBoundary = collectorBoundary(battery.getCollectorId(), mapping.getChannelCode());
|
ensureAfterPreviousBoundary(current.getTestId(), "COLLECTOR_CHANNEL", battery.getCollectorId(),
|
mapping.getChannelCode(), collectorBoundary);
|
intervals.add(interval(current, binding.getBindingId(), "COLLECTOR_CHANNEL", battery.getCollectorId(),
|
mapping.getChannelCode(), actionAt, collectorBoundary, "继续接收:采用采集器最近成功观测边界"));
|
}
|
}
|
for (TTestReceptionInterval interval : intervals) testMapper.insertReceptionInterval(interval);
|
if (testMapper.updateReceptionResumed(testId, updateBy, actionAt) != 1)
|
throw new ServiceException("测试状态已变更,请刷新后重试");
|
testMapper.insertTestAudit(testId, "RECEPTION_RESUMED", operatorId, updateBy, null,
|
JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "intervalCount", intervals.size(),
|
"intervals", intervals)));
|
return 1;
|
}
|
|
@Override
|
public List<TTestReceptionInterval> selectReceptionIntervals(Long testId)
|
{
|
if (testId == null) throw new ServiceException("测试ID不能为空");
|
selectTestById(testId);
|
return mutable(testMapper.selectReceptionIntervals(testId));
|
}
|
|
private Date confirmBoundary(TTestReceptionInterval interval, Date actionAt)
|
{
|
if ("CABINET_CHANNEL".equals(interval.getSourceType()))
|
{
|
TDetectionChannel channel = testMapper.selectReceptionChannelForUpdate(interval.getSourceId());
|
if (channel == null || !isEnabled(channel.getStatus()) || !isOnline(channel.getOnlineStatus())
|
|| channel.getLastSeenAtUtc() == null)
|
throw new ServiceException("检测柜通道 " + interval.getSourceId() + " 当前无法确认暂停接收边界");
|
Date boundary = Date.from(channel.getLastSeenAtUtc());
|
if (interval.getStartBoundaryAtUtc() != null && boundary.before(interval.getStartBoundaryAtUtc()))
|
throw new ServiceException("检测柜通道 " + interval.getSourceId() + " 的暂停边界早于区间开始");
|
return boundary;
|
}
|
if (!testMapper.collectorOnline(interval.getSourceId()))
|
throw new ServiceException("采集器 " + interval.getSourceId() + " 当前无法确认暂停接收边界");
|
Date boundary = collectorBoundary(interval.getSourceId(), interval.getSourceChannelCode());
|
if (interval.getStartBoundaryAtUtc() != null && boundary.before(interval.getStartBoundaryAtUtc()))
|
throw new ServiceException("采集器 " + interval.getSourceId() + " 的暂停边界早于区间开始");
|
return boundary;
|
}
|
|
private Date collectorBoundary(Long collectorId, String channelCode)
|
{
|
if (collectorSimulator == null) throw new ServiceException("采集器 " + collectorId + " 当前无法取得成功观测边界");
|
try
|
{
|
return collectorSimulator.collectorRealtime(collectorId.intValue()).measurements().stream()
|
.filter(item -> channelCode == null || channelCode.equals(item.channelCode()))
|
.map(CollectorRealtimeMeasurement::deviceTime).filter(java.util.Objects::nonNull)
|
.max(java.time.Instant::compareTo).map(Date::from)
|
.orElseThrow(() -> new ServiceException("采集器 " + collectorId + " 当前无法取得成功观测边界"));
|
}
|
catch (RuntimeException exception)
|
{
|
if (exception instanceof ServiceException serviceException) throw serviceException;
|
throw new ServiceException("采集器 " + collectorId + " 当前无法取得成功观测边界");
|
}
|
}
|
|
private void ensureAfterPreviousBoundary(Long testId, String sourceType, Long sourceId,
|
String sourceChannelCode, Date boundary)
|
{
|
TTestReceptionInterval previous = testMapper.selectLatestReceptionIntervalForUpdate(testId, sourceType,
|
sourceId, sourceChannelCode);
|
if (previous != null && previous.getEndBoundaryAtUtc() != null
|
&& !boundary.after(previous.getEndBoundaryAtUtc()))
|
throw new ServiceException("数据来源尚未取得新的接收边界,请稍后重试");
|
}
|
|
private boolean isEndedWorkStatus(String workStatus)
|
{
|
return "STOPPED".equalsIgnoreCase(workStatus) || "FINISHED".equalsIgnoreCase(workStatus)
|
|| "ENDED".equalsIgnoreCase(workStatus);
|
}
|
|
private TTestReceptionInterval interval(TTest test, Long bindingId, String sourceType, Long sourceId,
|
String sourceChannelCode, Date actionAt, Date boundaryAt, String note)
|
{
|
TTestReceptionInterval interval = new TTestReceptionInterval();
|
interval.setTestId(test.getTestId());
|
interval.setBindingId(bindingId);
|
interval.setSourceType(sourceType);
|
interval.setSourceId(sourceId);
|
interval.setSourceChannelCode(sourceChannelCode);
|
interval.setActionAtUtc(actionAt);
|
interval.setStartBoundaryAtUtc(boundaryAt);
|
interval.setBoundaryNote(note);
|
interval.setIntervalStatus("OPEN");
|
return interval;
|
}
|
|
private boolean isOnline(String status)
|
{
|
return status != null && ("ONLINE".equalsIgnoreCase(status) || "0".equals(status));
|
}
|
|
private boolean isEnabled(String status)
|
{
|
return status == null || "0".equals(status);
|
}
|
|
@Override
|
@Transactional(rollbackFor = Exception.class)
|
public int deleteTestByIds(Long[] testIds, Long operatorId, 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_STARTED.equals(current.getReceptionStatus())) throw new ServiceException("开始接收后不能删除测试");
|
loadConfiguration(current);
|
int deleted = testMapper.deleteTestById(testId, updateBy);
|
if (deleted != 1) throw new ServiceException("测试配置已变更,请刷新后重试");
|
testMapper.insertTestAudit(testId, "TEST_DELETED", operatorId, updateBy,
|
JSON.toJSONString(current), null);
|
rows += deleted;
|
}
|
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> loadedPlanSteps = testMapper.selectTestPlanSteps(testId);
|
List<TTestPlanStep> planSteps = loadedPlanSteps == null ? mutable(test.getPlanSteps()) : mutable(loadedPlanSteps);
|
for (TTestPlanStep step : planSteps)
|
{
|
try
|
{
|
step.setParameters(JSON.parseObject(step.getParametersJson()));
|
}
|
catch (JSONException exception)
|
{
|
throw new ServiceException("计划工步参数无法解析");
|
}
|
}
|
|
List<TTestBattery> loadedBatteries = testMapper.selectTestBatteries(testId);
|
List<TTestBattery> batteries = loadedBatteries == null || (loadedBatteries.isEmpty() && test.getBatteries() != null
|
&& !test.getBatteries().isEmpty()) ? mutable(test.getBatteries()) : mutable(loadedBatteries);
|
List<TTestBatteryField> loadedFields = testMapper.selectTestBatteryFields(testId);
|
List<TTestBatteryField> fields = loadedFields == null ? List.of() : mutable(loadedFields);
|
List<TTestCollectorChannel> loadedCollectorChannels = testMapper.selectTestCollectorChannels(testId);
|
List<TTestCollectorChannel> collectorChannels = loadedCollectorChannels == null ? List.of() : mutable(loadedCollectorChannels);
|
Map<Long, TTestChannelBinding> bindings = new HashMap<>();
|
List<TTestChannelBinding> loadedBindings = testMapper.selectTestChannelBindings(testId);
|
if (loadedBindings == null || (loadedBindings.isEmpty() && batteries.stream().anyMatch(item -> item.getBinding() != null)))
|
for (TTestBattery battery : batteries)
|
if (battery.getBinding() != null) bindings.put(battery.getRelationId(), battery.getBinding());
|
for (TTestChannelBinding binding : mutable(loadedBindings))
|
bindings.put(binding.getBatteryRelationId(), binding);
|
for (TTestBattery battery : batteries)
|
{
|
battery.setBinding(bindings.get(battery.getRelationId()));
|
if (loadedFields != null && (!loadedFields.isEmpty() || battery.getFields() == null))
|
battery.setFields(fields.stream()
|
.filter(field -> battery.getRelationId().equals(field.getRelationId()))
|
.toList());
|
if (loadedCollectorChannels != null && (!loadedCollectorChannels.isEmpty() || battery.getCollectorChannels() == null))
|
battery.setCollectorChannels(collectorChannels.stream()
|
.filter(mapping -> battery.getRelationId().equals(mapping.getBatteryRelationId()))
|
.toList());
|
}
|
test.setPlanSteps(planSteps);
|
test.setBatteries(batteries);
|
test.setBatteryNo(joinBatteryNos(batteries));
|
List<TTestWarningThreshold> loadedThresholds = testMapper.selectTestThresholds(testId);
|
test.setThresholds(loadedThresholds == null ? mutable(test.getThresholds()) : mutable(loadedThresholds));
|
List<TTestFieldSnapshot> loadedSnapshots = sharedFieldSnapshotService.selectByTestId(testId);
|
test.setFieldSnapshots(loadedSnapshots == null ? mutable(test.getFieldSnapshots()) : loadedSnapshots);
|
}
|
|
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)
|
{
|
// 新建测试只建立业务身份和通道关系,计划工步可在开始接收前补充。
|
if (test.getPlanSteps() == null) test.setPlanSteps(new ArrayList<>());
|
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<>();
|
Set<String> collectorChannelIdentities = new HashSet<>();
|
Map<Long, Integer> collectorChannelCounts = new HashMap<>();
|
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) throw new ServiceException("采集器不能为空");
|
if (testMapper.countCollectorEnabled(battery.getCollectorId()) == 0)
|
throw new ServiceException("采集器不存在或未启用");
|
Integer channelCount = collectorChannelCounts.computeIfAbsent(battery.getCollectorId(),
|
testMapper::selectCollectorChannelCount);
|
if (channelCount == null || channelCount < 1) throw new ServiceException("采集器通道配置无效");
|
validateCollectorChannels(battery.getCollectorId(), channelCount, battery.getCollectorChannels(),
|
collectorChannelIdentities);
|
validateBatteryFields(battery.getFields());
|
}
|
}
|
|
private void validateCollectorChannels(Long collectorId, int channelCount, List<TTestCollectorChannel> mappings,
|
Set<String> collectorChannelIdentities)
|
{
|
if (mappings == null || mappings.isEmpty())
|
throw new ServiceException("每个电池号必须绑定主温度、正电位主通道和负电位主通道");
|
Set<String> measurementCodes = new HashSet<>();
|
for (TTestCollectorChannel mapping : mappings)
|
{
|
if (mapping == null) throw new ServiceException("采集器通道映射不完整");
|
mapping.setMeasurementCode(trimToNull(mapping.getMeasurementCode()));
|
mapping.setMeasurementName(trimToNull(mapping.getMeasurementName()));
|
mapping.setChannelCode(normalizeCollectorChannel(mapping.getChannelCode()));
|
if (mapping.getMeasurementCode() == null || mapping.getMeasurementName() == null
|
|| mapping.getChannelCode() == null)
|
throw new ServiceException("采集器通道映射不完整");
|
if (mapping.getMeasurementCode().length() > 100 || mapping.getMeasurementName().length() > 100)
|
throw new ServiceException("采集器测量项长度不能超过100个字符");
|
int channelNumber = Integer.parseInt(mapping.getChannelCode().substring(2));
|
if (channelNumber < 1 || channelNumber > channelCount)
|
throw new ServiceException("采集器通道不存在或超出登记范围");
|
if (!measurementCodes.add(mapping.getMeasurementCode()))
|
throw new ServiceException("同一电池号的采集器测量项不可重复");
|
if (!collectorChannelIdentities.add(collectorId + ":" + mapping.getChannelCode()))
|
throw new ServiceException("同一采集器通道不可重复映射");
|
}
|
if (!measurementCodes.containsAll(REQUIRED_COLLECTOR_MEASUREMENTS))
|
throw new ServiceException("每个电池号必须绑定主温度、正电位主通道和负电位主通道");
|
}
|
|
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 populateBatteryNos(List<TTest> tests)
|
{
|
if (tests.isEmpty()) return;
|
Map<Long, List<TTestBattery>> batteriesByTest = new HashMap<>();
|
List<Long> testIds = tests.stream().map(TTest::getTestId).toList();
|
for (TTestBattery battery : mutable(testMapper.selectBatteryNosByTestIds(testIds)))
|
batteriesByTest.computeIfAbsent(battery.getTestId(), ignored -> new ArrayList<>()).add(battery);
|
for (TTest item : tests)
|
item.setBatteryNo(joinBatteryNos(batteriesByTest.get(item.getTestId())));
|
}
|
|
private String joinBatteryNos(List<TTestBattery> batteries)
|
{
|
return mutable(batteries).stream().map(TTestBattery::getBatteryNo).collect(java.util.stream.Collectors.joining("、"));
|
}
|
|
private void validatePlanSteps(List<TTestPlanStep> planSteps)
|
{
|
if (planSteps.isEmpty()) return;
|
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 (TTestCollectorChannel mapping : battery.getCollectorChannels())
|
{
|
mapping.setTestId(test.getTestId());
|
mapping.setBatteryRelationId(battery.getRelationId());
|
mapping.setCollectorId(battery.getCollectorId());
|
testMapper.insertTestCollectorChannel(mapping);
|
}
|
}
|
sharedFieldSnapshotService.saveForTest(test.getTestId(), test.getFieldSnapshots());
|
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 String normalizeCollectorChannel(String value)
|
{
|
String normalized = trimToNull(value);
|
if (normalized == null || !normalized.toUpperCase().matches("S-\\d+"))
|
throw new ServiceException("采集器通道必须使用S-01...S-N演示编码");
|
try
|
{
|
int channelNumber = Integer.parseInt(normalized.substring(2));
|
return String.format("S-%02d", channelNumber);
|
}
|
catch (NumberFormatException exception)
|
{
|
throw new ServiceException("采集器通道必须使用S-01...S-N演示编码");
|
}
|
}
|
|
private <T> List<T> mutable(List<T> values)
|
{
|
return values == null ? new ArrayList<>() : new ArrayList<>(values);
|
}
|
}
|