PAYATHON 2026

PHP 计算账单周期内的折扣适用百分比

支付小周

结论

先算出 invoice 日期范围与 discount 日期范围重叠了多少天,再除以当前 invoice cycle 的总天数:

适用比例 = 重叠天数 ÷ 账单周期总天数

applyDiscount() 返回一个 0.0 到 1.0 的系数。百分比折扣和固定金额 credit 都可以乘以这个系数。

如果首尾日期都计入有效期,那么 2023-03-10 至 2023-06-10 一共是 93 天,不是 90 天。按题目给出的日期,3 月共有 22 个重叠日,所以适用比例为 22 / 31 ≈ 70.97%,并非约 66%。

实现代码

下面的实现将四个日期的当天都计入日期范围:

<?php

function applyDiscount(
    string $firstDay,
    string $lastDay,
    string $startDisc,
    string $endDisc
): float {
    $timezone = new DateTimeZone('UTC');

    $invoiceStart  = new DateTimeImmutable($firstDay, $timezone);
    $invoiceEnd    = new DateTimeImmutable($lastDay, $timezone);
    $discountStart = new DateTimeImmutable($startDisc, $timezone);
    $discountEnd   = new DateTimeImmutable($endDisc, $timezone);

    if ($invoiceStart > $invoiceEnd) {
        throw new InvalidArgumentException(
            '$firstDay must not be later than $lastDay'
        );
    }

    if ($discountStart > $discountEnd) {
        throw new InvalidArgumentException(
            '$startDisc must not be later than $endDisc'
        );
    }

    // 两个日期范围没有交集。
    if ($discountEnd < $invoiceStart || $discountStart > $invoiceEnd) {
        return 0.0;
    }

    // 交集的开始日期取两个开始日期中较晚的一个。
    $overlapStart = $invoiceStart > $discountStart
        ? $invoiceStart
        : $discountStart;

    // 交集的结束日期取两个结束日期中较早的一个。
    $overlapEnd = $invoiceEnd < $discountEnd
        ? $invoiceEnd
        : $discountEnd;

    // DateTime::diff() 不包含结束当天,因此需要加 1。
    $invoiceDays = $invoiceStart->diff($invoiceEnd)->days + 1;
    $overlapDays = $overlapStart->diff($overlapEnd)->days + 1;

    return $overlapDays / $invoiceDays;
}

计算结果

$discountStart = '2023-03-10';
$discountEnd   = '2023-06-10';

$cycles = [
    ['2023-03-01', '2023-03-31'],
    ['2023-04-01', '2023-04-30'],
    ['2023-05-01', '2023-05-31'],
    ['2023-06-01', '2023-06-30'],
];

foreach ($cycles as [$firstDay, $lastDay]) {
    $factor = applyDiscount(
        $firstDay,
        $lastDay,
        $discountStart,
        $discountEnd
    );

    printf(
        "%s 至 %s:%.4f%%\n",
        $firstDay,
        $lastDay,
        $factor * 100
    );
}

输出结果如下:

2023-03-01 至 2023-03-31:70.9677%
2023-04-01 至 2023-04-30:100.0000%
2023-05-01 至 2023-05-31:100.0000%
2023-06-01 至 2023-06-30:33.3333%

各周期的计算明细为:

Invoice cycle重叠天数周期天数适用比例
2023-03-01 至 2023-03-31223170.9677%
2023-04-01 至 2023-04-303030100%
2023-05-01 至 2023-05-313131100%
2023-06-01 至 2023-06-30103033.3333%

应用于百分比折扣

假设原始折扣率为 50%,先调用 applyDiscount() 算出当前周期的适用系数,再乘以 0.50:

$factor = applyDiscount(
    '2023-03-01',
    '2023-03-31',
    '2023-03-10',
    '2023-06-10'
);

$originalDiscountRate = 0.50;
$effectiveDiscountRate = $originalDiscountRate * $factor;

printf("%.4f%%\n", $effectiveDiscountRate * 100);

3 月的实际折扣率是:

50% × 22 / 31 ≈ 35.4839%

如果账单金额为 $300,可以这样计算:

$invoiceAmount = 300.00;
$discountAmount = $invoiceAmount * $effectiveDiscountRate;
$amountDue = $invoiceAmount - $discountAmount;

应用于固定金额 credit

如果一个完整的 invoice cycle 可获得 $100 credit,也按相同方式乘以适用系数:

$factor = applyDiscount(
    '2023-03-01',
    '2023-03-31',
    '2023-03-10',
    '2023-06-10'
);

$fullCycleCredit = 100.00;
$appliedCredit = $fullCycleCredit * $factor;

echo number_format($appliedCredit, 2);

3 月应抵扣:

$100 × 22 / 31 ≈ $70.97

这里的 $100 指“每个完整 invoice cycle 的 credit”。如果 $100 是整个 90 天折扣期的 credit 总额,就不能让每个账单周期分别乘以这个系数,否则会造成重复抵扣。此时应按整个折扣期的总天数分摊。

90 天结束日期的处理

如果折扣从 2023-03-10 开始,且开始当天算第 1 天,那么第 90 天是 2023-06-07:

$discountStart = new DateTimeImmutable(
    '2023-03-10',
    new DateTimeZone('UTC')
);

$discountEnd = $discountStart->modify('+89 days');

echo $discountEnd->format('Y-m-d'); // 2023-06-07

这里使用 +89 days,因为开始日期本身已经是第 1 天。如果业务规则使用不包含结束日期的半开区间,可以改用 +90 days。同时,applyDiscount() 也必须改为不计算结束当天,不能混用两种日期规则。

注意事项

  • 先明确开始日期和结束日期是否计入有效期。上面的实现采用首尾日期都包含在内的规则。
  • applyDiscount() 最好返回 0.0 到 1.0 的系数,不要直接返回乘以 100 后的数值,这样更方便计算折扣和 credit。
  • 等金额最终入账时再舍入,例如使用 round($amount, 2),不要提前舍入适用比例。
  • 真实货币结算应优先使用整数分或高精度十进制运算,以免浮点误差不断累积。
  • 折扣日期与账单周期完全没有交集时返回 0.0,完全覆盖账单周期时返回 1.0。

备注:内容仅供参考。