PAYATHON 2026

H5场景如何接入包月周期扣款并唤起支付宝签约页面?

支付小周

结论

H5 场景应调用支付宝周期扣款签约接口 alipay.user.agreement.page.sign。商户服务端负责组装签约参数,并用应用私钥完成 RSA2 签名。随后,将支付宝 SDK 生成的表单页面返回浏览器,由页面自动提交表单,进入支付宝签约页。

签约后,不能只看前端跳转结果。服务端需要验证支付宝异步通知的签名;如有必要,再调用 alipay.user.agreement.query 查询协议状态。确认协议有效后,才能保存 agreement_no,用于后续的周期扣款。

接入流程

完整流程如下:

  1. H5 请求商户服务端创建签约。
  2. 服务端生成唯一的商户协议号 external_agreement_no。
  3. 服务端调用 alipay.user.agreement.page.sign。
  4. 服务端将支付宝 SDK 返回的 HTML 表单直接输出给浏览器。
  5. 浏览器自动提交表单,进入支付宝签约页面。
  6. 用户确认签约后,支付宝跳转到 return_url。
  7. 支付宝同时向 notify_url 发送异步通知。
  8. 服务端验签,并通过协议查询接口确认最终状态。
  9. 保存支付宝协议号 agreement_no,供后续扣款、查询和解约使用。

H5 不应自行拼接支付宝签约地址,也不能在前端生成签名。签约参数和应用私钥都必须由服务端处理。

服务端发起签约

下面以支付宝 Java SDK 为例。具体的 SDK 版本、网关地址和部分业务参数,需要以当前开放平台文档及已签约产品的配置为准。

import com.alipay.api.AlipayClient;
import com.alipay.api.DefaultAlipayClient;
import com.alipay.api.domain.AccessParams;
import com.alipay.api.domain.AlipayUserAgreementPageSignModel;
import com.alipay.api.domain.PeriodRuleParams;
import com.alipay.api.request.AlipayUserAgreementPageSignRequest;
import com.alipay.api.response.AlipayUserAgreementPageSignResponse;

import java.math.BigDecimal;

public class AlipayAgreementService {

    private final AlipayClient alipayClient;

    public AlipayAgreementService() {
        this.alipayClient = new DefaultAlipayClient(
            "https://openapi.alipay.com/gateway.do",
            "你的APP_ID",
            "你的应用私钥",
            "json",
            "UTF-8",
            "支付宝公钥或支付宝公钥证书配置",
            "RSA2"
        );
    }

    public String createSignPage(String externalAgreementNo) throws Exception {
        AlipayUserAgreementPageSignRequest request =
            new AlipayUserAgreementPageSignRequest();

        request.setReturnUrl(
            "https://www.example.com/alipay/agreement/result"
        );
        request.setNotifyUrl(
            "https://api.example.com/alipay/agreement/notify"
        );

        AlipayUserAgreementPageSignModel model =
            new AlipayUserAgreementPageSignModel();

        // 周期扣款产品码
        model.setPersonalProductCode("CYCLE_PAY_AUTH");

        // 必须使用开放平台为当前应用和产品配置的签约场景值
        model.setSignScene("YOUR_SIGN_SCENE");

        // 商户侧协议号,必须唯一并可用于幂等处理
        model.setExternalAgreementNo(externalAgreementNo);

        AccessParams accessParams = new AccessParams();

        // H5 唤起支付宝时常见配置为 ALIPAYAPP,
        // 最终以周期扣款产品文档及应用签约配置为准
        accessParams.setChannel("ALIPAYAPP");
        model.setAccessParams(accessParams);

        PeriodRuleParams periodRuleParams = new PeriodRuleParams();
        periodRuleParams.setPeriodType("MONTH");
        periodRuleParams.setPeriod(1L);

        // 月周期执行时间的格式和取值限制需按接口文档填写
        periodRuleParams.setExecuteTime("1");

        // 单次扣款上限,不要使用 double 计算金额
        periodRuleParams.setSingleAmount(new BigDecimal("99.00"));
        model.setPeriodRuleParams(periodRuleParams);

        request.setBizModel(model);

        AlipayUserAgreementPageSignResponse response =
            alipayClient.pageExecute(request);

        if (!response.isSuccess()) {
            throw new IllegalStateException(
                "创建支付宝签约页面失败,code=" + response.getCode()
                    + ", subCode=" + response.getSubCode()
                    + ", subMsg=" + response.getSubMsg()
            );
        }

        return response.getBody();
    }
}

控制器需要把 response.getBody() 作为 HTML 输出:

@GetMapping(
    value = "/api/alipay/agreement/sign",
    produces = "text/html;charset=UTF-8"
)
@ResponseBody
public String sign() throws Exception {
    String externalAgreementNo = createUniqueAgreementNo();
    return alipayAgreementService.createSignPage(externalAgreementNo);
}

