PAYATHON 2026

如何在 Hosted Payment Page 和 return url 获取客户信息

支付老李

结论

Hosted Payment Page 在部分场景下可以预填客户姓名、邮箱等信息,但预填字段并不等于绑定已有客户。使用面向新订阅的公共支付页时,即使填写了相同的邮箱,也可能创建新的 customer 和 subscription。要复用已有客户,应先在 Zoho Subscriptions 中取得 customer_id,再通过适用于该客户的订阅或支付流程生成 Hosted Payment Page。

支付完成后,不要认为 Zoho 会自动将 customer_id 添加到 return URL。创建支付流程时,可以在 return URL 中加入自己的关联标识,但更安全的做法是传递一次性 state。收到回调后,服务端通过 state 找到本地客户,再使用 Zoho API 或 webhook 核验支付结果。

预填 Hosted Payment Page

如果当前使用的 Hosted Payment Page 类型支持 URL 参数,可以对姓名、邮箱等字段进行 URL 编码,再附加到地址中。例如:

using System;
using System.Collections.Specialized;
using System.Web;

public static string BuildHostedPageUrl(
    string hostedPageUrl,
    string firstName,
    string lastName,
    string email)
{
    var builder = new UriBuilder(hostedPageUrl);
    NameValueCollection query = HttpUtility.ParseQueryString(builder.Query);

    query["first_name"] = firstName;
    query["last_name"] = lastName;
    query["email"] = email;

    builder.Query = query.ToString();
    return builder.Uri.ToString();
}

生成的地址类似:

https://example-hosted-page?...&first_name=Li&last_name=Ming&email=ming%40example.com

支持的字段及字段名称取决于当前 Zoho Subscriptions 账户使用的 Hosted Payment Page/API 版本。不要自行认定 customer_id、company_name 等参数一定可用。

URL 参数通常只负责填写页面字段,无法保证订单会绑定到已有客户。如果系统中已经存在该客户,建议按以下流程处理:

  1. 在自己的数据库中保存 Zoho 返回的 customer_id。
  2. 创建订阅或发起支付时,将该 customer_id 提交给 Zoho。
  3. 使用 API 返回的 Hosted Payment Page URL 引导客户付款。
  4. 不要根据姓名或邮箱判断是否为同一个 Zoho 客户。

如果客户需要修改已有订阅,也不要再次打开用于创建新订阅的公共页面,而应使用 Zoho 提供的订阅更新、付款或客户门户流程。否则,创建新的 subscription 很可能就是该流程的正常结果。

向 return URL 传递客户关联信息

query string 应使用 ?,不能写成:

{return_url}/customer_id={Id}

正确形式是:

{return_url}?customer_id={Id}

如果每次创建 Hosted Payment Page 时都能动态指定 return URL,可以在服务端构造地址:

using System.Web;

public static string BuildReturnUrl(string baseUrl, string state)
{
    return baseUrl + "?state=" + HttpUtility.UrlEncode(state);
}

例如:

https://merchant.example.com/payment/complete?state=RANDOM_ONE_TIME_TOKEN

不建议把内部客户编号、邮箱或其他客户资料直接放进 URL。URL 可能被记录在浏览器历史、访问日志和分析系统中,用户也可以自行修改其中的参数。更稳妥的实现如下:

// 创建支付页之前
string state = Guid.NewGuid().ToString("N");

// 将 state 与本地客户、待支付订单保存到数据库
SavePaymentState(
    state: state,
    localCustomerId: customer.Id,
    expectedPlanCode: planCode,
    status: "Pending");

// 将回调地址提交给 Zoho
string returnUrl = BuildReturnUrl(
    "https://merchant.example.com/payment/complete",
    state);

回调控制器只使用 state 查找记录:

public ActionResult Complete(string state)
{
    if (string.IsNullOrWhiteSpace(state))
    {
        return new HttpStatusCodeResult(400, "Missing state");
    }

    var payment = FindPaymentState(state);

    if (payment == null || payment.IsExpired || payment.IsConsumed)
    {
        return new HttpStatusCodeResult(400, "Invalid state");
    }

    // 这里仍不能仅凭浏览器跳转认定支付成功。
    // 应通过 Zoho API 查询支付/订阅状态,或等待并验证 webhook。
    var verifiedStatus = VerifyPaymentWithZoho(payment);

    if (!verifiedStatus.IsPaid)
    {
        return View("PaymentPending");
    }

    MarkPaymentStateAsConsumed(state);
    return View("PaymentSuccess", verifiedStatus);
}

如果 return URL 固定在产品配置中,不能随每次支付动态设置,就无法用它携带不同客户的 ID。此时应使用 Hosted Payment Page/API 返回的交易标识、订阅标识或经过签名的一次性关联值,再由服务端完成映射。

return URL 不能作为支付成功凭证

return URL 的作用只是将浏览器带回网站,不能单独证明付款成功,原因包括:

  • 用户可能关闭页面,浏览器并未跳转;
  • 用户可以手动访问或修改回调 URL;
  • 支付状态可能仍在处理中;
  • 查询参数可能被伪造。

可靠的确认流程如下:

  1. 创建支付请求时,保存本地订单和关联状态。
  2. 接收 Zoho webhook,校验其真实性并记录事件。
  3. 必要时调用 Zoho API,查询对应 subscription、invoice 或 payment 的实际状态。
  4. 核对金额、币种、套餐和本地订单,全部一致后再开通服务。
  5. 对 webhook 和回调处理进行幂等控制,避免重复开通。

这两个场景需要分开处理。Hosted Payment Page 的预填参数只能改善填写体验;已有客户绑定、支付确认和客户信息恢复,则应依靠服务端 API、数据库映射及 webhook,不能依赖浏览器地址中的客户资料。

备注:内容仅供参考。