支付寶小程序 沙箱環(huán)境切換擴(kuò)展

2020-09-16 16:08 更新

版本要求:小程序開(kāi)發(fā)者工具 0.70 及以上版本。

背景

沙箱環(huán)境可以讓開(kāi)發(fā)者在小程序上線(xiàn)到正式環(huán)境之前進(jìn)行調(diào)試和測(cè)試,不用擔(dān)心測(cè)試數(shù)據(jù)干擾正式環(huán)境,從而安全且輕松地驗(yàn)證支付等關(guān)鍵場(chǎng)景。

使用步驟

本篇文檔借助 demo 項(xiàng)目來(lái)演示沙箱環(huán)境的使用方法。

前提條件:下載小程序開(kāi)發(fā)者工具

下載并安裝 小程序開(kāi)發(fā)者工具(簡(jiǎn)稱(chēng) IDE)。

一、新建 demo 項(xiàng)目

  1. 啟動(dòng)小程序開(kāi)發(fā)者工具,選擇 支付寶 > 小程序 > 模版選取 > 開(kāi)放能力 > 小程序支付,點(diǎn)擊 下一步。 img

  1. 點(diǎn)擊 完成,完成基于 小程序支付 模版創(chuàng)建小程序項(xiàng)目。 image.png

二、安裝沙箱環(huán)境切換插件、切換到沙箱環(huán)境

  1. 在左側(cè)功能面板,點(diǎn)擊 擴(kuò)展市場(chǎng) 圖標(biāo),點(diǎn)擊沙箱環(huán)境切換插件的 安裝 按鈕。

image.png

  1. 安裝完成后,點(diǎn)擊 啟用。

img

  1. 啟用插件后,在 IDE 左上角,點(diǎn)擊 正式環(huán)境 下拉框,選擇 沙箱環(huán)境,切換到沙箱環(huán)境。

image.png

三、使用支付寶沙箱錢(qián)包掃碼登錄

  1. 下載沙箱錢(qián)包,使用沙箱賬號(hào)登錄沙箱錢(qián)包。詳情參見(jiàn) 小程序沙箱接入。

  1. 在 IDE 工具欄右側(cè),點(diǎn)擊 登錄 按鈕,彈出登錄二維碼。

image.png

  1. 使用沙箱錢(qián)包掃碼,確認(rèn)授權(quán),成功登錄沙箱環(huán)境。

img

四、修改小程序 demo 代碼,使用沙箱后端服務(wù)

  1. 打開(kāi) client/pages/index/index.js 文件。
  2. 修改 URL 常量為: https://sandboxdemo.alipaydev.com
  3. 配置 signType、gatewayUrlappId、appPrivateKey、alipayPublicKey 常量,并在調(diào)用支付寶開(kāi)放接口時(shí)傳入這些參數(shù)。

const URL = 'https://sandboxdemo.alipaydev.com';
const SIGN_TYPE = 'RSA2';


// 沙箱環(huán)境
const GATEWAY_URL = 'https://openapi.alipaydev.com/gateway.do';
// 線(xiàn)上環(huán)境
// const GATEWAY_URL = 'https://openapi.alipay.com/gateway.do';


const APP_ID = '{appId}';
const APP_PRIVATE_KEY = '{app私鑰}';
const ALIPAY_PUBLIC_KEY = '{app對(duì)應(yīng)的支付寶公鑰}';


