PAYATHON 2026

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 状态不一致。

备注:内容仅供参考。