Files
junhong_cmp_fiber/.scratch/ur94-card-state-events-callbacks/PRD.md
2026-07-21 15:26:07 +09:00

16 KiB
Raw Blame History

PRDUR#94 卡状态公共写入、事件触发与运营商实名回调

Status: ready-for-agent


Problem Statement

当前卡实名、流量和网络状态分别由轮询 Handler、手动刷新及部分业务 Service 直接写入,状态变化后的首次实名激活、套餐流量扣减、停复机评估和缓存更新也散落在各入口。若直接增加业务事件和运营商回调,会形成更多状态写入口,同一上游结果可能产生不同本地状态和副作用。

现有轮询已经有 PostgreSQL 配置、Redis 分片 Sorted Set、Asynq Handler、并发控制、手动触发、监控和卡级开关。七月迭代尚未形成新的轮询策略因此 UR#94 不改造轮询调度,只解决公共状态写入、事件埋点和已知运营商实名回调。

Solution

建立卡状态公共 DDD 边界:所有 Gateway 查询结果和运营商实名回调先转换成类型明确的标准观测,再由唯一的 ApplyCardObservation 用例锁定卡、应用实名/流量/网络规则并可靠发布副作用。现有实名、流量和卡状态轮询保持原调度行为,仅把查询结果的持久化和后续联动改为调用该公共用例。

在关键业务成功边界建立立即、事件发生后 3 分钟和 5 分钟的三次观测序列,提高本地状态收敛速度;读接口仍立即返回本地快照,开放接口同场景合并,避免被外部调用变成额外轮询器。移动、电信和联通只按当前已有报文能力建立回调 Translator不为没有已知协议的组合创建占位接口。

User Stories

  1. 作为运营人员,我希望停复机、实名和套餐等业务完成后能很快看到运营商侧状态收敛,而不必等待下一次常规轮询。
  2. 作为开放接口调用方,我希望查询仍立即返回本地快照,不因后台补同步增加接口耗时或改变响应契约。
  3. 作为系统维护人员,我希望轮询、事件、手动刷新和回调共用一套卡状态规则,避免多个入口分别写库和触发副作用。
  4. 作为运营人员,我希望移动、电信实名成功回调能及时更新卡实名状态,重复回调不会重复激活套餐。
  5. 作为风险控制人员,我希望解除实名回调本期只留痕,不因运营商误报直接把已实名卡改为未实名。
  6. 作为轮询运维人员,我希望本轮改造不改变现有轮询配置、间隔、队列、开关、监控和重排结果。
  7. 作为审计人员,我希望每次 Gateway 请求、事件尝试和运营商回调都能通过统一关联标识追踪。

Implementation Decisions

本期边界

  • 本需求交付三项能力:公共卡状态写入边界、业务事件 0/3/5 观测、运营商实名回调。
  • 现有轮询调度完全保持:tb_polling_config 的条件匹配和间隔、Redis 分片 Sorted Set、任务类型、并发配置、卡级 enable_polling、失败重排、手动触发和监控接口均不改变。
  • 不引入活跃/不活跃卡概念,不新增活跃状态字段或调度状态表,不调整轮询 QPS不把多套轮询配置合并成全局配置。
  • 轮询哪些卡、哪些类型以及 Handler 查询前的现有资格判断,本需求不重新设计。轮询策略何时改造由后续独立需求决定。
  • 只触碰 PollingRealnameHandlerPollingCarddataHandlerPollingCardStatusHandler 查询成功后的状态应用与业务联动;套餐轮询、保护期轮询和调度基础设施不迁移。

