Skip to content

会员订阅:到期检查、批量处理与用户提醒 ​

会员是否有效需要在访问权益时判断;到期状态整理和用户提醒由后台任务推进。登录时检查可以改善页面提示,但用户不登录时也可能需要收到提醒,因此不能成为唯一入口。

本篇先采用“预付费、到期后失效、允许提前续费”的模型,示例设置到期前 3 天、前 1 天和到期后各提醒一次。自动扣费订阅在文末单独讨论,不把一次扣款失败直接等同于权益失效。

从到期提示推导执行方案 ​

假设产品最初只要求“会员进入个人中心时显示剩余天数”。这时请求内读取期限就能满足需求,并不需要先引入提醒队列。

加入“不登录也在到期前收到提醒”后,执行入口必须独立于用户请求。再加入“到期立即停止权益”,就不能把停权完全寄托在下一轮后台扫描上。三条路径是由不同的时间承诺推导出来的。

候选方案能满足什么代价与边界
登录时读取期限页面提示、当前查询无法主动联系未登录用户
周期扫描会员表主动发现到期和提醒候选要维护索引、补扫与提醒去重
购买时保存未来任务每个提醒单独跟踪续费与撤销后要识别旧版本

本篇选择“访问时判断权益、后台产生持久化待办、渠道独立发送”。扫描与延迟任务可以按系统规模择一作为主路径,再用核对流程补缺。判断依据是承诺的时间与恢复能力,不是某种中间件是否流行。

权益访问与到期整理时序 ​

前半段由用户访问触发,后半段由后台时间检查触发。两者共用会员事实,但不要求后台先更新 expired,访问校验才生效。

用户保持登录也不改变 expires_at 的判断。通知的提交速度影响触达时效,不改变这条访问边界。

提醒、续费与渠道发送时序 ​

这张图展示任务已经生成后用户续费的情况。发送前复核可以阻止旧版本提醒,已经交给渠道的消息仍有无法撤回的窗口。

需求与权益契约 ​

业务问题示例决策实现约束
购买立即生效还是下期生效支付确认后立即生效不由客户端付款页面决定
提前续费是否损失剩余天数从约定的当前有效边界延长续费事务读取并锁定旧期限
到期是否允许宽限预付费示例没有;自动续费另定宽限与正常有效期分开保存
退款或封禁何时停权按产品规则撤销指定权益撤销事件与缓存失效接续
提醒成功意味着什么对应渠道接受本轮通知不等同于用户已读或已续费

以下流程针对单份当前会员记录。存在多份有效授权时,应先归并权益来源,再判断总体可用期限,不能因为其中一份到期就撤销全部会员能力。

三条路径分别负责什么 ​

路径触发时间负责什么
权益校验每次访问受保护的业务操作当次是否允许使用会员能力
到期处理后台发现已经到期更新展示状态、撤销派生资源、保存到期通知意图
提醒分发提前提醒时间或到期之后站内信、邮件、短信、App 推送

登录时可以复用权益查询,在页面提示“还有 2 天到期”。进入会员功能时仍要校验;长期不退出的会话也可能跨过到期时间。OWASP 的授权建议要求在每次请求上校验权限,具体实现可以复用公共鉴权组件。

保存什么数据 ​

这里假设每个用户有一份当前会员记录;多套餐、叠加赠送、冻结和分租户权益需要更完整的权益模型。

字段用途
membership_id、user_id定位会员与用户
stateactive、expired、revoked 等业务状态
expires_at明确的有效期结束时刻,使用带时区的时间
period_version续费或修改有效期时递增,区分新旧计划
notification_preferences可用渠道、用户选择和免打扰规则,通常独立存放

“10 月 31 日到期”应先转成业务定义的精确时刻。例如上海时间 11 月 1 日 00:00 是失效边界,此时前一天整天仍有效。后续比较使用 now < expires_at,避免日期显示和数据库判断各用一套规则。

示例的权益判断是:

text
允许使用 = state 为 active
           且当前可信服务端时间早于 expires_at
           且没有其他撤销或套餐限制

即使到期扫描晚了两分钟,受保护操作也不会因此多授权两分钟。若缓存权益,缓存存活时间不能跨过已知的到期边界;退款、封禁等提前撤销还需要失效通知或更短的再校验周期。

把会员标记写进长效会话或令牌后长期不核对,会让用户跨过到期时刻继续使用。会话有效与会员权益有效要分别判断。

购买、续费和查询接口 ​

入口输入与校验持久化结果
购买或续费请求套餐、用户、请求幂等键支付订单,返回待确认状态
支付确认处理器可信支付事实、交易身份、金额与套餐已处理支付流水与权益变更
权益查询当前用户与业务能力最新权益、期限及提示状态
撤销处理器退款或封禁依据、受影响授权版本化撤销及派生资源回收任务
提醒偏好修改用户身份、渠道、免打扰规则新偏好,发送前按当前值检查

