# Main 分支套餐接续饥饿热修 ## 修复边界 本热修仅适用于 `main` 的纯 Asynq 套餐接续链路,不引入 Outbox、数据库迁移、新任务类型或新依赖。 - 孤儿扫描先在 PostgreSQL 中按卡或设备选择队首套餐,并排除仍有 `status IN (1,2)` 占位主套餐的载体,最后取 100 个真实孤儿。 - 旧主套餐及加油包状态事务提交后,再投递现有 `package:queue:activation` 任务。 - Redis 激活锁冲突返回套餐激活冲突错误,由现有 `MaxRetry(3)` 重试。 - 只有套餐实际从待生效推进为生效中时,Handler 才记录“套餐激活成功”。 ## 部署观察 部署 Worker 后至少观察两个套餐轮询周期: 1. `孤儿套餐扫描完成` 的 `orphan_count` 应能覆盖真实无占位套餐,不再固定被同一批占位载体挡住。 2. `已提交套餐激活任务` 后应出现实际激活、明确跳过或可重试错误,不再出现未改状态却打印成功。 3. Redis 锁冲突应进入 Asynq 重试,不应确认任务成功。 可使用以下只读 SQL 检查仍未恢复的真实孤儿数量: ```sql SELECT COUNT(*) AS orphan_pending_count FROM tb_package_usage AS pending WHERE pending.status = 0 AND pending.master_usage_id IS NULL AND pending.deleted_at IS NULL AND (COALESCE(pending.iot_card_id, 0) > 0 OR COALESCE(pending.device_id, 0) > 0) AND NOT EXISTS ( SELECT 1 FROM tb_package_usage AS occupied WHERE occupied.status IN (1, 2) AND occupied.master_usage_id IS NULL AND occupied.deleted_at IS NULL AND ( (COALESCE(pending.iot_card_id, 0) > 0 AND occupied.iot_card_id = pending.iot_card_id) OR (COALESCE(pending.iot_card_id, 0) = 0 AND pending.device_id > 0 AND occupied.device_id = pending.device_id) ) ); ``` ## 回滚 本次无数据库迁移。回滚热修提交并重新部署 Worker 即可;已经正确激活的套餐属于有效业务事实,不执行反向 SQL。