10 KiB
数模式正式检测完成后通知第三方 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 秒
触发边界
唯一触发范围
本次功能只覆盖数模式正式检测成功完成路径。
推荐触发位置:
具体挂接点:
- 在数模式正式检测最终成功分支中
iPqDevService.updateResult(param.getDevIds(), valueType, param.getCode(), param.getUserId(), param.getTemperature(), param.getHumidity(), true)执行完成之后CnSocketUtil.quitSend(param)之前调用结果服务的第三方通知入口
为什么不放在 PqDevServiceImpl
PqDevServiceImpl是通用状态汇总与入库层,不等价于正式检测生命周期结束- 该类会被多条业务链路复用,挂接后容易误触发
- 本次需求明确要求“正式检测完成后”触发,因此应挂在正式检测成功收口边界
架构设计
入口职责
在 result service 层新增一个对外入口,例如:
void tryNotifyThirdPartyAfterFormalTest(PreDetectionParam param)
建议实现位置:
该入口负责:
- 校验当前是否满足触发条件
- 组装幂等 key
- 做内存去重
- 触发异步第三方调用
异步职责
在 ResultServiceImpl 中新增 @Async 方法,负责:
- 发起第三方 HTTP 请求
- 捕获异常
- 按指数退避重试
- 更新内存中的执行状态
- 记录成功或失败日志
触发条件
只有同时满足以下条件时才允许发起第三方通知:
- 当前链路属于数模式正式检测最终成功收口点
SourceOperateCodeEnum.ALL_TEST.getValue().equals(FormalTestManager.reCheckType)FormalTestManager.checkStartTime不为空param.getPlanId()不为空param.getDevIds()非空- 当前批次幂等 key 未处于
RUNNING或SUCCESS
任一条件不满足时直接返回,不抛业务异常。
请求设计
接口
- 方法:
POST - 路径:
/steady/checksquare/create - 头:
Content-Type: application/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、FAILfailCount:失败次数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 中新增:
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
planIddevIdslineIds数量timeStarttimeEnd- 当前重试次数
- 最终结果:成功或失败
- 异常摘要
日志用途:
- 排查重复触发
- 定位请求失败原因
- 还原本次批次通知上下文
测试与验收设计
至少覆盖以下场景:
单元验证
reCheckType不是ALL_TEST时不触发- 幂等 key 已为
RUNNING时不重复发起 - 幂等 key 已为
SUCCESS时不重复发起 lineIds正确来自FormalTestManager.monitorIdListCommindicatorCodes固定为空数组timeStart正确来自FormalTestManager.checkStartTimetimeEnd正确取本批次装置checkEndTime最大值
异步重试验证
- 第一次失败、第二次成功时,发生一次重试并最终标记
SUCCESS - 连续失败 3 次时,最终标记
FAIL - 重复进入正式检测完成收口点时,不会对同一 key 再次起新的通知流程
集成验证
- 数模式正式检测成功完成后,第三方接口被调用 1 次
- 同一批次重复命中成功收口逻辑时,第三方仍只收到 1 轮调用流程
- 比对式检测不触发该接口
风险与限制
- 纯内存幂等仅在单次服务进程生命周期内有效,服务重启后无法保证已通知批次不重复
- 如果
FormalTestManager上下文在成功收口时已丢失,则无法安全构造请求体,应直接放弃通知并记录日志 - 如果数据库中某个装置的
checkEndTime未正确写入,timeEnd可能无法按预期构造,应视为不满足触发条件 - 失败状态不持久化,因此无法跨重启继续重试
实施边界
本设计确认后,实施阶段仅应完成以下内容:
resultservice 层新增第三方通知入口- 数模式正式检测成功收口点接入该入口
- 第三方请求体组装
- 内存幂等控制
@Async异步调用与指数退避重试- 必要的配置项与测试
不应在本次实现中额外引入:
- 任务表
- MQ
- Redis 幂等
- 跨进程重试恢复
- 比对式适配
当前结论
本需求的最终设计为:
- 仅覆盖数模式正式检测成功完成场景
- 触发点放在
SocketDevResponseService正式检测成功收口处 - 第三方调用入口放在
com.njcn.gather.result.service层 - 使用纯内存
ConcurrentHashMap做单进程幂等 - 使用
@Async执行第三方调用 - 失败后按 2 秒、4 秒、8 秒重试,最多 3 次
indicatorCodes固定空数组timeEnd使用本批次装置写入数据库的最大checkEndTime