• 下载
  • 社区

沙箱环境切换扩展

版本要求:小程序开发者工具 0.70 及以上版本。 


背景

沙箱环境可以让开发者在小程序上线到正式环境之前进行调试和测试,不用担心测试数据干扰正式环境,从而安全且轻松地验证支付等关键场景。


使用步骤

本篇文档借助 demo 项目来演示沙箱环境的使用方法。


前提条件:下载小程序开发者工具

下载并安装 小程序开发者工具(简称 IDE)。


一、新建 demo 项目

启动小程序开发者工具,选择 支付宝 > 小程序 > 模版选取 > 开放能力 > 小程序支付,点击 下一步


image.png


点击 完成,完成基于“小程序支付”模版创建小程序项目。


image.png


二、安装沙箱环境切换插件、切换到沙箱环境

在左侧功能面板,点击 扩展市场 图标,点击沙箱环境切换插件的 安装 按钮。 


image.png


安装完成后,点击 启用



启用插件后,在IDE左上角,点击 正式环境 下拉框,选择 沙箱环境,切换到沙箱环境。


image.png


三、使用支付宝沙箱钱包扫码登录

1.下载沙箱钱包,使用沙箱账号登录沙箱钱包。详情参见 小程序沙箱接入

2.在 IDE 工具栏右侧,点击 登录 按钮,弹出登录二维码。


image.png

  

3.使用沙箱钱包扫码,确认授权,成功登录沙箱环境。


  


四、修改小程序 demo 代码,使用沙箱后端服务

  1. 打开 client/pages/index/index.js 文件。
  2. 修改 URL 常量为: https://sandboxdemo.alipaydev.com
  3. 配置 signTypegatewayUrlappIdappPrivateKeyalipayPublicKey 常量,并在调用支付宝开放接口时传入这些参数。


 1const URL = 'https://sandboxdemo.alipaydev.com';
 2const SIGN_TYPE = 'RSA2';
 3
 4// 沙箱环境
 5const GATEWAY_URL = 'https://openapi.alipaydev.com/gateway.do';
 6// 线上环境
 7// const GATEWAY_URL = 'https://openapi.alipay.com/gateway.do';
 8
 9const APP_ID = '{appId}';
