PAYATHON 2026

Hotcakes custom payment provider 回调处理与 return url 构造

支付阿杰

结论

可以在 ProcessCheckout 中准备支付请求,并将顾客重定向到支付平台。但判断订单是否支付成功,不能以顾客是否返回页面为依据。

支付平台的 server-to-server 通知应由独立且允许匿名访问的 HTTP endpoint 接收。这个 endpoint 负责验证签名、查询订单、幂等更新支付状态,并按照支付平台的要求返回响应。顾客的 return URL 只用于恢复前台结账流程和展示支付结果。

return URL 和通知 URL 都应通过 ASP.NET MVC 的路由机制生成绝对 URL,不要手动拼接域名和路径。

推荐的处理流程

完整流程通常如下:

  1. Hotcakes 创建订单或生成待支付订单。
  2. ProcessCheckout 构造支付平台要求的参数。
  3. 将 Hotcakes 订单号或不可预测的支付引用传给支付平台。
  4. 顾客被重定向到支付平台。
  5. 支付平台向通知 endpoint 发起 server-to-server 请求。
  6. 商店验证通知并更新订单付款状态。
  7. 支付平台将顾客重定向到 return URL。
  8. 返回页面重新读取订单状态并展示结果。

这里需要区分两个地址:

  • notifyUrl、callbackUrl 或 webhookUrl:供支付平台服务器调用。
  • returnUrl:供顾客浏览器在支付后返回。

不同支付平台使用的字段名可能不同,应以相应的接口文档为准。

在 ProcessCheckout 中重定向

如果使用 Hosted Payment Page,顾客需要离开商店并前往支付平台,那么在 ProcessCheckout 阶段准备跳转是合理的。

但在远程支付流程完成前,不应将订单标记为已支付。此时订单最多只能进入类似“等待付款”的状态。

下面是一个结构示例。具体的方法签名需要根据当前 Hotcakes 版本和自定义支付插件接口进行调整:

public ActionResult ProcessCheckout()
{
    var order = LoadCurrentOrder();

    var returnUrl = Url.Action(
        "PaymentReturn",
        "Checkout",
        new { orderId = order.bvin },
        Request.Url.Scheme
    );

    var notifyUrl = Url.Action(
        "PaymentNotification",
        "PaymentCallback",
        null,
        Request.Url.Scheme
    );

    var paymentReference = CreatePaymentReference(order);

    SavePaymentReference(order.bvin, paymentReference);

    var request = new PaymentRequest
    {
        Reference = paymentReference,
        Amount = order.TotalGrand,
        Currency = order.StoreId.ToString(), // 仅为结构示例,应替换为真实币种
        ReturnUrl = returnUrl,
        NotifyUrl = notifyUrl
    };

    var providerUrl = BuildProviderRedirectUrl(request);

    return Redirect(providerUrl);
}

币种、金额格式和签名字段必须按照支付平台协议填写,不能直接使用示例中的占位表达式。

如果 Hotcakes 的 ProcessCheckout 不直接返回 MVC ActionResult,可以让它返回支付平台地址或设置重定向结果,再由 Checkout Controller 执行 Redirect(...)。具体实现取决于当前版本提供的支付插件接口。

接收 server-to-server 通知

通知处理最好放在单独的 Controller 或 Web API endpoint 中,不要将支付确认的核心逻辑写进顾客返回时调用的 Checkout action。

示例:

public class PaymentCallbackController : Controller
{
    [HttpPost]
    [AllowAnonymous]
    public ActionResult PaymentNotification()
    {
        var payload = ReadRequestBody(Request);
        var signature = Request.Headers["X-Payment-Signature"];

        if (!VerifySignature(payload, signature))
        {
            return new HttpStatusCodeResult(400, "Invalid signature");
        }

        var notification = ParseNotification(payload);
        var order = FindOrderByPaymentReference(notification.Reference);

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

        if (!AmountsMatch(order, notification))
        {
            return new HttpStatusCodeResult(400, "Amount mismatch");
        }

        if (notification.Status == "paid")
        {
            if (!OrderAlreadyPaid(order))
            {
                RecordSuccessfulPayment(order, notification);
                MarkOrderAsPaid(order);
            }
        }
        else if (notification.Status == "failed")
        {
            RecordFailedPayment(order, notification);
        }

        return Content("OK");
    }
}

Content("OK") 仅用于说明结构。有些支付平台要求返回指定文本、JSON 内容或 HTTP 状态码,实际响应必须严格遵循其通知协议。

通知 endpoint 通常需要允许匿名访问,因为支付平台不会携带 DNN 用户的登录信息。不过,“允许匿名访问”并不表示可以省略请求验证,至少应检查:

  • 通知签名或消息摘要。
  • 商户账号是否匹配。
  • 订单或支付引用是否存在。
  • 金额和币种是否与本地订单一致。
  • 支付状态是否属于允许处理的状态。
  • 支付平台交易号是否已经处理。
  • 时间戳或 nonce 是否符合平台的防重放要求。

如果支付平台支持通过 API 主动查询交易状态,可以在收到通知后再查询一次,并用查询结果确认通知内容。

顾客返回页面的职责

