PAYATHON 2026

鸿蒙系统调起支付宝免密签约的代码示例

支付老李

结论

可以在鸿蒙应用中完成支付宝免密签约,但一般不能只靠一段客户端代码完成。完整流程至少包含三个部分:

  1. 商户服务端调用支付宝签约接口,生成经过 RSA2 签名的签约 URL。
  2. 鸿蒙客户端通过支付宝 URL Scheme 或浏览器打开该 URL。
  3. 服务端接收支付宝异步通知,并主动查询签约状态,确认签约是否成功。

签名私钥、支付宝开放平台接口调用和异步验签都必须放在服务端。鸿蒙客户端只负责请求签约地址、唤起支付宝和展示结果,不能根据客户端回跳直接判断签约成功。

推荐流程

鸿蒙客户端
    │ 申请签约
    ▼
商户服务端
    │ 调用支付宝签约接口
    ▼
返回签约 URL
    │
    ▼
鸿蒙客户端唤起支付宝
    │ 用户确认签约
    ▼
支付宝异步通知商户服务端
    │ 验签并查询协议状态
    ▼
鸿蒙客户端查询最终签约结果

支付宝的具体签约接口、biz_content 参数和产品码,取决于商户开通的代扣产品。常见场景会使用支付宝用户协议页面签约能力,但不能直接照搬其他项目中的 personal_product_code、sign_scene 等参数。实际取值应以当前商户的产品配置和支付宝开放平台文档为准。

一、服务端生成签约链接

下面的 Java 示例给出了基本结构。接口名称和业务参数需要根据实际开通的产品调整,私钥和签名逻辑不能放进鸿蒙客户端。

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

public class AlipayAgreementService {

    private final AlipayClient alipayClient;

    public AlipayAgreementService(
            String gateway,
            String appId,
            String merchantPrivateKey,
            String alipayPublicKey) {

        this.alipayClient = new DefaultAlipayClient(
                gateway,
                appId,
                merchantPrivateKey,
                "json",
                "UTF-8",
                alipayPublicKey,
                "RSA2"
        );
    }

    public String createAgreementUrl(
            String externalAgreementNo,
            String userIdentity) throws Exception {

        AlipayUserAgreementPageSignRequest request =
                new AlipayUserAgreementPageSignRequest();

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

        request.setReturnUrl(
                "https://m.example.com/alipay/agreement/result"
        );

        AlipayUserAgreementPageSignModel model =
                new AlipayUserAgreementPageSignModel();

        // 商户侧唯一签约编号,必须持久化并保证不重复。
        model.setExternalAgreementNo(externalAgreementNo);

        /*
         * 以下参数仅表示设置位置,不代表适用于所有产品。
         * 参数值应从支付宝开放平台对应产品文档或商户配置中取得。
         */
        model.setPersonalProductCode("YOUR_PERSONAL_PRODUCT_CODE");
        model.setSignScene("YOUR_SIGN_SCENE");

        /*
         * 用户标识的类型和取值由实际接口要求决定。
         * 不要把手机号等信息直接代入,除非接口明确支持。
         */
        model.setExternalLogonId(userIdentity);

        request.setBizModel(model);

        AlipayUserAgreementPageSignResponse response =
                alipayClient.pageExecute(request, "GET");

        if (!response.isSuccess()) {
            throw new IllegalStateException(
                    "生成签约链接失败,code=" + response.getCode()
                            + ", subCode=" + response.getSubCode()
                            + ", message=" + response.getSubMsg()
            );
        }

        /*
         * 不同版本支付宝 Java SDK 对 pageExecute 返回内容的处理方式
         * 可能存在差异。这里预期 getBody() 为可访问的签约地址;
         * 接入时应检查实际返回值,不能假定它一定是 URL。
         */
        return response.getBody();
    }
}

还需要向客户端提供一个业务接口,例如:

POST /api/agreement/create
Content-Type: application/json

{
  "userId": "10001"
}

响应可以这样设计:

{
  "agreementRequestId": "AGR202609090001",
  "signUrl": "https://openapi.alipay.com/..."
}

