SQL Server Stored procedure:用收款抵扣现有欠款
支付小周
结论
不需要使用 CURSOR。可以先用窗口函数计算每笔欠款之前的累计余额,再通过一次集合化 UPDATE,按日期顺序抵扣付款。
如果同一天有多笔欠款,必须使用唯一且稳定的字段(如 BillId)确定先后顺序。仅按 D_Date 排序,抵扣顺序并不确定。
以下实现采用题目中的更新规则:
- 付款已经覆盖的记录,将
N_Amount更新为0; - 完全抵扣的记录,将
Amount_Rest更新为0; - 部分抵扣的记录,在
Amount_Rest中保留尚欠金额; - 付款尚未抵扣到的记录保持不变。
存储过程示例
假设表中至少有以下字段:
BillId:唯一主键;D_Date:欠款日期;N_Amount:原题中的Amount;Amount_Rest:当前剩余欠款。
CREATE OR ALTER PROCEDURE dbo.ApplyPayment
@PaymentAmount decimal(19, 2)
AS
BEGIN
SET NOCOUNT ON;
SET XACT_ABORT ON;
IF @PaymentAmount IS NULL OR @PaymentAmount <= 0
BEGIN
THROW 50001, 'Payment amount must be greater than zero.', 1;
END;
BEGIN TRANSACTION;
;WITH OrderedBills AS
(
SELECT
b.BillId,
b.Amount_Rest,
PreviousBalance =
COALESCE(
SUM(b.Amount_Rest) OVER
(
ORDER BY b.D_Date, b.BillId
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
),
CONVERT(decimal(19, 2), 0)
)
FROM dbo.FD_Bills AS b WITH (UPDLOCK, HOLDLOCK)
WHERE b.Amount_Rest > 0
)
UPDATE b
SET
b.N_Amount =
CASE
WHEN @PaymentAmount > ob.PreviousBalance
THEN CONVERT(decimal(19, 2), 0)
ELSE b.N_Amount
END,
b.Amount_Rest =
CASE
-- 付款尚未抵扣到当前记录
WHEN @PaymentAmount <= ob.PreviousBalance
THEN b.Amount_Rest
-- 付款足以完全抵扣当前记录
WHEN @PaymentAmount >= ob.PreviousBalance + ob.Amount_Rest
THEN CONVERT(decimal(19, 2), 0)
-- 付款只能部分抵扣当前记录
ELSE ob.PreviousBalance
+ ob.Amount_Rest
- @PaymentAmount
END
FROM dbo.FD_Bills AS b
INNER JOIN OrderedBills AS ob
ON ob.BillId = b.BillId;
COMMIT TRANSACTION;
END;
GO
调用方式:
EXEC dbo.ApplyPayment
@PaymentAmount = 250.00;
假设最早的两笔欠款都是 150.00,抵扣过程如下:
- 第一笔抵扣
150.00,还剩100.00可用于付款; - 第二笔抵扣
100.00,这笔欠款还剩50.00; - 付款用完,后面的记录不变。
更新后,前两条记录为:
D_Date N_Amount Amount_Rest
---------- --------- -----------
2018-12-05 0.00 0.00
2018-12-05 0.00 50.00
计算原理
PreviousBalance 是当前记录之前所有欠款的累计余额:
SUM(Amount_Rest) OVER
(
ORDER BY D_Date, BillId
ROWS BETWEEN UNBOUNDED PRECEDING AND 1 PRECEDING
)
处理当前记录时:
- 如果付款金额不大于
PreviousBalance,说明付款已经被前面的记录用完,当前记录不变; - 如果付款金额大于等于
PreviousBalance + Amount_Rest,说明当前记录可以全部抵扣; - 如果付款金额介于两者之间,当前记录只能部分抵扣。
这段 SQL 会一次处理所有记录,不需要逐行操作游标,也能避免遗漏最后一条记录或因游标读取顺序不明确而出错。
如果 N_Amount 表示原始账单金额
原始账单金额通常需要保留,只更新 Amount_Rest。如果 N_Amount 保存的是账单最初的金额,而非本次处理后的金额,可以删除对 N_Amount 的更新:
UPDATE b
SET b.Amount_Rest =
CASE
WHEN @PaymentAmount <= ob.PreviousBalance
THEN b.Amount_Rest
WHEN @PaymentAmount >= ob.PreviousBalance + ob.Amount_Rest
THEN CONVERT(decimal(19, 2), 0)
ELSE ob.PreviousBalance
+ ob.Amount_Rest
- @PaymentAmount
END
FROM dbo.FD_Bills AS b
INNER JOIN OrderedBills AS ob
ON ob.BillId = b.BillId;
这样处理后,第一条记录的原始金额仍为 150.00,剩余欠款则变为 0.00。从账务审计的角度看,保留原始账单金额通常更合理。
注意事项
- 不要只用日期排序。同一天有多条记录时,还应加入
BillId、创建时间或业务序号。 - 金额建议使用
decimal(19, 2),避免使用近似数值类型。现有的money类型可以继续使用,但明确指定精度的decimal通常更方便控制计算规则。 - 事务与
UPDLOCK, HOLDLOCK可以避免两笔付款同时读取相同余额并重复抵扣。 - 如果表中包含多个客户或账户,需要增加账户参数,并在查询中加入
WHERE CustomerId = @CustomerId。 - 如果付款金额大于全部欠款,这个过程会将所有欠款清零,但不会记录多出的金额。需要保留预付款时,应计算剩余付款,并将其写入单独的付款表或账户余额表。
- 实际账务系统最好同时保存付款明细以及付款与账单的分摊关系,而不只是修改余额。这样才能查清每笔付款抵扣了哪些账单。
备注:内容仅供参考。
