1. 背景

支付宝官方虽然提供了小程序支付模版,但要把基于模版创建的项目运行起来,还需要在有公网域名的服务器上部署demo后端服务;即使部署起来了,小程序支付接口还得挂包签约才能使用;最关键的是还得要有企业账号才能挂小程序支付功能包,太难了。。。

好在支付宝沙箱提供了支持,部署了改造后的demo后端服务,基于小程序支付demo前端模版,稍做修改即可运行小程序支付服务,体验小程序支付场景。

2. Demo使用步骤

本章节完整的介绍了从下载IDE到运行demo的步骤。

2.1 下载支付宝小程序开发者工具

到支付宝开放平台下载并安装支付宝小程序开发者工具:https://docs.alipay.com/mini/developer/todo-demo

2.2 新建 支付宝->小程序 项目

打开小程序开发者工具,选择支付宝 > 小程序 > 模板选取 > 开放能力 > 小程序支付, 单击下一步。

image.png

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

image.png

2.3 安装沙箱环境切换插件,并切换到沙箱环境

在左侧工具栏,单击扩展市场图标,单击沙箱环境切换插件的安装按钮。 

安装完成后,单击启用。

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

使用支付宝沙箱钱包扫码登陆

  a). 下载沙箱钱包,使用沙箱账号登陆沙箱钱包,具体步骤可查看开放平台文档: https://openhome.alipay.com/platform/sandboxMini.htm

  b). IDE右上角,单击“登陆”按钮,弹出登陆二维码

  c). 使用沙箱钱包扫码,确认授权,成功登陆沙箱环境

  


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

修改 client/pages/index/index.js文件

修改URL到值为: "https://sandboxdemo.alipaydev.com".

增加signType,gatewayUrl,appId,appPrivateKey,alipayPublicKey 配置,并在所有传输数据到后端的url中增加这些参数。

修改后的 client/pages/index/index.js 文件代码参考 附录->文件内容

2.5 运行 demo,体验小程序支付

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

image.png

3. 特别提示

3.1 安全提醒

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

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

3.2 在线上环境体验demo

环境切换插件切换到线上环境。

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

将 APP_ID, APP_PRIVATE_KEY, ALIPAY_PUBLIC_KEY 配置为线上环境对应的值,且在所有的请求参数中加上GATEWAY_URL的值即可。

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


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

image.png


4. 附录

4.1 参考资料

沙箱环境介绍: https://docs.open.alipay.com/200/105311/

小程序沙箱接入: https://openhome.alipay.com/platform/sandboxMini.htm

4.2 文件内容

为了调用支付宝沙箱环境部署的改造后的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
    });
  }
});