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

133 lines
16 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.
# 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 查询前的现有资格判断,本需求不重新设计。轮询策略何时改造由后续独立需求决定。
- 只触碰 `PollingRealnameHandler``PollingCarddataHandler``PollingCardStatusHandler` 查询成功后的状态应用与业务联动;套餐轮询、保护期轮询和调度基础设施不迁移。
### 公共卡状态领域与应用用例
- 使用复杂写通道:`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`:解析电信 XML`RESULTMSG=成功``ACCEPTMSG` 包含“已完成实名信息补录”视为实名成功;同一路由收到“已完成实名信息清除”只记录忽略。
- `POST /api/callback/carriers/cmcc/realname`:解析移动 JSON外层 `status=0``message=正确`、首个结果 `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_19`20 位只精确查询 `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_id``correlation_id``series_id``attempt`、来源、场景、资源、结果、耗时、是否变化和脱敏上游摘要。
- 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 及后续套餐/停复机用例只能复用该边界,不各自复制状态规则。