跳转至

订阅发放与过期清理 Worker 改造方案(北京时间)

背景与目标

当前订阅主链路已在支付回调中完成“订单置 PAID + 当期权益发放”。
为提升可恢复性与长期稳定性,需要补齐后台任务能力:

  1. 年卡后续月份按计划发放(定时任务)
  2. 过期订阅状态迁移与待发放计划取消(定时任务)
  3. 重启/重试/并发场景下的幂等保护(数据库约束)

本方案要求:自然月发放口径统一为北京时间(UTC+8)


口径定义(UTC+8)

1) 年卡发放口径

  • 购买当日:立即发放当月额度(主链路已实现)
  • 后续发放:按购买日的 00:00(北京时间)发放一笔,目标月无对应日时取月末
  • 数据库存储:TIMESTAMPTZ(带时区)存储计划时间,业务计算使用 Asia/Shanghai

2) 过期口径

  • 订阅过期判定:valid_until <= now(UTC)status = ACTIVE
  • 过期动作:
  • 订阅状态置 EXPIRED
  • 取消该订阅下 PENDING 的发放计划(改 CANCELLED

总体设计

A. 主链路

支付回调继续做:

  1. 验签
  2. 订单置 PAID
  3. 锁定同用户、同套餐的 ACTIVE 订阅,当场过期 valid_until <= now 的旧订阅并取消其待发计划
  4. 仅对仍有效的旧订阅叠加;否则新建订阅并发放当期权益(含年卡首月)
  5. 提交事务

B. Worker 任务(新增)

subscription_maintenance_job 顺序执行:

  1. 扫描 user_subscriptions 中已过期且 ACTIVE 的订阅
  2. 状态改 EXPIRED
  3. 同时取消 PENDING 计划
  4. 扫描 subscription_quota_schedulesPENDINGscheduled_at <= now 的计划
  5. 仅对仍有效的订阅发放额度并写流水
  6. 状态改 RELEASED

合并为单一任务后,过期清理与额度发放不再同时争抢反向锁。


幂等与并发策略

1) 行锁防并发重复处理

  • 过期扫描使用 FOR UPDATE SKIP LOCKED
  • 额度发放与支付发放统一按“先 subscription,后 schedule”加锁
  • 单一维护任务固定先过期、后发放,避免两个 cron 之间的反向锁顺序

2) 数据库唯一约束防重复流水

新增唯一索引(部分索引):

  • 表:character_transactions
  • 字段:(user_id, biz_type, biz_id)
  • 条件:biz_type = 'monthly_release' AND biz_id IS NOT NULL

含义:同一用户同一 schedule 的月度发放流水只能写一次。

3) 状态机幂等

  • schedule 仅从 PENDING -> RELEASED/CANCELLED
  • subscription 仅从 ACTIVE -> EXPIRED
  • 重试任务不会重复发放

查询兜底(重要)

list_active_subscriptions() 增加有效期过滤:

  • status = ACTIVE
  • (valid_until IS NULL OR valid_until > now)

目的:即使过期任务短时延迟,也不会把过期订阅算进可用额度。


任务频率(当前配置)

  • Worker 启动时立即执行一次补扫
  • subscription_maintenance_job:每天 03:00 执行一次
  • 任务失败后最多执行 3 次,前两次分别延迟 60 秒、120 秒重试

当前按“每天一次”执行。后续如需缩短时效,可再提升频率(逻辑幂等)。


变更清单

  1. app/services/subscription/service.py
  2. 年卡后续发放时间计算改为 Asia/Shanghai 口径

  3. app/services/subscription/quota_aggregate.py

  4. list_active_subscriptions 增加 valid_until 过滤

  5. app/services/subscription/scheduler_service.py(新增)

  6. 实现两个任务服务方法:发放、过期

  7. app/worker/subscription_jobs.py

  8. 注册单一维护任务,配置启动补扫、cron 与失败重试

  9. migrations/versions/*(新增)

  10. subscription_quota_schedules(status, scheduled_at) 组合索引
  11. character_transactions 月度发放幂等唯一索引(部分索引)

验收要点

  1. 年卡支付成功后:
  2. 当期额度立即到账
  3. 生成后续月份 PENDING 计划(北京时间 1 日 00:00)

  4. Worker 执行到点后:

  5. 对应计划变 RELEASED
  6. 字数增加
  7. 仅写 1 条月度发放流水

  8. 过期后:

  9. 订阅变 EXPIRED
  10. 未执行计划变 CANCELLED
  11. /v1/users/quota 不再统计该订阅