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

10 KiB
Raw Blame History

数模式正式检测完成后通知第三方 checksquare 接口设计

背景

当前系统在数模式正式检测完成后,已经具备:

  • 正式检测上下文管理能力
  • 检测结果、监测点状态、装置状态入库能力
  • 装置检测开始时间与结束时间落库能力

现在需要在数模式正式检测成功完成后,调用第三方接口 POST /steady/checksquare/create,通知第三方系统开始后续处理。

第三方接口当前为免 token 模式,请求头仅需:

  • Content-Type: application/json

目标

  • 仅在数模式正式检测成功完成后触发第三方通知
  • 仅在 SourceOperateCodeEnum.ALL_TEST.getValue().equals(FormalTestManager.reCheckType) 时触发
  • 请求体中的 lineIdstimeStarttimeEnd 使用当前批次正式检测数据
  • indicatorCodes 固定传空数组
  • 调用采用异步方式,不阻塞当前正式检测完成主流程
  • 调用失败后按 2^failCount 秒退避重试,最多重试 3 次
  • 同一批次检测在单次进程生命周期内只通知一次

非目标

  • 不处理比对式检测
  • 不处理异常结束、失败结束或中断结束路径
  • 不解析第三方响应体中的 taskIdtaskNotaskStatus
  • 不把第三方响应体落库
  • 不引入数据库任务表、消息队列或持久化幂等机制
  • 不保证服务重启后的跨进程幂等

已确认决策

  • 方案采用纯内存幂等标记 + @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 请求
  • 捕获异常
  • 按指数退避重试
  • 更新内存中的执行状态
  • 记录成功或失败日志

触发条件

只有同时满足以下条件时才允许发起第三方通知:

  1. 当前链路属于数模式正式检测最终成功收口点
  2. SourceOperateCodeEnum.ALL_TEST.getValue().equals(FormalTestManager.reCheckType)
  3. FormalTestManager.checkStartTime 不为空
  4. param.getPlanId() 不为空
  5. param.getDevIds() 非空
  6. 当前批次幂等 key 未处于 RUNNINGSUCCESS

任一条件不满足时直接返回,不抛业务异常。

请求设计

接口

  • 方法: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 至少包含:

  • statusRUNNINGSUCCESSFAIL
  • failCount:失败次数
  • lastError:最后一次异常摘要
  • triggerTime:首次触发时间

幂等规则

  • key 已存在且状态为 RUNNING:直接返回,不重复发起
  • key 已存在且状态为 SUCCESS:直接返回,不重复发起
  • key 不存在:创建 RUNNING 状态并进入异步调用
  • key 为 FAIL:本轮进程内不自动重新发起新一轮通知

该规则保证:

  • 同一批次在单次进程生命周期内只会成功进入一次通知流程
  • 重试逻辑只在同一次异步流程内部进行,不因业务代码重复命中而再次启动

重试设计

执行方式

异步方法第一次立即发起调用。

如果失败,则在异步方法内部串行重试,不再额外启动新的业务线程入口。

退避规则

失败后按 2^failCount 秒等待后重试:

  • 第 1 次失败后,等待 2 秒
  • 第 2 次失败后,等待 4 秒
  • 第 3 次失败后,等待 8 秒

总重试上限:

  • 最多重试 3 次

成功判定

以下条件同时满足即可判定为成功:

  • HTTP 请求成功发出
  • 未抛出异常

不要求:

  • 解析响应体业务字段
  • 校验 taskStatus
  • 保存 taskIdtaskNo

失败处理

连续失败 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
  • 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