PAYATHON 2026

商家扣款支付并签约场景为何无法签约

支付老李

结论

支付页没有签约开关,支付成功后也只收到支付通知,通常说明支付宝把当前请求识别成了普通的 alipay.trade.app.pay 支付,没有正确识别其中的“支付并签约”参数。

先检查最终签名请求的 biz_content,确认其中包含完整且有效的 agreement_sign_params。如果该字段缺失、层级错误、字段值与已开通的商家扣款产品不一致,或者当前应用、商户和签约场景尚未开通相应能力,签约流程都不会出现。Android 端能够正常拉起支付,只能说明支付参数有效,不能证明支付并签约参数已经生效。

常见原因

1. 最终请求中没有 agreement_sign_params

支付并签约仍通过 alipay.trade.app.pay 拉起,但需要在 biz_content 中传入签约参数。如果最终参与签名的内容只有支付参数,支付宝展示的就会是普通支付页。

常见情况有:

  • 组装订单时遗漏了 agreement_sign_params。
  • 业务对象中设置了该字段,但服务端 SDK 版本不支持,序列化时将其丢弃。
  • agreement_sign_params 被放在了 biz_content 外层。
  • 先完成签名,再追加签约参数,导致该字段不在实际签名内容中。
  • biz_content 被重复进行 URL 编码,支付宝无法正常解析其中的嵌套结构。

排查时应查看服务端最终生成的完整订单串,不能只检查签名前的业务对象。

2. 签约参数与已开通产品不匹配

personal_product_code、sign_scene 等字段不能自行填写,必须与支付宝为当前应用和商户开通的商家扣款产品及签约场景一致。

文档示例中可能会出现以下结构:

{
  "personal_product_code": "GENERAL_WITHHOLDING_P",
  "sign_scene": "实际开通的签约场景",
  "external_agreement_no": "商户侧唯一签约号"
}

这些值只是为了说明字段结构。实际值应以支付宝开放平台中当前应用已开通的产品配置和对应接口文档为准,尤其不要直接复制其他业务场景的 sign_scene。

3. 应用或商户没有开通商家扣款能力

普通 App 支付和商家扣款属于不同的产品能力。alipay.trade.app.pay 能够正常完成支付,不代表商家扣款已经开通。

需要逐项确认:

  • 商家扣款产品已经签约并生效。
  • 请求中的 app_id 对应已开通该能力的应用。
  • 商户 PID、应用和签约产品属于同一套环境。
  • 沙箱参数没有发往正式环境,正式环境参数也没有发往沙箱。
  • 当前签约场景已经审核通过,而不只是创建了应用或开通了 App 支付。

4. 服务端 SDK 不支持相关字段

如果通过 SDK 的请求模型设置参数,旧版 SDK 可能没有对应属性,也可能在序列化时忽略未知字段。这会导致业务代码看似已经传值,但最终生成的 biz_content 中并没有该字段。

判断方法很直接:检查最终签名订单串中的 biz_content。如果解码后找不到 agreement_sign_params,问题出在服务端生成订单的环节,与 Android 端的调起代码无关。

5. 签约号不符合唯一性要求

external_agreement_no 是商户侧的签约标识,通常需要满足以下要求:

  • 在商户侧保持唯一。
  • 不与其他用户或历史签约记录混用。
  • 重试同一笔签约时遵守接口的幂等要求。
  • 不使用可能冲突或重复绑定的订单号、用户 ID 等值,除非业务上已经保证其唯一性。

如果签约号已经存在、解约后不能复用,或者已经绑定其他用户,具体表现会因产品规则而异。此时需要结合接口同步返回信息和支付宝侧记录继续判断。

正确的请求结构

支付参数和签约参数都应放在 biz_content 中,结构示例如下:

{
  "out_trade_no": "PAY_202609090001",
  "subject": "会员服务",
  "total_amount": "0.01",
  "product_code": "QUICK_MSECURITY_PAY",
  "agreement_sign_params": {
    "personal_product_code": "以实际开通值为准",
    "sign_scene": "以实际开通场景为准",
    "external_agreement_no": "AGREEMENT_202609090001"
  }
}

服务端生成 App 支付订单的示例代码如下:

AlipayTradeAppPayRequest request = new AlipayTradeAppPayRequest();

JSONObject bizContent = new JSONObject();
bizContent.put("out_trade_no", "PAY_202609090001");
bizContent.put("subject", "会员服务");
bizContent.put("total_amount", "0.01");
bizContent.put("product_code", "QUICK_MSECURITY_PAY");

