如何在 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 参数通常只负责填写页面字段,无法保证订单会绑定到已有客户。如果系统中已经存在该客户,建议按以下流程处理:
- 在自己的数据库中保存 Zoho 返回的
customer_id。 - 创建订阅或发起支付时,将该
customer_id提交给 Zoho。 - 使用 API 返回的 Hosted Payment Page URL 引导客户付款。
- 不要根据姓名或邮箱判断是否为同一个 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;
- 支付状态可能仍在处理中;
- 查询参数可能被伪造。
可靠的确认流程如下:
- 创建支付请求时,保存本地订单和关联状态。
- 接收 Zoho webhook,校验其真实性并记录事件。
- 必要时调用 Zoho API,查询对应 subscription、invoice 或 payment 的实际状态。
- 核对金额、币种、套餐和本地订单,全部一致后再开通服务。
- 对 webhook 和回调处理进行幂等控制,避免重复开通。
这两个场景需要分开处理。Hosted Payment Page 的预填参数只能改善填写体验;已有客户绑定、支付确认和客户信息恢复,则应依靠服务端 API、数据库映射及 webhook,不能依赖浏览器地址中的客户资料。
备注:内容仅供参考。
