PAYATHON 2026

支付宝小程序如何实现扫码下单、刷脸验证和信用免押

支付老李

结论

这套流程可以实现,但“订单详情、刷脸结果、信用判断”不能全部放在小程序前端处理。更稳妥的方案是:

  1. 服务端先创建订单,小程序码中只写入一次性 scene 或短令牌。
  2. 用户扫码进入小程序后,小程序用 scene 向服务端查询订单。
  3. 服务端初始化支付宝身份认证,小程序拉起刷脸,最终认证结果由服务端查询确认。
  4. 信用免押需要接入支付宝开放平台提供的信用免押产品。是否免押应以产品接口返回的结果为准,不能由小程序直接读取芝麻信用分。
  5. 用户不能免押时,由服务端创建押金支付订单,再由小程序通过 my.tradePay 唤起支付宝收银台。

刷脸认证和信用免押涉及的接口名称、准入条件及参数,可能因签约产品而异。开发前要先在支付宝开放平台确认当前账号实际可以申请哪些产品。下面的流程不能代替产品签约和权限申请。

推荐的整体架构

订单状态可以设计成清晰的状态机:

CREATED
  ↓ 扫码进入
IDENTITY_PENDING
  ↓ 刷脸通过
CREDIT_CHECKING
  ├─ 信用免押通过 → DEPOSIT_WAIVED
  └─ 不满足免押 → DEPOSIT_PENDING
                       ↓ 支付成功
                    DEPOSIT_PAID

小程序负责展示订单、获取用户授权以及调起支付宝能力。以下逻辑需要放在服务端:

  • 计算订单金额和押金金额
  • 校验二维码令牌
  • 签名支付宝接口请求
  • 确认刷脸结果
  • 判断信用免押资格
  • 确认支付结果
  • 验证异步通知签名
  • 控制订单状态流转

需要使用的能力和接口

环节推荐能力或接口用途
生成入口码alipay.open.app.qrcode.create生成支付宝小程序码并携带业务参数
获取用户授权码my.getAuthCode小程序端取得临时授权码
换取授权信息alipay.system.oauth.token服务端交换用户身份信息
身份认证支付宝身份认证产品对应的初始化、拉起、结果查询接口完成实名刷脸认证
信用免押已签约的芝麻信用免押或行业信用服务接口创建信用业务单并获取免押结果
创建押金支付单alipay.trade.create服务端创建支付宝交易
拉起支付my.tradePay小程序端唤起支付宝收银台
查询支付结果alipay.trade.query服务端主动确认交易状态
退还已支付押金alipay.trade.refund业务结束后退还押金

刷脸验证、信用免押和支付是三套独立的业务能力。一次刷脸结果不能代替支付确认,用户刷脸成功也不代表满足免押条件。

第一步:生成小程序码

不要把商品名称、金额、押金、用户信息等完整订单数据直接写进二维码。二维码可能被复制或篡改,能容纳的数据长度也有限。

二维码中只需携带一个不可预测、短期有效且只能使用一次的 scene:

{
  "scene": "Q7p3M8xK2n"
}

服务端保存对应关系:

scene → orderId + expiresAt + usedStatus

创建小程序码时,可以使用 alipay.open.app.qrcode.create,将目标页面和 scene 写入小程序码配置。具体字段要以当前开放平台文档和应用控制台为准。

如果业务使用普通二维码,也应让二维码指向支付宝认可的小程序跳转链接,不能假定任意自定义 URL 都能自动打开小程序。

第二步:扫码进入并拉取订单

页面加载时,小程序读取启动参数,并将 scene 发给自己的服务端:

Page({
  async onLoad(query) {
    const scene = query.scene;

    if (!scene) {
      my.alert({ content: '二维码参数无效' });
      return;
    }

    try {
      const result = await this.request('/api/orders/resolve-scene', {
        method: 'POST',
        data: { scene }
      });

      this.setData({
        order: result.order
      });
    } catch (error) {
      my.alert({ content: '订单不存在、二维码已失效或已被使用' });
    }
  },

  request(url, options) {
    return new Promise((resolve, reject) => {
      my.request({
        url: `https://api.example.com${url}`,
        ...options,
        success: (res) => {
          if (res.status === 200 && res.data.success) {
            resolve(res.data.data);
          } else {
            reject(res.data);
          }
        },
        fail: reject
      });
    });
  }
});

