| | |
| | | import java.util.HashMap; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Locale; |
| | | 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.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.TTestWarningThreshold; |
| | | import com.ruoyi.system.domain.TTestLogicStepMapping; |
| | | import com.ruoyi.system.domain.TTestConsoleDetail; |
| | | import com.ruoyi.system.domain.TDetectionChannel; |
| | | import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo; |
| | | import com.ruoyi.system.mapper.TTestMapper; |
| | | import com.ruoyi.system.service.ITTestService; |
| | |
| | | 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 String NOT_STARTED = "NOT_STARTED"; |
| | | /** 新流程只允许四种测试状态;历史状态仅在旧数据展示层做兼容映射。 */ |
| | | private static final Set<String> TEST_STATUSES = Set.of(WAITING, "RUNNING", "PAUSED", "COMPLETED"); |
| | | 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; |
| | |
| | | test.setTestName(trimToNull(test.getTestName())); |
| | | test.setBatteryNo(trimToNull(test.getBatteryNo())); |
| | | validateQueryStatus(test.getTestStatus(), TEST_STATUSES, "测试状态无效"); |
| | | validateQueryStatus(test.getMonitoringStatus(), MONITORING_STATUSES, "监测状态无效"); |
| | | return testMapper.selectTestList(test); |
| | | validateQueryStatus(test.getReceptionStatus(), RECEPTION_STATUSES, "接收状态无效"); |
| | | List<TTest> tests = mutable(testMapper.selectTestList(test)); |
| | | populateBatteryNos(tests); |
| | | return tests; |
| | | } |
| | | |
| | | @Override |
| | |
| | | validateBase(test, false); |
| | | validateConfiguration(test); |
| | | test.setTestStatus(WAITING); |
| | | test.setMonitoringStatus(NOT_MONITORED); |
| | | test.setReceptionStatus(NOT_STARTED); |
| | | 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)); |
| | | // 最终基线移除了 t_test_audit;Controller 的 @Log 负责记录创建操作,业务表只保存配置事实。 |
| | | return test.getTestId(); |
| | | } |
| | | |
| | |
| | | TTest current = testMapper.selectTestByIdForUpdate(test.getTestId()); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | loadConfiguration(current); |
| | | if (!NOT_MONITORED.equals(current.getMonitoringStatus())) |
| | | throw new ServiceException("开始监测后不能修改测试配置"); |
| | | if (!stateMatches(current, WAITING, NOT_STARTED)) |
| | | throw new ServiceException("开始接收后不能修改测试配置"); |
| | | |
| | | test.setCreatorId(current.getCreatorId()); |
| | | test.setTestStatus(current.getTestStatus()); |
| | | test.setMonitoringStatus(current.getMonitoringStatus()); |
| | | test.setReceptionStatus(current.getReceptionStatus()); |
| | | if (test.getMonitorTimeoutMinutes() == null) |
| | | test.setMonitorTimeoutMinutes(current.getMonitorTimeoutMinutes()); |
| | | validateBase(test, true); |
| | |
| | | int rows = testMapper.updateTest(test); |
| | | if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试"); |
| | | |
| | | // 监测开始前没有原始采集事实,配置关系可在同一事务内整体替换,审计记录仍只追加。 |
| | | // 开始接收前没有数据归属事实,配置关系可在同一事务内整体替换;操作审计由 Controller @Log 追加。 |
| | | testMapper.deleteTestCollectorChannelsByTestId(test.getTestId()); |
| | | 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("开始监测由后续监测动作处理"); |
| | | 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 (!stateMatches(current, WAITING, NOT_STARTED)) |
| | | throw new ServiceException("当前测试不是未开始接收状态"); |
| | | loadConfiguration(current); |
| | | if (current.getBatteries().isEmpty()) throw new ServiceException("测试未配置电池号"); |
| | | |
| | | Date actionAt = new Date(); |
| | | 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())) |
| | | throw new ServiceException("检测柜通道 " + binding.getChannelId() + " 不存在或未启用"); |
| | | // 只有设备事实为运行中时才标记“运行中接入”;设备暂停属于独立设备状态。 |
| | | runtimeAttach |= "WORKING".equalsIgnoreCase(channel.getWorkStatus()) |
| | | || "RUNNING".equalsIgnoreCase(channel.getWorkStatus()); |
| | | if (battery.getCollectorId() == null || testMapper.countCollectorEnabled(battery.getCollectorId()) == 0) |
| | | throw new ServiceException("电池号 " + battery.getBatteryNo() + " 的采集器不存在或未启用"); |
| | | Integer collectorChannelCount = testMapper.selectCollectorChannelCount(battery.getCollectorId()); |
| | | validateCollectorChannels(battery.getCollectorId(), collectorChannelCount == null ? 0 : collectorChannelCount, |
| | | battery.getCollectorChannels(), collectorChannels); |
| | | |
| | | } |
| | | |
| | | try |
| | | { |
| | | if (testMapper.updateReceptionStarted(testId, actionAt, runtimeAttach, updateBy) != 1) |
| | | throw new ServiceException("测试状态已变更,请刷新后重试"); |
| | | testMapper.insertTestAudit(testId, "RECEPTION_STARTED", operatorId, updateBy, null, |
| | | JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "runtimeAttach", runtimeAttach))); |
| | | return 1; |
| | | } |
| | | catch (DuplicateKeyException exception) |
| | | { |
| | | throw new ServiceException("数据来源已被其他测试占用,请刷新后重试"); |
| | | } |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int deleteTestByIds(Long[] testIds, String updateBy) |
| | | 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 (!stateMatches(current, "RUNNING", "RECEIVING")) |
| | | throw new ServiceException("当前测试不是接收中状态"); |
| | | |
| | | Date actionAt = new Date(); |
| | | if (testMapper.updateReceptionPaused(testId, updateBy, actionAt) != 1) |
| | | throw new ServiceException("测试状态已变更,请刷新后重试"); |
| | | // 暂停期间停止请求数据;继续接收后仅追加新的成功接收事实。 |
| | | testMapper.insertTestAudit(testId, "RECEPTION_PAUSED", operatorId, updateBy, null, |
| | | JSON.toJSONString(Map.of("receptionStatus", "PAUSED", "polling", "STOPPED"))); |
| | | 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 (!stateMatches(current, "PAUSED", "PAUSED")) |
| | | throw new ServiceException("当前测试不是暂停接收状态"); |
| | | // 加锁查询只返回测试主表,继续接收前重新加载绑定配置以校验来源仍可用。 |
| | | loadConfiguration(current); |
| | | Date actionAt = new Date(); |
| | | if (testMapper.updateReceptionResumed(testId, updateBy, actionAt) != 1) |
| | | throw new ServiceException("测试状态已变更,请刷新后重试"); |
| | | testMapper.insertTestAudit(testId, "RECEPTION_RESUMED", operatorId, updateBy, null, |
| | | JSON.toJSONString(Map.of("receptionStatus", "RECEIVING", "polling", "RESUMED"))); |
| | | return 1; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int stopTest(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 (!(stateMatches(current, "RUNNING", "RECEIVING") |
| | | || stateMatches(current, "PAUSED", "PAUSED"))) |
| | | throw new ServiceException("当前测试不能停止"); |
| | | Date actionAt = new Date(); |
| | | if (testMapper.updateReceptionStopped(testId, updateBy, actionAt) != 1) |
| | | throw new ServiceException("测试状态已变更,请刷新后重试"); |
| | | testMapper.insertTestAudit(testId, "TEST_STOPPED", operatorId, updateBy, null, |
| | | JSON.toJSONString(Map.of("testStatus", "COMPLETED", "receptionStatus", "ENDED", |
| | | "polling", "STOPPED", "release", "IMMEDIATE"))); |
| | | return 1; |
| | | } |
| | | |
| | | private boolean isEnabled(String status) |
| | | { |
| | | return status == null || "0".equals(status); |
| | | } |
| | | |
| | | /** 生命周期动作必须同时匹配测试状态和接收状态,避免并发请求后两个状态字段互相矛盾。 */ |
| | | private boolean stateMatches(TTest test, String expectedTestStatus, String expectedReceptionStatus) |
| | | { |
| | | return expectedReceptionStatus.equals(test.getReceptionStatus()) |
| | | && expectedTestStatus.equals(test.getTestStatus()); |
| | | } |
| | | |
| | | @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; |
| | |
| | | { |
| | | 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); |
| | | if (!stateMatches(current, WAITING, NOT_STARTED)) throw new ServiceException("开始接收后不能删除测试"); |
| | | loadConfiguration(current); |
| | | // 测试整体删除是无痕例外:先清理全部测试绑定和阈值,再删除主记录,不写审计。 |
| | | testMapper.deleteTestRealtimeRecordsByTestId(testId); |
| | | testMapper.deleteTestRealtimeSnapshotsByTestId(testId); |
| | | testMapper.deleteTestStepRecordsByTestId(testId); |
| | | testMapper.deleteTestCycleRecordsByTestId(testId); |
| | | testMapper.deleteTestDataRevisionActionsByTestId(testId); |
| | | testMapper.deleteTestDataOverridesByTestId(testId); |
| | | testMapper.deleteTestConsoleImportRecordsByTestId(testId); |
| | | testMapper.deleteTestCollectorChannelsByTestId(testId); |
| | | testMapper.deleteTestBindingsByTestId(testId); |
| | | testMapper.deleteTestThresholdRowsByTestId(testId); |
| | | testMapper.deleteTestBatteriesByTestId(testId); |
| | | int deleted = testMapper.deleteTestById(testId, updateBy); |
| | | if (deleted != 1) throw new ServiceException("测试配置已变更,请刷新后重试"); |
| | | rows += deleted; |
| | | } |
| | | return rows; |
| | | } |
| | |
| | | return testMapper.selectTestResults(result); |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int saveLogicStepMappings(Long testId, List<TTestLogicStepMapping> mappings, Long operatorId, String updateBy) |
| | | { |
| | | if (testId == null || operatorId == null) throw new ServiceException("测试和操作人不能为空"); |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | List<TTestLogicStepMapping> normalized = mappings == null ? List.of() : mappings; |
| | | int previousEnd = 0; |
| | | int expectedNo = 1; |
| | | for (TTestLogicStepMapping mapping : normalized) |
| | | { |
| | | if (mapping == null || mapping.getLogicalNo() == null || mapping.getLogicalNo() != expectedNo |
| | | || mapping.getStartStepNo() == null || mapping.getEndStepNo() == null |
| | | || mapping.getStartStepNo() < 1 || mapping.getEndStepNo() < mapping.getStartStepNo() |
| | | || mapping.getStartStepNo() <= previousEnd || StringUtils.isEmpty(trimToNull(mapping.getLogicalName()))) |
| | | throw new ServiceException("逻辑工步必须按循环内连续位置升序且互不重叠"); |
| | | mapping.setLogicalName(trimToNull(mapping.getLogicalName())); |
| | | previousEnd = mapping.getEndStepNo(); |
| | | expectedNo++; |
| | | } |
| | | String oldJson = current.getLogicStepMappingsJson(); |
| | | String newJson = JSON.toJSONString(normalized); |
| | | int rows = testMapper.updateLogicStepMappings(testId, newJson, updateBy, new Date()); |
| | | if (rows != 1) throw new ServiceException("工步设置已变更,请刷新后重试"); |
| | | testMapper.insertTestAudit(testId, "LOGIC_STEP_MAPPING_UPDATED", operatorId, updateBy, oldJson, newJson); |
| | | return rows; |
| | | } |
| | | |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int saveConsoleDetail(Long testId, TTestConsoleDetail detail, Long operatorId, String updateBy) |
| | | { |
| | | if (testId == null || operatorId == null || detail == null) throw new ServiceException("测试详情不能为空"); |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | String experimentName = trimToNull(detail.getExperimentName()); |
| | | if (experimentName != null && experimentName.length() > 200) throw new ServiceException("实验名称长度不能超过200个字符"); |
| | | detail.setExperimentName(experimentName); |
| | | validateConsoleFields(detail.getBatteryFields()); |
| | | validateConsoleFields(detail.getSpecialFields()); |
| | | validateConsoleValues(testId, detail); |
| | | int rows = testMapper.updateConsoleDetail(testId, JSON.toJSONString(detail), updateBy, new Date()); |
| | | if (rows != 1) throw new ServiceException("测试详情保存失败,请刷新后重试"); |
| | | return rows; |
| | | } |
| | | |
| | | private void validateConsoleFields(List<String> fields) |
| | | { |
| | | if (fields == null) return; |
| | | Set<String> names = new HashSet<>(); |
| | | for (String field : fields) |
| | | { |
| | | String normalized = trimToNull(field); |
| | | if (normalized == null || normalized.length() > 100 || !names.add(normalized)) |
| | | throw new ServiceException("测试详情字段不能为空、长度不能超过100且不可重复"); |
| | | } |
| | | } |
| | | |
| | | private void validateConsoleValues(Long testId, TTestConsoleDetail detail) |
| | | { |
| | | Set<String> allowedFields = new HashSet<>(); |
| | | if (detail.getBatteryFields() != null) allowedFields.addAll(detail.getBatteryFields()); |
| | | if (detail.getSpecialFields() != null) allowedFields.addAll(detail.getSpecialFields()); |
| | | Set<String> batteryNos = new HashSet<>(); |
| | | List<TTestBattery> batteries = testMapper.selectTestBatteries(testId); |
| | | if (batteries != null) for (TTestBattery battery : batteries) batteryNos.add(battery.getBatteryNo()); |
| | | if (detail.getFieldValues() == null) return; |
| | | for (Map.Entry<String, Map<String, String>> entry : detail.getFieldValues().entrySet()) |
| | | { |
| | | if (!allowedFields.contains(entry.getKey()) || entry.getValue() == null) |
| | | throw new ServiceException("测试详情字段值结构无效"); |
| | | for (Map.Entry<String, String> value : entry.getValue().entrySet()) |
| | | { |
| | | if (!batteryNos.contains(value.getKey()) || (value.getValue() != null && value.getValue().length() > 500)) |
| | | throw new ServiceException("测试详情电池号或字段值无效"); |
| | | } |
| | | } |
| | | } |
| | | |
| | | 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)); |
| | | 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> currentFields = testMapper.selectCurrentBatteryFields(testId); |
| | | Map<Long, List<TTestBatteryField>> fieldsByBattery = new HashMap<>(); |
| | | for (TTestBatteryField field : mutable(currentFields)) |
| | | fieldsByBattery.computeIfAbsent(field.getRelationId(), ignored -> new ArrayList<>()).add(field); |
| | | List<TTestCollectorChannel> loadedCollectorChannels = testMapper.selectTestCollectorChannels(testId); |
| | | List<TTestCollectorChannel> collectorChannels = loadedCollectorChannels == null ? List.of() : mutable(loadedCollectorChannels); |
| | | Map<Long, TTestChannelBinding> bindings = new HashMap<>(); |
| | | for (TTestChannelBinding binding : mutable(testMapper.selectTestChannelBindings(testId))) |
| | | 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())); |
| | | battery.setFields(fields.stream() |
| | | .filter(field -> battery.getRelationId().equals(field.getRelationId())) |
| | | .toList()); |
| | | battery.setFields(fieldsByBattery.getOrDefault(battery.getRelationId(), List.of())); |
| | | 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.setThresholds(mutable(testMapper.selectTestThresholds(testId))); |
| | | test.setBatteryNo(joinBatteryNos(batteries)); |
| | | List<TTestWarningThreshold> loadedThresholds = testMapper.selectTestThresholds(testId); |
| | | test.setThresholds(loadedThresholds == null ? mutable(test.getThresholds()) : mutable(loadedThresholds)); |
| | | // 测试不保存字段快照;字段值始终随电池组当前配置读取。 |
| | | test.setFieldSnapshots(new ArrayList<>()); |
| | | String mappingJson = test.getLogicStepMappingsJson(); |
| | | try { test.setLogicStepMappings(mappingJson == null || mappingJson.isBlank() |
| | | ? new ArrayList<>() : JSON.parseArray(mappingJson, TTestLogicStepMapping.class)); } |
| | | catch (JSONException exception) { throw new ServiceException("逻辑工步设置无法解析"); } |
| | | String detailJson = test.getConsoleDetailJson(); |
| | | try { test.setConsoleDetail(detailJson == null || detailJson.isBlank() |
| | | ? null : JSON.parseObject(detailJson, TTestConsoleDetail.class)); } |
| | | catch (JSONException exception) { throw new ServiceException("测试详情无法解析"); } |
| | | } |
| | | |
| | | private void validateBase(TTest test, boolean update) |
| | |
| | | |
| | | private void validateConfiguration(TTest test) |
| | | { |
| | | // 计划工步来自设备观测,不属于测试创建配置,也不写入业务库。 |
| | | test.setPlanSteps(new ArrayList<>()); |
| | | validateBatteries(test.getBatteries()); |
| | | validatePlanSteps(test.getPlanSteps()); |
| | | validateThresholds(test.getThresholds()); |
| | |
| | | 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) |
| | | { |
| | |
| | | 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) |
| | | 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(normalizeMeasurementCode(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) |
| | |
| | | } |
| | | } |
| | | |
| | | 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 == null || planSteps.isEmpty()) throw new ServiceException("计划工步不能为空"); |
| | | if (planSteps.isEmpty()) return; |
| | | Set<String> identities = new HashSet<>(); |
| | | Set<Integer> orders = new HashSet<>(); |
| | | for (TTestPlanStep step : planSteps) |
| | |
| | | |
| | | 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); |
| | | } |
| | | } |
| | | for (TTestWarningThreshold threshold : test.getThresholds()) |
| | | { |
| | |
| | | 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 String normalizeMeasurementCode(String value) |
| | | { |
| | | String normalized = trimToNull(value); |
| | | return normalized == null ? null : normalized.toUpperCase(Locale.ROOT); |
| | | } |
| | | |
| | | private <T> List<T> mutable(List<T> values) |
| | | { |
| | | return values == null ? new ArrayList<>() : new ArrayList<>(values); |