PAYATHON 2026

PHP 从 12 月 29 日起生成错误的 next payment date

支付老李

结论

不要直接以当前日期为基准计算 +2 month。先将日期固定为当月第一天,再增加相应的月份:

<?php

$today = (int) date('j');
$monthsToAdd = $today < 15 ? 1 : 2;

$nextPaymentDate = date(
    'Y-m-d',
    strtotime("first day of this month +{$monthsToAdd} months")
);

这样即使当前日期是 28、29、30 或 31 日,也能正确得到目标月份的第一天。

原因

strtotime('+2 month') 会保留当前日期的“日”。例如,以 2014-12-29 为基准计算时,PHP 会先尝试得到:

2015-02-29

由于 2015 年 2 月没有 29 日,这个日期会溢出,并被规范化为 3 月。此时再使用:

date('Y-m-01', ...)

只能将已经溢出到 3 月的日期格式化为当月第一天,因此结果是:

2015-03-01

date('Y-m-01') 中的 01 只控制最终的输出格式,无法阻止 strtotime() 在计算月份时发生日期溢出。

推荐写法:使用 DateTimeImmutable

支付日期属于业务数据,使用对象式日期 API 通常更清楚:

<?php

$now = new DateTimeImmutable('now');
$monthsToAdd = (int) $now->format('j') < 15 ? 1 : 2;

$nextPaymentDate = $now
    ->modify('first day of this month')
    ->modify("+{$monthsToAdd} months")
    ->format('Y-m-d');

echo $nextPaymentDate;

代码先执行:

->modify('first day of this month')

将日期固定到当月 1 日,然后再增加一个月或两个月。每个月都有 1 日,因此不会出现月底溢出。

如果系统使用固定的业务时区,建议明确指定:

<?php

$timezone = new DateTimeZone('Asia/Shanghai');
$now = new DateTimeImmutable('now', $timezone);

$monthsToAdd = (int) $now->format('j') < 15 ? 1 : 2;

$nextPaymentDate = $now
    ->modify('first day of this month')
    ->modify("+{$monthsToAdd} months")
    ->format('Y-m-d');

关于 15 日的边界

题目原来的判断是:

$today >= $checkDate

对应的规则是:

  • 1~14 日购买:下个月 1 日付款
  • 15 日及以后购买:下下个月 1 日付款

这符合“15 日之前”的字面含义,因为 15 日当天不算在 15 日之前。如果业务要求 15 日当天仍按下个月处理,则应改为:

$monthsToAdd = (int) $now->format('j') <= 15 ? 1 : 2;

不建议的写法

不要连续调用两次 strtotime('next month'):

$nextPaymentDate = date('Y-m-01', strtotime('next month'));
$nextPaymentDate = date($nextPaymentDate, strtotime('next month'));

第二次调用并不是以第一次生成的日期为计算基准。另外,传给 date() 的第一个参数会被解释为格式字符串,而不是基准日期。

如果需要从某个日期继续计算,应明确传入对应的时间戳:

$firstDate = strtotime('first day of next month');
$nextPaymentDate = date('Y-m-d', strtotime('+1 month', $firstDate));

对于当前规则,最直接的做法仍然是先固定到当月第一天,再增加 1 或 2 个月。这样可以避开月末和闰年带来的日期溢出。

备注:内容仅供参考。