订阅发放与过期清理 Worker 改造方案(北京时间)¶
背景与目标¶
当前订阅主链路已在支付回调中完成“订单置 PAID + 当期权益发放”。
为提升可恢复性与长期稳定性,需要补齐后台任务能力:
- 年卡后续月份按计划发放(定时任务)
- 过期订阅状态迁移与待发放计划取消(定时任务)
- 重启/重试/并发场景下的幂等保护(数据库约束)
本方案要求:自然月发放口径统一为北京时间(UTC+8)。
口径定义(UTC+8)¶
1) 年卡发放口径¶
- 购买当日:立即发放当月额度(主链路已实现)
- 后续发放:按购买日的 00:00(北京时间)发放一笔,目标月无对应日时取月末
- 数据库存储:
TIMESTAMPTZ(带时区)存储计划时间,业务计算使用Asia/Shanghai
2) 过期口径¶
- 订阅过期判定:
valid_until <= now(UTC)且status = ACTIVE - 过期动作:
- 订阅状态置
EXPIRED - 取消该订阅下
PENDING的发放计划(改CANCELLED)
总体设计¶
A. 主链路¶
支付回调继续做:
- 验签
- 订单置
PAID - 锁定同用户、同套餐的
ACTIVE订阅,当场过期valid_until <= now的旧订阅并取消其待发计划 - 仅对仍有效的旧订阅叠加;否则新建订阅并发放当期权益(含年卡首月)
- 提交事务
B. Worker 任务(新增)¶
subscription_maintenance_job 顺序执行:
- 扫描
user_subscriptions中已过期且ACTIVE的订阅 - 状态改
EXPIRED - 同时取消
PENDING计划 - 扫描
subscription_quota_schedules中PENDING且scheduled_at <= now的计划 - 仅对仍有效的订阅发放额度并写流水
- 状态改
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 秒重试
当前按“每天一次”执行。后续如需缩短时效,可再提升频率(逻辑幂等)。
变更清单¶
app/services/subscription/service.py-
年卡后续发放时间计算改为
Asia/Shanghai口径 -
app/services/subscription/quota_aggregate.py -
list_active_subscriptions增加valid_until过滤 -
app/services/subscription/scheduler_service.py(新增) -
实现两个任务服务方法:发放、过期
-
app/worker/subscription_jobs.py -
注册单一维护任务,配置启动补扫、cron 与失败重试
-
migrations/versions/*(新增) subscription_quota_schedules(status, scheduled_at)组合索引character_transactions月度发放幂等唯一索引(部分索引)
验收要点¶
- 年卡支付成功后:
- 当期额度立即到账
-
生成后续月份
PENDING计划(北京时间 1 日 00:00) -
Worker 执行到点后:
- 对应计划变
RELEASED - 字数增加
-
仅写 1 条月度发放流水
-
过期后:
- 订阅变
EXPIRED - 未执行计划变
CANCELLED /v1/users/quota不再统计该订阅