公共卡状态领域与应用用例

  • 使用复杂写通道:Handler/Worker -> Application -> Domain -> Repository/Infrastructure。公共边界至少包含请求上游观测和应用标准观测两个职责Gateway、运营商报文、GORM、Redis 和 Asynq 类型不得进入领域模型。
  • 标准观测使用类型明确的实名观测、流量观测和网络观测,不使用 map[string]any 表达领域输入。公共元数据至少包含来源、场景、观测时间、请求/关联标识和上游结果摘要。
  • ApplyCardObservation 是实名、流量和网络上游观测写入 tb_iot_card 的唯一入口。它在事务中锁定卡并按当前值判断是否变化,保存卡状态及 Audit Event/Outbox缓存失效或回填在提交后执行。
  • 原三个轮询 Handler 已有的业务语义必须完整迁入公共边界,而不是只迁移字段更新:
    • 实名:更新检查时间;仅首次从未实名变为已实名时写 first_realname_at,触发卡/设备套餐首次激活及复机评估;重复已实名观测不重复触发。
    • 实名逆转:周期查询仍保留当前连续三次确认及 10 分钟计数窗口,达到阈值后才应用未实名及停机评估;运营商解除实名回调不进入该计数。
    • 流量:保留运营商重置日、跨月归档、非重置日读数下降保护、正增量累计、使用记录、套餐流量扣减和停复机评估;同一卡流量观测保持串行。
    • 网络:保留 Gateway 状态映射、gateway_extend、IMEI、运营商停机原因、状态变化后的停复机评估以及独立风险卡停止后续轮询的当前行为。
  • 无状态变化的有效观测仍更新对应最后检查/同步时间并记录 Integration Log但不得重复发布首次实名、流量扣减或网络变化事件。
  • 未知 Gateway 状态、缺少关键字段或无效读数不得以零值覆盖本地状态;记录失败结果后由原事件后续尝试或现有轮询兜底。
  • ApplyCardObservation 的领域事件由 Outbox 可靠投递。UR#73 的统一复机用例、UR#53 的设备实名投影和套餐激活等消费者复用这些事件,不允许轮询或回调自行再写一套副作用。

事件埋点与三次观测序列

  • 事件不是新增公开同步 API而是业务 UseCase/Query 的内部端口。写操作在业务事务成功边界通过 Outbox 产生触发;读操作在返回本地快照前 Best Effort 入队,入队失败只记录日志,不改变原接口响应。
  • 每个触发序列固定建立三个独立 Asynq 任务:立即、事件发生后 3 分钟、事件发生后 5 分钟;三次任务的 Asynq 自动重试均为 0,单次失败不取消后两次,结束后继续由现有轮询兜底。
  • 任务使用 series_id + attempt 确定性幂等键。调用项目 EnqueueTask 时载荷必须传 struct 或 map禁止预先序列化成 []byte
  • 有明确预期状态时,每次执行前先查本地快照;已达到预期则把当前和剩余尝试标记完成,不再访问 Gateway。典型预期包括停机、复机、实名完成和设备切到目标 ICCID。
  • 同一 scene + resource_type + resource_id + sync_type 存在未结束序列时合并新触发,不创建第二组三任务。不同业务场景互不压制,不建立全局五分钟冷却。
  • 单卡、运营商接入和同步类型只在一次实际 Gateway 请求执行期间互斥;流量继续复用现有卡级锁。事件尝试命中执行中互斥时跳过本次,原定后续尝试仍保留。
  • Gateway 返回超频时记录本次 rate_limited,不为事件任务建立 30/60/120 秒退避,也不改变现有轮询重排策略。
  • 以下入口在成功边界创建序列:
    • C 端资产详情、后台资产实时状态,以及 OpenAPI 卡/设备的流量、网络、实名查询:按被查询资源和同步类型触发,无明确预期;接口仍返回本地快照。
    • C 端或后台获取实名入口:实名观测,预期已实名;realname_link_type=none 的卡不创建实名观测。
    • 后台、C 端、OpenAPI 和自动任务的停复机:网络观测,预期停机或开机。
    • 订单支付、钱包购包和套餐激活:实名、流量、网络观测,无明确预期。
    • 设备切卡/切模式:设备信息、源卡/目标卡网络及目标卡流量观测,预期目标 ICCID。
    • 设备重启、恢复出厂和 WiFi 设置:设备信息及绑定卡网络观测,无明确预期。
  • 卡绑定/解绑、设备或卡分配/回收若没有实际调用上游,只记录业务事件,不创建上游同步序列。
  • 现有后台和 C 端手动刷新继续直接执行一次同步并保持当前响应契约,不额外创建 0/3/5 序列。
  • 轮询观测到状态变化以及回调成功均不得反向创建新的三次序列;实名成功回调应使同一卡未执行的实名序列提前完成。
  • OpenAPI 的场景码稳定且按调用方无关的资源维度合并,连续查询同一卡不会不断续建序列;这项合并是防止外部调用变相形成轮询的强制约束。