服务端不能按照小程序传来的金额创建支付单。小程序只提交 scene 或 orderId,金额必须从服务端保存的订单记录中读取。

第三步:取得用户授权

后续产品需要支付宝用户身份时,小程序可以调用 my.getAuthCode:

my.getAuthCode({
  scopes: ['auth_user'],
  success: async ({ authCode }) => {
    await this.request('/api/alipay/login', {
      method: 'POST',
      data: { authCode }
    });
  },
  fail: () => {
    my.alert({ content: '需要完成支付宝授权后才能继续' });
  }
});

服务端收到 authCode 后调用:

alipay.system.oauth.token

授权码需要立即使用,不能保存下来反复交换。服务端还要把取得的支付宝用户标识与当前业务会话绑定,防止甲用户领取由乙用户扫码产生的订单。

如果用户打开小程序后只能查看公开订单,页面加载时未必需要立即申请授权。可以等用户点击“确认下单”后再申请,减少不必要的授权。

第四步:进行刷脸身份认证

身份认证通常包含三个步骤:

  1. 服务端创建认证请求,取得认证流水号或认证地址。
  2. 小程序调用当前产品提供的 JSAPI,拉起认证页面。
  3. 服务端使用认证流水号查询最终结果。

下面是示意代码。由于不同身份认证产品使用的 JSAPI、参数和准入条件可能不同,在没有确定签约产品前,不应写死方法名:

async function startIdentityVerification(orderId) {
  const initResult = await request('/api/identity/init', {
    method: 'POST',
    data: { orderId }
  });

  // 按已签约身份认证产品的文档调用实际 JSAPI。
  my.startAPVerify({
    url: initResult.certifyUrl,
    success: async () => {
      const result = await request('/api/identity/query', {
        method: 'POST',
        data: {
          orderId,
          certifyId: initResult.certifyId
        }
      });

      if (result.passed) {
        await checkDepositPolicy(orderId);
      } else {
        my.alert({ content: '身份认证未通过' });
      }
    },
    fail: () => {
      my.alert({ content: '身份认证未完成' });
    }
  });
}

这里的 my.startAPVerify 只表示一种常见的客户端拉起形式。实际开发时,必须根据控制台中已开通产品的文档,核对 JSAPI 名称、支持的基础库版本和参数。

不能看到客户端触发 success 回调,就把订单状态改成“认证通过”。在某些情况下,success 只表示认证流程已经成功拉起或正常结束。最终结果仍需由服务端查询和校验。

第五步:判断是否免押

不要把流程设计成“小程序读取信用分,再执行 score >= 650”,原因如下:

  • 芝麻信用分不是普通小程序可以随意读取的用户字段。
  • 相关能力通常要求企业资质、行业准入和产品签约。
  • 某些历史信用分查询接口可能不再向新应用开放。
  • 免押结果还可能受行业、订单金额、用户授权和风险策略影响,不能只看一个分数。

应接入当前可以申请的信用免押产品:

刷脸通过
  ↓
服务端创建信用业务单
  ↓
支付宝信用产品返回受理或准入结果
  ├─ 免押成功:记录信用业务单号
  └─ 不满足条件:进入押金支付流程

伪代码如下:

async function decideDeposit(order, user) {
  const creditResult = await alipayCreditService.createOrder({
    outOrderNo: order.orderNo,
    userId: user.alipayUserId,
    subject: order.subject,
    depositAmount: order.depositAmount
  });

  if (creditResult.waived === true) {
    await orderRepository.markDepositWaived(
      order.id,
      creditResult.creditOrderNo
    );

    return { action: 'WAIVED' };
  }

  return {
    action: 'PAY_DEPOSIT',
    amount: order.depositAmount
  };
}

alipayCreditService.createOrder 是针对已签约信用产品封装的业务方法,并非确定的支付宝官方接口名。实际接口要以申请通过后控制台提供的文档为准。

如果业务方确实拥有信用分查询权限,也要由服务端查询信用分并执行阈值规则。不能把信用分返回给小程序,再由前端判断。

