自营餐饮小程序如何接入支付宝支付能力?
明确结论
自营餐饮小程序应优先申请并接入支付宝开放平台的“小程序支付”能力。
常见的支付流程如下:
- 小程序获取支付宝用户的授权信息;
- 服务端调用
alipay.trade.create创建交易; - 服务端把支付宝返回的交易号
trade_no传给小程序; - 小程序调用
my.tradePay唤起支付宝收银台; - 服务端通过异步通知或
alipay.trade.query确认最终支付结果。
如果小程序只销售自家门店的餐品,资金直接结算给本企业,不涉及代其他商户收款、分账或管理二级商户,通常不需要接入平台型支付产品。
为什么选择小程序支付
小程序支付适合用户在支付宝小程序内选餐、下单和付款。与扫码支付、网页支付相比,它可以直接从小程序唤起支付宝收银台,支付结束后再回到小程序。
以下几类场景需要分清:
- 自营餐饮通常使用小程序支付。
- 到店展示付款码,由商家扫码收款,属于条码支付,不是标准的小程序下单支付流程。
- 如果一个小程序要为多家独立商户收款,可能需要平台型支付、商户入驻和分账等能力,不能直接按普通自营支付处理。
- 如果只是跳转到支付宝生活号或 H5 页面付款,应根据实际载体选择相应的支付产品。
申请前需要满足的条件
支付宝开放平台可能调整具体的准入规则,应以应用后台的签约页面和最新官方文档为准。申请前通常需要准备:
- 完成支付宝开放平台账号注册和实名认证;
- 申请主体是能够合法经营餐饮业务的企业或个体工商户;
- 创建支付宝小程序,并明确应用主体与收款主体的关系;
- 小程序类目与实际餐饮业务一致;
- 按平台要求提交营业执照、经营信息及可能需要的行业资质;
- 小程序已有完整的商品展示、下单、退款、客服和用户协议等页面;
- 开通商家收款与结算能力,并完成小程序支付产品签约;
- 服务器提供可通过 HTTPS 访问的支付接口和异步通知地址;
- 正确配置应用私钥、公钥和支付宝公钥。
是否必须提交餐饮经营许可证、食品经营许可证等材料,取决于经营类目、主体类型和申请页面的实际要求,不能只根据通用流程判断。
开放平台配置流程
1. 创建小程序应用
登录支付宝开放平台,在控制台创建“小程序”应用,填写应用名称、简介、图标、服务类目和主体信息。
创建后记下应用的 app_id。服务端后续调用支付宝接口时,需要用它标识应用。
2. 完成开发设置
在应用的开发设置中完成以下配置:
- 设置接口加签方式,通常使用 RSA2;
- 上传应用公钥,或配置支付宝提供的密钥方案;
- 保存应用私钥;
- 获取并保存支付宝公钥;
- 配置小程序服务器域名;
- 配置支付异步通知地址;
- 根据业务需要设置授权回调地址。
应用私钥只能保存在服务端,不能写入小程序前端代码、代码仓库或公开配置文件。
3. 申请小程序支付能力
在应用的能力或产品管理页面找到“小程序支付”,按照页面指引提交申请并签约。
申请时通常需要确认:
- 收款主体;
- 结算账户;
- 经营类目;
- 业务场景;
- 小程序与商户主体的关系;
- 费率及协议。
如果控制台中没有这项能力,可以先检查应用类型、主体认证、经营类目和商户签约状态。
4. 获取用户标识
创建小程序交易时,服务端通常需要识别当前的支付宝用户。常见做法是由小程序调用 my.getAuthCode 获取授权码,再交给服务端调用支付宝授权接口,换取用户标识。
my.getAuthCode({
scopes: ['auth_base'],
success: async (res) => {
const result = await my.request({
url: 'https://api.example.com/alipay/session',
method: 'POST',
data: {
authCode: res.authCode
}
});
// 将服务端返回的业务登录态安全保存
console.log(result.data);
},
fail: (error) => {
console.error('获取授权码失败', error);
}
});
授权码有有效期,而且通常只能使用一次。小程序获取授权码后应立即发送给服务端处理,不要把它当作长期用户标识保存。
5. 服务端创建业务订单
服务端应先创建业务订单,再调用支付宝的 alipay.trade.create。业务订单至少需要保存:
- 商户订单号;
- 用户标识;
- 商品名称;
- 应付金额;
- 订单状态;
- 支付宝交易号;
- 创建时间和过期时间。
下面是逻辑示例。不同版本的官方 SDK 可能使用不同的类名和配置方式,应以项目实际依赖的版本为准。
AlipayClient alipayClient = new DefaultAlipayClient(
"https://openapi.alipay.com/gateway.do",
APP_ID,
APP_PRIVATE_KEY,
"json",
"UTF-8",
ALIPAY_PUBLIC_KEY,
"RSA2"
);
AlipayTradeCreateRequest request = new AlipayTradeCreateRequest();
request.setNotifyUrl("https://api.example.com/alipay/notify");
AlipayTradeCreateModel model = new AlipayTradeCreateModel();
model.setOutTradeNo("FOOD202609090001");
model.setTotalAmount("38.80");
model.setSubject("门店餐饮订单");
model.setBuyerId(alipayUserId);
request.setBizModel(model);
AlipayTradeCreateResponse response = alipayClient.execute(request);
if (!response.isSuccess()) {
throw new IllegalStateException(
"创建支付宝交易失败: " + response.getSubCode() + " " + response.getSubMsg()
);
}
String tradeNo = response.getTradeNo();
金额必须由服务端根据商品、优惠和配送费用重新计算,不能直接采用小程序前端提交的 total_amount。
正式环境通常使用:
https://openapi.alipay.com/gateway.do
沙箱网关、账号和应用配置应从开放平台的沙箱环境中获取,不能与正式环境的密钥、账号或应用 ID 混用。
6. 小程序唤起支付
服务端创建交易后,把 trade_no 返回给小程序,再调用 my.tradePay:
my.tradePay({
tradeNO: tradeNo,
success: (result) => {
// 前端结果只用于页面提示,不能直接作为发货或出餐依据
console.log('支付调用完成', result);
my.request({
url: 'https://api.example.com/orders/payment-status',
method: 'GET',
data: {
orderNo: businessOrderNo
},
success: (statusResult) => {
console.log('服务端订单状态', statusResult.data);
}
});
},
fail: (error) => {
console.error('支付未完成', error);
}
});
用户取消支付、网络中断或前端回调丢失,都不一定表示支付宝没有扣款。小程序应向业务服务端查询订单状态,不能只依赖 my.tradePay 的回调结果。
正确处理支付结果
是否支付成功,应以服务端确认的结果为准。常见的确认方式有两种:
- 接收支付宝发送的异步通知;
- 主动调用
alipay.trade.query查询交易状态。
处理异步通知时,接口应依次完成以下检查:
- 使用支付宝公钥验证签名;
- 检查通知中的
app_id; - 检查
out_trade_no是否存在; - 核对订单金额和币种;
- 核对收款方信息;
- 判断交易状态;
- 通过幂等更新把业务订单改为已支付;
- 处理成功后,按照支付宝协议返回指定响应。
伪代码如下:
@PostMapping("/alipay/notify")
public String handleNotify(HttpServletRequest request) {
Map<String, String> params = extractAlipayParams(request);
boolean verified = AlipaySignature.rsaCheckV1(
params,
ALIPAY_PUBLIC_KEY,
"UTF-8",
"RSA2"
);
if (!verified) {
return "failure";
}
if (!APP_ID.equals(params.get("app_id"))) {
return "failure";
}
String orderNo = params.get("out_trade_no");
String tradeNo = params.get("trade_no");
String tradeStatus = params.get("trade_status");
String totalAmount = params.get("total_amount");
Order order = orderService.findByOrderNo(orderNo);
if (order == null || !order.amountEquals(totalAmount)) {
return "failure";
}
if ("TRADE_SUCCESS".equals(tradeStatus)
|| "TRADE_FINISHED".equals(tradeStatus)) {
orderService.markPaidIdempotently(orderNo, tradeNo);
}
return "success";
}
extractAlipayParams、amountEquals 和 markPaidIdempotently 都是业务示意方法,需要自行实现。验证签名时必须使用支付宝公钥,不能使用应用公钥或应用私钥。
退款和订单关闭
餐饮业务还需要处理退款、超时关闭和重复支付:
- 用户取消订单且尚未付款时,可以关闭业务订单;
- 对于已经创建但长期未付款的支付宝交易,可以根据业务规则调用交易关闭接口;
- 已支付订单通常通过
alipay.trade.refund发起退款; - 退款前需要检查订单是否允许退款,并核对可退金额;
- 同一个退款请求应使用稳定且唯一的退款请求号,防止重复退款;
- 部分退款时,需要记录每次退款的明细和累计退款金额。
退款是否成功,也应以支付宝接口的返回结果和后续查询结果为准,不能只修改本地订单状态。
接入时容易踩的坑
不要在前端创建交易
应用私钥和支付宝服务端接口必须放在业务服务器上。小程序前端只能请求自己的服务端,再使用服务端返回的交易号唤起支付。
商户订单号必须唯一
out_trade_no 在商户系统内必须唯一。用户重复点击“立即支付”时,应优先复用仍然有效的待支付订单。如果需要创建新订单,应先明确关闭旧交易,避免一笔业务订单对应多笔有效支付。
金额统一使用十进制定点规则
计算金额时不要直接累计浮点数。服务端可以使用以“分”为单位的整数计算,提交支付宝接口时再转换为保留两位小数的字符串。
例如:
BigDecimal amount = new BigDecimal("38.80");
String totalAmount = amount.setScale(2, RoundingMode.UNNECESSARY).toPlainString();
前端回调不能触发出餐
my.tradePay 返回成功,只说明客户端支付流程得到了相应结果。自动出餐、打印小票和增加积分等操作,必须等服务端验签并确认订单已经支付后再执行。
异步通知必须支持幂等
支付宝可能重复发送通知。订单状态更新、库存扣减、优惠券核销和出餐任务创建都必须支持安全的重复执行,不能因此多次扣减库存或打印多张小票。
正式环境与沙箱环境要完全隔离
以下配置不能交叉使用:
app_id;- 应用私钥;
- 支付宝公钥;
- 商户账号;
- 网关地址;
- 回调地址及业务订单数据。
上线前还需要检查域名、HTTPS 证书、服务器时间、通知地址的公网可达性和生产密钥配置。
推荐的最终接入结构
较稳妥的业务流程如下:
小程序选择餐品
→ 业务服务端计算价格并创建订单
→ 小程序获取授权码
→ 服务端换取并关联支付宝用户标识
→ 服务端调用 alipay.trade.create
→ 小程序调用 my.tradePay
→ 支付宝发送异步通知
→ 服务端验签、核对金额并幂等更新订单
→ 小程序查询订单状态
→ 门店出餐或打印小票
自营餐饮小程序的支付接入不能只在前端增加一个支付按钮,还需要完成小程序支付签约、服务端创建交易、客户端唤起收银台、服务端验签确认,以及退款和订单补偿等环节。如果实际的能力名称、准入材料或控制台入口与本文描述不同,应以当前支付宝开放平台控制台和对应的官方接口文档为准。
备注:内容仅供参考。