10const APP_PRIVATE_KEY = '{app私钥}';
11const ALIPAY_PUBLIC_KEY = '{app对应的支付宝公钥}';
12
13// 调用支付宝开放接口,沙箱环境传参示例。
14// 在正式环境中请勿从前端传递密钥!
15my.request({
16  url: '{exampleApi}',
17  data: {
18    appId: APP_ID,
19    appPrivateKey: APP_PRIVATE_KEY,
20    alipayPublicKey: ALIPAY_PUBLIC_KEY,
21    gatewayUrl: GATEWAY_URL,
22    signType: SIGN_TYPE,
23  }
24}



五、运行 demo 体验小程序支付

点击 预览 按钮,即生成二维码,使用沙箱钱包扫码即可体验 demo。


image.png


特别提示


安全提醒

本 demo 是为了支持开发者使用自己的 appId 体验小程序支付服务,所以采取了前端传输 appId、appprivatekey、alipaypublickey 到后端的方式。


上线小程序到生产环境,为了避免安全风险,请将这些信息直接配置到后端应用中,不要从前端传到后端。


在线上环境体验 demo

  1. 环境切换插件切换到 正式环境
  2. GATEWAY_URL 配置为:https://openapi.alipay.com/gateway.do
  3. APP_IDAPP_PRIVATE_KEYALIPAY_PUBLIC_KEY 配置为线上环境对应的值,并在所有的请求参数中传入正式环境的 GATEWAY_URL


为避免安全风险,在小程序正式上线时,请不要使用在本 demo 中使用过的密钥。


提示:使用线上环境的 appId,需要绑定“小程序支付”功能包,只有企业账号才能绑定,如下图所示:


image.png



附录


参考资料


文件内容

为了调用支付宝沙箱环境部署的改造后的demo后端服务,修改后的 client/pages/index/index.js 文件如下:

请将 APP_ID、APP_PRIVATE_KEY、ALIPAY_PUBLIC_KEY 改为自己沙箱小程序的 appId、应用私钥、对应的支付宝公钥。


沙箱小程序信息查看地址: https://openhome.alipay.com/platform/sandboxMini.htm


  1import format from './utils';
  2const URL = 'https://sandboxdemo.alipaydev.com';
  3
  4const SIGN_TYPE = 'RSA2';
  5// 沙箱环境
  6const GATEWAY_URL = 'https://openapi.alipaydev.com/gateway.do';
  7// 线上环境
  8// const GATEWAY_URL = 'https://openapi.alipay.com/gateway.do';
  9
 10const APP_ID = '{appId}';
 11const APP_PRIVATE_KEY = '{app私钥}';
 12const ALIPAY_PUBLIC_KEY = '{app对应的支付宝公钥}';
 13
 14Page({
 15  data: {
 16    paymentHistory: null, //支付历史记录
 17    isPaying: false, //支付状态
 18    uid: null, //用户ID
 19    isLogin: false //登录状态
 20  },
 21  /**
 22   *  @name onClickHandler
 23   *  @description 查看/支付按钮操作
 24   */
 25  async onClickHandler() {
 26    this.setData({
 27      isPaying: true
 28    });
 29    if (!this.data.isLogin) {
 30      //未登录状态
 31      try {
 32        const auth = await this.getAuthCode('auth_user');
 33        const user = await this.getUserByAuthCode(auth.authCode);
 34        const history = await this.getPaymentHistoryByUID(user.userId);
 35        this.setData({
 36          isPaying: false,
 37          paymentHistory: history,
 38          isLogin: true,
 39          uid: user.userId
 40        });
 41      } catch (error) {
 42        this.setData({
 43          isPaying: false
 44        });
 45        this.showToast(error.message, 'exception');
 46      }
 47    } else {
 48      // 已登录
 49      try {
 50        const auth = await this.getAuthCode('auth_user');
 51        const trade = await this.getTradeNo(auth.authCode, this.data.uid);
 52        const payStatus = await this.cashPaymentTrade(trade.tradeNo);
 53        this.showToast(payStatus.message);
 54        const updatePayment = await this.updatePaymentListByTradeNo(trade.tradeNo);
 55        this.setData({
 56          paymentHistory: updatePayment,
 57          isPaying: false
 58        });
 59      } catch (error) {
 60        this.setData({
 61          isPaying: false
 62        });
 63        this.showToast(error.message, 'exception');
 64      }
 65    }
 66  },
 67 getAvatarHandler() {
 68    return new Promise(async (resolve, reject) => {
 69      try {
 70        await this.getAuthCode('auth_user');
 71        const user = await this.getAuthUserInfo();
 72        resolve(user);
 73      } catch (error) {
 74        reject(error);
 75      }
 76    });
 77  },
 78
 79  getAuthUserInfo() {
 80    return new Promise((resolve, reject) => {
 81      my.getAuthUserInfo({
 82        success: (user) => {
 83          resolve(user);
 84        },
 85        fail: (error) => {
 86          reject({
 87            message: '获取用户头像失败',
 88            error
 89          });
 90        }
 91      });
 92    });
 93  },
 94  toast(message) {
 95    my.showToast({
 96      content: message,
 97      duration: 3000
 98    });
 99  },
100
101  /**
102   * @name onRefundPayHandler
103   * @description 发起退款
104   * @param {*} event
105   */
106  async onRefundPayHandler(event) {
107    const { key } = event.target.dataset;
108    const refundItem = await this.findActiveTradeByNo(key);
109    try {
110      if (refundItem !== null) {
111        const refundOrder = await this.refundPaymentByTradeNo(
112          refundItem.tradeNo,
113          refundItem.totalAmount
114        );
115        const updatePayment = await this.updatePaymentListByTradeNo(refundOrder.tradeNo);
116        this.showToast('退款成功');
117        this.setData({
118          paymentHistory: updatePayment
119        });
120      } else {
121        this.showToast('未知支付订单', 'exception');
122      }
123    } catch (error) {
124      this.showToast(error.message, 'exception');
125    }
126  },
127  /**
128   * @name onRepeatPayHandler
129   * @description 列表重新付款
130   * @param {*} event
131   */
132  async onRepeatPayHandler(event) {
133    const { key } = event.target.dataset;
134    const repeatItem = await this.findActiveTradeByNo(key);
135    try {
136      if (repeatItem !== null) {
137        const payStatus = await this.cashPaymentTrade(repeatItem.tradeNo);
138        this.showToast(payStatus.message);
139        const updatePayment = await this.updatePaymentListByTradeNo(repeatItem.tradeNo);
140        this.setData({
141          paymentHistory: updatePayment
142        });
143      } else {
144        this.showToast('未知支付订单', 'exception');
145      }
146    } catch (error) {
147      this.showToast(error.message, 'exception');
148    }
149  },
150  /**
151   * @name findActiveTradeByNo
152   * @description 查找当前操作项
153   * @param {*} tradeNo
154   * @returns
155   */
156  async findActiveTradeByNo(tradeNo) {
157    const findItem = this.data.paymentHistory.find((item) => {
158      return item.key === tradeNo;
159    });
160    if (findItem !== undefined) {
161      findItem.actionStatus = true;
162      this.setData({
163        paymentHistory: this.data.paymentHistory
164      });
165      return findItem;
166    } else {
167      return null;
168    }
169  },
170
171  /**
172   * @name updatePaymentListByTradeNo
173   * @description 根据tradeNo更新列表数据
174   * @param {*} tradeNo
175   * @returns
176   */
177  async updatePaymentListByTradeNo(tradeNo) {
178    let isExistOrder = false;
179    const order = await this.queryPaymentByTradeNo(tradeNo);
180    const formatHistory = this.data.paymentHistory.map((item) => {
181      if (item.tradeNo === order.tradeNo) {
182        isExistOrder = true;
183        item.key = order.tradeNo;
184        item.tradeNo = order.tradeNo;
185        item.actionStatus = false;
186        item.totalAmount = order.totalAmount;
187        item.tradeStatus = order.tradeStatus;
188        item.viewTime = format(order.sendPayDate, 'yyyy-MM-dd hh:mm:ss');
189      }
190      return item;
191    });
192    if (!isExistOrder) {
193      const addOrder = {};
194      addOrder.key = order.tradeNo;
195      addOrder.actionStatus = false;
196      addOrder.tradeNo = order.tradeNo;
197      addOrder.totalAmount = order.totalAmount;
198      addOrder.tradeStatus = order.tradeStatus;
199      addOrder.viewTime = format(order.sendPayDate, 'yyyy-MM-dd hh:mm:ss');
200      formatHistory.unshift(addOrder);
201    }
202    return formatHistory;
203  },
204
205  /***************************/
206  /******* 封装服务端 API ******/
207  /***************************/
208  /**
209   * @name getUserByAuthCode
210   * @description 获取用户信息
211   * @param {*} authCode
212   * @returns
213   */
214  getUserByAuthCode(authCode) {
215    return new Promise((resolve, reject) => {
216      my.request({
217        url: `${URL}/alipay/pay/alipayUserInfo`,
218        data: {
219          appId: APP_ID,
220          appPrivateKey: APP_PRIVATE_KEY,
221          alipayPublicKey: ALIPAY_PUBLIC_KEY,
222          gatewayUrl: GATEWAY_URL,
223          signType: SIGN_TYPE,
224          authCode: authCode
225        },
226        success: (result) => {
227          if (!result.data.success) {
228            reject({
229              ...result.data,
230              message: '获取用户信息失败'
231            });
232          }
233          resolve(result.data);
234        },
235        fail: (err) => {
236          reject({
237            ...err,
238            message: '获取用户信息异常'
239          });
240        }
241      });
242    });
243  },
244  /**
245   * @name getPaymentHistoryByUID
246   * @description 获取登录用户的支付历史记录
247   * @param {*} uid
248   * @returns {Array/object}
249   */
250  getPaymentHistoryByUID(uid) {
251    return new Promise((resolve, reject) => {
252      my.request({
253        url: `${URL}/alipay/pay/userPay`,
254        headers: {
255          'content-type': 'application/x-www-form-urlencoded'
256        },
257        data: {
258          appId: APP_ID,
259          appPrivateKey: APP_PRIVATE_KEY,
260          alipayPublicKey: ALIPAY_PUBLIC_KEY,
261          gatewayUrl: GATEWAY_URL,
262          signType: SIGN_TYPE,
263          userId: uid
264        },
265        success: (result) => {
266          if (!result.data.success) {
267            reject({
268              ...result.data,
269              message: '获取支付历史失败'
270            });
271          } else {
272            const formatHistory = result.data.alipayTradeQueryList.map((item) => {
273              const order = {};
274              order.key = item.tradeNo;
275              order.tradeNo = item.tradeNo;
276              order.actionStatus = false;
277              order.totalAmount = item.totalAmount;
278              order.tradeStatus = item.tradeStatus;
279              order.viewTime = format(item.sendPayDate, 'yyyy-MM-dd hh:mm:ss');
280              return order;
281            });
282            resolve(formatHistory);
283          }
284        },
285        fail: (err) => {
286          reject({
287            ...err,
288            message: '获取支付历史异常'
289          });
290        }
291      });
292    });
293  },
294  /**
295   * @name getTradeNo
296   * @description 创建支付交易订单
297   * @param {*} authCode
298   * @param {*} uid
299   * @returns {object}
300   */
301  getTradeNo(authCode, uid) {
302    return new Promise((resolve, reject) => {
303      my.request({
304        url: `${URL}/alipay/pay/alipayTradeCreate`,
305        data: {
306          appId: APP_ID,
307          appPrivateKey: APP_PRIVATE_KEY,
308          alipayPublicKey: ALIPAY_PUBLIC_KEY,
309          gatewayUrl: GATEWAY_URL,
310          signType: SIGN_TYPE,
311          total_amount: '0.01',
312          out_trade_no: `${new Date().getTime()}_demo_pay`,
313          scene: 'bar_code',
314          auth_code: authCode,
315          subject: '小程序支付演示DEMO',
316          buyer_id: uid
317        },
318        success: (result) => {
319          if (!result.data.success) {
320            reject({
321              ...result.data,
322              message: '创建支付订单失败'
323            });
324          } else {
325            resolve(result.data);
326          }
327        },
328        fail: (err) => {
329          reject({
330            ...err,
331            message: '创建支付订单异常'
332          });
333        }
334      });
335    });
336  },
337  /**
338   * @name queryPaymentByTradeNo
339   * @description 查询单笔订单
340   * @param {*} tradeNo
341   * @returns
342   */
343  queryPaymentByTradeNo(tradeNo) {
344    return new Promise((resolve, reject) => {
345      my.request({
346        url: `${URL}/alipay/pay/alipayTradeQuery`,
347        data: {
348          appId: APP_ID,
349          appPrivateKey: APP_PRIVATE_KEY,
350          alipayPublicKey: ALIPAY_PUBLIC_KEY,
351          gatewayUrl: GATEWAY_URL,
352          signType: SIGN_TYPE,
353          trade_no: tradeNo
354        },
355        success: (result) => {
356          if (!result.data.success) {
357            reject({
358              message: '支付查询失败',
359              ...result.data
360            });
361          } else {
362            resolve(result.data);
363          }
364        },
365        fail: (err) => {
366          reject({
367            message: '支付查询异常',
368            ...err
369          });
370        }
371      });
372    });
373  },
374  /**
375   * @name refundPaymentByTradeNo
376   * @description 退款流程
377   * @param {*} tradeNo
378   * @param {*} refundAmount
379   */
380  refundPaymentByTradeNo(tradeNo, refundAmount) {
381    return new Promise((resolve, reject) => {
382      my.request({
383        url: `${URL}/alipay/pay/alipayTradeRefund`,
384        data: {
385          appId: APP_ID,
386          appPrivateKey: APP_PRIVATE_KEY,
387          alipayPublicKey: ALIPAY_PUBLIC_KEY,
388          gatewayUrl: GATEWAY_URL,
389          signType: SIGN_TYPE,
390          trade_no: tradeNo,
391          refund_amount: refundAmount
392        },
393        success: (result) => {
394          if (!result.data.success) {
395            reject({
396              message: '退款失败',
397              ...result.data
398            });
399          } else {
400            resolve(result.data);
401          }
402        },
403        fail: (err) => {
404          reject({
405            message: '退款异常',
406            ...err
407          });
408        }
409      });
410    });
411  },
412
413  /***************************/
414  /******* 封装小程序 API ******/
415  /***************************/
416  /**
417   * @name getAuthCode
418   * @description 获取用户授权
419   * @param {string} [scopeCode='auth_user']
420   * @returns {object}
421   */
422  getAuthCode(scopeCode = 'auth_user') {
423    return new Promise((resolve, reject) => {
424      my.getAuthCode({
425        scopes: scopeCode,
426        success: (auth) => {
427          console.log(auth);
428          resolve(auth);
429        },
430        fail: (err) => {
431          console.log(err);
432          reject({ ...err, message: '获取用户授权失败' });
433        }
434      });
435    });
436  },
437  /**
438   * @name cashPaymentTrade
439   * @description 发起支付
440   * @param {*} tradeNo
441   * @returns
442   */
443  cashPaymentTrade(tradeNo) {
444    return new Promise((resolve, reject) => {
445      my.tradePay({
446        tradeNO: tradeNo,
447        success: (result) => {
448          if (result.resultCode != 9000) {
449            resolve({
450              status: false,
451              message: result.memo,
452              ...result
453            });
454          } else {
455            resolve({
456              status: true,
457              message: '支付成功',
458              ...result
459            });
460          }
461        },
462        fail: (err) => {
463          reject({
464            status: false,
465            message: '支付异常',
466            ...err
467          });
468        }
469      });
470    });
471  },
472  /**
473   * @name showToast
474   * @description 通用提示信息
475   * @param {*} message
476   * @param {string} [type='none']
477   */
478  showToast(message, type = 'none') {
479    my.showToast({
480      type,
481      content: message,
482      duration: 3000
483    });
484  }
485});