支付宝小程序 沙箱环境切换扩展
版本要求:小程序开发者工具 0.70 及以上版本。
背景
沙箱环境可以让开发者在小程序上线到正式环境之前进行调试和测试,不用担心测试数据干扰正式环境,从而安全且轻松地验证支付等关键场景。
使用步骤
本篇文档借助 demo 项目来演示沙箱环境的使用方法。
前提条件:下载小程序开发者工具
下载并安装 小程序开发者工具(简称 IDE)。
一、新建 demo 项目
- 启动小程序开发者工具,选择 支付宝 > 小程序 > 模版选取 > 开放能力 > 小程序支付,点击 下一步。
- 点击 完成,完成基于 小程序支付 模版创建小程序项目。
二、安装沙箱环境切换插件、切换到沙箱环境
- 在左侧功能面板,点击 扩展市场 图标,点击沙箱环境切换插件的 安装 按钮。
- 安装完成后,点击 启用。
- 启用插件后,在 IDE 左上角,点击 正式环境 下拉框,选择 沙箱环境,切换到沙箱环境。
三、使用支付宝沙箱钱包扫码登录
- 下载沙箱钱包,使用沙箱账号登录沙箱钱包。详情参见 小程序沙箱接入。
- 在 IDE 工具栏右侧,点击 登录 按钮,弹出登录二维码。
- 使用沙箱钱包扫码,确认授权,成功登录沙箱环境。
四、修改小程序 demo 代码,使用沙箱后端服务
- 打开 client/pages/index/index.js 文件。
- 修改 URL 常量为:
https://sandboxdemo.alipaydev.com
。 - 配置 signType、gatewayUrl、appId、appPrivateKey、alipayPublicKey 常量,并在调用支付宝开放接口时传入这些参数。
const URL = 'https://sandboxdemo.alipaydev.com';
const SIGN_TYPE = 'RSA2';
// 沙箱环境
const GATEWAY_URL = 'https://openapi.alipaydev.com/gateway.do';
// 线上环境
// const GATEWAY_URL = 'https://openapi.alipay.com/gateway.do';
const APP_ID = '{appId}';
const APP_PRIVATE_KEY = '{app私钥}';
const ALIPAY_PUBLIC_KEY = '{app对应的支付宝公钥}';
// 调用支付宝开放接口,沙箱环境传参示例。
// 在正式环境中请勿从前端传递密钥!
my.request({
url: '{exampleApi}',
data: {
appId: APP_ID,
appPrivateKey: APP_PRIVATE_KEY,
alipayPublicKey: ALIPAY_PUBLIC_KEY,
gatewayUrl: GATEWAY_URL,
signType: SIGN_TYPE,
}
}
五、运行 demo 体验小程序支付
点击 预览 按钮,即生成二维码,使用沙箱钱包扫码即可体验 demo。
特别提示
安全提醒
本 demo 是为了支持开发者使用自己的 appId 体验小程序支付服务,所以采取了前端传输 appId、appprivatekey、alipaypublickey 到后端的方式。
上线小程序到生产环境,为了避免安全风险,请将这些信息直接配置到后端应用中,不要从前端传到后端。
在线上环境体验 demo
- 环境切换插件切换到 正式环境。
- GATEWAY_URL 配置为:
https://openapi.alipay.com/gateway.do
- 将 APP_ID、APP_PRIVATE_KEY、ALIPAY_PUBLIC_KEY 配置为线上环境对应的值,并在所有的请求参数中传入正式环境的 GATEWAY_URL。
为避免安全风险,在小程序正式上线时,请不要使用在本 demo 中使用过的密钥。
提示:使用线上环境的 appId,需要绑定“小程序支付”功能包,只有企业账号才能绑定,如下图所示:
文件内容
为了调用支付宝沙箱环境部署的改造后的demo后端服务,修改后的 client/pages/index/index.js 文件如下:
请将 APP_ID、APP_PRIVATE_KEY、ALIPAY_PUBLIC_KEY 改为自己沙箱小程序的 appId、应用私钥、对应的支付宝公钥。
沙箱小程序信息查看地址: https://openhome.alipay.com/platform/sandboxMini.htm
import format from './utils';
const URL = 'https://sandboxdemo.alipaydev.com';
const SIGN_TYPE = 'RSA2';
// 沙箱环境
const GATEWAY_URL = 'https://openapi.alipaydev.com/gateway.do';
// 线上环境
// const GATEWAY_URL = 'https://openapi.alipay.com/gateway.do';
const APP_ID = '{appId}';
const APP_PRIVATE_KEY = '{app私钥}';
const ALIPAY_PUBLIC_KEY = '{app对应的支付宝公钥}';
Page({
data: {
paymentHistory: null, //支付历史记录
isPaying: false, //支付状态
uid: null, //用户ID
isLogin: false //登录状态
},
/**
* @name onClickHandler
* @description 查看/支付按钮操作
*/
async onClickHandler() {
this.setData({
isPaying: true
});
if (!this.data.isLogin) {
//未登录状态
try {
const auth = await this.getAuthCode('auth_user');
const user = await this.getUserByAuthCode(auth.authCode);
const history = await this.getPaymentHistoryByUID(user.userId);
this.setData({
isPaying: false,
paymentHistory: history,
isLogin: true,
uid: user.userId
});
} catch (error) {
this.setData({
isPaying: false
});
this.showToast(error.message, 'exception');
}
} else {
// 已登录
try {
const auth = await this.getAuthCode('auth_user');
const trade = await this.getTradeNo(auth.authCode, this.data.uid);
const payStatus = await this.cashPaymentTrade(trade.tradeNo);
this.showToast(payStatus.message);
const updatePayment = await this.updatePaymentListByTradeNo(trade.tradeNo);
this.setData({
paymentHistory: updatePayment,
isPaying: false
});
} catch (error) {
this.setData({
isPaying: false
});
this.showToast(error.message, 'exception');
}
}
},
getAvatarHandler() {
return new Promise(async (resolve, reject) => {
try {
await this.getAuthCode('auth_user');
const user = await this.getAuthUserInfo();
resolve(user);
} catch (error) {
reject(error);
}
});
},
getAuthUserInfo() {
return new Promise((resolve, reject) => {
my.getAuthUserInfo({
success: (user) => {
resolve(user);
},
fail: (error) => {
reject({
message: '获取用户头像失败',
error
});
}
});
});
},
toast(message) {
my.showToast({
content: message,
duration: 3000
});
},
/**
* @name onRefundPayHandler
* @description 发起退款
* @param {*} event
*/
async onRefundPayHandler(event) {
const { key } = event.target.dataset;
const refundItem = await this.findActiveTradeByNo(key);
try {
if (refundItem !== null) {
const refundOrder = await this.refundPaymentByTradeNo(
refundItem.tradeNo,
refundItem.totalAmount
);
const updatePayment = await this.updatePaymentListByTradeNo(refundOrder.tradeNo);
this.showToast('退款成功');
this.setData({
paymentHistory: updatePayment
});
} else {
this.showToast('未知支付订单', 'exception');
}
} catch (error) {
this.showToast(error.message, 'exception');
}
},
/**
* @name onRepeatPayHandler
* @description 列表重新付款
* @param {*} event
*/
async onRepeatPayHandler(event) {
const { key } = event.target.dataset;
const repeatItem = await this.findActiveTradeByNo(key);
try {
if (repeatItem !== null) {
const payStatus = await this.cashPaymentTrade(repeatItem.tradeNo);
this.showToast(payStatus.message);
const updatePayment = await this.updatePaymentListByTradeNo(repeatItem.tradeNo);
this.setData({
paymentHistory: updatePayment
});
} else {
this.showToast('未知支付订单', 'exception');
}
} catch (error) {
this.showToast(error.message, 'exception');
}
},
/**
* @name findActiveTradeByNo
* @description 查找当前操作项
* @param {*} tradeNo
* @returns
*/
async findActiveTradeByNo(tradeNo) {
const findItem = this.data.paymentHistory.find((item) => {
return item.key === tradeNo;
});
if (findItem !== undefined) {
findItem.actionStatus = true;
this.setData({
paymentHistory: this.data.paymentHistory
});
return findItem;
} else {
return null;
}
},
/**
* @name updatePaymentListByTradeNo
* @description 根据tradeNo更新列表数据
* @param {*} tradeNo
* @returns
*/
async updatePaymentListByTradeNo(tradeNo) {
let isExistOrder = false;
const order = await this.queryPaymentByTradeNo(tradeNo);
const formatHistory = this.data.paymentHistory.map((item) => {
if (item.tradeNo === order.tradeNo) {
isExistOrder = true;
item.key = order.tradeNo;
item.tradeNo = order.tradeNo;
item.actionStatus = false;
item.totalAmount = order.totalAmount;
item.tradeStatus = order.tradeStatus;
item.viewTime = format(order.sendPayDate, 'yyyy-MM-dd hh:mm:ss');
}
return item;
});
if (!isExistOrder) {
const addOrder = {};
addOrder.key = order.tradeNo;
addOrder.actionStatus = false;
addOrder.tradeNo = order.tradeNo;
addOrder.totalAmount = order.totalAmount;
addOrder.tradeStatus = order.tradeStatus;
addOrder.viewTime = format(order.sendPayDate, 'yyyy-MM-dd hh:mm:ss');
formatHistory.unshift(addOrder);
}
return formatHistory;
},
/***************************/
/******* 封装服务端 API ******/
/***************************/
/**
* @name getUserByAuthCode
* @description 获取用户信息
* @param {*} authCode
* @returns
*/
getUserByAuthCode(authCode) {
return new Promise((resolve, reject) => {
my.request({
url: `${URL}/alipay/pay/alipayUserInfo`,
data: {
appId: APP_ID,
appPrivateKey: APP_PRIVATE_KEY,
alipayPublicKey: ALIPAY_PUBLIC_KEY,
gatewayUrl: GATEWAY_URL,
signType: SIGN_TYPE,
authCode: authCode
},
success: (result) => {
if (!result.data.success) {
reject({
...result.data,
message: '获取用户信息失败'
});
}
resolve(result.data);
},
fail: (err) => {
reject({
...err,
message: '获取用户信息异常'
});
}
});
});
},
/**
* @name getPaymentHistoryByUID
* @description 获取登录用户的支付历史记录
* @param {*} uid
* @returns {Array/object}
*/
getPaymentHistoryByUID(uid) {
return new Promise((resolve, reject) => {
my.request({
url: `${URL}/alipay/pay/userPay`,
headers: {
'content-type': 'application/x-www-form-urlencoded'
},
data: {
appId: APP_ID,
appPrivateKey: APP_PRIVATE_KEY,
alipayPublicKey: ALIPAY_PUBLIC_KEY,
gatewayUrl: GATEWAY_URL,
signType: SIGN_TYPE,
userId: uid
},
success: (result) => {
if (!result.data.success) {
reject({
...result.data,
message: '获取支付历史失败'
});
} else {
const formatHistory = result.data.alipayTradeQueryList.map((item) => {
const order = {};
order.key = item.tradeNo;
order.tradeNo = item.tradeNo;
order.actionStatus = false;
order.totalAmount = item.totalAmount;
order.tradeStatus = item.tradeStatus;
order.viewTime = format(item.sendPayDate, 'yyyy-MM-dd hh:mm:ss');
return order;
});
resolve(formatHistory);
}
},
fail: (err) => {
reject({
...err,
message: '获取支付历史异常'
});
}
});
});
},
/**
* @name getTradeNo
* @description 创建支付交易订单
* @param {*} authCode
* @param {*} uid
* @returns {object}
*/
getTradeNo(authCode, uid) {
return new Promise((resolve, reject) => {
my.request({
url: `${URL}/alipay/pay/alipayTradeCreate`,
data: {
appId: APP_ID,
appPrivateKey: APP_PRIVATE_KEY,
alipayPublicKey: ALIPAY_PUBLIC_KEY,
gatewayUrl: GATEWAY_URL,
signType: SIGN_TYPE,
total_amount: '0.01',
out_trade_no: `${new Date().getTime()}_demo_pay`,
scene: 'bar_code',
auth_code: authCode,
subject: '小程序支付演示DEMO',
buyer_id: uid
},
success: (result) => {
if (!result.data.success) {
reject({
...result.data,
message: '创建支付订单失败'
});
} else {
resolve(result.data);
}
},
fail: (err) => {
reject({
...err,
message: '创建支付订单异常'
});
}
});
});
},
/**
* @name queryPaymentByTradeNo
* @description 查询单笔订单
* @param {*} tradeNo
* @returns
*/
queryPaymentByTradeNo(tradeNo) {
return new Promise((resolve, reject) => {
my.request({
url: `${URL}/alipay/pay/alipayTradeQuery`,
data: {
appId: APP_ID,
appPrivateKey: APP_PRIVATE_KEY,
alipayPublicKey: ALIPAY_PUBLIC_KEY,
gatewayUrl: GATEWAY_URL,
signType: SIGN_TYPE,
trade_no: tradeNo
},
success: (result) => {
if (!result.data.success) {
reject({
message: '支付查询失败',
...result.data
});
} else {
resolve(result.data);
}
},
fail: (err) => {
reject({
message: '支付查询异常',
...err
});
}
});
});
},
/**
* @name refundPaymentByTradeNo
* @description 退款流程
* @param {*} tradeNo
* @param {*} refundAmount
*/
refundPaymentByTradeNo(tradeNo, refundAmount) {
return new Promise((resolve, reject) => {
my.request({
url: `${URL}/alipay/pay/alipayTradeRefund`,
data: {
appId: APP_ID,
appPrivateKey: APP_PRIVATE_KEY,
alipayPublicKey: ALIPAY_PUBLIC_KEY,
gatewayUrl: GATEWAY_URL,
signType: SIGN_TYPE,
trade_no: tradeNo,
refund_amount: refundAmount
},
success: (result) => {
if (!result.data.success) {
reject({
message: '退款失败',
...result.data
});
} else {
resolve(result.data);
}
},
fail: (err) => {
reject({
message: '退款异常',
...err
});
}
});
});
},
/***************************/
/******* 封装小程序 API ******/
/***************************/
/**
* @name getAuthCode
* @description 获取用户授权
* @param {string} [scopeCode='auth_user']
* @returns {object}
*/
getAuthCode(scopeCode = 'auth_user') {
return new Promise((resolve, reject) => {
my.getAuthCode({
scopes: scopeCode,
success: (auth) => {
console.log(auth);
resolve(auth);
},
fail: (err) => {
console.log(err);
reject({ ...err, message: '获取用户授权失败' });
}
});
});
},
/**
* @name cashPaymentTrade
* @description 发起支付
* @param {*} tradeNo
* @returns
*/
cashPaymentTrade(tradeNo) {
return new Promise((resolve, reject) => {
my.tradePay({
tradeNO: tradeNo,
success: (result) => {
if (result.resultCode != 9000) {
resolve({
status: false,
message: result.memo,
...result
});
} else {
resolve({
status: true,
message: '支付成功',
...result
});
}
},
fail: (err) => {
reject({
status: false,
message: '支付异常',
...err
});
}
});
});
},
/**
* @name showToast
* @description 通用提示信息
* @param {*} message
* @param {string} [type='none']
*/
showToast(message, type = 'none') {
my.showToast({
type,
content: message,
duration: 3000
});
}
});