PAYATHON 2026

MS SQL:计算指定月份的套餐费用

支付阿杰

结论

以套餐开始日期为固定锚点,生成每个 billing cycle 的开始日期。统计账单区间 [@billStartDate, @billEndDate) 内已经开始、且开始时套餐仍然有效的周期数,再乘以 @price,即可得到费用。

套餐结束时不按比例退款。只要某个 billing cycle 已经开始,就收取完整的周期费用。如果套餐在周期中途结束,则不再生成下一个周期。

计算规则

以下代码基于这些前提:

  • billing cycle 从 @planStartDate 开始,每隔一个月产生一次费用。
  • 账单月份采用左闭右开区间:>= @billStartDate AND < @billEndDate。
  • 套餐有效期也采用左闭右开区间,周期开始日期必须 < @planEndDate。
  • @planEndDate IS NULL 表示套餐仍在运行。
  • “指定月份的费用”是指该月份内发生的周期扣费,不是按自然月天数拆分周期费用。

例如,周期从每月 4 日开始,套餐在 10 日结束。由于当月周期已在 4 日开始,因此仍会收取整个周期的费用,但下个月 4 日不会再产生费用。

SQL Server 实现

DECLARE @planStartDate date = '2014-04-16';
DECLARE @planEndDate   date = NULL;
DECLARE @billStartDate date = '2015-02-01';
DECLARE @billEndDate   date = '2015-03-01';
DECLARE @price         decimal(18, 2) = 100.00;

;WITH BillingCycles AS
(
    -- n 表示从套餐开始日期起经过的月份数
    SELECT
        0 AS n,
        @planStartDate AS cycleStartDate

    UNION ALL

    SELECT
        n + 1,
        DATEADD(month, n + 1, @planStartDate)
    FROM BillingCycles
    WHERE DATEADD(month, n + 1, @planStartDate) < @billEndDate
)
SELECT
    COUNT(*) * @price AS chargeAmount
FROM BillingCycles
WHERE cycleStartDate >= @billStartDate
  AND cycleStartDate <  @billEndDate
  AND (
        @planEndDate IS NULL
        OR cycleStartDate < @planEndDate
      )
OPTION (MAXRECURSION 0);

这里特意使用:

DATEADD(month, n, @planStartDate)

而不是在上一个周期日期的基础上不断增加一个月。这样每个周期都以原始开始日期为基准,可以减少连续计算月末日期造成的偏移。

查看具体计费周期

调试时,可以先列出账单月份内实际匹配的周期:

;WITH BillingCycles AS
(
    SELECT
        0 AS n,
        @planStartDate AS cycleStartDate

    UNION ALL

    SELECT
        n + 1,
        DATEADD(month, n + 1, @planStartDate)
    FROM BillingCycles
    WHERE DATEADD(month, n + 1, @planStartDate) < @billEndDate
)
SELECT
    cycleStartDate,
    DATEADD(month, n + 1, @planStartDate) AS cycleEndDate,
    @price AS cyclePrice
FROM BillingCycles
WHERE cycleStartDate >= @billStartDate
  AND cycleStartDate <  @billEndDate
  AND (
        @planEndDate IS NULL
        OR cycleStartDate < @planEndDate
      )
ORDER BY cycleStartDate
OPTION (MAXRECURSION 0);

例如,billing cycle 在 2 月 16 日开始,那么账单区间 [2015-02-01, 2015-03-01) 内会产生一次完整的 @price 费用。即使客户在 2 月 20 日结束套餐,这笔费用也不会按比例退还。

需要注意的边界情况

套餐恰好在周期开始日结束

代码使用:

cycleStartDate < @planEndDate

因此,如果套餐恰好在某个周期的开始日结束,该周期不会收费。这里把结束日期理解为”从该时刻起,套餐不再有效”。

如果业务规定结束当天仍然有效,应改为:

cycleStartDate <= @planEndDate

具体采用哪种判断,应以实际计费约定为准。

月末开始的套餐

SQL Server 的 DATEADD 会自动处理目标月份中不存在的日期。例如:

DATEADD(month, 1, CAST('2024-01-31' AS date))

结果为 2024-02-29。由于计算始终以原始 @planStartDate 为锚点,后续周期会尽可能回到月末:

2024-01-31
2024-02-29
2024-03-31
2024-04-30

如果业务要求在 2 月调整到 29 日后,后续月份都固定在 29 日续费,就需要改用其他周期推进规则,不能采用固定锚点算法。

日期时间字段

如果变量的实际类型是 datetime 或 datetime2,比较时也会包含时间部分。建议仍然使用左闭右开的区间:

cycleStartDate >= @billStartDate
AND cycleStartDate < @billEndDate

不要使用 BETWEEN,否则相邻的两个账单区间可能同时包含 @billEndDate 这个边界。

数据量较大时

递归 CTE 适合计算单个套餐或少量套餐。如果需要一次处理大量客户,或者套餐可能跨越很多年,最好准备日历表或数字表,再根据月份序号批量生成周期,避免为每条套餐执行较长的递归。

备注:内容仅供参考。