PAYATHON 2026

如何将两个站点接入 nopcommerce 的单一支付网关

支付小周

结论

可以实现,但不建议让 def.com 直接调用 nopCommerce 支付插件。更合适的做法是在 abc.com 中增加一个“支付桥接服务”。def.com 创建业务订单后,通过受保护的服务端 API 请求 abc.com 创建支付会话,再把用户跳转到 abc.com,由 nopCommerce 已集成的支付插件完成付款。

支付结果由 abc.com 验证,并通过服务端通知同步给 def.com。浏览器跳转仅用于展示结果,不能用来确认付款是否成功。

实施前需要确认两个前提:

  • 支付网关的商户协议允许为 def.com 上的交易收款。
  • 当前支付插件支持这种接入方式。部分插件要求交易关联 nopCommerce 的 Order、购物车、客户或回调路由,无法脱离订单流程单独调用。

推荐架构

完整流程如下:

  1. 用户在 def.com 提交订单。
  2. def.com 创建状态为 PendingPayment 的本地订单。
  3. def.com 的服务器调用 abc.com 的支付创建接口。
  4. abc.com 校验签名、金额、币种、订单号和回跳地址。
  5. abc.com 创建对应的 nopCommerce 支付订单或桥接订单,并生成一次性支付会话。
  6. abc.com 返回支付地址,例如:
https://abc.com/external-payment/pay/{paymentToken}
  1. def.com 将用户重定向到该地址。
  2. abc.com 通过现有的 nopCommerce 支付插件发起付款。
  3. 支付网关向 abc.com 发送异步通知。
  4. abc.com 验证通知并更新支付状态,再调用 def.com 的服务端通知接口。
  5. 最后将用户跳回 def.com 的支付结果页面。

订单之间应保留以下关联:

def.com OrderId
        ↓
abc.com BridgeOrderId / nopCommerce OrderId
        ↓
Payment Gateway TransactionId

退款、对账或排查异常时,都可以通过这条关联找到对应记录。

不要直接跨站调用支付插件

nopCommerce 支付插件通常运行在 nopCommerce 的应用上下文中,可能依赖:

  • nopCommerce Order 和客户数据
  • 当前店铺及币种配置
  • 依赖注入容器
  • 插件配置和密钥
  • 路由、回调控制器及数据库
  • ProcessPayment、PostProcessPayment 等支付流程

因此,不要在 def.com 中引用插件程序集,也不要直接操作 nopCommerce 数据库。否则两个系统会产生很强的耦合,升级插件或 nopCommerce 后可能直接失效。

定义支付创建接口

下面的接口需要在 abc.com 中自行实现,并非 nopCommerce 自带接口。字段可以按实际业务调整。

public sealed class CreatePaymentRequest
{
    public string MerchantOrderNo { get; set; } = default!;
    public decimal Amount { get; set; }
    public string Currency { get; set; } = default!;
    public string ReturnUrl { get; set; } = default!;
    public string NotifyUrl { get; set; } = default!;
    public long Timestamp { get; set; }
    public string Nonce { get; set; } = default!;
    public string Signature { get; set; } = default!;
}

public sealed class CreatePaymentResponse
{
    public string PaymentId { get; set; } = default!;
    public string PaymentUrl { get; set; } = default!;
}

def.com 创建订单后,由服务器发起调用:

POST https://abc.com/api/external-payments
Content-Type: application/json

{
  "merchantOrderNo": "DEF-20260901-10001",
  "amount": 299.00,
  "currency": "CNY",
  "returnUrl": "https://def.com/payment/return",
  "notifyUrl": "https://def.com/api/payment-notifications",
  "timestamp": 1788220800,
  "nonce": "f302df8d81294bafbba2e6ebea746ad2",
  "signature": "..."
}

abc.com 返回:

{
  "paymentId": "01K4...",
  "paymentUrl": "https://abc.com/external-payment/pay/01K4..."
}

随后,def.com 将用户重定向到支付地址:

public async Task<ActionResult> Pay(long orderId)
{
    var order = await _orderService.GetPendingOrderAsync(orderId);

    var result = await _abcPaymentClient.CreatePaymentAsync(new CreatePaymentRequest
    {
        MerchantOrderNo = order.OrderNumber,
        Amount = order.TotalAmount,
        Currency = order.Currency,
        ReturnUrl = "https://def.com/payment/return",
        NotifyUrl = "https://def.com/api/payment-notifications"
    });

    return Redirect(result.PaymentUrl);
}

示例省略了签名生成和错误处理,实际实现时必须补齐。

在 abc.com 中接入 nopCommerce 支付流程

abc.com 收到请求后,应先创建桥接记录,例如:

PaymentId
SourceSite
MerchantOrderNo
NopOrderId
Amount
Currency
Status
GatewayTransactionId
CreatedAt
PaidAt

