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 适合计算单个套餐或少量套餐。如果需要一次处理大量客户,或者套餐可能跨越很多年,最好准备日历表或数字表,再根据月份序号批量生成周期,避免为每条套餐执行较长的递归。
备注:内容仅供参考。
