如何将两个站点接入 nopcommerce 的单一支付网关
结论
可以实现,但不建议让 def.com 直接调用 nopCommerce 支付插件。更合适的做法是在 abc.com 中增加一个“支付桥接服务”。def.com 创建业务订单后,通过受保护的服务端 API 请求 abc.com 创建支付会话,再把用户跳转到 abc.com,由 nopCommerce 已集成的支付插件完成付款。
支付结果由 abc.com 验证,并通过服务端通知同步给 def.com。浏览器跳转仅用于展示结果,不能用来确认付款是否成功。
实施前需要确认两个前提:
- 支付网关的商户协议允许为
def.com上的交易收款。 - 当前支付插件支持这种接入方式。部分插件要求交易关联 nopCommerce 的
Order、购物车、客户或回调路由,无法脱离订单流程单独调用。
推荐架构
完整流程如下:
- 用户在
def.com提交订单。 def.com创建状态为PendingPayment的本地订单。def.com的服务器调用abc.com的支付创建接口。abc.com校验签名、金额、币种、订单号和回跳地址。abc.com创建对应的 nopCommerce 支付订单或桥接订单,并生成一次性支付会话。abc.com返回支付地址,例如:
https://abc.com/external-payment/pay/{paymentToken}
def.com将用户重定向到该地址。abc.com通过现有的 nopCommerce 支付插件发起付款。- 支付网关向
abc.com发送异步通知。 abc.com验证通知并更新支付状态,再调用def.com的服务端通知接口。- 最后将用户跳回
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 可靠地完成支付。
备注:内容仅供参考。