请求幂等键与支付交易去重是两层约束:前者防止重复发起购买,后者防止同一付款重复延长权益。

支付确认后的开发流程 ​

text
事务外验证或查询支付事实,确定要确认的交易身份
BEGIN
  按支付来源与交易 ID 建立唯一处理资格
  锁定会员与本地购买订单,核对用户、套餐和金额
  如果该交易已处理:返回原权益变更结果
  根据套餐规则计算新期限,递增 period_version
  保存权益变更流水,更新订单与当前会员
  保存延迟提醒计划或供扫描核对的变更事件
  保存缓存失效与派生资源更新意图
COMMIT
后台可靠投递派生动作,不阻塞支付确认等待短信发送

两个不同付款同时续费同一会员时,会员行的串行更新让后一次基于已提交的新期限计算;同一交易重复到达只读取原结果。权益变更流水保留每笔付款延长的范围,方便退款时判断影响。

具体时间线上发生什么 ​

假设某会员在 2026-11-01 00:00(上海时间)到期:

时刻系统行为用户不登录会怎样
购买成功保存会员期限与版本无需用户额外操作
10 月 29 日生成提前 3 天的提醒按用户可用渠道发送
10 月 31 日生成提前 1 天的提醒与登录无关
11 月 1 日 00:00请求校验判定权益到期不依赖登录来触发失效
后台下一次处理将派生状态转为 expired,创建通知状态整理和通知继续进行
下一次登录展示当前权益和历史通知不重复发送同一轮提醒

提醒时间需要结合用户时区与免打扰窗口调整。例如前 1 天的提醒可以安排在当地白天;权益失效时刻保持业务定义的边界。发送时间错峰不能偷偷延长权益。

怎样触发提前提醒 ​

方案实现方式适合的约束
周期扫描每分钟查询已经进入提醒窗口且未建任务的会员分钟级提醒、已有数据库和任务队列
延迟任务购买或续费时保存多个未来提醒任务需要单任务跟踪、精度由调度系统保证
事件配合兜底购买、续费、撤销事件更新计划,扫描补缺实时更新计划,并保留修复路径

对“同一天 1000 人到期”的样例,可以先用索引扫描和持久化任务队列。是否需要更复杂的延迟系统,取决于总记录数、允许延迟和数据库负载,而不只看 1000 这个数字。

购买或续费时创建延迟任务,应与会员变更处在同一事务,或通过 Outbox 接续;扫描方案则在候选发现时补建任务。两种方式都要在发送前重新读取会员版本。

按批发现需要提醒的人 ​

下面使用 PostgreSQL 18 和数据库任务表。会员表记作 memberships,字段 id 对应上文的 membership_id;SQL 是设计示例,尚未连接数据库执行。

sql
CREATE INDEX memberships_active_expiry_idx
    ON memberships (expires_at, id)
    WHERE state = 'active';

WITH scan_time AS (
    SELECT statement_timestamp() AS as_of
),
candidates AS (
    SELECT m.id, m.expires_at, m.period_version
    FROM memberships AS m
    CROSS JOIN scan_time AS s
    WHERE m.state = 'active'
      AND m.expires_at > s.as_of
      AND m.expires_at <= s.as_of + interval '3 days'
      AND NOT EXISTS (
          SELECT 1
          FROM background_tasks AS t
          WHERE t.task_type = 'membership_reminder'
            AND t.business_key =
                m.id::text || ':' || m.period_version::text || ':before_3d'
      )
    ORDER BY m.expires_at, m.id
    LIMIT 100
)
INSERT INTO background_tasks (
    task_type, business_key, payload, scheduled_at, available_at
)
SELECT
    'membership_reminder',
    id::text || ':' || period_version::text || ':before_3d',
    jsonb_build_object(
        'membership_id', id,
        'period_version', period_version,
        'stage', 'before_3d'
    ),
    expires_at - interval '3 days',
    statement_timestamp()
FROM candidates
ON CONFLICT (task_type, business_key) DO NOTHING
RETURNING id;

这条语句发现“已进入 3 天提醒窗口、尚未到期、这一版本还没建过任务”的候选,每批最多保存 100 条。已经建过任务的记录被排除,下一批才能继续前进;仅反复 LIMIT 100 查询相同用户会一直停留在第一页。

并发扫描器可能选中相同候选,唯一约束负责防止重复建任务;不要把某次冲突后插入 0 条理解为所有扫描工作已经完成。可以用单个扫描计划的租约协调发现流程,Worker 仍然并行领取已保存的任务。

