支付宝小程序如何实现扫码下单、刷脸验证和信用免押
结论
这套流程可以实现,但“订单详情、刷脸结果、信用判断”不能全部放在小程序前端处理。更稳妥的方案是:
- 服务端先创建订单,小程序码中只写入一次性
scene或短令牌。 - 用户扫码进入小程序后,小程序用
scene向服务端查询订单。 - 服务端初始化支付宝身份认证,小程序拉起刷脸,最终认证结果由服务端查询确认。
- 信用免押需要接入支付宝开放平台提供的信用免押产品。是否免押应以产品接口返回的结果为准,不能由小程序直接读取芝麻信用分。
- 用户不能免押时,由服务端创建押金支付订单,再由小程序通过
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
授权码需要立即使用,不能保存下来反复交换。服务端还要把取得的支付宝用户标识与当前业务会话绑定,防止甲用户领取由乙用户扫码产生的订单。
如果用户打开小程序后只能查看公开订单,页面加载时未必需要立即申请授权。可以等用户点击“确认下单”后再申请,减少不必要的授权。
第四步:进行刷脸身份认证
身份认证通常包含三个步骤:
- 服务端创建认证请求,取得认证流水号或认证地址。
- 小程序调用当前产品提供的 JSAPI,拉起认证页面。
- 服务端使用认证流水号查询最终结果。
下面是示意代码。由于不同身份认证产品使用的 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
);
}
}
同一条通知可能会被重复发送,因此回调处理必须具备幂等性。
容易踩坑的地方
二维码不是可信订单数据
二维码中只应存放随机令牌。令牌需要设置有效期、可用次数和绑定规则,避免二维码截图被转发后由其他人重复领取。
刷脸不等于登录或支付
刷脸认证解决的是身份真实性问题。用户授权、信用免押和资金支付仍需分别完成。
不要默认可以读取信用分
是否可以查询信用分或使用信用免押,取决于主体资质、所属行业、应用类型和签约结果。申请产品前,需要准备业务场景、履约流程、违约处理方式和用户协议。
免押不代表没有履约订单
即使押金为零,也要保存信用业务单号、履约状态和完结结果。业务结束后,通常还需要调用对应信用产品的履约完结接口。
押金支付不一定是资金预授权
“直接收取押金”和“资金预授权冻结”是两种不同的方案。如果需要冻结额度,而不是实际收取资金,应评估支付宝资金预授权产品,不能简单使用普通支付接口。资金预授权也需要单独签约,并涉及冻结、解冻、转支付等完整生命周期。
必须先确定产品权限
建议先按以下顺序确认并申请所需权限:
- 小程序基础能力。
- 身份认证或刷脸认证产品。
- 芝麻信用免押或所属行业的信用产品。
- 当面付、小程序支付或其他适用的支付产品。
- 如果业务需要,再申请资金预授权。
确定权限后,再根据控制台提供的当前文档实现具体接口和字段。否则按旧文档完成开发后,可能会发现应用主体没有相应的接口调用权限。
备注:内容仅供参考。
