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.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.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.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; /** 测试配置服务,负责状态边界、关系校验、事务写入和只追加审计。 */ @Service public class TTestServiceImpl implements ITTestService { private static final String WAITING = "WAITING"; private static final String NOT_STARTED = "NOT_STARTED"; /** 新流程只允许四种测试状态;历史状态仅在旧数据展示层做兼容映射。 */ private static final Set TEST_STATUSES = Set.of(WAITING, "RUNNING", "PAUSED", "COMPLETED"); private static final Set RECEPTION_STATUSES = Set.of(NOT_STARTED, "RECEIVING", "PAUSED", "ENDED"); private static final Set THRESHOLD_UNITS = Set.of("A", "V", "Ah", "Wh", "℃"); private static final Set REQUIRED_COLLECTOR_MEASUREMENTS = Set.of( "PRIMARY_TEMPERATURE", "PRIMARY_POSITIVE_POTENTIAL", "PRIMARY_NEGATIVE_POTENTIAL"); @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 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 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.setDelFlag("0"); test.setCreateTime(new Date()); testMapper.insertTest(test); saveConfiguration(test); // 最终基线移除了 t_test_audit;Controller 的 @Log 负责记录创建操作,业务表只保存配置事实。 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 (!stateMatches(current, WAITING, NOT_STARTED)) 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); test.setUpdateTime(new Date()); int rows = testMapper.updateTest(test); if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试"); // 开始接收前没有数据归属事实,配置关系可在同一事务内整体替换;操作审计由 Controller @Log 追加。 testMapper.deleteTestCollectorChannelsByTestId(test.getTestId()); testMapper.deleteTestBindingsByTestId(test.getTestId()); testMapper.deleteTestBatteriesByTestId(test.getTestId()); testMapper.deleteTestThresholdsByTestId(test.getTestId()); saveConfiguration(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 (!stateMatches(current, WAITING, NOT_STARTED)) throw new ServiceException("当前测试不是未开始接收状态"); loadConfiguration(current); if (current.getBatteries().isEmpty()) throw new ServiceException("测试未配置电池号"); Date actionAt = new Date(); Set channelIds = new HashSet<>(); Set 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 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; for (Long testId : testIds) { TTest current = testMapper.selectTestByIdForUpdate(testId); if (current == null) throw new ServiceException("测试不存在"); if (!stateMatches(current, WAITING, NOT_STARTED)) throw new ServiceException("开始接收后不能删除测试"); loadConfiguration(current); // 测试整体删除是无痕例外:先清理全部测试绑定和阈值,再删除主记录,不写审计。 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; } @Override public List selectTestResults(TTestResult result) { if (result == null || result.getTestId() == null) throw new ServiceException("测试ID不能为空"); selectTestById(result.getTestId()); return testMapper.selectTestResults(result); } @Override @Transactional(rollbackFor = Exception.class) public int saveLogicStepMappings(Long testId, List 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 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 fields) { if (fields == null) return; Set 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 allowedFields = new HashSet<>(); if (detail.getBatteryFields() != null) allowedFields.addAll(detail.getBatteryFields()); if (detail.getSpecialFields() != null) allowedFields.addAll(detail.getSpecialFields()); Set batteryNos = new HashSet<>(); List batteries = testMapper.selectTestBatteries(testId); if (batteries != null) for (TTestBattery battery : batteries) batteryNos.add(battery.getBatteryNo()); if (detail.getFieldValues() == null) return; for (Map.Entry> entry : detail.getFieldValues().entrySet()) { if (!allowedFields.contains(entry.getKey()) || entry.getValue() == null) throw new ServiceException("测试详情字段值结构无效"); for (Map.Entry 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 loadedBatteries = testMapper.selectTestBatteries(testId); List batteries = loadedBatteries == null || (loadedBatteries.isEmpty() && test.getBatteries() != null && !test.getBatteries().isEmpty()) ? mutable(test.getBatteries()) : mutable(loadedBatteries); // 字段属于电池组当前配置;每次查询测试都重新读取,避免产生测试字段快照。 List currentFields = testMapper.selectCurrentBatteryFields(testId); Map> fieldsByBattery = new HashMap<>(); for (TTestBatteryField field : mutable(currentFields)) fieldsByBattery.computeIfAbsent(field.getRelationId(), ignored -> new ArrayList<>()).add(field); List loadedCollectorChannels = testMapper.selectTestCollectorChannels(testId); List collectorChannels = loadedCollectorChannels == null ? List.of() : mutable(loadedCollectorChannels); Map bindings = new HashMap<>(); List 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(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.setBatteries(batteries); test.setBatteryNo(joinBatteryNos(batteries)); List 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) { 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) { // 计划工步来自设备观测,不属于测试创建配置,也不写入业务库。 test.setPlanSteps(new ArrayList<>()); validateBatteries(test.getBatteries()); validatePlanSteps(test.getPlanSteps()); validateThresholds(test.getThresholds()); } private void validateBatteries(List batteries) { if (batteries == null || batteries.isEmpty()) throw new ServiceException("测试至少绑定一个电池号"); Set batteryNos = new HashSet<>(); Set channelIds = new HashSet<>(); Set collectorChannelIdentities = new HashSet<>(); Map 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 mappings, Set collectorChannelIdentities) { if (mappings == null || mappings.isEmpty()) throw new ServiceException("每个电池号必须绑定主温度、正电位主通道和负电位主通道"); Set 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 fields) { if (fields == null) return; Set 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 tests) { if (tests.isEmpty()) return; Map> batteriesByTest = new HashMap<>(); List 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 batteries) { return mutable(batteries).stream().map(TTestBattery::getBatteryNo).collect(java.util.stream.Collectors.joining("、")); } private void validatePlanSteps(List planSteps) { if (planSteps.isEmpty()) return; Set identities = new HashSet<>(); Set 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 thresholds) { if (thresholds == null || thresholds.isEmpty()) throw new ServiceException("阈值配置不能为空"); Set 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 (TTestBattery battery : test.getBatteries()) { battery.setTestId(test.getTestId()); testMapper.insertTestBattery(battery); 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()) { threshold.setTestId(test.getTestId()); testMapper.insertTestThreshold(threshold); } } private void validateQueryStatus(String status, Set 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 String normalizeMeasurementCode(String value) { String normalized = trimToNull(value); return normalized == null ? null : normalized.toUpperCase(Locale.ROOT); } private List mutable(List values) { return values == null ? new ArrayList<>() : new ArrayList<>(values); } }