Constraint: 在线热修前必须保存当前迭代分支全部有效代码进展 Confidence: medium Scope-risk: broad Directive: 后续修改需保持审计事件与业务事务边界一致 Tested: git diff --cached --check Not-tested: 未运行全量测试,提交用于切换分支前保存既有工作
7.2 KiB
Context
线上诊断得到旧孤儿扫描窗口 scanned_count=100、skipped_as_occupied=100、waiting_realname=0。这证明“先取前 100 条待生效记录,再在 Go 中逐条排除占位载体”的实现会让窗口之外的真实孤儿永久饥饿。
七月迭代分支虽然仍保留旧 enqueueActivationTask,但已经具备更合适的本地接续边界:ActivationService.ActivateNextPendingMainPackage 负责载体锁、队首选择、条款快照、状态事务和提交后复机;实际激活事务还会原子追加 card.observation.series.requested Outbox。套餐状态推进本身是本地数据库用例,不需要再经过一次 Asynq 才能执行。
本设计仅适用于 Iteration/7-11,不包含跨分支兼容或移植决策。
Goals / Non-Goals
Goals:
- 每轮最多 100 个恢复名额只用于无占位主套餐的真实孤儿载体,同一载体只选择队首套餐。
- 旧主套餐过期事实提交后,直接调用七月分支现有接续能力推进下一套餐。
- 孤儿恢复直接复用同一接续能力,不再创建第二条异步激活链。
- 套餐激活状态与既有卡观测 Outbox 保持同一事务,后续观测继续可靠投递。
- 日志准确区分实际激活、幂等、锁冲突和业务条件未满足。
Non-Goals:
- 不提供跨分支兼容或移植方案。
- 不新增套餐激活 Outbox 事件、消费者、迁移或队列类型。
- 不删除仍可能被其他旧入口使用的
TaskTypePackageQueueActivation和 Handler;只停止过期接续与孤儿恢复继续走该路径。 - 不修改购买、优先级分配、实名激活、流量扣减、退款失效和停复机规则。
- 不新增 API、DTO、路由、依赖、数据库字段或索引。
- 按用户要求,不新增、修改或运行自动化测试。
Decisions
决策 1:数据库先选择真实孤儿队首,再执行 LIMIT
孤儿查询通过 GORM 执行 PostgreSQL CTE/窗口查询:
- 从
status=0 AND master_usage_id IS NULL AND deleted_at IS NULL按卡或设备载体分组。 - 每个载体按
priority ASC, created_at ASC, id ASC选择唯一队首。 - 通过相关
NOT EXISTS排除仍有status IN (1,2)主套餐的载体。 - 对真实孤儿稳定排序后执行
LIMIT 100。
查询结果直接进入同步接续循环,删除逐条 Count 和 Go map 二次分组。
拒绝:仅增大 LIMIT。 数据增长后仍会复现,并放大 N+1。
拒绝:游标扫描所有待生效记录。 需要维护扫描状态,复杂度高于一次正确查询。
决策 2:过期事务提交后同步调用现有接续服务
processExpiredPackage 的事务继续负责旧主套餐 status=3 和关联加油包 status=4。事务成功提交后,Handler 调用 ActivateNextPendingMainPackage(ctx, carrierType, carrierID),不再调用 activateNextPackage 投递 Asynq。
若进程在两次事务之间退出,数据库会留下“无占位主套餐 + 有待生效套餐”的持久状态,下一轮修正后的孤儿扫描会直接调用同一接续能力恢复,最长增加一个轮询周期。
拒绝:事务内直接投递 Asynq。 消费者可能早于事务提交读取旧状态。
拒绝:新增套餐激活 Outbox。 状态推进是本地同步用例,现有服务已经具备完整事务;新增事件只会形成第二套调度和消费状态。需要可靠投递的后续卡观测已经由激活事务写入现有 Outbox。
决策 3:孤儿恢复同步调用同一接续服务
每个真实孤儿候选只携带载体类型和 ID,调用 ActivateNextPendingMainPackage。服务在事务内重新检查占位状态和队首,避免依赖扫描快照执行写操作;载体级 Redis 锁防止多个轮询实例并发激活。
锁冲突返回现有 CodePackageActivationConflict。轮询记录原因后结束本次候选,下一轮扫描继续恢复,不依赖 Asynq 重试。
决策 4:复用现有卡观测 Outbox
成功激活仍通过 appendActivationObservation 在套餐激活事务内追加稳定事件:
- 事件类型:
card.observation.series.requested - 稳定事件 ID:
card-observation:package-usage:{usage_id}:activated - 同步类型:实名、流量、网络
- 资源:实际卡或设备载体
Outbox 写入失败时套餐激活事务回滚,避免状态已生效但后续观测请求丢失。不新增套餐激活事件、Relay 或消费者。
决策 5:日志以实际结果为准
ActivateNextPendingMainPackage 已返回 activated bool。调用方仅在 activated=true 时记录本轮成功;false,nil 表示幂等或条件暂不满足,记录明确结果;锁冲突和数据库错误按错误路径记录,不伪造成功。
决策 6:公共能力决定
- Audit Event:N/A。 系统自动生命周期推进,不是人工敏感操作。
- Domain Ledger:N/A。
tb_package_usage是套餐状态、激活和到期的权威事实。 - Integration Log:N/A。 本修复不新增外部请求。
- Outbox:复用。 成功激活继续在同一事务写
card.observation.series.requested;不新增事件类型。
实施时增量维护 .scratch/tech-global-audit/审计覆盖基线.md。
决策 7:不包含自动化测试
按用户明确要求,本 Change 不创建、修改或运行自动化测试。验证使用:
gofmt和现有静态检查;go build ./...;- 只读候选 SQL及
EXPLAIN (ANALYZE, BUFFERS); - 日志顺序和激活结果核验;
- 激活事务与卡观测 Outbox 记录的一致性查询。
Risks / Trade-offs
- [风险] 候选查询扫描大量待生效记录 → 用现有索引和查询计划验证;没有性能证据前不新增索引。
- [风险] 旧套餐提交后、接续调用前进程退出 → 下一轮真实孤儿扫描从数据库权威状态恢复。
- [风险] 多实例重复处理同一载体 → 服务内载体级 Redis 锁、事务内占位复检和状态幂等共同收敛。
- [风险] 同步接续增加单轮耗时 → 单轮最多 100 个真实孤儿,接续仅执行本地 Redis/数据库事务;记录耗时后再决定是否需要批次调整。
- [权衡] 保留旧 Asynq Handler → 避免扩大未触碰调用方;本 Change 只让过期和孤儿两条路径停止使用它。
- [权衡] 不新增自动化测试 → 遵循用户边界,以构建、SQL、查询计划和日志证据替代。
Migration Plan
- 在
Iteration/7-11实施真实孤儿查询和同步接续调用。 - 更新审计覆盖基线与功能总结,执行格式化、静态检查和
go build ./...。 - 用只读 SQL/查询计划验证候选公平性,用运行日志和数据库事实核验同步接续及卡观测 Outbox。
- 复核七月分支专属差异并保留在当前工作树,不创建提交。
- 发布七月迭代时观察至少两个轮询周期,确认真实孤儿收敛且卡观测 Outbox 正常投递。
回滚
- 无数据库迁移,发布后如需回滚,则回退七月专属修复代码并重新部署 Worker。
- 已正确激活的套餐保持业务事实,不执行反向 SQL。
- 回滚后出现孤儿时,使用带状态条件的单卡修复 SQL逐条处理。
Open Questions
无。本 Change 明确仅属于七月迭代分支。