agreementRequestId 应关联服务端保存的签约记录。对于客户端传入的用户编号、协议编号、金额等业务参数,服务端必须重新校验,不能直接信任。

二、鸿蒙客户端唤起支付宝

支付宝通常可以通过 URL Scheme 打开指定页面,常见格式如下:

alipays://platformapi/startapp?appId=20000067&url=ENCODED_SIGN_URL

其中,signUrl 必须经过 URL 编码。否则,签约地址中的 &、=、? 等字符会破坏外层 Scheme。

下面是 ArkTS 示例:

import { common, Want } from '@kit.AbilityKit';

export class AlipayAgreementLauncher {
  private context: common.UIAbilityContext;

  constructor(context: common.UIAbilityContext) {
    this.context = context;
  }

  async open(signUrl: string): Promise<void> {
    if (!this.isValidSignUrl(signUrl)) {
      throw new Error('签约地址无效');
    }

    const encodedUrl = encodeURIComponent(signUrl);
    const alipayUri =
      `alipays://platformapi/startapp?appId=20000067&url=${encodedUrl}`;

    const want: Want = {
      action: 'ohos.want.action.viewData',
      uri: alipayUri
    };

    try {
      await this.context.startAbility(want);
    } catch (error) {
      console.error(`唤起支付宝失败: ${JSON.stringify(error)}`);

      // 唤端失败时可降级到系统浏览器打开签约地址。
      await this.openInBrowser(signUrl);
    }
  }

  private async openInBrowser(url: string): Promise<void> {
    const browserWant: Want = {
      action: 'ohos.want.action.viewData',
      uri: url
    };

    await this.context.startAbility(browserWant);
  }

  private isValidSignUrl(url: string): boolean {
    return url.startsWith('https://');
  }
}

在页面中调用:

const launcher = new AlipayAgreementLauncher(
  getContext(this) as common.UIAbilityContext
);

async function startAgreement(): Promise<void> {
  // signUrl 应通过 HTTPS 从商户服务端取得。
  const signUrl: string = await requestAgreementUrl();

  await launcher.open(signUrl);
}

网络请求可以沿用项目现有的 HTTP 封装:

interface AgreementResponse {
  agreementRequestId: string;
  signUrl: string;
}

async function requestAgreementUrl(): Promise<string> {
  const response: AgreementResponse = await agreementApi.create();

  if (!response.signUrl) {
    throw new Error('服务端未返回签约地址');
  }

  return response.signUrl;
}

@kit.AbilityKit、Want 和 startAbility 的具体导入方式可能随鸿蒙 SDK/API 版本而变化。如果现有工程仍使用旧模块路径,应以当前 SDK 的类型定义为准。

三、处理签约结果

签约结果通常通过两条链路返回:

  • return_url:用户完成操作后的跳转地址,主要用于改善页面体验。
  • notify_url:支付宝向商户服务端发送异步通知,是业务处理的主要依据。

客户端发生回跳,只能说明用户离开了支付宝签约页面,不能证明签约已经成功。用户可能取消操作、关闭页面或遇到网络中断,回跳 URI 也可能被伪造。

服务端异步通知处理

下面的代码展示了基本处理结构。具体字段名称应以实际接口的通知内容为准:

@PostMapping("/alipay/agreement/notify")
public String handleAgreementNotify(
        HttpServletRequest request) throws Exception {

    Map<String, String> parameters = extractParameters(request);

    boolean verified = AlipaySignature.rsaCheckV1(
            parameters,
            alipayPublicKey,
            "UTF-8",
            "RSA2"
    );

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

    String externalAgreementNo =
            parameters.get("external_agreement_no");

    String agreementNo = parameters.get("agreement_no");
    String status = parameters.get("status");

    /*
     * 1. 根据 externalAgreementNo 查询本地签约记录。
     * 2. 校验 app_id、商户身份及业务关联关系。
     * 3. 必要时调用支付宝协议查询接口二次确认状态。
     * 4. 使用事务和唯一键保证重复通知不会重复处理。
     */
    updateAgreementStatus(
            externalAgreementNo,
            agreementNo,
            status
    );

    return "success";
}

