package com.ruoyi.system.service.impl; import java.util.ArrayList; import java.util.Date; import java.util.HashMap; import java.util.HashSet; import java.util.List; import java.util.Map; import java.util.Set; import com.alibaba.fastjson2.JSON; import com.alibaba.fastjson2.JSONException; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import org.springframework.transaction.annotation.Transactional; import com.ruoyi.common.exception.ServiceException; import com.ruoyi.common.utils.StringUtils; import com.ruoyi.system.domain.TTest; import com.ruoyi.system.domain.TTestBattery; import com.ruoyi.system.domain.TTestBatteryField; import com.ruoyi.system.domain.TTestChannelBinding; import com.ruoyi.system.domain.TTestPlanStep; import com.ruoyi.system.domain.TTestResult; import com.ruoyi.system.domain.TTestWarningThreshold; import com.ruoyi.system.domain.vo.TestConfigurationOptionsVo; import com.ruoyi.system.mapper.TTestMapper; import com.ruoyi.system.service.ITTestService; /** 测试配置服务,负责状态边界、关系校验、事务写入和只追加审计。 */ @Service public class TTestServiceImpl implements ITTestService { private static final String WAITING = "WAITING"; private static final String NOT_MONITORED = "NOT_MONITORED"; private static final Set TEST_STATUSES = Set.of( WAITING, "RUNNING", "PAUSED", "RUNNING_ABNORMAL", "COMPLETION_PENDING", "COMPLETED", "TERMINATED"); private static final Set MONITORING_STATUSES = Set.of( NOT_MONITORED, "WAITING_DEVICE_START", "MONITORING", "BACKFILLING", "MONITORING_ENDED", "CANCELLED"); private static final Set THRESHOLD_UNITS = Set.of("A", "V", "Ah", "Wh", "℃"); @Autowired private TTestMapper testMapper; @Override public TTest selectTestById(Long testId) { if (testId == null) throw new ServiceException("测试ID不能为空"); TTest test = testMapper.selectTestById(testId); if (test == null) throw new ServiceException("测试不存在"); loadConfiguration(test); return test; } @Override public List selectTestList(TTest test) { if (test == null) test = new TTest(); test.setTestName(trimToNull(test.getTestName())); test.setBatteryNo(trimToNull(test.getBatteryNo())); validateQueryStatus(test.getTestStatus(), TEST_STATUSES, "测试状态无效"); validateQueryStatus(test.getMonitoringStatus(), MONITORING_STATUSES, "监测状态无效"); return testMapper.selectTestList(test); } @Override public TestConfigurationOptionsVo selectConfigurationOptions() { TestConfigurationOptionsVo options = new TestConfigurationOptionsVo(); options.setUsers(testMapper.selectEnabledUserOptions()); options.setTestTypes(testMapper.selectEnabledTestTypeOptions()); options.setBatteries(testMapper.selectEnabledBatteryOptions()); options.setChannels(testMapper.selectRegisteredChannelOptions()); options.setCollectors(testMapper.selectEnabledCollectorOptions()); return options; } @Override @Transactional(rollbackFor = Exception.class) public Long createTest(TTest test) { if (test == null) throw new ServiceException("测试信息不能为空"); test.setCreatorId(test.getOperatorId()); if (test.getMonitorTimeoutMinutes() == null) test.setMonitorTimeoutMinutes(30); validateBase(test, false); validateConfiguration(test); test.setTestStatus(WAITING); test.setMonitoringStatus(NOT_MONITORED); test.setDelFlag("0"); test.setCreateTime(new Date()); testMapper.insertTest(test); saveConfiguration(test); testMapper.insertTestAudit(test.getTestId(), "TEST_CREATED", test.getOperatorId(), test.getCreateBy(), null, JSON.toJSONString(test)); return test.getTestId(); } @Override @Transactional(rollbackFor = Exception.class) public int updateTest(TTest test) { if (test == null || test.getTestId() == null) throw new ServiceException("测试ID不能为空"); TTest current = testMapper.selectTestByIdForUpdate(test.getTestId()); if (current == null) throw new ServiceException("测试不存在"); loadConfiguration(current); if (!NOT_MONITORED.equals(current.getMonitoringStatus())) throw new ServiceException("开始监测后不能修改测试配置"); test.setCreatorId(current.getCreatorId()); test.setTestStatus(current.getTestStatus()); test.setMonitoringStatus(current.getMonitoringStatus()); if (test.getMonitorTimeoutMinutes() == null) test.setMonitorTimeoutMinutes(current.getMonitorTimeoutMinutes()); validateBase(test, true); validateConfiguration(test); test.setUpdateTime(new Date()); int rows = testMapper.updateTest(test); if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试"); // 监测开始前没有原始采集事实,配置关系可在同一事务内整体替换,审计记录仍只追加。 testMapper.deleteTestBindingsByTestId(test.getTestId()); testMapper.deleteTestPlanStepsByTestId(test.getTestId()); testMapper.deleteTestFieldsByTestId(test.getTestId()); testMapper.deleteTestBatteriesByTestId(test.getTestId()); testMapper.deleteTestThresholdsByTestId(test.getTestId()); saveConfiguration(test); testMapper.insertTestAudit(test.getTestId(), "TEST_CONFIGURATION_UPDATED", test.getOperatorId(), test.getUpdateBy(), JSON.toJSONString(current), JSON.toJSONString(test)); return rows; } @Override public int startTest(Long testId, String updateBy) { throw new ServiceException("开始监测由后续监测动作处理"); } @Override @Transactional(rollbackFor = Exception.class) public int deleteTestByIds(Long[] testIds, 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_MONITORED.equals(current.getMonitoringStatus())) 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 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 planSteps = mutable(testMapper.selectTestPlanSteps(testId)); for (TTestPlanStep step : planSteps) { try { step.setParameters(JSON.parseObject(step.getParametersJson())); } catch (JSONException exception) { throw new ServiceException("计划工步参数无法解析"); } } List batteries = mutable(testMapper.selectTestBatteries(testId)); List fields = mutable(testMapper.selectTestBatteryFields(testId)); Map bindings = new HashMap<>(); for (TTestChannelBinding binding : mutable(testMapper.selectTestChannelBindings(testId))) bindings.put(binding.getBatteryRelationId(), binding); for (TTestBattery battery : batteries) { battery.setBinding(bindings.get(battery.getRelationId())); battery.setFields(fields.stream() .filter(field -> battery.getRelationId().equals(field.getRelationId())) .toList()); } test.setPlanSteps(planSteps); test.setBatteries(batteries); test.setThresholds(mutable(testMapper.selectTestThresholds(testId))); } private void validateBase(TTest test, boolean update) { test.setTestName(trimToNull(test.getTestName())); test.setTestPurpose(trimToNull(test.getTestPurpose())); test.setRemark(trimToNull(test.getRemark())); if (StringUtils.isEmpty(test.getTestName())) throw new ServiceException("测试名称不能为空"); if (test.getTestName().length() > 100) throw new ServiceException("测试名称长度不能超过100个字符"); if (StringUtils.isEmpty(test.getTestPurpose())) throw new ServiceException("测试目的不能为空"); if (test.getRemark() != null && test.getRemark().length() > 500) throw new ServiceException("备注长度不能超过500个字符"); if (test.getMonitorTimeoutMinutes() == null || test.getMonitorTimeoutMinutes() < 1 || test.getMonitorTimeoutMinutes() > 1440) throw new ServiceException("监测超时分钟数必须在1到1440之间"); if (test.getCreatorId() == null || testMapper.countUserEnabled(test.getCreatorId()) == 0) throw new ServiceException("创建人不存在或已停用"); if (test.getPrincipalId() == null || testMapper.countUserEnabled(test.getPrincipalId()) == 0) throw new ServiceException("测试负责人不存在或已停用"); if (test.getTestTypeId() == null || testMapper.countTestTypeEnabled(test.getTestTypeId()) == 0) throw new ServiceException("测试类型不存在或未启用"); TTest same = testMapper.selectTestByName(test.getTestName()); if (same != null && (!update || !test.getTestId().equals(same.getTestId()))) throw new ServiceException("测试名称已存在"); } private void validateConfiguration(TTest test) { // 新建测试只建立业务身份和通道关系,计划工步可在开始监测前补充。 if (test.getPlanSteps() == null) 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<>(); Long groupId = null; for (TTestBattery battery : batteries) { if (battery == null || battery.getGroupId() == null) throw new ServiceException("电池组不能为空"); if (groupId == null) groupId = battery.getGroupId(); else if (!groupId.equals(battery.getGroupId())) throw new ServiceException("同一测试只能选择一个电池组"); battery.setBatteryNo(trimToNull(battery.getBatteryNo())); if (battery.getBatteryNo() == null) throw new ServiceException("电池号不能为空"); if (!batteryNos.add(battery.getBatteryNo())) throw new ServiceException("测试内电池号不可重复"); if (testMapper.countBatteryInEnabledGroup(battery.getGroupId(), battery.getBatteryNo()) == 0) throw new ServiceException("电池号不属于所选启用电池组"); TTestChannelBinding binding = battery.getBinding(); if (binding == null || binding.getChannelId() == null) throw new ServiceException("主要检测柜通道不能为空"); if (!channelIds.add(binding.getChannelId())) throw new ServiceException("同一检测柜通道不可重复绑定"); if (testMapper.countRegisteredChannel(binding.getChannelId()) == 0) throw new ServiceException("检测柜通道不存在、未登记或未启用"); binding.setExpectedBarcode(trimToNull(binding.getExpectedBarcode())); if (binding.getExpectedBarcode() != null && binding.getExpectedBarcode().length() > 100) throw new ServiceException("预期条码长度不能超过100个字符"); if (battery.getCollectorId() != null && testMapper.countCollectorEnabled(battery.getCollectorId()) == 0) throw new ServiceException("采集器不存在或未启用"); validateBatteryFields(battery.getFields()); } } private void validateBatteryFields(List 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 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 (TTestPlanStep step : test.getPlanSteps()) { step.setTestId(test.getTestId()); testMapper.insertTestPlanStep(step); } for (TTestBattery battery : test.getBatteries()) { battery.setTestId(test.getTestId()); testMapper.insertTestBattery(battery); if (battery.getFields() != null) { for (TTestBatteryField field : battery.getFields()) { field.setRelationId(battery.getRelationId()); testMapper.insertTestBatteryField(field); } } TTestChannelBinding binding = battery.getBinding(); binding.setTestId(test.getTestId()); binding.setBatteryRelationId(battery.getRelationId()); binding.setClaimStatus("PENDING"); testMapper.insertTestChannelBinding(binding); } for (TTestWarningThreshold threshold : test.getThresholds()) { threshold.setTestId(test.getTestId()); testMapper.insertTestThreshold(threshold); } } private void validateQueryStatus(String status, Set 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 List mutable(List values) { return values == null ? new ArrayList<>() : new ArrayList<>(values); } }