本文约 4,282 字,预计阅读 11 分钟。文中命令与配置均经过实测,可直接参考执行。
引言:到期那天,用户和站长都会出问题
订阅制会员站上线三个月后,会迎来第一批到期用户。典型投诉有两种:用户说”我明明续费了却被踢出来了”,站长说”到期了怎么还能看”。前者多为支付通知与站点时钟不一致,后者多为定时任务没跑或者权限判断时机不对。续费与到期是会员站里唯一”有状态迁移”的环节——权限要从 A 变到 B,还必须在正确的时间点变,且只能变一次。这一节讲清楚这条状态机怎么落地。
原理分析:WP-Cron 不是真正的定时器
WP-Cron 的机制是:每次有人访问站点时,WordPress 检查是否有到期的计划任务,有就在后台发起一次 HTTP 请求去执行。这带来两个后果——站点长期没人访问,任务就一直拖着;访问量大时,任务可能被并发触发。
所以会员到期这种”必须在准确时间执行”的任务,正确做法是把 WP-Cron 交给操作系统的 cron:在 wp-config.php 里禁用 DISABLE_WP_CRON,然后由系统每分钟拉一次 wp-cron.php。
状态迁移的时机也要选对。不要在 template_redirect 里改会员等级(会与页面渲染竞争),应该在专用钩子里改,通常是插件的等级变更钩子 pmpro_after_change_membership_level,再配合一次每日扫描兜底。
| 环节 | 推荐触发方式 | 说明 |
|---|---|---|
| 到期扫描 | 系统 cron 每日执行 | 兜底,处理漏掉的异步通知 |
| 支付成功 | 网关 Webhook | 实时开通 |
| 到期提醒 | 每日扫描时发信 | 到期前 7 天 / 1 天 |
| 权限校验 | 每次请求实时判断 | 不依赖缓存中的等级 |
保姆级实操
步骤 1:把 WP-Cron 交给系统。 先在配置里关掉访问触发,再加一条系统定时任务。
// wp-config.php 中:关闭由页面访问触发的 WP-Cron
define( 'DISABLE_WP_CRON', true );
# 系统 crontab:每分钟由 PHP CLI 触发一次计划任务
* * * * * /usr/bin/php /var/www/html/wp-cron.php doing_wp_cron >/dev/null 2>&1
步骤 2:确认计划任务已注册。 用 WP-CLI 查看,避免插件升级后任务被清掉。
wp cron event list --fields=hook,next_run,recurrence --format=table
wp cron event run pmpro_cron_expire_memberships
步骤 3:注册自己的每日扫描任务。 激活时注册,停用时清理,这是插件开发的基本礼貌。
register_activation_hook( __FILE__, 'mimangfei_schedule_expiry_scan' );
function mimangfei_schedule_expiry_scan() {
if ( ! wp_next_scheduled( 'mimangfei_daily_expiry_scan' ) ) {
wp_schedule_event( strtotime( '03:00:00' ), 'daily', 'mimangfei_daily_expiry_scan' );
}
}
register_deactivation_hook( __FILE__, 'mimangfei_clear_expiry_scan' );
function mimangfei_clear_expiry_scan() {
$timestamp = wp_next_scheduled( 'mimangfei_daily_expiry_scan' );
if ( $timestamp ) {
wp_unschedule_event( $timestamp, 'mimangfei_daily_expiry_scan' );
}
}
步骤 4:写扫描逻辑。 找出到期用户,先发提醒,再执行降级。注意用 date_default_timezone_set 或统一用 current_time('timestamp'),避免服务器时区与站点时区不一致。
add_action( 'mimangfei_daily_expiry_scan', 'mimangfei_handle_expirations' );
function mimangfei_handle_expirations() {
global $wpdb;
$now = current_time( 'mysql' );
$soon = date( 'Y-m-d H:i:s', current_time( 'timestamp' ) + 7 * DAY_IN_SECONDS );
// 1. 到期前 7 天提醒
$remind = $wpdb->get_col( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->usermeta}
WHERE meta_key = '_expires_at'
AND meta_value > %s AND meta_value <= %s",
$now, $soon
) );
foreach ( $remind as $user_id ) {
$user = get_userdata( $user_id );
wp_mail(
$user->user_email,
'您的会员即将到期',
'您的会员将在 7 天内到期,续费可保留原有权益。'
);
}
// 2. 已到期:执行降级
$expired = $wpdb->get_col( $wpdb->prepare(
"SELECT user_id FROM {$wpdb->usermeta}
WHERE meta_key = '_expires_at' AND meta_value <= %s",
$now
) );
foreach ( $expired as $user_id ) {
mimangfei_downgrade_user( $user_id );
}
}
function mimangfei_downgrade_user( $user_id ) {
if ( function_exists( 'pmpro_changeMembershipLevel' ) ) {
pmpro_changeMembershipLevel( 0, $user_id ); // 0 表示取消会员
}
delete_user_meta( $user_id, '_expires_at' );
update_user_meta( $user_id, '_last_downgraded', current_time( 'mysql' ) );
}
步骤 5:用等级变更钩子做收尾。 降级后清缓存、记日志、发通知,都挂在插件的等级变更钩子上。
add_action( 'pmpro_after_change_membership_level', 'mimangfei_on_level_change', 10, 2 );
function mimangfei_on_level_change( $level_id, $user_id ) {
clean_user_cache( $user_id );
if ( 0 === (int) $level_id ) {
$user = get_userdata( $user_id );
wp_mail( $user->user_email, '会员已到期', '您的会员权限已回收,续费后可立即恢复。' );
}
}
步骤 6:处理”续费后立刻恢复”。 支付网关的 Webhook 到达时,先更新到期时间,再调用 pmpro_changeMembershipLevel() 升级。注意幂等:同一个 Webhook 可能重复投递,要用订单号去重。
步骤 7:给宽限期。 到期后留 3 天宽限,宽限期内保留权限但页面顶部提示,能显著降低”忘了续费”导致的流失。
常见踩坑
- 站点没人访问导致任务不跑。 典型症状是会员到期后仍能看好几天,因为 WP-Cron 一直没被触发。
- 案例: 某站把到期任务挂在默认的
wp_schedule_event上,站点凌晨无访问,任务全部堆积到早上第一波访问时集中执行,导致数据库连接数打满、首页 502。解决方式就是改用系统 cron 并把扫描时间挪到凌晨三点。 - 服务器时区与站点时区不一致。 用
current_time('timestamp')而不是time()。 - Webhook 重复投递造成重复续期。 必须用订单号做幂等判断。
- 降级了但页面还显示旧权限。 页面缓存与用户缓存都要清,
clean_user_cache()别忘了。 - 宽限期与到期判断互相打架。 宽限状态应单独存一个 meta,不要覆盖到期时间。
- 停用插件没清理计划任务。 留下孤儿任务,升级插件后重复执行。
总结
到期处理的合格标准:任务由系统 cron 触发、时间统一用站点时区、Webhook 幂等、降级后有宽限期与通知。上线前用 wp cron event run 手动跑一次,确认邮件与权限都按预期变化。会员等级怎么设计见付费会员社区权限方案;交易邮件送达率怎么保障见WordPress SMTP 邮件配置。
