MVC 3 累加 Payments 并确认 Invoice 为 Paid
结论
new ClientPayments() 只会创建一条尚未保存的付款记录。此时 PaymentAmount 的初始值是 0,其中不包含任何历史付款。
应先查询 ClientPayments 表中当前 Invoice 已保存的付款记录,通过 Sum() 计算历史付款总额,再加上本次付款金额。如果累计金额达到或超过发票金额,就将状态设为 "Paid",也可以沿用现有系统中的 "Confirmed";否则设为 "Partly Paid"。
实现示例
金额字段建议使用 decimal。参数名可以改为 paymentAmount,以免与发票总额混淆。
public ActionResult Confirm(int id, decimal paymentAmount, string paymentType)
{
var invoice = db.Invoice.Find(id);
if (invoice == null)
{
return HttpNotFound();
}
// 这里还应检查当前用户是否有权操作该 Invoice。
if (paymentAmount <= 0)
{
ModelState.AddModelError("paymentAmount", "付款金额必须大于 0。");
return View(invoice);
}
// 查询当前 Invoice 已保存的历史付款总额。
// 转为 nullable 可以避免没有付款记录时 Sum() 出错。
decimal previousPaidAmount =
db.ClientPayments
.Where(p => p.InvoiceNumberID == id)
.Select(p => (decimal?)p.PaymentAmount)
.Sum() ?? 0m;
decimal totalPaidAmount = previousPaidAmount + paymentAmount;
if (totalPaidAmount >= invoice.InvoiceAmount)
{
invoice.InvoiceStatus = "Paid";
// 如果现有系统把 Confirmed 作为全额支付状态,
// 可以改为:invoice.InvoiceStatus = "Confirmed";
}
else
{
invoice.InvoiceStatus = "Partly Paid";
}
var clientPayment = new ClientPayments
{
InvoiceNumberID = id,
PaymentAmount = paymentAmount,
PaymentType = paymentType,
PaymentDate = DateTime.Now
};
db.ClientPayments.Add(clientPayment);
db.Entry(invoice).State = EntityState.Modified;
db.SaveChanges();
return RedirectToAction("Details", new { id = id });
}
核心计算如下:
totalPaidAmount = historicalPayments.Sum() + currentPaymentAmount;
不能用下面的写法计算累计金额:
var clientPayment = new ClientPayments();
InvoiceAmount + clientPayment.PaymentAmount
这里的 clientPayment 只是刚创建的新对象,并未从数据库读取任何付款记录,因此 clientPayment.PaymentAmount 通常只是数值类型的默认值 0。
如果金额字段是 long
如果数据库中的 PaymentAmount 和 InvoiceAmount 都是整数类型,可以这样查询:
long previousPaidAmount =
db.ClientPayments
.Where(p => p.InvoiceNumberID == id)
.Select(p => (long?)p.PaymentAmount)
.Sum() ?? 0L;
long totalPaidAmount = previousPaidAmount + paymentAmount;
只要付款金额可能带小数,数据库字段和 C# 模型就应使用 decimal,不要用 float 或 double 处理金额。
状态判断
判断是否付清时应使用 >=,不能只判断是否相等:
if (totalPaidAmount >= invoice.InvoiceAmount)
如果只判断相等,一旦出现多付,累计付款额会大于发票金额,代码反而会把发票标记为 "Partly Paid"。
如果系统不允许超额付款,还要在保存前检查剩余应付金额:
decimal remainingAmount = invoice.InvoiceAmount - previousPaidAmount;
if (paymentAmount > remainingAmount)
{
ModelState.AddModelError(
"paymentAmount",
"本次付款金额不能超过剩余应付金额。");
return View(invoice);
}
还需要注意的事项
即使付款金额来自 radio buttons,也不能直接信任提交的数据。用户可以绕过页面自行发送请求,所以服务端仍需验证:
- 当前用户是否有权确认这张 Invoice;
- 付款金额是否大于
0; - 付款金额是否属于允许的选项;
- 系统是否允许超额付款;
- Invoice 是否已经完全支付;
paymentType是否有效。
并发提交时,两个请求可能同时读到相同的历史付款总额。业务量不大时,上面的代码足以解决累计计算问题。如果同一张 Invoice 可能由多人同时付款,还需使用数据库事务、并发标记或更严格的数据库级锁定,避免付款记录与 Invoice 状态不一致。
备注:内容仅供参考。