// 調(diào)用支付寶開(kāi)放接口,沙箱環(huán)境傳參示例。
// 在正式環(huán)境中請(qǐng)勿從前端傳遞密鑰!
my.request({
  url: '{exampleApi}',
  data: {
    appId: APP_ID,
    appPrivateKey: APP_PRIVATE_KEY,
    alipayPublicKey: ALIPAY_PUBLIC_KEY,
    gatewayUrl: GATEWAY_URL,
    signType: SIGN_TYPE,
  }
}

五、運(yùn)行 demo 體驗(yàn)小程序支付

點(diǎn)擊 預(yù)覽 按鈕,即生成二維碼,使用沙箱錢(qián)包掃碼即可體驗(yàn) demo。

image.png

特別提示

安全提醒

本 demo 是為了支持開(kāi)發(fā)者使用自己的 appId 體驗(yàn)小程序支付服務(wù),所以采取了前端傳輸 appId、appprivatekey、alipaypublickey 到后端的方式。

上線(xiàn)小程序到生產(chǎn)環(huán)境,為了避免安全風(fēng)險(xiǎn),請(qǐng)將這些信息直接配置到后端應(yīng)用中,不要從前端傳到后端。

在線(xiàn)上環(huán)境體驗(yàn) demo

  1. 環(huán)境切換插件切換到 正式環(huán)境。

  1. GATEWAY_URL 配置為:https://openapi.alipay.com/gateway.do

  1. APP_ID、APP_PRIVATE_KEY、ALIPAY_PUBLIC_KEY 配置為線(xiàn)上環(huán)境對(duì)應(yīng)的值,并在所有的請(qǐng)求參數(shù)中傳入正式環(huán)境的 GATEWAY_URL。

為避免安全風(fēng)險(xiǎn),在小程序正式上線(xiàn)時(shí),請(qǐng)不要使用在本 demo 中使用過(guò)的密鑰。

提示:使用線(xiàn)上環(huán)境的 appId,需要綁定“小程序支付”功能包,只有企業(yè)賬號(hào)才能綁定,如下圖所示:image.png

文件內(nèi)容

為了調(diào)用支付寶沙箱環(huán)境部署的改造后的demo后端服務(wù),修改后的 client/pages/index/index.js 文件如下:

請(qǐng)將 APP_ID、APP_PRIVATE_KEY、ALIPAY_PUBLIC_KEY 改為自己沙箱小程序的 appId、應(yīng)用私鑰、對(duì)應(yīng)的支付寶公鑰。

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

import format from './utils';
const URL = 'https://sandboxdemo.alipaydev.com';


const SIGN_TYPE = 'RSA2';
// 沙箱環(huán)境
const GATEWAY_URL = 'https://openapi.alipaydev.com/gateway.do';
// 線(xiàn)上環(huán)境
// const GATEWAY_URL = 'https://openapi.alipay.com/gateway.do';


const APP_ID = '{appId}';
const APP_PRIVATE_KEY = '{app私鑰}';
const ALIPAY_PUBLIC_KEY = '{app對(duì)應(yīng)的支付寶公鑰}';


Page({
  data: {
    paymentHistory: null, //支付歷史記錄
    isPaying: false, //支付狀態(tài)
    uid: null, //用戶(hù)ID
    isLogin: false //登錄狀態(tài)
  },
  /**
   *  @name onClickHandler
   *  @description 查看/支付按鈕操作
   */
  async onClickHandler() {
    this.setData({
      isPaying: true
    });
    if (!this.data.isLogin) {
      //未登錄狀態(tài)
      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: '獲取用戶(hù)頭像失敗',
            error
          });
        }
      });
    });
  },
  toast(message) {
    my.showToast({
      content: message,
      duration: 3000
    });
  },


  /**
   * @name onRefundPayHandler
   * @description 發(fā)起退款
   * @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 查找當(dāng)前操作項(xiàng)
   * @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 根據(jù)tradeNo更新列表數(shù)據(jù)
   * @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;
  },


  /***************************/
  /******* 封裝服務(wù)端 API ******/
  /***************************/
  /**
   * @name getUserByAuthCode
   * @description 獲取用戶(hù)信息
   * @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: '獲取用戶(hù)信息失敗'
            });
          }
          resolve(result.data);
        },
        fail: (err) => {
          reject({
            ...err,
            message: '獲取用戶(hù)信息異常'
          });
        }
      });
    });
  },
  /**
   * @name getPaymentHistoryByUID
   * @description 獲取登錄用戶(hù)的支付歷史記錄
   * @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 創(chuàng)建支付交易訂單
   * @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: '創(chuàng)建支付訂單失敗'
            });
          } else {
            resolve(result.data);
          }
        },
        fail: (err) => {
          reject({
            ...err,
            message: '創(chuàng)建支付訂單異常'
          });
        }
      });
    });
  },
  /**
   * @name queryPaymentByTradeNo
   * @description 查詢(xún)單筆訂單
   * @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: '支付查詢(xún)失敗',
              ...result.data
            });
          } else {
            resolve(result.data);
          }
        },
        fail: (err) => {
          reject({
            message: '支付查詢(xún)異常',
            ...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 獲取用戶(hù)授權(quán)
   * @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: '獲取用戶(hù)授權(quán)失敗' });
        }
      });
    });
  },
  /**
   * @name cashPaymentTrade
   * @description 發(fā)起支付
   * @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
    });
  }
});
以上內(nèi)容是否對(duì)您有幫助:
在線(xiàn)筆記
App下載
App下載

掃描二維碼

下載編程獅App

公眾號(hào)
微信公眾號(hào)

編程獅公眾號(hào)