支付宝 H5 如何接入周期性扣款和免密支付?
结论
H5 通常不需要单独接入所谓的“周期扣款 H5 API”。商户服务端调用 alipay.user.agreement.page.sign 生成签约页面,H5 前端跳转到支付宝提供的页面。签约成功后,服务端保存支付宝返回的协议号,再通过 alipay.trade.pay 发起后续扣款。
“免密支付”并不是一个可以直接搜索、任意接入的通用接口名称。在支付宝开放平台中,这类能力通常归入周期扣款等代扣产品。周期扣款适用于金额、周期和服务内容相对明确的连续服务,不能用于实现任意时间、任意金额的后台扣款。
能否接入,要看商户是否已经开通对应产品、所属行业是否符合准入要求,以及开放平台为当前应用配置了哪些签约产品参数。如果没有产品权限,即使接口代码没有问题,也无法完成签约或扣款。
为什么文档没有单独列出 H5
小程序、App 和 PC 的区别主要在签约入口和页面唤起方式。底层流程相同,都是用户签约、商户保存协议、服务端扣款。
H5 没有独立的支付容器,一般使用页面接口:
- 用户在商户 H5 页面点击开通自动续费。
- H5 请求商户服务端创建签约。
- 服务端调用
alipay.user.agreement.page.sign。 - 服务端把 SDK 生成的 HTML 表单返回给浏览器。
- 浏览器提交表单,跳转到支付宝签约页面。
- 用户确认签约后,支付宝向商户服务端发送异步通知。
- 商户保存协议号,后续由服务端发起扣款。
如果 H5 位于微信等限制外部应用跳转的浏览器中,能否直接唤起支付宝还会受浏览器策略影响。遇到这种情况,通常需要提示用户使用系统浏览器打开,不能只靠前端 JavaScript 绕过限制。
接入前需要确认的事项
接入前,先在支付宝开放平台确认:
- 商户账号已经开通周期扣款或对应的代扣产品。
- 当前应用已经绑定该产品权限。
- 应用、公钥、商户主体和签约产品使用的是同一套配置。
- 业务属于平台允许的周期性服务场景。
- 已取得当前产品要求的
product_code、personal_product_code、sign_scene等参数。 - 已配置可通过公网访问的 HTTPS 异步通知地址。
- 已确认单笔金额、扣款周期、扣款时间和解约规则。
产品参数可能随行业方案和商户权限而变化,应以开放平台控制台及产品开通后展示的接口文档为准,不要直接照搬其他商户的参数。
H5 签约流程
1. 创建商户侧签约记录
跳转到支付宝之前,先生成唯一的商户协议编号,例如:
external_agreement_no=AUTO_RENEW_20260909_100001
该编号需要满足以下要求:
- 在商户系统内唯一。
- 创建后不能重复分配给其他用户。
- 与当前登录用户、套餐和订单绑定。
- 不包含手机号、身份证号等敏感信息。
- 可用于异步通知的幂等处理。
2. 服务端生成签约页面
以下代码用 Java SDK 展示基本结构。参数值只是格式示例,实际接入时必须使用开放平台为当前产品提供的配置。
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
appId,
merchantPrivateKey,
"json",
"UTF-8",
alipayPublicKey,
"RSA2"
);
AlipayUserAgreementPageSignRequest request =
new AlipayUserAgreementPageSignRequest();
request.setReturnUrl("https://merchant.example.com/alipay/sign-result");
request.setNotifyUrl("https://merchant.example.com/api/alipay/agreement-notify");
JSONObject periodRule = new JSONObject();
periodRule.put("period_type", "MONTH");
periodRule.put("period", 1);
periodRule.put("execute_time", "9");
periodRule.put("single_amount", "39.90");
JSONObject bizContent = new JSONObject();
bizContent.put("product_code", "GENERAL_WITHHOLDING");
bizContent.put("personal_product_code", "CYCLE_PAY_AUTH");
bizContent.put("sign_scene", "INDUSTRY|业务场景值");
bizContent.put(
"external_agreement_no",
"AUTO_RENEW_20260909_100001"
);
bizContent.put("period_rule", periodRule);
request.setBizContent(bizContent.toJSONString());
AlipayUserAgreementPageSignResponse response =
alipayClient.pageExecute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
response.getCode() + ": " + response.getSubMsg()
);
}
String formHtml = response.getBody();
pageExecute() 通常返回一个可自动提交的 HTML 表单。商户接口应以 text/html;charset=UTF-8 返回该内容,由浏览器跳转到支付宝签约页:
@GetMapping(
value = "/alipay/agreement/sign",
produces = "text/html;charset=UTF-8"
)
@ResponseBody
public String sign() throws AlipayApiException {
return createAgreementSignForm();
}
也可以让 H5 请求服务端,取得签约地址或表单后再跳转。商户私钥不能保存在浏览器中,支付宝请求也不能由前端自行拼接或签名。
3. 处理签约结果
return_url 只负责用户完成操作后的页面跳转,不能作为签约成功的最终依据。用户可能关闭页面或重复跳转,浏览器参数也可能被伪造。
签约结果应以 notify_url 收到的异步通知为准,并完成以下处理:
- 验证支付宝通知签名。
- 检查通知中的
app_id和商户协议编号。 - 确认协议状态已经生效。
- 保存支付宝协议号
agreement_no。 - 使用商户协议编号进行幂等处理。
- 处理成功后,按支付宝要求返回成功响应。
伪代码如下:
Map<String, String> params = extractRequestParameters(request);
boolean valid = AlipaySignature.rsaCheckV1(
params,
alipayPublicKey,
"UTF-8",
"RSA2"
);
if (!valid) {
response.setStatus(400);
return "failure";
}
String externalAgreementNo = params.get("external_agreement_no");
String agreementNo = params.get("agreement_no");
String status = params.get("status");
checkAppId(params.get("app_id"));
updateAgreementIdempotently(
externalAgreementNo,
agreementNo,
status
);
return "success";
通知字段名称和成功状态值应以当前签约接口的通知文档为准。不能因为前端返回页显示“成功”,就直接把协议标记为可扣款状态。
如果迟迟没有收到异步通知,应调用对应的协议查询接口核实状态,不要让用户反复签约。
后续免密扣款
签约成功后,扣款必须由服务端发起。常见做法是调用 alipay.trade.pay,并在请求中传入有效的支付宝协议号。
结构示例如下:
AlipayTradePayRequest request = new AlipayTradePayRequest();
JSONObject agreementParams = new JSONObject();
agreementParams.put("agreement_no", agreementNo);
JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", "RENEW_20261009_100001");
bizContent.put("total_amount", "39.90");
bizContent.put("subject", "会员连续包月服务");
bizContent.put("product_code", "CYCLE_PAY_AUTH");
bizContent.put("agreement_params", agreementParams);
request.setBizContent(bizContent.toJSONString());
AlipayTradePayResponse response = alipayClient.execute(request);
if (response.isSuccess()) {
// 仍需结合交易状态和异步通知更新订单
} else {
// 保存 code、sub_code、sub_msg,按错误类型处理
}
这里的 product_code、协议参数结构和必填字段,都必须以商户实际开通的周期扣款产品文档为准。不同代扣产品的协议号不能混用,也不能自行认定某个产品参数一定适用于当前应用。
每次扣款都要使用全局唯一的 out_trade_no。如果发生网络超时,应使用原订单号查询或重试,不能更换新订单号后直接再次扣款,否则可能产生重复支付。
解约处理
系统至少要支持两种解约方式:
- 用户在商户页面主动关闭自动续费。
- 用户在支付宝侧解除协议后,由支付宝通知商户。
商户主动解约时,应调用当前产品对应的协议解约接口,例如开放平台实际授权的用户协议解约能力。解约成功后,系统要立即停止创建新的扣款订单。
本地数据库建议同时保存:
user_id
external_agreement_no
agreement_no
agreement_status
product_code
sign_time
invalid_time
last_notify_time
不能只删除本地记录而不调用支付宝解约接口,否则支付宝侧的协议可能仍然有效。
免密支付与周期扣款的关系
免密支付和周期扣款不能简单画等号:
- “免密支付”描述的是用户授权后,后续付款不再逐笔输入密码的体验。
- “周期扣款”是免密代扣的一种具体产品形态,要求业务具有明确的服务周期。
- 连续包月、会员订阅等业务通常适合使用周期扣款。
- 扣款时间、用途不固定,或由商户任意触发的业务,不一定符合周期扣款要求。
- 如果只是希望用户支付普通 H5 订单时不用输入密码,商户无法自行控制。支付宝会根据账户情况和风控策略决定是否要求用户验证。
实际业务如果是自动续费,可以申请周期扣款。如果没有明确的服务周期,应向支付宝确认适用的代扣产品,不能为了获得“免密”体验而套用周期扣款接口。
常见注意事项
- 私钥和签名操作只能放在服务端。
return_url不能代替异步通知。- 签约和扣款通知都要验签,并进行幂等处理。
- 协议号应加密保存,或按照敏感业务数据妥善保管。
- 扣款金额和时间必须符合签约时约定的周期规则。
- 扣款失败后不能无限重试,应根据错误码区分余额不足、协议失效、风控拦截和系统异常。
- 用户解约、协议过期或协议状态异常后,必须停止扣款。
- 页面应明确展示扣款金额、扣款周期、服务内容、取消方式和自动续费规则。
- 正式环境与沙箱环境的网关、应用、公钥和协议数据不能混用。
- 如果开放平台控制台中没有周期扣款产品或相关 API 权限,问题就不在 H5 实现,而在产品准入或应用授权。此时需要先向支付宝开放平台确认开通资格。
备注:内容仅供参考。
