本文目录导读:

- 基础数据模型设计(假设)
- 实时校验(核心拦截器)
- 定时任务(Cron)批量处理
- 过渡期与宽限期(Grace Period)
- 立即失效与“永久”处理
- 前端表现与体验
- 防止日期篡改(安全)
- 完整处理流程图(伪代码)
- 如果使用第三方支付(如 Stripe/支付宝)
- 总结建议
处理 PHP 中会员过期(Subscription Expiry)的逻辑,不仅仅是写一个 if 判断当前时间是否大于到期时间,良好的设计需要考虑数据模型、定时任务、权限校验以及用户体验。
以下是分层次的完整处理方案:
基础数据模型设计(假设)
在数据库用户表中,通常需要存储以下日期字段:
plan_expiry:当前套餐的到期时间戳(或 DateTime)。plan_type:套餐类型(如monthly,yearly,admin)。
核心校验规则(伪代码):
if ($user->plan_expiry > now()) {
// 有效期内,放行
} else {
// 已过期,拒访 / 降级为游客
}
实时校验(核心拦截器)
在用户每次请求“受保护”接口或页面时,执行实时检查。
中间件/基类控制器示例:
public function checkMembership(User $user) {
// 判断是否过期
$isExpired = $user->plan_expiry->isPast();
if ($isExpired) {
// 方案A:直接拒绝访问
abort(403, '您的会员已过期,请续费');
// 方案B:自动降级为普通用户(修改角色ID)
// $user->update(['role' => 'guest']);
}
}
注意: 即使有定时任务,实时校验也不能省略,因为定时任务可能延迟或失败。
定时任务(Cron)批量处理
使用 Laravel Task Scheduling 或 Linux Crontab,每天凌晨运行脚本清理过期用户。
定时任务操作逻辑(Laravel 示例):
// app/Console/Commands/ExpireMembers.php
$expiredUsers = User::where('plan_type', '!=', 'free')
->where('plan_expiry', '<', now())
->get();
foreach ($expiredUsers as $user) {
// 1. 修改用户角色为“游客”
$user->role = 'guest';
// 2. 清理登录缓存/Token权限
// $user->tokens()->delete(); // 如果用了 Sanctum/Passport
// 3. 发送过期邮件通知(可选)
Mail::to($user)->send(new MembershipExpired($user));
$user->save();
}
Cron 配置(Laravel):
* * * * * php /path/to/artisan schedule:run >> /dev/null 2>&1
过渡期与宽限期(Grace Period)
为了避免用户“秒变”无权访问,可以设置宽限期(过期后 3 天内)。
$graceEnd = $user->plan_expiry->addDays(3);
if (now()->between($user->plan_expiry, $graceEnd)) {
// 宽限期内:允许看部分资源,但弹窗提示续费
} elseif (now()->greaterThan($graceEnd)) {
// 完全锁定
}
立即失效与“永久”处理
(1)立即失效:
如果是 VIP 账号,建议在用户点击“取消订阅”或支付失败时,立即调用 update 方法将 plan_expiry 设置为当前时间,而不是等它自然过期。
(2)永久会员(Admin/终身):
设定一个超大值或使用 is_lifetime 布尔字段。
if ($user->is_lifetime || $user->plan_expiry == null) {
// 放行
}
前端表现与体验
不要只显示“403 错误”,要让用户知道如何解决。
-
API 响应:
return response()->json([ 'status' => 'expired', 'message' => '会员已过期', 'renew_url' => '/subscription/renew' ], 403); -
页面重定向: 如果已过期,重定向到“续费页面”,并带上当前用户ID,方便支付后直接更新到期时间。
防止日期篡改(安全)
- 避免前端传参决定权限:权限判断必须是服务端根据数据库记录,不能依赖
request传来的时间。 - 加密签名:如果会员在微信/APP端,注意防止用户修改
plan_expiry参数。 - 使用 JWT Token 时,在 Token 中携带过期时间,中间件校验 Token 和 数据库时间,以数据库为准。
完整处理流程图(伪代码)
graph TD
A[用户请求] --> B{Is Lifetime?}
B -- Yes --> C[允许访问]
B -- No --> D{plan_expiry > now?}
D -- Yes --> C
D -- No --> E{在宽限期?}
E -- Yes --> F[允许访问 + 提示续费]
E -- No --> G[强制登出/降级为游客]
G --> H[跳转续费页面]
如果使用第三方支付(如 Stripe/支付宝)
订阅场景:建议使用 Webhook(回调) 来处理续费/到期,而不是完全依赖定时任务。
invoice.payment_succeeded-> 延长到期时间。customer.subscription.deleted-> 立即将plan_expiry置为当前时间。
总结建议
最推荐的组合是:“数据库时间戳 + 中间件实时校验 + 定时任务清理(重体力操作)+ 优雅的降级提示”,不要为了减少数据库查询而将所有过期逻辑依赖定时任务,因为实时校验才是“最后一道防线”。