PAYATHON 2026

鸿蒙Next系统使用alipays协议支付提示INVALID_PARAMETER

支付小周

结论

单看这条链接,没有哪个业务参数能直接判定为错误。同一订单在 iOS 和 Android 上可以支付,鸿蒙 Next 也能唤起支付宝,但返回 INVALID_PARAMETER,问题更可能出在以下环节:

  1. 鸿蒙侧打开 alipays:// 时,对嵌套的 url 参数做了额外解码、重新编码或 URI 规范化。
  2. 内层支付 URL 中的 +、%2B、%252B、%2F、%26 等字符发生变化,导致支付宝最终解析出的 sign、biz_content、timestamp 等参数与签名时不一致。
  3. 手工拼接 alipays://platformapi/startapp 的兼容性较弱。不同系统处理自定义 Scheme 和嵌套 URL 的方式可能不同。

排查重点不应放在反复修改某个业务字段上,而应保证支付网关 URL 由支付宝服务端 SDK 完整生成。客户端不要解析或修改该 URL,只在把它放入外层 url 参数时,对整体执行一次 encodeURIComponent()。

示例链接中包含姓名、身份证号、订单号和完整签名,不应继续出现在日志、论坛或前端埋点中。建议立即脱敏,并确认对应的订单链接已经失效。

为什么优先怀疑 URL 编码

这个地址实际嵌套了两层 URL:

alipays://platformapi/startapp
  ?appId=20000067
  &url=<支付宝网关完整地址>

支付宝网关地址内部还有一层查询参数:

https://openapi.alipay.com/gateway.do
  ?app_id=...
  &biz_content=...
  &sign=...
  &timestamp=...

因此,同一个字符可能需要经过多层编码。例如,签名中的 + 在支付宝网关地址中应写成:

%2B

当整个网关地址再次作为外层 url 参数编码时,它应变成:

%252B

如果鸿蒙侧的某个 API 提前执行了解码,字符可能按以下过程变化:

%252B -> %2B -> +

而在表单式查询参数中,+ 又可能被解析为空格。类似问题还会影响:

%252F -> %2F -> /
%2526 -> %26 -> &
%253D -> %3D -> =

如果 & 被过早还原,内层参数就会被拆成外层参数。如果签名中的 + 变成空格,签名值也会被破坏。支付宝能正常唤起,只能证明 Scheme 跳转成功,不能证明传入的订单参数仍然完整。

链接中需要重点检查的部分

1. sign

RSA2 签名通常包含 +、/、=,因此最容易在多层 URL 编码中损坏。

需要逐字节比较以下三个阶段的值:

  • 服务端支付宝 SDK 生成的原始 sign
  • 鸿蒙应用打开 Scheme 前,从内层 URL 解析出的 sign
  • 对外层 url 解码一次后,网关 URL 中的 sign

重点检查:

+ 是否变成空格
%2B 是否提前变成 +
/ 是否仍为 %2F
= 是否仍为 %3D

2. biz_content

biz_content 必须是合法 JSON,其实际内容也必须与参与签名时的内容一致。服务端完成签名后,不要再执行以下操作:

  • 重新序列化 JSON
  • 改变字段值
  • 替换空格
  • 重复进行 URL 编码
  • 将 %20 与 + 相互替换
  • 先拆分参数再重新拼接

示例中的 time_expire 包含日期与时间之间的空格:

2025-06-06 17:18:29

这个空格在不同编码层级中可能表现为 +、%20 或 %2B。签名完成后,只要它又被重新解释,就可能导致验签失败或参数解析错误。

3. timestamp

示例中 timestamp 的空格使用了 +。包装外层 URL 时,原有的 + 必须作为普通字符编码成 %2B,不能直接保留。

否则:

2025-06-06+16:48:33

在不同的 URI 实现中可能被解释为:

2025-06-06 16:48:33

虽然某些查询参数解析器认为这两种形式等价,但多层 URI 和签名场景不能依赖这种行为。

4. 支付接口类型

当前链接使用的是:

method=alipay.trade.page.pay
product_code=GENERAL_WITHHOLDING

alipay.trade.page.pay 通常用于电脑网站支付。如果实际场景是移动端 H5 支付,需要根据商户已签约的产品确认是否应使用:

alipay.trade.wap.pay

不过,同一条完整链接在 iOS 和 Android 上可以成功,因此接口类型不像是鸿蒙独有故障的首要原因。不要直接替换接口后上线,因为接口类型、产品签约、代扣协议参数和回调逻辑必须保持一致。

推荐的生成方式

服务端生成完整网关 URL

让支付宝 Java SDK 完成业务参数序列化、签名和网关地址生成。生成后不要再拆分或修改参数。

示意代码:

AlipayTradePagePayRequest request = new AlipayTradePagePayRequest();
request.setNotifyUrl(notifyUrl);
request.setReturnUrl(returnUrl);
request.setBizContent(bizContentJson);

// pageExecute 的具体返回类型和重载方式以项目使用的 SDK 版本为准。
AlipayTradePagePayResponse response =
        alipayClient.pageExecute(request, "GET");

String gatewayUrl = response.getBody();

如果实际使用移动网站支付,应先确认产品能力,再改用对应的请求类型,不要只修改 method 字符串。

包装外层 Scheme 时,只对完整的 gatewayUrl 编码一次:

import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;