这里保留原始 scheduled_at 以衡量晚到时间,available_at 表示现在可以领取。前 1 天提醒采用另一种 stage 独立建任务。生产环境的查询计划还需要在实际数据量上检查。

系统停机后怎样补扫 ​

只查询“恰好在这一分钟进入提醒时间”的会员,会在停机期间漏人。可以持续查询已经满足条件但缺少对应任务的记录,或保存扫描水位并在恢复后补扫。

补扫也要遵守通知价值:如果距到期只剩半小时,没必要同时补发“还剩 3 天”和“还剩 1 天”。示例策略是只保留仍适用的最近一档提醒;已经到期时改发到期通知,旧提前提醒记录 skipped。

上面的 SQL 展示候选发现,最终是否保留这一档由 Worker 的时效判断决定。任务历史归档后,还要保留必要的提醒去重记录,否则扫描会把同一通知重新建立。

1000 人同一天到期怎样处理 ​

首先区分同一天与同一时刻。如果 1000 人的期限分布在一天内,可以持续按时间范围查询;如果全部在午夜到期,则会产生一批集中到期记录。

下面是容量计算样例,不是特定供应商的默认配额:

环节样例配置实际含义
发现候选每批 100 条1000 条约需 10 批,批次可连续推进
状态整理每批一个短事务避免一笔事务锁住全部用户
Worker总并发 10同时处理最多 10 个动作
通知渠道全系统最多每秒 20 次请求所有 Worker 共用发送额度
提醒时窗允许 10 分钟内分散可错峰,不修改权益期限

每个用户只发一个渠道、每次请求对应一人、没有失败且 Worker 能供给足够吞吐时,1000 次请求按 20 次/秒提交,理论下界约 50 秒。若平均单次请求耗时为 1 秒、并发只有 10,则吞吐至多约 10 次/秒,提交至少约 100 秒。

如果同时发邮件和推送,还需要按渠道分别计算任务量与额度。供应商接受请求不代表用户已经收到;送达与展示可能更晚。

一条任务处理一个会员周期,通知渠道再拆成独立结果。第 537 人发送失败时,只重试这一人的失败渠道,不重新处理整批 1000 人。

全局限速必须覆盖所有进程。每个 Worker 都各自限制 20 次/秒,启动 10 个 Worker 后就可能发送 200 次/秒。SES 同时有发送总量和速率配额;FCM 也要求避免集中突发并对限流进行退避。见 SES 配额与 FCM 扩量建议。

到期状态更新与通知分离 ​

扫描器每批锁定一小组到期记录,条件更新其状态,同时保存到期通知任务,提交后再发送:

text
BEGIN
  领取 active 且 expires_at <= 当前时间的一批会员记录
  对本次实际转换的记录:
    按当前 period_version 更新为 expired
    保存到期通知任务,使用稳定业务键
COMMIT

Worker 独立领取通知任务:
  重新读取会员与当前版本
  判断到期事实、通知价值和用户可用渠道
  保存渠道级通知意图,按全局额度发送

如果并行扫描,可以采用 PostgreSQL 的 FOR UPDATE SKIP LOCKED 分散领取;相关语义见行锁说明。更新和任务创建必须同事务,避免状态已经过期却没有通知任务。

状态转换失败可以重试,通知失败只影响通知结果。无论通知队列有没有积压,权益请求都按期限判断。

续费后旧提醒怎么办 ​

续费事务增加 period_version,更新到期时间,并刷新派生状态或计划。对于预付费延长,常见的示例规则是从 max(当前时间, 原到期时间) 往后计算;月度套餐是否按自然月计算需要另行定义。

发送前检查“任务版本等于当前版本、仍处于对应提醒窗口”。不符合就跳过。已入队的旧任务不必全部成功物理删除,也不会因此发送“你已到期”的旧文案。

检查与外部发送之间仍有并发窗口:用户可能刚续费,旧请求已经送到供应商。可以通过通知意图、版本检查和发送状态降低误发,但已经提交到外部的消息未必能撤回。通知落地页应再次读取最新权益,避免展示旧结论。

用户不登录、离线或不允许推送 ​

  • 站内信可以先保存,用户下次访问时读取;不要把“尚未打开”当成发送失败反复建消息。
  • App 推送需要可用设备标识与通知授权;无效设备应停止对应渠道重试。
  • 邮件和短信按用户可用联系方式、产品约定与通知偏好选择;免打扰时段可以推迟提醒。
  • 没有可用渠道时保存原因,下一次访问仍能展示当前权益;不要为了提高送达率自动尝试所有联系方式。

渠道级逻辑键可以是 membership_id + period_version + stage + channel,设备投递记录再细分。它用于内部去重;外部发送能否幂等,仍需核对供应商能力,见可靠性边界。

