| | |
| | | ## 设备路径 |
| | | |
| | | - 每个设备适配器实例从数据库读取名称、类型、主机、端口、启停、轮询周期、时区和能力,不在代码中固定连接参数。 |
| | | - 每个实例只有一个串行请求队列;一次 `inquire` 批量查询需监测通道。状态查询默认每 5 秒,历史下载使用较低优先级。 |
| | | - 连接拒绝、最长约 30 秒响应超时、断线和后端重启均进入对账与恢复流程。恢复时重新查询设备事实并从持久化游标补采,不推断设备已停止。 |
| | | - 历史补采按测试结束时间和通道公平轮转,每批最多 1000 条;数据保存成功后才推进数据库游标。 |
| | | - 每个实例只有一个串行请求队列;一次 `inquire` 批量查询需监测通道。状态查询默认每 5 秒。 |
| | | - 连接拒绝、响应超时、断线和后端重启只记录设备/请求异常并继续观测,不自动恢复或追回历史数据。 |
| | | - 解析器拒绝 DOCTYPE/外部实体,限制响应大小,并显式处理缺字段、非法数值和未知枚举。 |
| | | - 协议报文压缩保存到文件系统,数据库仅保存索引、校验值和解析结果;普通报文保留 30 天,解析失败或冲突报文保留 180 天。 |
| | | |
| | |
| | | |
| | | - 开始接收在一个事务内校验全部检测柜和采集器在线可观测及必填映射完整;任一条件失败则整体回滚。暂停接收停止该测试的数据请求,继续接收恢复请求,停止测试进入不可恢复终态并释放业务占用。 |
| | | - 检测柜通道允许运行中接入;同一次设备运行可依次服务多个测试,数据归属按测试级动作时间判断,不建立来源接收区间约束。 |
| | | - 停止测试或设备自然结束后进入内部“完成确认中”;只有所有来源补采完成且接收时段内没有未解释缺口、冲突或异常,才聚合为“检测完成”。 |
| | | - 停止测试或设备自然结束后直接进入“检测完成”;成功接收的数据追加保存,缺失、序号跳跃和采样间隔异常只作为可见事实。 |
| | | - “检测完成”和“已终止”是不可恢复终态。异常恢复、数据缺口处置、终止和负责人转交都追加审计。 |
| | | - 相同“测试 + 通道 + 异常类型”在未恢复期间聚合首次时间、最近时间和次数;恢复后关闭,再次发生创建新事件。 |
| | | |
| | |
| | | |
| | | ## 验证 |
| | | |
| | | - 设备协议或调度改动至少覆盖正常多通道、拒绝连接、断线恢复、超时、畸形 XML、重复/乱序/缺失历史和重启补采中受影响的场景。 |
| | | - 设备协议或调度改动至少覆盖正常多通道、拒绝连接、断线、超时、畸形 XML 和实时数据幂等冲突等受影响场景。 |
| | | - 运行设备模块测试:`mvn -pl ruoyi-device test`。 |
| | | - 跨模块改动运行全量测试:`mvn test`。 |
| | | - 验证可执行包:`mvn -pl ruoyi-admin -am package -DskipTests`。启动验证时使用可写的 `LOG_PATH`,完成后停止临时进程。 |
| | |
| | | @GetMapping("/{testId}/live-state") |
| | | public AjaxResult liveState(@PathVariable Long testId) { return success(liveService.current(testId)); } |
| | | |
| | | /** 查询测试完整性问题和可追溯的历史异常事实。 */ |
| | | /** 推送实时快照、设备状态和可追溯的异常事实。 */ |
| | | @PreAuthorize("@ss.hasPermi('system:test:query')") |
| | | @GetMapping(value = "/{testId}/live", produces = MediaType.TEXT_EVENT_STREAM_VALUE) |
| | | public SseEmitter live(@PathVariable Long testId) { return liveService.stream(testId); } |
| | |
| | | |
| | | DROP TABLE t_test_integrity_issue; |
| | | DROP TABLE t_test_backfill_cursor; |
| | | DROP TABLE t_data_gap; |
| | | DROP TABLE t_history_sync_cursor; |
| | | DROP TABLE t_test_collector_exception_interval; |
| | | DROP TABLE t_test_reception_interval; |
| | | DROP TABLE t_collector_channel_lease; |
| | |
| | | assertTrue(sql.contains("DROP TABLE t_test_reception_interval")); |
| | | assertTrue(sql.contains("DROP TABLE t_collector_channel_lease")); |
| | | assertTrue(sql.contains("DROP TABLE t_channel_lease")); |
| | | assertTrue(sql.contains("DROP TABLE t_data_gap")); |
| | | assertTrue(sql.contains("DROP TABLE t_history_sync_cursor")); |
| | | assertTrue(sql.contains("DROP FOREIGN KEY fk_sample_reception_interval")); |
| | | assertTrue(sql.contains("DROP FOREIGN KEY fk_collector_sample_reception_interval")); |
| | | assertTrue(sql.contains("DROP COLUMN reception_interval_id")); |
| | |
| | | DeviceAdapter adapter = Mockito.mock(DeviceAdapter.class); |
| | | ChannelIdentity identity = new ChannelIdentity("lab-bts", 22, 1, 1, 1); |
| | | when(testMapper.hasReceivingTestForAdapter("lab-bts")).thenReturn(0); |
| | | when(testMapper.hasPausedOrEndedTestForAdapter("lab-bts")).thenReturn(1); |
| | | when(adapter.discoverChannels()).thenReturn(List.of(new DetectedChannel(identity, "127.0.0.1", true))); |
| | | when(registry.execute(eq("lab-bts"), any())).thenAnswer(invocation -> { |
| | | @SuppressWarnings("unchecked") |
| | |
| | | { |
| | | this.stateStore = stateStore; |
| | | state = stateStore.loadOrInitialize(defaultState()); |
| | | // 仅清理 Ticket13 之前嵌在主状态中的旧演示历史;独立历史由存储实现保留并继续补采。 |
| | | // 仅清理 Ticket13 之前嵌在主状态中的旧演示历史;独立历史由模拟器存储保留。 |
| | | state = stateStore.resetHistoryForDemoStartup(state); |
| | | restoreBtsHistory(); |
| | | validateRestoredState(); |
| | |
| | | requiredAttribute(element, "steptype"), optionalLong(element, "testtime"), parseDeviceTime(rawTime), rawTime, |
| | | optionalDecimal(element, "volt"), optionalDecimal(element, "curr"), optionalDecimal(element, "cap"), |
| | | optionalDecimal(element, "eng"), dynamicMeasurements(element, HISTORY_ATTRIBUTES))); |
| | | // 解析层保留重复和乱序事实;完整性检查按 seqid 另行判定并解释缺口。 |
| | | // 解析层保留重复和乱序事实;业务层只展示并记录这些设备数据事实。 |
| | | next = Math.max(next, sequence + 1); |
| | | } |
| | | return new HistoryPage(channel, btsTestId, requestedStart, next, samples); |
| | |
| | | List<TTestChannelBinding> selectTestChannelBindings(Long testId); |
| | | List<TTestCollectorChannel> selectTestCollectorChannels(Long testId); |
| | | List<TTestWarningThreshold> selectTestThresholds(Long testId); |
| | | /** 按来源和起始边界顺序返回测试接收区间。 */ |
| | | /** 查询当前正在接收的测试,供接收调度绑定测试事实。 */ |
| | | List<Long> selectReceivingTestIds(); |
| | | List<java.util.Map<String, Object>> selectPersistedRecords(@org.apache.ibatis.annotations.Param("testId") Long testId); |
| | |
| | | |
| | | /** 锁定检测柜通道的最新可观测事实,供开始接收校验使用。 */ |
| | | TDetectionChannel selectReceptionChannelForUpdate(Long channelId); |
| | | /** 锁定当前检测柜通道租约;返回空表示可接入。 */ |
| | | /** 为每个绑定来源追加一个开放接收区间;区间关闭后仅保留历史事实。 */ |
| | | /** 在停止动作事务内关闭该测试全部开放区间,返回实际关闭数量。 */ |
| | | int hasReceivingTestForAdapter(@Param("adapterCode") String adapterCode); |
| | | int hasPausedOrEndedTestForAdapter(@Param("adapterCode") String adapterCode); |
| | | List<TDetectionChannel> selectReceivingChannelsForAdapter(@Param("adapterCode") String adapterCode); |
| | | boolean collectorOnline(Long collectorId); |
| | | int updateReceptionStarted(@Param("testId") Long testId, @Param("startedAtUtc") Date startedAtUtc, |
| | |
| | | int updateReceptionStopped(@Param("testId") Long testId, @Param("updateBy") String updateBy, |
| | | @Param("endedAtUtc") Date endedAtUtc); |
| | | |
| | | /** 查询仍在接收中的采集器来源。 */ |
| | | /** 查询来源当前开放的断线区间。 */ |
| | | String selectBtsSampleContentHash(@Param("channelId") Long channelId, @Param("btsTestId") String btsTestId, |
| | | @Param("sequenceId") Long sequenceId); |
| | | |
| | | /** 查询需要自然结束或继续补采的测试。 */ |
| | | /** 按稳定来源身份锁定补采游标,避免应用重启重复推进。 */ |
| | | /** 按测试事实查询通道序号,避免补采重复写入已即时接收的采样。 */ |
| | | String selectTestSampleContentHash(@Param("testId") Long testId, @Param("channelId") Long channelId, |
| | | @Param("sequenceId") Long sequenceId); |
| | | int insertBtsSample(@Param("testId") Long testId, @Param("bindingId") Long bindingId, |
| | | @Param("channelId") Long channelId, @Param("intervalId") Long intervalId, |
| | | @Param("channelId") Long channelId, |
| | | @Param("btsTestId") String btsTestId, @Param("sequenceId") Long sequenceId, |
| | | @Param("deviceTimeUtc") Date deviceTimeUtc, @Param("deviceTimeRaw") String deviceTimeRaw, |
| | | @Param("receivedAtUtc") Date receivedAtUtc, @Param("cycleNo") Integer cycleNo, |
| | |
| | | int insertBtsMeasurement(@Param("sampleId") Long sampleId, @Param("metricCode") String metricCode, |
| | | @Param("metricValue") java.math.BigDecimal metricValue, @Param("unit") String unit, |
| | | @Param("sourceType") String sourceType); |
| | | Long selectBtsSampleId(@Param("channelId") Long channelId, @Param("btsTestId") String btsTestId, |
| | | @Param("sequenceId") Long sequenceId); |
| | | int insertCollectorSample(@Param("testId") Long testId, @Param("bindingId") Long bindingId, |
| | | @Param("intervalId") Long intervalId, @Param("collectorId") Long collectorId, |
| | | @Param("collectorId") Long collectorId, |
| | | @Param("channelCode") String channelCode, |
| | | @Param("sequenceId") Long sequenceId, @Param("deviceTimeUtc") Date deviceTimeUtc, |
| | | @Param("deviceTimeRaw") String deviceTimeRaw, @Param("receivedAtUtc") Date receivedAtUtc, |
| | | @Param("metricCode") String metricCode, @Param("metricValue") java.math.BigDecimal metricValue, |
| | | @Param("unit") String unit, @Param("contentSha256") byte[] contentSha256); |
| | | Long selectCollectorSampleId(@Param("collectorId") Long collectorId, @Param("channelCode") String channelCode, |
| | | @Param("sequenceId") Long sequenceId, @Param("metricCode") String metricCode); |
| | | String selectCollectorSampleContentHash(@Param("collectorId") Long collectorId, |
| | | @Param("channelCode") String channelCode, @Param("sequenceId") Long sequenceId, |
| | | @Param("metricCode") String metricCode); |
| | | int insertDataRevisionAction(TTestDataRevisionAction action); |
| | | List<TTestDataRevisionAction> selectDataRevisionActions(TTestDataRevisionAction query); |
| | | TTestDataRevisionAction selectDataRevisionActionById(Long actionId); |
| | |
| | | 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; |
| | |
| | | if (current == null) throw new ServiceException("测试不存在"); |
| | | if (!"PAUSED".equals(current.getReceptionStatus())) |
| | | throw new ServiceException("当前测试不是暂停接收状态"); |
| | | // 加锁查询只返回测试主表,继续接收需要完整绑定配置来重建各来源接收区间。 |
| | | // 加锁查询只返回测试主表,继续接收前重新加载绑定配置以校验来源仍可用。 |
| | | loadConfiguration(current); |
| | | Date actionAt = new Date(); |
| | | if (testMapper.updateReceptionResumed(testId, updateBy, actionAt) != 1) |
| | |
| | | "实时接收内容冲突 testid=" + btsTestId + ", seqid=" + record.sequenceId()); |
| | | return; |
| | | } |
| | | int inserted = mapper.insertBtsSample(test.getTestId(), binding.getBindingId(), binding.getChannelId(), null, |
| | | int inserted = mapper.insertBtsSample(test.getTestId(), binding.getBindingId(), binding.getChannelId(), |
| | | btsTestId, record.sequenceId(), Date.from(record.deviceTime()), |
| | | record.deviceTime().toString(), new Date(), record.cycleNo(), record.stepNo(), record.stepType(), |
| | | record.stepName(), null, |
| | |
| | | <result property="maxValue" column="max_value" /> |
| | | <result property="unit" column="unit" /> |
| | | <result property="unitMetadata" column="unit_metadata" /> |
| | | <result property="integrityConclusion" column="integrity_conclusion" /> |
| | | <result property="severity" column="severity" /> |
| | | <result property="delFlag" column="del_flag" /> |
| | | </resultMap> |
| | |
| | | for update |
| | | </select> |
| | | |
| | | <update id="updateTestCompleted"> |
| | | update t_test set test_status = 'COMPLETED', update_time = #{updatedAtUtc} |
| | | where test_id = #{testId} and reception_status = 'ENDED' and test_status = 'COMPLETION_PENDING' |
| | | </update> |
| | | <select id="selectBtsSampleContentHash" resultType="string"> |
| | | select hex(content_sha256) from t_test_sample |
| | | where channel_id = #{channelId} and bts_test_id = #{btsTestId} and seq_id = #{sequenceId} |
| | |
| | | (sample_id, metric_code, metric_value, unit, source_type) |
| | | values (#{sampleId}, #{metricCode}, #{metricValue}, #{unit}, #{sourceType}) |
| | | </insert> |
| | | <select id="selectBtsSampleId" resultType="long"> |
| | | select sample_id from t_test_sample |
| | | where channel_id = #{channelId} and bts_test_id = #{btsTestId} and seq_id = #{sequenceId} |
| | | </select> |
| | | <insert id="insertCollectorSample"> |
| | | insert ignore into t_test_collector_sample |
| | | (test_id, binding_id, collector_id, collector_channel_code, sequence_id, device_time_utc, |
| | |
| | | values (#{testId}, #{bindingId}, #{collectorId}, #{channelCode}, #{sequenceId}, #{deviceTimeUtc}, |
| | | #{deviceTimeRaw}, #{receivedAtUtc}, #{metricCode}, #{metricValue}, #{unit}, #{contentSha256}) |
| | | </insert> |
| | | <select id="selectCollectorSampleId" resultType="long"> |
| | | select sample_id from t_test_collector_sample |
| | | where collector_id = #{collectorId} and collector_channel_code = #{channelCode} |
| | | and sequence_id = #{sequenceId} and metric_code = #{metricCode} |
| | | </select> |
| | | <select id="selectCollectorSampleContentHash" resultType="string"> |
| | | select hex(content_sha256) from t_test_collector_sample |
| | | where collector_id = #{collectorId} and collector_channel_code = #{channelCode} |
| | | and sequence_id = #{sequenceId} and metric_code = #{metricCode} |
| | | </select> |
| | | <insert id="insertSampleConflict"> |
| | | insert into t_test_exception |
| | | (test_id, channel_id, exception_type, exception_status, severity, |
| | |
| | | TRUNCATE TABLE t_test_sample_measurement; |
| | | TRUNCATE TABLE t_test_sample; |
| | | TRUNCATE TABLE t_test_collector_sample; |
| | | TRUNCATE TABLE t_test_backfill_cursor; |
| | | TRUNCATE TABLE t_history_sync_cursor; |
| | | TRUNCATE TABLE t_data_gap; |
| | | TRUNCATE TABLE t_test_integrity_issue; |
| | | TRUNCATE TABLE t_test_reception_interval; |
| | | TRUNCATE TABLE t_channel_lease; |
| | | TRUNCATE TABLE t_collector_channel_lease; |
| | | TRUNCATE TABLE t_channel_snapshot; |
| | | TRUNCATE TABLE t_device_run_event; |
| | | TRUNCATE TABLE t_test_exception; |