运营商实名回调防腐层

  • 只实现现有材料已经给出报文的三个接入能力:
    • POST /api/callback/carriers/ctcc/realname:解析电信 XMLRESULTMSG=成功ACCEPTMSG 包含“已完成实名信息补录”视为实名成功;同一路由收到“已完成实名信息清除”只记录忽略。
    • POST /api/callback/carriers/cmcc/realname:解析移动 JSON外层 status=0message=正确、首个结果 regStatus=00000 且携带 ICCID 时视为实名成功。
    • POST /api/callback/carriers/cucc/realname/remove:解析联通外层 JSON 字符串字段 data 中的 iccid/dateChanged,仅记录解除实名并忽略状态变更。
  • 不为当前没有报文契约的“移动解除实名、联通实名成功、电信独立解除实名”创建占位路由;获得真实样例后再增加对应 Translator。
  • 移动回调 ICCID 为空时不得登录旧管理平台按 MSISDN 补查,不保留材料中的账号密码和第三方抓取逻辑;记录 invalid_payload 后返回接入约定成功响应。
  • 回调业务结论按当前评审口径直接信任,不验证来源真实性,也不再请求 Gateway 二次确认。每个运营商 Adapter 负责本方报文、成功条件和成功应答,领域层只接收标准实名观测。
  • 实名成功调用 ApplyCardObservation(verified=true);解除实名本期只写 Integration Log结果为 ignored,不修改卡、不触发停机,也不增加实名逆转计数。
  • 回调 ICCID 去除首尾空白后只接受合法 19 或 20 位值19 位只精确查询 iccid_1920 位只精确查询 iccid_20;禁止截断、补位、模糊查找或跨列降级。
  • 回调使用系统级 Repository不套用当前登录账号的店铺权限过滤。找不到卡记录 not_found;多条匹配记录 conflict,均不得任取一条写入。
  • 发布前再次检查未删除卡的 iccid_19 重复并将现有普通部分索引升级为部分唯一索引;iccid_20 也必须保证精确唯一。开发库只读核验时 19 位重复组为 0但发布不得假设生产数据相同。
  • 重复成功回调幂等返回成功,不重复写首次实名时间、不重复激活套餐。找不到卡、重复回调和能够安全识别的无效业务结果均按运营商协议返回成功,避免不可控重推;报文级解析失败仍记录 invalid_payload
  • 禁止调用旧示例中的第三方推送、删除实名接口、旧平台登录或 inner_callback。日志不得保存完整敏感报文、Cookie、Authorization 或明文 ICCID只保留脱敏摘要和必要解析字段。

审计、接口与前端

  • 每次实际 Gateway 请求、被合并/互斥/限频的事件尝试和每次运营商回调写统一 Integration Log卡业务状态变化同事务写 Audit Event。记录至少携带 request_idcorrelation_idseries_idattempt、来源、场景、资源、结果、耗时、是否变化和脱敏上游摘要。
  • Integration Log、Audit Event 和 Outbox 是公共基础能力依赖;公共能力尚未可用时不得为 UR#94 新建第二套同步运行表或旧式文本日志。
  • 本需求不新增显式同步按钮或同步 API不新增“活跃状态”“下次轮询时间”等字段也不改轮询监控接口。
  • 资产详情可增加“查看同步轨迹”入口,跳转统一审计中心的外部集成视角;没有独立卡同步监控页面。
  • 现有手动刷新接口、路由、权限和外层响应不变。新增回调 Handler 必须按项目规范注册路由,并同步更新两个 OpenAPI 文档生成器;回调协议样例作为契约测试 fixture 保存,敏感值脱敏。

