Files
CN_Gather/docs/superpowers/specs/2026-06-18-formal-test-third-party-checksquare-design.md

360 lines
10 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

# 数模式正式检测完成后通知第三方 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<String, NotifyState>`
`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`