# PRD：UR#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 可独立关闭而不影响事件和轮询兜底。

## 公共能力发布依赖

- 最终 13 号发布票阻塞于公共基础 12 号票和全局审计 19 号票；生产者放量前必须确认事件消费者、Integration Log、Audit Writer 和监控同时装配。
- 每个卡状态事件登记稳定类型、载荷版本和消费者幂等键；卡状态是领域事实，运营商请求/回调进入 Integration Log，状态变化、人工触发及连续失败进入 Audit Event。

## 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 及后续套餐/停复机用例只能复用该边界，不各自复制状态规则。