支付宝 SDK 返回的内容通常是一个自动提交的表单,因此 H5 可以直接跳转到商户服务端提供的这个地址:

const externalAgreementNo = "AGR_" + Date.now();

window.location.href =
  "/api/alipay/agreement/sign?externalAgreementNo=" +
  encodeURIComponent(externalAgreementNo);

实际项目中,最好不要完全由前端生成 external_agreement_no。由服务端先创建业务记录并生成协议号,可以避免重复签约和协议号冲突。

为什么不要使用 Ajax 获取表单

如果通过 fetch 或 Ajax 获取支付宝 SDK 返回的 HTML,再用 innerHTML 写入页面,HTML 中的自动提交脚本可能不会执行。此外,这种方式还可能受到 CSP、跨域限制和浏览器安全策略的影响。

更可靠的方式是让浏览器直接访问商户的签约接口:

window.location.assign("/api/alipay/agreement/sign");

商户接口返回自动提交表单后,浏览器会继续跳转到支付宝签约页。

如果业务流程必须先通过 Ajax 创建签约,服务端可以返回经过确认可用的跳转地址,或者由前端明确地创建并提交表单。不要直接执行来源不明的 HTML。

签约结果处理

return_url 只用于用户完成签约后的页面跳转。用户可能关闭页面或重复刷新,跳转请求也可能中断,所以不能仅凭 return_url 判断签约成功。

服务端还需要处理 notify_url:

@PostMapping("/alipay/agreement/notify")
@ResponseBody
public String agreementNotify(HttpServletRequest request) throws Exception {
    Map<String, String> params = new HashMap<>();

    request.getParameterMap().forEach((key, values) -> {
        if (values != null && values.length > 0) {
            params.put(key, values[0]);
        }
    });

    boolean valid = AlipaySignature.rsaCheckV1(
        params,
        "支付宝公钥",
        "UTF-8",
        "RSA2"
    );

    if (!valid) {
        return "failure";
    }

    String externalAgreementNo = params.get("external_agreement_no");
    String agreementNo = params.get("agreement_no");

    // 还需要核对 app_id、商户协议号和协议状态等业务字段。
    // 更新操作必须具备幂等性,避免重复通知造成重复处理。
    saveAgreement(externalAgreementNo, agreementNo, params);

    return "success";
}

通知字段和表示成功的状态值应以当前接口文档为准。如果没有收到通知、通知字段不完整,或者协议状态无法确定,应调用:

alipay.user.agreement.query

可以使用 agreement_no 或 external_agreement_no 查询协议,并根据查询结果确认协议是否可用。

后续周期扣款

签约成功只表示商户取得了用户授权,并不代表支付宝会自动为商户生成每一期订单。商户系统通常还需要完成以下工作:

  1. 按照包月规则生成每一期账单。
  2. 到期后调用周期扣款对应的支付接口。
  3. 在支付请求中关联有效的 agreement_no。
  4. 处理支付异步通知。
  5. 对扣款失败的情况执行有限次数的重试或补缴流程。
  6. 用户解约后,立即停止创建新的扣款请求。

具体使用哪个扣款接口、协议参数如何组织、允许扣款的时间窗口以及重试规则,都取决于商户开通的周期扣款产品和支付宝当前规则。应以开放平台控制台及相应的产品文档为准,不能只根据签约接口的参数推断。

注意事项

  • 周期扣款场景下,personal_product_code 一般为 CYCLE_PAY_AUTH,但仍要与实际开通的产品一致。
  • sign_scene 不能随意填写,必须使用支付宝为当前业务场景配置或认可的值。
  • period_rule_params 必须符合真实的包月规则,包括周期类型、周期长度、执行日期和单次扣款金额上限。
  • 金额应使用 BigDecimal 或十进制定点类型处理,不能使用 float、double。
  • external_agreement_no 必须唯一,同时要为重复请求做好幂等控制。
  • 应用私钥只能保存在服务端,不能写入 H5、JavaScript 或 App 安装包。
  • return_url 和 notify_url 应使用公网可访问的 HTTPS 地址。
  • 收到异步通知后,必须验证支付宝签名,并核对 app_id、协议号和业务归属。
  • 保存 agreement_no 时,需要同时关联用户、会员套餐和商户协议号,不能只保存一个没有业务关联的协议号。
  • 不能仅凭页面显示”签约成功”就开通会员权益,应等服务端确认协议有效。
  • 用户取消签约、协议失效或扣款权限终止后,应及时停用自动续费,并在业务页面显示当前订阅状态。
  • 外部浏览器能否直接拉起支付宝客户端,取决于浏览器、操作系统和支付宝客户端环境。需要准备无法唤起时的降级提示,让用户通过支付宝提供的标准页面继续操作。

备注:内容仅供参考。