第六步:不能免押时支付押金

服务端使用 alipay.trade.create 创建交易,然后把支付宝返回的交易号交给小程序:

async function createDepositTrade(order) {
  const response = await alipay.trade.create({
    out_trade_no: `${order.orderNo}-DEPOSIT`,
    total_amount: order.depositAmount.toFixed(2),
    subject: `${order.subject}押金`,
    buyer_id: order.alipayUserId,
    notify_url: 'https://api.example.com/alipay/payment/notify'
  });

  return {
    tradeNo: response.trade_no
  };
}

代码中的 SDK 调用方式只用于说明结构。不同语言 SDK 的方法名和参数封装方式可能不同。

小程序端通过 my.tradePay 拉起支付宝收银台:

async function payDeposit(orderId) {
  const trade = await request('/api/deposit/create-trade', {
    method: 'POST',
    data: { orderId }
  });

  my.tradePay({
    tradeNO: trade.tradeNo,
    success: async () => {
      const result = await request('/api/deposit/query', {
        method: 'POST',
        data: { orderId }
      });

      if (result.paid) {
        my.redirectTo({
          url: `/pages/order/success?orderId=${encodeURIComponent(orderId)}`
        });
      } else {
        my.alert({ content: '支付结果确认中,请稍后刷新订单' });
      }
    },
    fail: () => {
      my.alert({ content: '押金支付未完成' });
    }
  });
}

这里同样不能只依赖 my.tradePay 的前端回调。服务端要处理支付宝异步通知,并使用 alipay.trade.query 主动补查。只有服务端确认交易成功后,才能把订单状态改为 DEPOSIT_PAID。

回调处理要点

收到支付宝异步通知后,服务端必须验证签名并校验关键字段:

async function handleAlipayNotify(params) {
  if (!verifyAlipaySignature(params)) {
    throw new Error('INVALID_SIGNATURE');
  }

  const trade = await tradeRepository.findByOutTradeNo(
    params.out_trade_no
  );

  if (!trade) {
    throw new Error('TRADE_NOT_FOUND');
  }

  if (params.app_id !== EXPECTED_APP_ID) {
    throw new Error('INVALID_APP_ID');
  }

  if (params.total_amount !== trade.amount.toFixed(2)) {
    throw new Error('INVALID_AMOUNT');
  }

  if (
    params.trade_status === 'TRADE_SUCCESS' ||
    params.trade_status === 'TRADE_FINISHED'
  ) {
    await tradeRepository.markPaidIdempotently(
      trade.id,
      params.trade_no
    );
  }
}

同一条通知可能会被重复发送,因此回调处理必须具备幂等性。

容易踩坑的地方

二维码不是可信订单数据

二维码中只应存放随机令牌。令牌需要设置有效期、可用次数和绑定规则,避免二维码截图被转发后由其他人重复领取。

刷脸不等于登录或支付

刷脸认证解决的是身份真实性问题。用户授权、信用免押和资金支付仍需分别完成。

不要默认可以读取信用分

是否可以查询信用分或使用信用免押,取决于主体资质、所属行业、应用类型和签约结果。申请产品前,需要准备业务场景、履约流程、违约处理方式和用户协议。

免押不代表没有履约订单

即使押金为零,也要保存信用业务单号、履约状态和完结结果。业务结束后,通常还需要调用对应信用产品的履约完结接口。

押金支付不一定是资金预授权

“直接收取押金”和“资金预授权冻结”是两种不同的方案。如果需要冻结额度,而不是实际收取资金,应评估支付宝资金预授权产品,不能简单使用普通支付接口。资金预授权也需要单独签约,并涉及冻结、解冻、转支付等完整生命周期。

必须先确定产品权限

建议先按以下顺序确认并申请所需权限:

  1. 小程序基础能力。
  2. 身份认证或刷脸认证产品。
  3. 芝麻信用免押或所属行业的信用产品。
  4. 当面付、小程序支付或其他适用的支付产品。
  5. 如果业务需要,再申请资金预授权。

确定权限后,再根据控制台提供的当前文档实现具体接口和字段。否则按旧文档完成开发后,可能会发现应用主体没有相应的接口调用权限。

备注:内容仅供参考。