如果现有插件依赖标准的 nopCommerce 订单,建议先创建对应的 nopCommerce 订单,再按正常结账后的流程调用插件。具体调用方式取决于 nopCommerce 版本和支付插件类型,不能假定所有插件都提供同一套公开方法。

常见插件大致有两类:

  • 跳转型支付:插件生成网关地址,用户从 abc.com 跳转到第三方支付页面。
  • 站内支付:用户在 abc.com 输入支付信息,插件再向网关提交交易。

尽量复用插件原有的订单创建、支付发起、回调验证和状态更新流程,不要复制插件内部代码。

支付结果处理

abc.com 确认付款成功后,可以向 def.com 发送以下通知:

{
  "merchantOrderNo": "DEF-20260901-10001",
  "paymentId": "01K4...",
  "status": "Paid",
  "amount": 299.00,
  "currency": "CNY",
  "transactionId": "GW-839201",
  "timestamp": 1788220860,
  "signature": "..."
}

def.com 的通知接口需要验证签名,并保证幂等:

[HttpPost]
public async Task<ActionResult> PaymentNotification(
    PaymentNotification notification)
{
    if (!_signatureService.Verify(notification))
        return new HttpStatusCodeResult(401);

    var order = await _orderService.FindByOrderNumberAsync(
        notification.MerchantOrderNo);

    if (order == null)
        return HttpNotFound();

    if (order.TotalAmount != notification.Amount ||
        order.Currency != notification.Currency)
    {
        return new HttpStatusCodeResult(400);
    }

    if (order.PaymentStatus == PaymentStatus.Paid)
        return new HttpStatusCodeResult(200);

    await _orderService.MarkAsPaidAsync(
        order.Id,
        notification.TransactionId);

    return new HttpStatusCodeResult(200);
}

实际更新订单时,还要使用数据库事务或条件更新,避免同一通知并发到达后触发重复发货。

签名和安全设计

两个站点之间的请求不能只依赖登录 Cookie。服务端身份认证可以采用 HMAC-SHA256 或客户端证书。

签名原文应包含这些字段:

MerchantOrderNo
Amount
Currency
Timestamp
Nonce

HMAC 示例:

public static string CreateSignature(string canonicalText, string secret)
{
    using var hmac = new HMACSHA256(Encoding.UTF8.GetBytes(secret));
    var hash = hmac.ComputeHash(Encoding.UTF8.GetBytes(canonicalText));
    return Convert.ToHexString(hash).ToLowerInvariant();
}

同时需要满足以下要求:

  • 全程使用 HTTPS。
  • 金额必须由服务器读取订单数据,不能接受浏览器提交的最终金额。
  • 校验币种、金额和外部订单号。
  • 限制时间戳的有效期,防止旧请求被重放。
  • 保存并检查 Nonce。
  • 对 ReturnUrl 和 NotifyUrl 使用白名单,禁止任意跳转。
  • 支付令牌必须随机生成、只能使用一次,并设置有效期。
  • API 密钥只能保存在服务器端。
  • 日志中不得记录银行卡号、CVV、密钥或完整支付凭据。

如果要在 abc.com 接收银行卡信息,还需要评估 PCI DSS 等合规要求。优先使用支付网关托管页面或 tokenization,避免两个站点直接接触敏感卡数据。

回跳页面不能确认支付成功

用户回到:

https://def.com/payment/return

只能说明浏览器完成了跳转,不能证明款项已经到账。用户可能伪造请求或中途关闭页面,网关通知也可能晚于浏览器回跳。

回跳页面应根据订单号向 def.com 后端查询状态:

  • Paid:显示支付成功。
  • Pending:显示“支付结果确认中”,并进行短暂轮询。
  • Failed 或 Cancelled:允许用户重新支付。
  • 状态长时间无法确定:由后台主动向 abc.com 查询支付状态。

还需要考虑的异常情况

生产环境至少需要处理以下情况:

  • 创建支付请求重复提交
  • 用户重复点击付款
  • 网关重复发送通知
  • 通知乱序或延迟
  • abc.com 通知 def.com 失败
  • 用户付款后没有返回页面
  • 金额或币种不一致
  • 订单取消后支付成功
  • 退款、部分退款和支付撤销
  • 两个站点之间短暂断网

abc.com 应保存通知的投递状态,并通过带退避策略的重试机制再次投递。def.com 也应提供按 MerchantOrderNo 或 PaymentId 查询支付状态的接口,用于定时补偿。

这个需求并不是简单地让两个站点“共享一个支付插件”,而是把 abc.com 作为受控的支付入口。只要支付网关协议允许这种收款方式,插件能够关联外部业务订单,并且签名、幂等、异步通知和对账机制都设计完整,def.com 就可以通过 abc.com 可靠地完成支付。

备注:内容仅供参考。