发布与兼容

  • 切换顺序为:公共 Audit/Integration Log 与 Outbox 可用 → 公共卡状态 Application/Domain → 三个轮询 Handler 改用公共应用入口 → 事件埋点 → 回调路由。
  • 三个轮询 Handler 的公共入口切换必须一次完成,不允许同一观测既由 Handler 直接写库、又由公共用例双写。
  • 发布前记录并对比当前启用的轮询配置、队列深度、卡级开关和监控结果;发布后这些调度事实应保持一致。
  • 回调 URL 需在测试环境用运营商原始样例验证后再配置到运营商平台;正式启用顺序按运营商逐个灰度,单个 Adapter 可独立关闭而不影响事件和轮询兜底。

Testing Decisions

  • 领域单元测试覆盖实名首次成功/重复成功/逆转确认、流量正增量/跨月/运营商重置/异常下降、网络状态映射/未知状态/运营商停机原因,以及每类观测不变时不重复发布副作用。
  • Application 并发测试验证同一卡观测串行、重复 series_id + attempt 幂等、Outbox 与状态同事务、事务失败不留下部分卡状态或业务事件。
  • 轮询回归测试固定同一组 tb_polling_config、Redis 队列和卡级开关,验证改造前后下次入队时间、失败重排、并发控制、风险卡终止和监控统计保持现状;只替换查询成功后的状态应用路径。
  • 事件测试验证一次触发恰好生成 0/3/5 三任务且 MaxRetry(0),单次失败不取消后续任务,达到预期后跳过剩余任务,不同场景不被错误合并。
  • OpenAPI 压测/集成测试连续查询同一资源,验证接口延迟和响应结构不变、未结束序列被合并、不会按每次请求新增三任务。
  • 三个回调 Adapter 使用脱敏后的真实 XML/JSON fixture 做契约测试,覆盖成功、业务失败、空 ICCID、19/20 位精确命中、非法长度、找不到、多匹配、重复成功和解除实名忽略。
  • HTTP 集成测试穿过真实 Fiber 路由和统一错误处理PostgreSQL、Redis 使用开发配置Gateway 与运营商请求使用测试 Adapter禁止操作真实业务卡或向运营商平台发送请求。
  • 验证移动空 ICCID 不发起旧平台登录20 位 ICCID 不截成 19 位,回调日志及 Access Log 不泄露完整报文或凭证。
  • 人工验收核对卡状态、套餐激活/扣减、停复机评估、Outbox、Audit Event、Integration Log、Asynq 序列和原轮询队列,确认轮询策略没有随本需求变化。

Out of Scope

  • 不做活跃/不活跃轮询、不调整轮询间隔、不合并或废弃现有轮询配置、不新增卡同步调度状态表。
  • 不重新决定周期轮询的卡资格、卡类别过滤或不同同步类型覆盖范围。
  • 不新增显式同步 API、第二个刷新按钮、独立同步监控页或同步执行事实表。
  • 不处理运营商解除实名导致的自动回滚;本期一律留痕后忽略。
  • 不为没有真实回调协议的运营商/动作组合预建六套空路由;广电继续依赖事件和现有轮询。
  • 不验证运营商回调来源真实性不建设签名、IP 白名单或 Gateway 二次确认。
  • 不迁移套餐轮询、保护期轮询以及 UR#94 未触碰的旧模块。

Further Notes

  • 当前开发库启用的轮询配置仍是现有全局条件与既有各类型间隔;该事实用于回归基线,不是本需求要修改的配置。
  • 当前三个轮询 Handler 均直接写卡并执行后续业务,是本需求必须收口的已证实入口;当前代码尚无运营商回调路由和统一 ApplyCardObservation
  • 原标准评审稿中“活跃/不活跃轮询”相关内容已被本次用户确认覆盖,不得带入 UR#94 实现。
  • UR#94 先提供公共状态写入边界UR#73、UR#53 及后续套餐/停复机用例只能复用该边界,不各自复制状态规则。