From f298a7bb56d8442cbd117e103d8e83a99d38faf3 Mon Sep 17 00:00:00 2001 From: caozehui <2427765068@qq.com> Date: Thu, 18 Jun 2026 14:53:44 +0800 Subject: [PATCH] docs: add formal test third-party checksquare design --- ...mal-test-third-party-checksquare-design.md | 359 ++++++++++++++++++ 1 file changed, 359 insertions(+) create mode 100644 docs/superpowers/specs/2026-06-18-formal-test-third-party-checksquare-design.md diff --git a/docs/superpowers/specs/2026-06-18-formal-test-third-party-checksquare-design.md b/docs/superpowers/specs/2026-06-18-formal-test-third-party-checksquare-design.md new file mode 100644 index 00000000..9e0569c4 --- /dev/null +++ b/docs/superpowers/specs/2026-06-18-formal-test-third-party-checksquare-design.md @@ -0,0 +1,359 @@ +# 数模式正式检测完成后通知第三方 checksquare 接口设计 + +## 背景 + +当前系统在数模式正式检测完成后,已经具备: + +- 正式检测上下文管理能力 +- 检测结果、监测点状态、装置状态入库能力 +- 装置检测开始时间与结束时间落库能力 + +现在需要在数模式正式检测成功完成后,调用第三方接口 `POST /steady/checksquare/create`,通知第三方系统开始后续处理。 + +第三方接口当前为免 token 模式,请求头仅需: + +- `Content-Type: application/json` + +## 目标 + +- 仅在数模式正式检测成功完成后触发第三方通知 +- 仅在 `SourceOperateCodeEnum.ALL_TEST.getValue().equals(FormalTestManager.reCheckType)` 时触发 +- 请求体中的 `lineIds`、`timeStart`、`timeEnd` 使用当前批次正式检测数据 +- `indicatorCodes` 固定传空数组 +- 调用采用异步方式,不阻塞当前正式检测完成主流程 +- 调用失败后按 `2^failCount` 秒退避重试,最多重试 3 次 +- 同一批次检测在单次进程生命周期内只通知一次 + +## 非目标 + +- 不处理比对式检测 +- 不处理异常结束、失败结束或中断结束路径 +- 不解析第三方响应体中的 `taskId`、`taskNo`、`taskStatus` +- 不把第三方响应体落库 +- 不引入数据库任务表、消息队列或持久化幂等机制 +- 不保证服务重启后的跨进程幂等 + +## 已确认决策 + +- 方案采用纯内存幂等标记 + `@Async` 异步调用与重试 +- 第三方调用方法放在 `com.njcn.gather.result.service` 层 +- 触发位置不放在 `PqDevServiceImpl` +- 触发位置放在数模式正式检测完成后的成功收口点 +- 第三方调用成功判定只看 HTTP 调用成功且未抛异常 +- 失败重试规则为最多 3 次,间隔分别为 2 秒、4 秒、8 秒 + +## 触发边界 + +### 唯一触发范围 + +本次功能只覆盖数模式正式检测成功完成路径。 + +推荐触发位置: + +- [SocketDevResponseService](D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\detection\handler\SocketDevResponseService.java) + +具体挂接点: + +- 在数模式正式检测最终成功分支中 +- `iPqDevService.updateResult(param.getDevIds(), valueType, param.getCode(), param.getUserId(), param.getTemperature(), param.getHumidity(), true)` 执行完成之后 +- `CnSocketUtil.quitSend(param)` 之前调用结果服务的第三方通知入口 + +### 为什么不放在 `PqDevServiceImpl` + +- `PqDevServiceImpl` 是通用状态汇总与入库层,不等价于正式检测生命周期结束 +- 该类会被多条业务链路复用,挂接后容易误触发 +- 本次需求明确要求“正式检测完成后”触发,因此应挂在正式检测成功收口边界 + +## 架构设计 + +### 入口职责 + +在 `result` service 层新增一个对外入口,例如: + +```java +void tryNotifyThirdPartyAfterFormalTest(PreDetectionParam param) +``` + +建议实现位置: + +- [IResultService](D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\result\service\IResultService.java) +- [ResultServiceImpl](D:\njcn\test\CN_Gather\detection\src\main\java\com\njcn\gather\result\service\impl\ResultServiceImpl.java) + +该入口负责: + +- 校验当前是否满足触发条件 +- 组装幂等 key +- 做内存去重 +- 触发异步第三方调用 + +### 异步职责 + +在 `ResultServiceImpl` 中新增 `@Async` 方法,负责: + +- 发起第三方 HTTP 请求 +- 捕获异常 +- 按指数退避重试 +- 更新内存中的执行状态 +- 记录成功或失败日志 + +## 触发条件 + +只有同时满足以下条件时才允许发起第三方通知: + +1. 当前链路属于数模式正式检测最终成功收口点 +2. `SourceOperateCodeEnum.ALL_TEST.getValue().equals(FormalTestManager.reCheckType)` +3. `FormalTestManager.checkStartTime` 不为空 +4. `param.getPlanId()` 不为空 +5. `param.getDevIds()` 非空 +6. 当前批次幂等 key 未处于 `RUNNING` 或 `SUCCESS` + +任一条件不满足时直接返回,不抛业务异常。 + +## 请求设计 + +### 接口 + +- 方法:`POST` +- 路径:`/steady/checksquare/create` +- 头:`Content-Type: application/json` + +### 请求体 + +```json +{ + "lineIds": ["LINE_001", "LINE_002"], + "indicatorCodes": [], + "timeStart": "2026-06-13 00:00:00", + "timeEnd": "2026-06-13 01:00:00" +} +``` + +### 字段来源 + +- `lineIds` + - 取 `FormalTestManager.monitorIdListComm` + - 含义为当前批次正式检测涉及的监测点 ID 集合 + +- `indicatorCodes` + - 固定传空数组 `[]` + +- `timeStart` + - 取 `FormalTestManager.checkStartTime` + - 格式化为 `yyyy-MM-dd HH:mm:ss` + +- `timeEnd` + - 根据 `param.getDevIds()` 查询本批次装置对应的 `PqDevSub.checkEndTime` + - 取最大值作为本批次装置写入的最终结束时间 + - 只统计本次 `devIds` 对应装置,不扫描其他无关装置 + +## 时间结束值设计 + +`timeEnd` 不取通知触发时刻,也不取计划级别推导值,而取本批次装置已写入数据库的最终结束时间。 + +原因: + +- 更符合“状态修改入库后再通知”的业务语义 +- 可以避免异步线程调度延迟导致结束时间被人为拉晚 +- 可以保证第三方拿到的是本次批次真实落库完成时间 + +## 内存幂等设计 + +### 幂等键 + +同一批次检测的内存幂等 key 定义为: + +`planId + "|" + sortedDevIds + "|" + timeStart + "|" + reCheckType` + +说明: + +- `planId` 标识所属计划 +- `sortedDevIds` 标识当前批次参与检测的装置集合,排序后再拼接,避免顺序影响 +- `timeStart` 标识本轮正式检测开始时间 +- `reCheckType` 用于区分全部检测与其他复检类型 + +### 内存状态 + +建议在 `ResultServiceImpl` 内维护一个 `ConcurrentHashMap`。 + +`NotifyState` 至少包含: + +- `status`:`RUNNING`、`SUCCESS`、`FAIL` +- `failCount`:失败次数 +- `lastError`:最后一次异常摘要 +- `triggerTime`:首次触发时间 + +### 幂等规则 + +- key 已存在且状态为 `RUNNING`:直接返回,不重复发起 +- key 已存在且状态为 `SUCCESS`:直接返回,不重复发起 +- key 不存在:创建 `RUNNING` 状态并进入异步调用 +- key 为 `FAIL`:本轮进程内不自动重新发起新一轮通知 + +该规则保证: + +- 同一批次在单次进程生命周期内只会成功进入一次通知流程 +- 重试逻辑只在同一次异步流程内部进行,不因业务代码重复命中而再次启动 + +## 重试设计 + +### 执行方式 + +异步方法第一次立即发起调用。 + +如果失败,则在异步方法内部串行重试,不再额外启动新的业务线程入口。 + +### 退避规则 + +失败后按 `2^failCount` 秒等待后重试: + +- 第 1 次失败后,等待 2 秒 +- 第 2 次失败后,等待 4 秒 +- 第 3 次失败后,等待 8 秒 + +总重试上限: + +- 最多重试 3 次 + +### 成功判定 + +以下条件同时满足即可判定为成功: + +- HTTP 请求成功发出 +- 未抛出异常 + +不要求: + +- 解析响应体业务字段 +- 校验 `taskStatus` +- 保存 `taskId` 或 `taskNo` + +### 失败处理 + +连续失败 3 次后: + +- 内存状态更新为 `FAIL` +- 记录错误日志 +- 停止继续重试 +- 不影响正式检测主流程结果 + +## 配置设计 + +建议在 [application.yml](D:\njcn\test\CN_Gather\entrance\src\main\resources\application.yml) 中新增: + +```yaml +third-party: + checksquare: + enabled: true + url: http://third-party-host/steady/checksquare/create + connect-timeout-ms: 3000 + read-timeout-ms: 5000 + max-retries: 3 +``` + +说明: + +- `enabled` + - 联调和现场排障时可快速关闭能力 +- `url` + - 第三方接口地址 +- `connect-timeout-ms` + - 建连超时 +- `read-timeout-ms` + - 读超时 +- `max-retries` + - 默认值为 3,与当前需求一致 + +## HTTP 调用方式 + +项目内已存在 `RestTemplateUtil` 使用习惯,本次设计延续现有风格,不引入新的 HTTP 客户端框架。 + +调用建议: + +- 复用项目现有 `RestTemplateUtil` +- 以 JSON 方式发送请求体 +- 设置合理超时 + +## 日志设计 + +建议至少记录以下日志信息: + +- 幂等 key +- `planId` +- `devIds` +- `lineIds` 数量 +- `timeStart` +- `timeEnd` +- 当前重试次数 +- 最终结果:成功或失败 +- 异常摘要 + +日志用途: + +- 排查重复触发 +- 定位请求失败原因 +- 还原本次批次通知上下文 + +## 测试与验收设计 + +至少覆盖以下场景: + +### 单元验证 + +- `reCheckType` 不是 `ALL_TEST` 时不触发 +- 幂等 key 已为 `RUNNING` 时不重复发起 +- 幂等 key 已为 `SUCCESS` 时不重复发起 +- `lineIds` 正确来自 `FormalTestManager.monitorIdListComm` +- `indicatorCodes` 固定为空数组 +- `timeStart` 正确来自 `FormalTestManager.checkStartTime` +- `timeEnd` 正确取本批次装置 `checkEndTime` 最大值 + +### 异步重试验证 + +- 第一次失败、第二次成功时,发生一次重试并最终标记 `SUCCESS` +- 连续失败 3 次时,最终标记 `FAIL` +- 重复进入正式检测完成收口点时,不会对同一 key 再次起新的通知流程 + +### 集成验证 + +- 数模式正式检测成功完成后,第三方接口被调用 1 次 +- 同一批次重复命中成功收口逻辑时,第三方仍只收到 1 轮调用流程 +- 比对式检测不触发该接口 + +## 风险与限制 + +- 纯内存幂等仅在单次服务进程生命周期内有效,服务重启后无法保证已通知批次不重复 +- 如果 `FormalTestManager` 上下文在成功收口时已丢失,则无法安全构造请求体,应直接放弃通知并记录日志 +- 如果数据库中某个装置的 `checkEndTime` 未正确写入,`timeEnd` 可能无法按预期构造,应视为不满足触发条件 +- 失败状态不持久化,因此无法跨重启继续重试 + +## 实施边界 + +本设计确认后,实施阶段仅应完成以下内容: + +- `result` service 层新增第三方通知入口 +- 数模式正式检测成功收口点接入该入口 +- 第三方请求体组装 +- 内存幂等控制 +- `@Async` 异步调用与指数退避重试 +- 必要的配置项与测试 + +不应在本次实现中额外引入: + +- 任务表 +- MQ +- Redis 幂等 +- 跨进程重试恢复 +- 比对式适配 + +## 当前结论 + +本需求的最终设计为: + +- 仅覆盖数模式正式检测成功完成场景 +- 触发点放在 `SocketDevResponseService` 正式检测成功收口处 +- 第三方调用入口放在 `com.njcn.gather.result.service` 层 +- 使用纯内存 `ConcurrentHashMap` 做单进程幂等 +- 使用 `@Async` 执行第三方调用 +- 失败后按 2 秒、4 秒、8 秒重试,最多 3 次 +- `indicatorCodes` 固定空数组 +- `timeEnd` 使用本批次装置写入数据库的最大 `checkEndTime`