会员过期计算必须统一使用DateTime对象操作,避免time()与strtotime()混用;全程固定Asia/Shanghai时区,存储用getTimestamp(),比较和续费均基于DateTime日历逻辑,杜绝时间戳算术运算。

time() 和 strtotime() 在会员过期计算中不能混用
PHP 中 time() 返回当前 Unix 时间戳(秒级整数),而 strtotime() 接收字符串并尝试解析,但对模糊日期(如 "+1 month")的处理依赖当前日期和系统时区规则,容易在月末触发“跳月”或“截断”行为。比如 1 月 31 日执行 strtotime("+1 month"),多数情况下返回 3 月 1 日而非预期的 2 月 28/29 日。
会员到期日必须是确定、可复现、不随执行时间浮动的值,所以首次开通时应统一用 DateTime 对象做加法,避免字符串解析歧义:
$start = new DateTime('2024-05-31');
$start->modify('+1 month');
echo $start->format('Y-m-d'); // 输出 2024-06-30,不是 2024-07-01关键点:
-
DateTime::modify()按日历逻辑递进,会自动处理大小月和闰年 - 不要用
strtotime($timestamp . ' +30 days')这类拼接,$timestamp是整数时会强制转成字符串再解析,结果不可控 - 存储到期时间一律用
DateTime::getTimestamp()转为整数存数据库,便于后续比较
判断会员是否过期:别直接比时间戳大小
看似简单:$now > $expire_timestamp?问题在于时区。如果 $expire_timestamp 是按北京时间生成,但 PHP 默认时区是 UTC,time() 返回的就是 UTC 时间戳,二者差 8 小时,导致凌晨 0 点刚过就误判为已过期。
立即学习“PHP免费学习笔记(深入)”;
正确做法是全程在统一时区下运算:
- 设置默认时区:
date_default_timezone_set('Asia/Shanghai'); - 读取数据库中的到期时间戳后,用
new DateTime('@' . $expire_timestamp)构造对象,并显式设时区:->setTimezone(new DateTimeZone('Asia/Shanghai')) - 比较时用
DateTime对象的getTimestamp(),而不是直接比原始整数 - 或者更稳妥:所有比较都用
DateTime对象,调用$now->getTimestamp() > $expire->getTimestamp()
续费时延长有效期:不能在原时间戳上加秒数
常见错误是拿到数据库里的 $expire_timestamp,然后写 $new_expire = $expire_timestamp + 30 * 24 * 3600。这假设每月都是 30 天、每天都是 86400 秒——忽略了夏令时切换(可能少 1 小时或多 1 小时)、闰秒(极少见但存在)、以及“1 个月”的真实日历长度。
续费必须基于原始开通时间或当前到期日重新计算日历周期:
- 如果会员支持“从当前到期日起续 1 年”,就用
$expire = new DateTime('@' . $expire_timestamp); $expire->setTimezone(new DateTimeZone('Asia/Shanghai')); $expire->modify('+1 year'); - 如果规则是“无论当前状态,续费即从今天起算新周期”,那就用
new DateTime('today')开始加,而不是拿旧过期时间加 - 永远不要对时间戳做算术加减来表示日历偏移
MySQL 存储与查询 VIP 到期时间的坑
数据库字段类型选 INT 存时间戳没问题,但要注意 MySQL 的 FROM_UNIXTIME() 默认按系统时区转换,可能导致 SELECT 出来的时间显示错乱;而 BETWEEN 查询若混用 NOW() 和时间戳字段,也会因时区隐式转换出错。
推荐组合方案:
- 字段用
INT UNSIGNED,存DateTime::getTimestamp()结果(确保是北京时间对应的时间戳) - 查询是否过期:
SELECT * FROM users WHERE expire_time < UNIX_TIMESTAMP(NOW())——前提是 PHP 和 MySQL 都设为Asia/Shanghai时区 - 如果无法统一 MySQL 时区,改用
SELECT * FROM users WHERE expire_time < UNIX_TIMESTAMP(CONVERT_TZ(NOW(), '+00:00', '+08:00')) - 避免在 SQL 里用
DATE_ADD(FROM_UNIXTIME(expire_time), INTERVAL 1 MONTH),这种跨函数嵌套极易因时区和精度丢失失效
最易被忽略的是:不同环境(开发机、测试服、线上)的 date.timezone 配置可能不一致,同一个时间戳在本地显示为 2024-06-01,在线上却变成 2024-05-31。上线前务必确认所有环节时区对齐。



















