如何在钉钉小程序中接入支付宝支付?
结论
钉钉小程序可以唤起支付宝付款,但支付能力不能只在小程序前端接入。完整流程包括:
- 在支付宝开放平台开通所需的支付产品,完成商户签约。
- 配置支付宝应用、公私钥和异步通知地址。
- 由业务服务端创建支付宝订单,生成支付参数。
- 小程序调用钉钉支付 API,唤起支付宝收银台。
- 服务端接收支付宝异步通知,完成验签并更新订单状态。
是否付款成功,必须以服务端收到的异步通知或主动查单结果为准,不能只看小程序回调。
接入前需要确认的条件
开发前先确认以下事项:
- 当前钉钉应用类型是否支持支付宝支付。
- 支付场景属于企业内部应用、第三方企业应用,还是面向个人用户的小程序。
- 支付宝商户账号是否已完成实名认证和支付产品签约。
- 钉钉应用与支付宝应用是否需要关联,是否要单独申请支付权限。
- 目标客户端是否支持该支付 API,包括 Android、iOS、PC 和 Web 端的支持范围。
- 当前钉钉客户端版本是否符合接口要求。
应用类型、开放平台配置和客户端版本不同,可用的支付接口也可能不同。常见接口名是 dd.tradePay,实际接入时应查看钉钉开放平台中对应应用类型的最新支付文档。如果控制台或文档中的接口名称、入参格式与本文示例不同,以对应文档为准。
一、完成支付宝侧配置
在支付宝开放平台创建应用,并完成支付相关配置,通常包括:
- 获取支付宝应用的
app_id。 - 开通适合当前业务的支付产品。
- 设置应用公钥,保存支付宝公钥或支付宝公钥证书。
- 配置签名算法,通常使用
RSA2。 - 配置支付异步通知地址
notify_url。 - 发布或上线支付宝应用。
- 确认签约商户与实际收款主体一致。
私钥只能保存在服务端,不能写入钉钉小程序代码、配置文件或前端请求参数。
使用证书模式时,还需要妥善保存并配置:
- 应用公钥证书
- 支付宝公钥证书
- 支付宝根证书
密钥模式和证书模式采用不同的验签方式,不能混用。
二、完成钉钉侧配置
进入钉钉开放平台中的对应应用,检查并配置支付能力。具体操作取决于应用类型,可能包括:
- 申请支付接口权限。
- 配置应用安全域名或服务器出口信息。
- 配置服务端接口调用凭证。
- 关联支付宝开放平台应用或商户主体。
- 将应用发布为可测试或正式使用的版本。
- 将测试人员加入应用可见范围。
如果控制台中找不到支付能力入口,先检查应用类型、主体认证状态和支付能力的开放范围,再开始编写前端代码。
三、设计服务端订单
建议先在业务系统中创建订单,再请求支付宝创建支付交易。订单至少需要保存:
- 业务订单号
- 支付金额
- 商品标题
- 用户或企业标识
- 支付宝交易号
- 当前支付状态
- 创建时间和支付完成时间
- 异步通知原始数据
- 退款状态
订单状态不能只有“成功”和“失败”,至少可以设计为:
CREATED 已创建
PAYING 支付处理中
PAID 已支付
CLOSED 已关闭
REFUNDING 退款处理中
REFUNDED 已退款
业务订单号必须唯一。支付金额应由服务端根据商品和订单数据计算,不能信任小程序提交的金额。
四、服务端创建支付宝订单
小程序将商品或订单信息提交给业务服务端。服务端完成校验后,再调用支付宝接口生成支付参数。
下面用 Node.js 伪代码说明整体结构。支付宝 SDK 不同版本的类名和方法签名可能不同,请根据项目实际使用的官方 SDK 调整。
import express from "express";
const app = express();
app.use(express.json());
app.post("/api/payment/create", async (req, res) => {
const { productId } = req.body;
// 1. 从数据库读取商品,不接受前端直接指定最终金额
const product = await findProductById(productId);
if (!product || !product.enabled) {
return res.status(400).json({
code: "INVALID_PRODUCT",
message: "商品不存在或不可购买"
});
}
// 2. 创建本地业务订单
const order = await createBusinessOrder({
productId: product.id,
amount: product.price,
status: "CREATED"
});
// 3. 调用支付宝 SDK 创建支付参数
const orderStr = await createAlipayOrder({
outTradeNo: order.orderNo,
totalAmount: product.price,
subject: product.name,
notifyUrl: "https://api.example.com/api/payment/alipay/notify"
});
await updateOrder(order.orderNo, {
status: "PAYING"
});
res.json({
code: "OK",
data: {
orderNo: order.orderNo,
orderStr
}
});
});
createAlipayOrder 应通过支付宝官方 SDK 在服务端实现,并用应用私钥完成签名。服务端应向前端返回支付 API 所需的完整支付参数,例如 orderStr。不要把应用私钥或未签名的敏感参数交给前端处理。
五、小程序唤起支付宝支付
如果当前应用使用的钉钉支付接口是 dd.tradePay,可以这样调用:
async function pay(productId) {
try {
const response = await fetch("https://api.example.com/api/payment/create", {
method: "POST",
headers: {
"Content-Type": "application/json"
},
body: JSON.stringify({ productId })
});
const result = await response.json();
if (result.code !== "OK") {
throw new Error(result.message || "创建支付订单失败");
}
const { orderNo, orderStr } = result.data;
dd.tradePay({
orderStr,
success(payResult) {
// 前端回调只表示收银台调用结果,不能直接作为到账依据
queryOrderStatus(orderNo);
},
fail(error) {
console.error("唤起支付失败", error);
}
});
} catch (error) {
console.error("支付请求失败", error);
}
}
部分支付接入方式返回支付宝交易号 tradeNO,而不是 orderStr。如果当前钉钉开放平台文档要求传入 tradeNO,服务端应先创建交易并返回该字段,再按照文档调用支付接口。例如:
dd.tradePay({
tradeNO,
success(result) {
queryOrderStatus(orderNo);
},
fail(error) {
console.error("支付失败", error);
}
});
orderStr 和 tradeNO 对应不同的下单及唤起方式,不能互相替换。使用哪一种,取决于当前支付产品、钉钉接口定义和支付宝下单接口。
六、处理支付宝异步通知
付款完成后,支付宝会向 notify_url 发送异步通知。服务端收到通知后需要:
- 读取支付宝发送的原始表单参数。
- 使用支付宝公钥或证书验签。
- 校验
app_id、商户身份和交易状态。 - 根据
out_trade_no查询本地订单。 - 校验金额、币种和订单内容。
- 以幂等方式更新订单状态。
- 按照支付宝协议返回成功响应。
示意代码如下:
app.post(
"/api/payment/alipay/notify",
express.urlencoded({ extended: false }),
async (req, res) => {
const params = req.body;
const verified = verifyAlipaySignature(params);
if (!verified) {
return res.send("failure");
}
const order = await findOrderByOrderNo(params.out_trade_no);
if (!order) {
return res.send("failure");
}
if (params.app_id !== process.env.ALIPAY_APP_ID) {
return res.send("failure");
}
if (String(params.total_amount) !== String(order.amount)) {
return res.send("failure");
}
const paid =
params.trade_status === "TRADE_SUCCESS" ||
params.trade_status === "TRADE_FINISHED";
if (paid) {
// 必须保证重复通知不会产生重复发货、重复记账等问题
await markOrderPaidOnce({
orderNo: order.orderNo,
alipayTradeNo: params.trade_no,
paidAt: params.gmt_payment
});
}
res.send("success");
}
);
示例中的 verifyAlipaySignature 和 markOrderPaidOnce 需要结合实际使用的 SDK、数据库和事务机制实现。
验签前不要修改通知参数,也不要进行多余的编码转换,否则可能造成验签失败。
七、提供订单查询接口
用户付款后,可能遇到这些情况:
- 支付已经成功,但小程序回调丢失。
- 用户付款后立即关闭收银台。
- 网络中断,前端没有收到结果。
- 支付宝异步通知尚未到达。
- 异步通知暂时处理失败。
因此,小程序返回业务页面后,应向自己的服务端查询订单状态:
async function queryOrderStatus(orderNo) {
const response = await fetch(
`https://api.example.com/api/payment/status?orderNo=${encodeURIComponent(orderNo)}`
);
const result = await response.json();
if (result.data.status === "PAID") {
showPaymentSuccess();
return;
}
showPaymentPending();
}
如果订单长时间停留在 PAYING 状态,服务端可以调用支付宝查单接口核实最终结果。前端不能直接调用支付宝查单接口,因为相关签名凭证必须保存在服务端。
安全与业务注意事项
不要以前端回调作为支付凭证
dd.tradePay 的成功回调可能只表示支付流程正常返回。是否真正付款,应以服务端验签后的异步通知或支付宝查单结果为准。
金额必须在服务端确认
前端可以提交商品编号、优惠券编号或订单编号,但最终金额要由服务端重新计算,不能直接把前端传入的价格作为收款金额。
金额建议使用精确的十进制类型,或按最小货币单位存储,避免让二进制浮点数直接参与账务计算。
异步通知必须支持幂等
支付宝可能重复发送通知。更新订单、发放权益、增加余额或触发发货时,必须保证同一个订单只会被成功处理一次。
必须校验通知内容
完成验签后,还要核对:
app_idout_trade_notrade_nototal_amountseller_id或实际收款主体trade_status
这样可以避免签名合法的通知被错误关联到其他订单。
不要在小程序中保存私钥
支付宝应用私钥、证书私钥和服务端密钥都不能打包进小程序。小程序代码可能被下载和分析,放在前端的长期密钥都应视为已经泄露。
正确处理取消和处理中状态
用户取消付款,不代表订单永久失败。服务端可以保留业务订单,允许用户在有效期内再次付款。订单状态不明确时,应先查单,再决定关闭订单还是重新发起支付。
配置可访问的通知地址
notify_url 必须是支付宝服务器能够访问的 HTTPS 地址。异步通知不能依赖钉钉登录状态,也不能要求浏览器提供 Cookie。
区分沙箱与生产环境
如果开发阶段使用支付宝沙箱,应用、网关、账号和密钥必须属于同一套沙箱环境。上线时要整体切换到生产配置,避免混用沙箱密钥、生产 app_id 和生产网关。
推荐的完整调用流程
用户点击支付
↓
钉钉小程序请求业务服务端
↓
服务端创建本地订单
↓
服务端调用支付宝接口并完成签名
↓
服务端返回 orderStr 或 tradeNO
↓
小程序调用钉钉支付 API
↓
用户在支付宝收银台完成付款
↓
支付宝向服务端发送异步通知
↓
服务端验签、核对金额并更新订单
↓
小程序查询服务端订单状态
↓
展示支付成功或处理中
如果调用时出现“接口不存在”“无权限”或“参数错误”,可以依次检查应用类型、支付权限、客户端版本、支付宝产品签约状态,以及当前接口要求的是 orderStr 还是 tradeNO。这些配置不匹配,是钉钉小程序接入支付宝支付时最常见的问题。
备注:内容仅供参考。