生产环境还需要检查以下内容:

  • 通知是否已通过支付宝公钥验签。
  • app_id、商户身份等信息是否与当前应用匹配。
  • external_agreement_no 是否属于当前用户和当前业务。
  • 通知状态是否符合当前状态机的转换规则。
  • 相同通知重复到达时,处理结果能否保持幂等。
  • 是否需要调用协议查询接口二次确认。

只有验签和业务校验全部通过后,才能返回支付宝要求的成功响应。

鸿蒙客户端查询结果

用户从支付宝回到应用后,客户端应使用之前保存的 agreementRequestId 查询商户服务端:

type AgreementStatus =
  'PENDING' |
  'SIGNED' |
  'FAILED' |
  'CANCELED' |
  'UNKNOWN';

interface AgreementStatusResponse {
  status: AgreementStatus;
  agreementNo?: string;
}

async function refreshAgreementStatus(
  agreementRequestId: string
): Promise<void> {
  const result: AgreementStatusResponse =
    await agreementApi.queryStatus(agreementRequestId);

  switch (result.status) {
    case 'SIGNED':
      showToast('免密签约成功');
      break;

    case 'FAILED':
    case 'CANCELED':
      showToast('签约未完成');
      break;

    case 'PENDING':
    case 'UNKNOWN':
      showToast('签约结果确认中,请稍后刷新');
      break;
  }
}

如果需要回跳到应用内,可以为鸿蒙应用配置自定义 URI,例如:

myapp://alipay/agreement/result

支付宝是否允许把该 URI 直接配置为 return_url,取决于具体产品规则和跳转策略。更稳妥的做法是将 return_url 指向商户的 HTTPS 页面,再由该页面展示结果或尝试唤回应用。无论使用哪种方式,客户端返回后都要重新向服务端查询状态。

签约状态建议

服务端最好维护一套独立的签约状态机:

CREATED
   │
   ▼
PENDING
   ├──► SIGNED
   ├──► FAILED
   └──► CANCELED

生成签约链接不代表签约成功,因此不能在这一步把状态设为成功,也不能只依赖一次异步通知。对于长时间停留在 PENDING 状态的记录,可以通过定时任务调用支付宝协议查询接口,补偿确认最终状态。

注意事项

  1. 私钥只能保存在服务端

    鸿蒙应用包可以被分析。如果把商户私钥、应用私钥或完整签名算法放进客户端,攻击者可能伪造签名。

  2. 签约链接应由服务端实时生成

    服务端需要校验登录用户、业务资格和当前签约状态,并为每次请求生成新的签约记录,或者返回仍然有效的记录。

  3. 不要硬编码产品参数

    personal_product_code、sign_scene、用户标识类型等参数与商户开通的代扣产品有关。参数不正确时,即使成功唤起支付宝,也可能无法进入对应的签约流程。

  4. 正确编码唤端参数

    内层 signUrl 需要执行一次 encodeURIComponent。不要随意重复编码,否则支付宝可能无法解析目标地址。

  5. 准备未安装支付宝时的降级方案

    startAbility 失败后,可以用浏览器打开 HTTPS 签约地址,也可以提示用户安装支付宝。是否支持完全通过浏览器完成签约,要以对应产品的能力为准。

  6. 以服务端状态作为最终结果

    应用回到前台、收到自定义 URI 或打开 return_url,都不能作为签约成功的凭证。最终状态必须来自通过验签的通知或支付宝协议查询接口。

  7. 处理好幂等和重复请求

    用户可能连续点击签约按钮,支付宝也可能重复发送通知。建议为商户签约编号设置唯一约束,并让通知接口支持幂等更新。

  8. 记录必要的排查信息

    可以记录商户签约编号、支付宝协议号、接口错误码和状态更新时间,但不要在日志中写入商户私钥、完整签名串或不必要的个人敏感信息。

上述代码可以作为服务端生成链接、鸿蒙端唤起支付宝以及服务端确认结果的实现骨架,但不能把示例中的产品参数和返回字段直接套用到所有商户。接入前还需确认当前开通的支付宝代扣产品、开放平台接口权限,以及鸿蒙工程使用的 SDK/API 版本。

备注:内容仅供参考。