| | |
| | | @Autowired |
| | | private TTestMapper testMapper; |
| | | |
| | | /** |
| | | * 按测试主键读取测试配置,同时装配电池号、阈值、通道绑定等配置关系。 |
| | | * |
| | | * @param testId 测试主键 |
| | | * @return 测试配置及其业务状态 |
| | | * @throws ServiceException 测试ID为空或测试不存在 |
| | | */ |
| | | @Override |
| | | public TTest selectTestById(Long testId) |
| | | { |
| | |
| | | return test; |
| | | } |
| | | |
| | | /** |
| | | * 按正式测试状态、接收状态及配置条件查询测试列表。 |
| | | * |
| | | * <p>查询条件中的测试状态与接收状态都须是现行取值,历史兼容状态不参与筛选。</p> |
| | | * |
| | | * @param test 查询条件;为空时按无条件查询处理 |
| | | * @return 测试列表,含各测试绑定的电池号 |
| | | * @throws ServiceException 传入的测试状态或接收状态不是现行取值 |
| | | */ |
| | | @Override |
| | | public List<TTest> selectTestList(TTest test) |
| | | { |
| | |
| | | return tests; |
| | | } |
| | | |
| | | /** |
| | | * 返回创建或编辑测试时可选的来源:启用用户、启用测试类型、启用电池组的电池号、已正式登记的通道与采集器。 |
| | | * |
| | | * @return 测试配置选项,五类选项都可能为空列表 |
| | | */ |
| | | @Override |
| | | public TestConfigurationOptionsVo selectConfigurationOptions() |
| | | { |
| | |
| | | return options; |
| | | } |
| | | |
| | | /** |
| | | * 创建一条待测试记录,在同一事务内写入测试配置和电池号、阈值、通道绑定关系。 |
| | | * |
| | | * <p>前置条件:调用方已从登录会话取得操作人,测试配置通过基本信息与配置关系校验。 |
| | | * 创建人取当前操作人,监测超时缺省 30 分钟,初始状态固定为“待测试 + 未开始接收”。</p> |
| | | * |
| | | * @param test 测试配置,含电池号、阈值、检测柜通道绑定与采集器映射 |
| | | * @return 新建测试主键 |
| | | * @throws ServiceException 测试信息为空、操作人缺失或配置校验不通过 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public Long createTest(TTest test) |
| | | { |
| | | if (test == null) throw new ServiceException("测试信息不能为空"); |
| | | // STEP 1: 规范化系统赋值字段,创建人取当前操作人,监测超时缺省 30 分钟。 |
| | | test.setCreatorId(test.getOperatorId()); |
| | | if (test.getMonitorTimeoutMinutes() == null) test.setMonitorTimeoutMinutes(30); |
| | | // STEP 2: 校验基本信息与电池号、阈值、通道绑定等配置关系。 |
| | | validateBase(test, false); |
| | | validateConfiguration(test); |
| | | // STEP 3: 初始化为“待测试 + 未开始接收”,测试状态只在开始接收时流转。 |
| | | test.setTestStatus(WAITING); |
| | | test.setReceptionStatus(NOT_STARTED); |
| | | test.setDelFlag("0"); |
| | | test.setCreateTime(new Date()); |
| | | testMapper.insertTest(test); |
| | | // STEP 4: 在同一事务内写入电池号、阈值、通道绑定与采集器映射。 |
| | | saveConfiguration(test); |
| | | // 最终基线移除了 t_test_audit;Controller 的 @Log 负责记录创建操作,业务表只保存配置事实。 |
| | | return test.getTestId(); |
| | | } |
| | | |
| | | /** |
| | | * 仅在开始接收之前整体替换测试配置,测试状态与接收状态保持不变。 |
| | | * |
| | | * <p>前置条件:测试处于“待测试 + 未开始接收”。开始接收后设备数据已按配置归属, |
| | | * 配置不再允许修改;替换范围包括电池号、阈值和通道绑定,在同一事务内完成。</p> |
| | | * |
| | | * @param test 新的测试配置,须带测试主键 |
| | | * @return 更新行数,成功固定为 1 |
| | | * @throws ServiceException 测试ID为空、测试不存在、已开始接收或配置校验不通过 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int updateTest(TTest test) |
| | | { |
| | | if (test == null || test.getTestId() == null) throw new ServiceException("测试ID不能为空"); |
| | | // STEP 1: 取测试行锁并确认仍处于“待测试 + 未开始接收”。 |
| | | TTest current = testMapper.selectTestByIdForUpdate(test.getTestId()); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | loadConfiguration(current); |
| | | if (!stateMatches(current, WAITING, NOT_STARTED)) |
| | | throw new ServiceException("开始接收后不能修改测试配置"); |
| | | |
| | | // STEP 2: 保留创建人与两个状态字段,只接受配置类字段的修改。 |
| | | test.setCreatorId(current.getCreatorId()); |
| | | test.setTestStatus(current.getTestStatus()); |
| | | test.setReceptionStatus(current.getReceptionStatus()); |
| | |
| | | int rows = testMapper.updateTest(test); |
| | | if (rows != 1) throw new ServiceException("测试配置已变更,请刷新后重试"); |
| | | |
| | | // 开始接收前没有数据归属事实,配置关系可在同一事务内整体替换;操作审计由 Controller @Log 追加。 |
| | | // STEP 3: 开始接收前没有数据归属事实,配置关系可在同一事务内整体替换;操作审计由 Controller @Log 追加。 |
| | | testMapper.deleteTestCollectorChannelsByTestId(test.getTestId()); |
| | | testMapper.deleteTestBindingsByTestId(test.getTestId()); |
| | | testMapper.deleteTestBatteriesByTestId(test.getTestId()); |
| | |
| | | } |
| | | |
| | | /** |
| | | * 建立测试接收边界;来源离线不阻断状态流转,只有成功观测数据才会写入测试事实。 |
| | | * 开始接收:校验全部绑定来源后建立接收边界,边界之后的成功观测才归属本测试。 |
| | | * |
| | | * <p>前置条件:测试处于“待测试 + 未开始接收”,每个电池号都绑定启用的检测柜通道与采集器, |
| | | * 且同一检测柜通道在本测试内不重复。来源离线不阻断状态流转,允许从已运行设备中途接入; |
| | | * 本动作只改变系统数据请求和测试状态,不向设备发送控制命令。</p> |
| | | * |
| | | * @param testId 测试主键 |
| | | * @param operatorId 操作人用户 ID |
| | | * @param updateBy 更新人账号,写入测试行的 update_by |
| | | * @return 更新行数,成功固定为 1 |
| | | * @throws ServiceException 入参为空、测试不存在、状态不匹配、来源校验失败或数据来源已被其他测试占用 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | |
| | | { |
| | | if (testId == null) throw new ServiceException("测试ID不能为空"); |
| | | if (operatorId == null) throw new ServiceException("操作人不能为空"); |
| | | // STEP 1: 取测试行锁,确认状态为“待测试 + 未开始接收”并加载绑定配置。 |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | if (!stateMatches(current, WAITING, NOT_STARTED)) |
| | |
| | | loadConfiguration(current); |
| | | if (current.getBatteries().isEmpty()) throw new ServiceException("测试未配置电池号"); |
| | | |
| | | // STEP 2: 逐电池号校验检测柜通道唯一、通道启用、采集器存在且映射完整。 |
| | | Date actionAt = new Date(); |
| | | Set<Long> channelIds = new HashSet<>(); |
| | | Set<String> collectorChannels = new HashSet<>(); |
| | |
| | | |
| | | } |
| | | |
| | | // STEP 3: 在测试行锁内推进为“检测中 + 接收中”,并把此刻写为接收边界。 |
| | | try |
| | | { |
| | | if (testMapper.updateReceptionStarted(testId, actionAt, runtimeAttach, updateBy) != 1) |
| | |
| | | } |
| | | catch (DuplicateKeyException exception) |
| | | { |
| | | // 检测柜通道已被其他接收中的测试占用,唯一索引保证不会同时归属两个测试。 |
| | | throw new ServiceException("数据来源已被其他测试占用,请刷新后重试"); |
| | | } |
| | | } |
| | | |
| | | /** |
| | | * 暂停接收:停止为本次测试请求实时数据,保留已持久化的历史事实。 |
| | | * |
| | | * <p>前置条件:测试处于“检测中 + 接收中”。设备不接收暂停命令, |
| | | * 暂停期间停止请求数据且不补采,暂停空档不视为数据缺口。</p> |
| | | * |
| | | * @param testId 测试主键 |
| | | * @param operatorId 操作人用户 ID |
| | | * @param updateBy 更新人账号,写入测试行的 update_by |
| | | * @return 更新行数,成功固定为 1 |
| | | * @throws 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("操作人不能为空"); |
| | | // STEP 1: 取测试行锁,确认状态为“检测中 + 接收中”。 |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | if (!stateMatches(current, "RUNNING", "RECEIVING")) |
| | | throw new ServiceException("当前测试不是接收中状态"); |
| | | |
| | | // STEP 2: 推进为“暂停 + 暂停接收”;不记录接收边界,恢复时再重置边界。 |
| | | Date actionAt = new Date(); |
| | | if (testMapper.updateReceptionPaused(testId, updateBy, actionAt) != 1) |
| | | throw new ServiceException("测试状态已变更,请刷新后重试"); |
| | |
| | | return 1; |
| | | } |
| | | |
| | | /** |
| | | * 继续接收:为全部绑定来源恢复实时数据请求,只追加新的成功接收事实。 |
| | | * |
| | | * <p>前置条件:测试处于“暂停 + 暂停接收”,且绑定来源仍可用。 |
| | | * 继续接收会刷新接收边界,暂停空档不补采;设备已经结束时不得继续接收。</p> |
| | | * |
| | | * @param testId 测试主键 |
| | | * @param operatorId 操作人用户 ID |
| | | * @param updateBy 更新人账号,写入测试行的 update_by |
| | | * @return 更新行数,成功固定为 1 |
| | | * @throws ServiceException 入参为空、测试不存在或当前不是暂停接收状态 |
| | | */ |
| | | @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("操作人不能为空"); |
| | | // STEP 1: 取测试行锁,确认状态为“暂停 + 暂停接收”。 |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | if (!stateMatches(current, "PAUSED", "PAUSED")) |
| | | throw new ServiceException("当前测试不是暂停接收状态"); |
| | | // 加锁查询只返回测试主表,继续接收前重新加载绑定配置以校验来源仍可用。 |
| | | // STEP 2: 加锁查询只返回测试主表,继续接收前重新加载绑定配置以校验来源仍可用。 |
| | | loadConfiguration(current); |
| | | // STEP 3: 推进为“检测中 + 接收中”,并把此刻刷新为新的接收边界。 |
| | | Date actionAt = new Date(); |
| | | if (testMapper.updateReceptionResumed(testId, updateBy, actionAt) != 1) |
| | | throw new ServiceException("测试状态已变更,请刷新后重试"); |
| | | return 1; |
| | | } |
| | | |
| | | /** |
| | | * 停止测试:停止数据请求并进入“检测完成 + 已结束接收”的不可恢复终态。 |
| | | * |
| | | * <p>前置条件:测试处于“检测中 + 接收中”或“暂停 + 暂停接收”。数据缺失、序号跳跃和 |
| | | * 采样间隔异常不阻止完成;不向检测柜或采集器发送停止命令,设备仍按现场事实独立运行。</p> |
| | | * |
| | | * @param testId 测试主键 |
| | | * @param operatorId 操作人用户 ID |
| | | * @param updateBy 更新人账号,写入测试行的 update_by |
| | | * @return 更新行数,成功固定为 1 |
| | | * @throws ServiceException 入参为空、测试不存在或当前状态不允许停止 |
| | | */ |
| | | @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("操作人不能为空"); |
| | | // STEP 1: 取测试行锁,确认处于可停止的两种状态之一。 |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | if (!(stateMatches(current, "RUNNING", "RECEIVING") |
| | | || stateMatches(current, "PAUSED", "PAUSED"))) |
| | | throw new ServiceException("当前测试不能停止"); |
| | | // STEP 2: 推进为“检测完成 + 已结束接收”并记录结束时间,不执行完整性门禁。 |
| | | Date actionAt = new Date(); |
| | | if (testMapper.updateReceptionStopped(testId, updateBy, actionAt) != 1) |
| | | throw new ServiceException("测试状态已变更,请刷新后重试"); |
| | |
| | | && expectedTestStatus.equals(test.getTestStatus()); |
| | | } |
| | | |
| | | /** |
| | | * 在任意生命周期整体删除测试及其全部子数据,删除后不可恢复。 |
| | | * |
| | | * <p>前置条件:调用方已确认操作人身份。逐条测试加测试行锁后清理绑定、阈值、三类数据、 |
| | | * 逻辑工步、预警异常和修订记录,最后删除测试主记录;按 ADR-0047 不写操作日志、修订记录或墓碑, |
| | | * 也不删除电池组、设备登记和独立保存的报表。</p> |
| | | * |
| | | * @param testIds 待删除的测试主键数组 |
| | | * @param operatorId 操作人用户 ID |
| | | * @param updateBy 更新人账号 |
| | | * @return 实际删除的测试数量 |
| | | * @throws ServiceException 未选择测试或测试不存在 |
| | | */ |
| | | @Override |
| | | @Transactional(rollbackFor = Exception.class) |
| | | public int deleteTestByIds(Long[] testIds, Long operatorId, String updateBy) |
| | |
| | | int rows = 0; |
| | | for (Long testId : testIds) |
| | | { |
| | | // STEP 1: 加测试行锁,与接收事务互斥;删除提交后不能再向该测试写入数据。 |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | // 与接收事务使用同一测试行锁,删除提交后不能再向该测试写入数据;整体删除不留审计或墓碑。 |
| | | // STEP 2: 在同一事务内清理全部测试子数据,整体删除不留审计或墓碑。 |
| | | testMapper.deleteTestRealtimeRecordsByTestId(testId); |
| | | testMapper.deleteTestStepRecordsByTestId(testId); |
| | | testMapper.deleteTestCycleRecordsByTestId(testId); |
| | |
| | | testMapper.deleteTestBindingsByTestId(testId); |
| | | testMapper.deleteTestThresholdRowsByTestId(testId); |
| | | testMapper.deleteTestBatteriesByTestId(testId); |
| | | // STEP 3: 最后删除测试主记录,行数不为 1 说明配置已被并发改动。 |
| | | int deleted = testMapper.deleteTestById(testId, updateBy); |
| | | if (deleted != 1) throw new ServiceException("测试配置已变更,请刷新后重试"); |
| | | rows += deleted; |
| | |
| | | return rows; |
| | | } |
| | | |
| | | /** |
| | | * 保存当前测试的逻辑工步合并规则,整体替换原有映射。 |
| | | * |
| | | * <p>前置条件:映射须按循环内连续位置升序、互不重叠、编号从 1 起,且每项都有名称。 |
| | | * 逻辑工步只用于统计图表和报表等派生视图,不改写原始采样记录。</p> |
| | | * |
| | | * @param testId 测试主键 |
| | | * @param mappings 逻辑工步映射列表,为空表示清空 |
| | | * @param operatorId 操作人用户 ID |
| | | * @param updateBy 更新人账号 |
| | | * @return 更新行数,成功固定为 1 |
| | | * @throws ServiceException 测试或操作人为空、测试不存在或映射不满足连续升序且互不重叠 |
| | | */ |
| | | @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("测试不存在"); |
| | | // STEP 1: 校验映射为连续的循环内位置区间,编号从 1 起且互不重叠。 |
| | | List<TTestLogicStepMapping> normalized = mappings == null ? List.of() : mappings; |
| | | int previousEnd = 0; |
| | | int expectedNo = 1; |
| | |
| | | previousEnd = mapping.getEndStepNo(); |
| | | expectedNo++; |
| | | } |
| | | // STEP 2: 整体替换映射 JSON,作用于该测试的全部循环。 |
| | | String newJson = JSON.toJSONString(normalized); |
| | | int rows = testMapper.updateLogicStepMappings(testId, newJson, updateBy, new Date()); |
| | | if (rows != 1) throw new ServiceException("工步设置已变更,请刷新后重试"); |
| | | return rows; |
| | | } |
| | | |
| | | /** |
| | | * 保存测试详情当前最新版,允许测试处于任意状态。 |
| | | * |
| | | * <p>前置条件:实验名称不超过 200 个字符,字段名称与字段值通过校验。 |
| | | * 详情只保留当前最新版,直接读取电池组当前字段值,不创建测试字段快照,也不产生修订记录。</p> |
| | | * |
| | | * @param testId 测试主键 |
| | | * @param detail 测试补充详情,含实验名称、字段名称与人工填写值 |
| | | * @param operatorId 操作人用户 ID |
| | | * @param updateBy 更新人账号 |
| | | * @return 更新行数,成功固定为 1 |
| | | * @throws ServiceException 测试或操作人为空、测试不存在、实验名称或字段校验不通过 |
| | | */ |
| | | @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("测试详情不能为空"); |
| | | // STEP 1: 加测试行锁并确认测试存在。 |
| | | TTest current = testMapper.selectTestByIdForUpdate(testId); |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | // STEP 2: 规范化并校验实验名称、字段名称与字段值。 |
| | | 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); |
| | | // STEP 3: 覆盖保存最新版详情,不追加修订记录。 |
| | | int rows = testMapper.updateConsoleDetail(testId, JSON.toJSONString(detail), updateBy, new Date()); |
| | | if (rows != 1) throw new ServiceException("测试详情保存失败,请刷新后重试"); |
| | | return rows; |