顾客返回时,不能仅凭 query string 中的 status=success 将订单改为已支付。浏览器参数可能被修改,顾客也可能直接关闭页面,导致 return URL 根本没有被访问。

返回 action 应以查询和展示为主:

[HttpGet]
[AllowAnonymous]
public ActionResult PaymentReturn(string orderId)
{
    var order = LoadOrder(orderId);

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

    if (!CanCurrentVisitorView(order))
    {
        return new HttpStatusCodeResult(403);
    }

    var model = new PaymentReturnViewModel
    {
        OrderNumber = order.OrderNumber,
        IsPaid = OrderAlreadyPaid(order),
        StatusMessage = OrderAlreadyPaid(order)
            ? "付款已确认。"
            : "付款结果仍在确认,请稍后刷新。"
    };

    return View(model);
}

在实际项目中,不宜在公开 URL 中直接使用容易枚举的内部订单标识。可以改用一次性 token、随机支付引用,或者同时校验当前登录用户和订单归属。

server-to-server 通知和浏览器跳转是两个独立请求,无法保证两者的到达顺序。顾客可能先返回,而服务器通知稍后才到,因此返回页面需要支持显示“正在确认付款”。

正确构造绝对 return URL

如果代码运行在 MVC Controller 中,优先使用 Url.Action:

var returnUrl = Url.Action(
    "PaymentReturn",
    "Checkout",
    new { orderId = order.bvin },
    Request.Url.Scheme
);

指定 protocol 后,MVC 通常会生成绝对 URL,例如:

https://shop.example.com/checkout/payment-return?orderId=...

通知地址也可以用相同方式生成:

var notifyUrl = Url.Action(
    "PaymentNotification",
    "PaymentCallback",
    null,
    Request.Url.Scheme
);

如果当前重载只能生成相对路径,可以基于当前请求创建绝对地址:

var relativeUrl = Url.Action(
    "PaymentReturn",
    "Checkout",
    new { orderId = order.bvin }
);

var returnUrl = new Uri(Request.Url, relativeUrl).AbsoluteUri;

对于 DNN/Hotcakes,还需要确认生成结果是否包含正确的 portal alias、语言路径和虚拟目录。如果网站部署在反向代理之后,也要确认 ASP.NET 能识别原始 HTTPS scheme,否则可能生成错误的 http:// 地址。

生产环境还可以将商店的公开基准地址保存为受控配置,再与路由生成的相对路径组合:

var publicBaseUrl = new Uri(configuredStoreUrl);
var relativeUrl = Url.Action(
    "PaymentReturn",
    "Checkout",
    new { orderId = order.bvin }
);

var returnUrl = new Uri(publicBaseUrl, relativeUrl).AbsoluteUri;

基准地址必须来自可信配置,不能直接采用未经校验的 Host header,否则可能引发 Host Header Injection 风险。

Hotcakes 集成时的注意事项

不要重复记账

如果支付平台请求超时,或没有收到预期响应,可能会多次发送同一条通知。应使用支付平台交易号或事件 ID 进行幂等判断:

if (PaymentTransactionExists(notification.TransactionId))
{
    return Content("OK");
}

数据库更新最好放在事务中,防止两个并发通知同时创建付款记录。

不要只修改订单状态文字

支付成功通常不仅会改变订单状态,还涉及付款交易记录、金额、支付方式、平台交易号和 Hotcakes 的后续工作流。应尽量调用当前 Hotcakes 版本提供的订单和支付业务 API,而不是直接修改数据库表。

不同 Hotcakes 版本的支付事务 API 可能并不相同,这部分应以项目实际引用的程序集和示例支付方式为准。

不要依赖 Session

server-to-server 请求不会携带顾客浏览器中的 Session 或认证 Cookie。回调必须能通过支付引用、订单标识或商户订单号独立找到订单。

避免把回调放进标准 Checkout action

直接修改 Hotcakes 自带的 CheckoutController 可能可以工作,但后续升级容易发生冲突。更稳妥的做法是创建独立 Controller,并通过模块或应用的路由注册公开 endpoint。

如果当前 DNN/Hotcakes 安装限制了自定义 MVC Controller 的公开路由,可以改用 DNN Web API endpoint。无论选择哪种方式,都要确认支付平台可以从公网直接访问该 URL,而且请求不会被登录跳转、CSRF 页面、URL 重写或防火墙规则拦截。

正确处理 CSRF 与签名

浏览器表单通常使用 anti-forgery token,但第三方支付平台无法获得这个 token。因此,通知 action 一般不能要求普通的 MVC anti-forgery token,而应使用支付平台规定的签名验证机制。不能为了让请求通过而关闭所有验证。

建议的最终结构

支付集成可以拆分为三个组件:

  • 支付方法:创建支付请求并提供跳转地址。
  • 通知 endpoint:验证服务器通知,并更新 Hotcakes 订单和支付交易。
  • 返回页面:读取订单的最终状态,并向顾客展示结果。

采用这种结构后,即使顾客没有返回商店、支付平台重复发送通知,或者通知与浏览器跳转的到达顺序发生变化,订单仍然可以得到可靠处理。

备注:内容仅供参考。