自动续费与宽限期 ​

自动续费通常需要处理账单、扣款、重试、取消和宽限期,不应仅根据本地旧期限发送“请手动续费”。用户已开启自动续费时,文案与触发条件都应不同。

以 Stripe 为例,订阅 Webhook 文档区分付款成功、扣款失败及订阅状态变化。应用应核对账单和订阅当前状态,再更新权益期限。

支付 Webhook 还可能重复或乱序,不能按回调到达顺序直接覆盖本地状态,见 Webhook 事件顺序说明。这里的预付费 SQL 不构成完整的自动续费账务实现。

一次续费与旧提醒的记录演变 ​

以会员 M42 为例,原到期时间为 11 月 1 日 00:00,period_version 为 7,提前 1 天提醒已经排队:

时刻或操作数据变化处理依据
10 月 31 日 00:00 扫描保存 M42:7:before_1d 意图业务键确定这一周期与阶段
用户完成续费支付流水只确认一次,期限更新,版本变为 8同一付款不能再次延长
旧通知开始执行读取到版本 8,旧意图记为 skipped参数中的期限不是当前事实
同一付款回调重发返回原权益变更结果,版本仍为 8不把重复回调当新购买
用户之后访问会员功能根据新期限与当前撤销状态判断不依赖旧通知是否发送成功

如果在续费提交前,旧消息已经交给渠道,版本检查无法撤回它。该窗口需要通过发送记录解释,并让消息落地页展示最新权益。由此可以看出,版本控制保证的是旧动作能够被识别,不是所有外部动作都可以取消。

验证哪些业务结果 ​

场景预期结果
用户一周未登录后台提醒仍会处理,权益按期限失效
用户保持登录跨过到期时刻下一次受保护操作按期限重新校验
1000 人午夜同时到期权益及时拒绝,状态分批整理,通知限速分发
提醒已入队后用户续费旧版本在发送前检查时跳过
提醒扫描停机两天补扫仍有价值的提醒,过期文案不集中补发
邮件成功、推送失败只重试失败渠道,不重复邮件
扫描器重复执行相同周期和提醒阶段只产生一份业务通知意图
同一付款回调重复到达不重复延长会员,返回原权益变更
两笔不同付款同时续费按套餐规则串行计算期限,变更流水完整
退款撤销与到期任务并发按当前权益来源与版本处理,不重新激活已撤销权益

监控应同时覆盖权益判定、到期整理和通知三个结果:过期请求是否仍被放行、最老待整理记录年龄、当前阶段提醒的有效提交比例。它们不能由一个“任务成功率”替代。

从阿里云续费通知看提醒阶段与服务状态 ​

阿里云 ECS 的续费提醒说明提供了一个真实产品规则:到期前分阶段通知,还区分到期停机与释放前提醒,并允许配置接收人和渠道。它展示的是对外服务契约,没有披露内部采用扫描还是延迟队列。

另一个可对照的产品是 RocketMQ 包年包月实例:该文档区分到期停服、保留元数据期间可续费恢复,以及保留期后释放。服务不可用、数据仍保留和数据已删除是不同状态;这里的保留期也不能直接解释成仍可使用权益的宽限期。

据此扩展会员实现时,应分别定义权益结束时间、数据保留时间和提醒阶段。1000 人同一天到期,可以按同一阶段批量发现,逐会员创建意图,再按渠道限速;即使通知还在排队,请求校验也按权益期限执行。企业套餐还需区分使用者、付款联系人与管理员,发送前读取有效接收配置。批次、表结构和权限校验属于本文建议,不能据上述产品规则推断阿里云内部实现。

腾讯扣费前通知实践:调度不能等待渠道响应 ​

腾讯 WXG 工程师刘若愚的分布式定时器实践有直接的业务背景:扣费前先通知、等待后再执行扣费,以及优惠券到期通知。文章将任务持久化、到期扫描、事件投递与业务回调分开,并分别限制任务写入和下游通知的频率。这是工程师对当时团队实现的介绍,文中的等待时长和内部组件不应直接当成当前支付产品规则。

它补充了会员批量处理中的一个关键点:扫描器不要逐个等待短信、邮件或扣费接口返回,再推进到下一个时间桶。可先可靠保存每个会员的独立动作,由发送或支付处理器限速执行。否则,同一天到期的 1000 人中,前几个渠道超时就会拖住后面所有人的发现过程。

本文进一步建议把“提前提醒”和“实际扣费”建成不同的业务动作,分别保留期限、授权、账单及结果。提醒调用成功不能证明扣款已被授权;自动续费被取消后,已经排队的扣费动作也要检查当前授权。原文展示的是调度与交付链路,完整的扣款账务仍由支付业务负责。

参考资料 ​