String encodedGatewayUrl = URLEncoder.encode(
        gatewayUrl,
        StandardCharsets.UTF_8
).replace("+", "%20");

String alipaysUrl =
        "alipays://platformapi/startapp?appId=20000067&url="
        + encodedGatewayUrl;

Java 的 URLEncoder 使用表单编码规则,会把空格编码成 +。嵌套 URI 参数使用 %20 通常更明确,所以示例中进行了替换。不要先对 gatewayUrl 编码,再把编码后的结果传入这里。

鸿蒙客户端整体编码

更稳妥的做法是让服务端返回尚未包装外层 Scheme 的完整网关 URL,再由鸿蒙客户端包装一次:

function buildAlipayScheme(gatewayUrl: string): string {
  return 'alipays://platformapi/startapp'
    + '?appId=20000067'
    + '&url='
    + encodeURIComponent(gatewayUrl);
}

调用时不要再执行 decodeURIComponent(),也不要通过查询参数组件拆开后重新组装:

const gatewayUrl: string = response.payUrl;
const alipayScheme: string = buildAlipayScheme(gatewayUrl);

// 将 alipayScheme 原样交给系统的外部 URI 打开能力。
// 具体调用方式取决于项目使用的 HarmonyOS API 版本。

避免以下处理:

// 错误示例:可能造成重复编码
const encoded = encodeURIComponent(response.payUrl);
const scheme = buildAlipayScheme(encoded);

也不要提前解码:

// 错误示例:提前解码可能使内层 &、+、= 失去边界
const decoded = decodeURIComponent(response.payUrl);
const scheme = buildAlipayScheme(decoded);

建议的排查方法

第一步:保存三个阶段的摘要

链接包含敏感信息,不要记录完整地址。可以分别计算 SHA-256:

serverGatewayUrlHash
harmonyReceivedUrlHash
beforeOpenGatewayUrlHash

如果服务端返回值与鸿蒙接收值的摘要不同,问题出在网络层、JSON 处理或客户端取值过程。如果接收值相同,但打开前的摘要不同,问题就在 Scheme 构造过程。

第二步:只解码外层一次

从最终 Scheme 中提取 url 参数,只执行一次百分号解码。结果应是一条完整的支付宝网关地址:

https://openapi.alipay.com/gateway.do?alipay_sdk=...&app_id=...&biz_content=...&sign=...

需要检查:

  • 网关地址是否被截断
  • sign 是否完整
  • 是否出现空格
  • 是否出现多余的外层参数
  • biz_content 解码后是否为合法 JSON
  • timestamp、return_url、notify_url 是否完整

第三步:逐字符对比

不要只靠肉眼观察。对 iOS、Android 和鸿蒙实际打开前的字符串进行字符级比较,优先搜索:

+
%2B
%252B
/
%2F
%252F
&
%26
%2526
=
%3D
%253D

如果鸿蒙版本的链接长度不同,或者第一次差异出现在 sign、biz_content 附近,基本可以确定问题出在编码链路。

第四步:绕开手工 Scheme 做对照实验

可以进行两组测试:

  • 直接使用系统浏览器打开服务端生成的 HTTPS 支付网关 URL
  • 使用 alipays://platformapi/startapp 包装同一个网关 URL

如果 HTTPS 方式可以支付,而 Scheme 方式失败,问题集中在 Scheme 包装过程,或者鸿蒙向支付宝传递 URI 的环节。

如果两种方式都失败,再检查接口类型、商户签约、订单时效和业务参数。

还需要排除的业务问题

确认编码无误后,再检查以下项目:

  • 订单是否已经过期。示例中的 time_expire 与 timestamp 只相差约 30 分钟,复测时必须生成新订单。
  • out_trade_no 是否重复、已关闭或已支付。
  • agreement_sign_params 是否与商户签约的代扣产品一致。
  • GENERAL_WITHHOLDING、GENERAL_WITHHOLDING_P 是否属于当前商户已开通的能力。
  • ext_user_info 中的实名信息是否符合接口要求。
  • return_url 和 notify_url 是否满足当前支付产品的配置要求。
  • 服务端时间和时区是否正确。
  • 支付宝服务端错误日志中是否包含更具体的 sub_code、sub_msg 或请求流水号。

示例中的 notify_url 使用了 HTTP:

http://pay.cpic.com.cn/...

建议改为 HTTPS,但仅凭这一点不能认定它就是鸿蒙返回 INVALID_PARAMETER 的原因,因为其他系统使用同一链接可以成功。

更稳妥的最终方案

生产环境应优先采用支付宝明确支持鸿蒙 Next 的 SDK、H5 支付入口或官方推荐的唤起方式,避免长期依赖手工拼装的 alipays://platformapi/startapp。具体支持范围需要结合当前支付宝客户端版本、HarmonyOS SDK 版本和商户签约产品,向支付宝技术支持确认。不能只根据 iOS 和 Android 的表现,推断鸿蒙一定兼容。

如果必须继续使用 Scheme,应保证整条链路如下:

业务参数
→ 支付宝服务端 SDK 序列化并签名
→ 得到完整 gatewayUrl
→ 对 gatewayUrl 整体 encodeURIComponent 一次
→ 拼入 alipays:// 的 url 参数
→ 原样交给系统打开

最可能的修复点,是删除鸿蒙侧多余的编码、解码或 URI 重建逻辑,并重点核对 sign 中的 + 是否在跳转过程中变成了空格。

备注:内容仅供参考。