JSONObject agreementSignParams = new JSONObject();
agreementSignParams.put(
    "personal_product_code",
    "以支付宝实际开通的产品码为准"
);
agreementSignParams.put(
    "sign_scene",
    "以支付宝实际开通的签约场景为准"
);
agreementSignParams.put(
    "external_agreement_no",
    "AGREEMENT_202609090001"
);

bizContent.put("agreement_sign_params", agreementSignParams);

request.setBizContent(bizContent.toJSONString());
request.setNotifyUrl("https://example.com/alipay/notify");

AlipayTradeAppPayResponse response = alipayClient.sdkExecute(request);
String orderInfo = response.getBody();

代码中的产品码、签约场景和通知地址都是占位值,不能直接用于生产环境。其他必填字段也应以当前商家扣款支付并签约文档和已开通的产品配置为准。

Android 端调用方式

Android 端一般仍使用支付接口拉起:

Runnable payRunnable = () -> {
    PayTask payTask = new PayTask(activity);
    Map<String, String> result = payTask.payV2(orderInfo, true);

    activity.runOnUiThread(() -> {
        String resultStatus = result.get("resultStatus");
        String resultData = result.get("result");

        // 这里只能作为客户端展示依据。
        // 最终支付和签约状态必须由服务端查询或异步通知确认。
    });
};

Thread payThread = new Thread(payRunnable);
payThread.start();

orderInfo 必须是服务端生成并完成签名的完整订单串。不要在 Android 端解析后重新拼接、修改或追加 agreement_sign_params,否则签名会失效。

客户端使用 pay() 还是 payV2(),通常不会影响签约页是否展示。支付宝是否将订单识别为有效的支付并签约请求,取决于服务端生成的订单内容。

推荐排查步骤

第一步:检查最终订单串

拆分服务端返回给 Android 的原始 orderInfo,对其中的 biz_content 进行一次 URL 解码,然后检查:

  • 是否包含 agreement_sign_params。
  • agreement_sign_params 是否位于 biz_content 内。
  • 该字段是否为合法的 JSON 对象,而不是格式错误的字符串。
  • JSON 中的双引号和反斜杠是否因重复序列化出现异常。
  • 是否进行了两次 URL 编码。
  • 订单串是否在签名完成后又被修改。

如果最终解码结果中没有该字段,应修复服务端的参数组装逻辑或升级 SDK。

第二步:核对应用和产品配置

在支付宝开放平台中检查当前 app_id,确认:

  • 已开通商家扣款。
  • 已开通支付并签约对应的场景。
  • personal_product_code 与实际开通结果一致。
  • sign_scene 与审核通过的业务场景一致。
  • 商户身份、应用公钥和运行环境没有混用。

第三步:保存支付宝同步结果

排查时不要只读取 resultStatus,还应完整保存 payV2() 返回的以下字段:

resultStatus
result
memo

日志必须脱敏,不能记录完整签名、用户身份信息、授权信息或其他敏感数据。

第四步:分别确认支付状态和签约状态

支付成功和签约成功是两个独立状态。不能因为支付已经成功,就直接把协议标记为已签约。服务端需要分别核实:

  • 交易是否成功。
  • 是否已经生成有效协议。
  • 协议状态是否允许后续扣款。

如果没有收到签约通知,先确认用户是否在页面上实际完成过签约。如果页面根本没有签约入口,也不会产生签约通知。

第五步:携带完整信息向支付宝侧核查

如果最终 biz_content 没有问题,产品也已开通,但页面仍然只显示普通支付,应准备以下信息交由支付宝侧核查:

  • app_id
  • 商户 PID
  • out_trade_no
  • external_agreement_no
  • 请求时间
  • 脱敏后的完整请求参数
  • SDK 同步返回内容
  • 支付宝交易号
  • 使用的环境和网关地址

不要只提供 Android 调起代码,因为这类问题大多与服务端参数或产品权限有关。

注意事项

  • 签名必须在服务端完成,不能把私钥放在 Android 客户端。
  • biz_content 应先完成 JSON 序列化,再交给支付宝 SDK 统一签名和编码。
  • 不要手动修改已经签名的订单串。
  • 支付通知和签约通知需要分别验签、分别处理,并做好幂等控制。
  • 客户端返回结果不能作为最终业务凭证。
  • 如果产品同时支持“仅签约”和“支付并签约”,需要确认接入的是支付并签约方案,不能在调用普通 App 支付后期待系统自动完成签约。
  • 产品配置、用户账户状态和风控规则也可能影响支付宝是否展示签约开关。如果页面完全没有签约入口,应先检查当前请求是否被识别成了普通支付。

备注:内容仅供参考。