Преглед на файлове

feat 添加微信支付

tumobi преди 7 години
родител
ревизия
885ee4cc2d
променени са 7 файла, в които са добавени 179 реда и са изтрити 52 реда
  1. 4 1
      package.json
  2. 2 1
      src/api/config/config.js
  3. 1 10
      src/api/controller/auth.js
  4. 37 38
      src/api/controller/pay.js
  5. 22 0
      src/api/model/order.js
  6. 108 0
      src/api/service/weixin.js
  7. 5 2
      src/common/config/config.js

+ 4 - 1
package.json

@@ -13,6 +13,7 @@
     "jsonwebtoken": "^8.0.0",
     "kcors": "^2.2.1",
     "lodash": "^4.17.4",
+    "md5": "^2.2.1",
     "moment": "^2.18.1",
     "request": "^2.81.0",
     "request-promise": "^4.2.1",
@@ -21,7 +22,9 @@
     "think-logger3": "^1.0.0",
     "think-model": "^1.0.0",
     "think-model-mysql": "^1.0.0",
-    "thinkjs": "^3.0.0"
+    "thinkjs": "^3.0.0",
+    "weixinpay": "^1.0.12",
+    "xml2js": "^0.4.19"
   },
   "devDependencies": {
     "babel-cli": "^6.24.1",

+ 2 - 1
src/api/config/config.js

@@ -23,6 +23,7 @@ module.exports = {
     'cart/checked',
     'cart/update',
     'cart/delete',
-    'cart/goodscount'
+    'cart/goodscount',
+    'pay/notify'
   ]
 };

+ 1 - 10
src/api/controller/auth.js

@@ -3,15 +3,6 @@ const rp = require('request-promise');
 const _ = require('lodash');
 
 module.exports = class extends Base {
-  /**
-   * index action
-   * @return {Promise} []
-   */
-  async indexAction() {
-    const avatarPath = think.RESOURCE_PATH + '/static/user/avatar/1.' + _.last(_.split('https://img6.bdstatic.com/img/image/smallpic/liutaoxiaotu.jpg', '.'));
-    return this.success(avatarPath);
-  }
-
   async loginByWeixinAction() {
     const code = this.post('code');
     const fullUserInfo = this.post('userInfo');
@@ -43,7 +34,7 @@ module.exports = class extends Base {
       return this.fail('登录失败');
     }
 
-    // 用户数据
+    // 解释用户数据
     const WeixinSerivce = this.service('weixin', 'api');
     const weixinUserInfo = await WeixinSerivce.decryptUserInfoData(sessionData.session_key, fullUserInfo.encryptedData, fullUserInfo.iv);
     if (think.isEmpty(weixinUserInfo)) {

+ 37 - 38
src/api/controller/pay.js

@@ -1,17 +1,14 @@
+/* eslint-disable no-multi-spaces */
 const Base = require('./base.js');
-const rp = require('request-promise');
 
 module.exports = class extends Base {
-  // 支付类型 1 微信支付 2支付宝
-  // TODO 支付功能由于没有公司账号和微信支付账号,所以没有经过测试,如您可以提供相关账号测试,可联系 tumobi@163.com
 
   /**
    * 获取支付的请求参数
    * @returns {Promise<PreventPromise|void|Promise>}
    */
-  async payPrepayAction() {
+  async prepayAction() {
     const orderId = this.get('orderId');
-    // const payType = this.get('payType');
 
     const orderInfo = await this.model('order').where({id: orderId}).find();
     if (think.isEmpty(orderInfo)) {
@@ -20,42 +17,44 @@ module.exports = class extends Base {
     if (parseInt(orderInfo.pay_status) !== 0) {
       return this.fail(400, '订单已支付,请不要重复操作');
     }
+    const openid = await this.model('user').where({id: orderInfo.user_id}).getField('weixin_openid', true);
+    if (think.isEmpty(openid)) {
+      return this.fail('微信支付失败');
+    }
+    const WeixinSerivce = this.service('weixin', 'api');
+    try {
+      const returnParams = await WeixinSerivce.createUnifiedOrder({
+        openid: openid,
+        body: '订单编号:' + orderInfo.order_sn,
+        out_trade_no: orderInfo.order_sn,
+        total_fee: parseInt(orderInfo.actual_price * 100),
+        spbill_create_ip: ''
+      });
+      return this.success(returnParams);
+    } catch (err) {
+      return this.fail('微信支付失败');
+    }
+  }
 
-    // 微信支付统一调用接口,body参数请查看微信支付文档:https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_sl_api.php?chapter=9_1
-    const options = {
-      method: 'POST',
-      url: 'https://api.mch.weixin.qq.com/pay/unifiedorder',
-      body: {
-        appid: 'payload',
-        mch_id: '',
-        sub_appid: '',
-        sub_mch_id: '',
-        device_info: '',
-        nonce_str: think.uuid(32),
-        sign: '',
-        sign_type: 'MD5',
-        body: '',
-        out_trade_no: '',
-        total_fee: orderInfo.actual_price * 100,
-        spbill_create_ip: '',
-        notify_url: '',
-        trade_type: 'JSAPI',
-        openid: '',
-        sub_openid: ''
-      }
-    };
-    const payParam = await rp(options);
-    if (payParam) {
+  async notifyAction() {
+    const WeixinSerivce = this.service('weixin', 'api');
+    const result = WeixinSerivce.payNotify(this.post('xml'));
+    console.log('WeixinSerivce.payNotify ' + result);
+    if (!result) {
+      return `<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[支付失败]]></return_msg></xml>`;
+    }
+
+    const orderModel = this.model('order');
+    const orderInfo = await orderModel.getOrderByOrderSn(result.out_trade_no);
+    if (think.isEmpty(orderInfo)) {
+      return `<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>`;
+    }
 
+    if (orderModel.updatePayStatus(orderInfo.id, 2)) {
+    } else {
+      return `<xml><return_code><![CDATA[FAIL]]></return_code><return_msg><![CDATA[订单不存在]]></return_msg></xml>`;
     }
 
-    // 统一返回成功,方便测试
-    return this.success({
-      'timeStamp': this.getTime(),
-      'nonceStr': think.uuid(16),
-      'package': 'prepay_id=wx201410272009395522657a690389285100',
-      'signType': 'MD5',
-      'paySign': 'jdsdlsdsd'
-    });
+    return `<xml><return_code><![CDATA[SUCCESS]]></return_code><return_msg><![CDATA[OK]]></return_msg></xml>`;
   }
 };

+ 22 - 0
src/api/model/order.js

@@ -80,4 +80,26 @@ module.exports = class extends think.Model {
 
     return statusText;
   }
+
+  /**
+   * 更改订单支付状态
+   * @param orderId
+   * @param payStatus
+   * @returns {Promise.<boolean>}
+   */
+  async updatePayStatus(orderId, payStatus = 0) {
+    return this.where({id: orderId}).limit(1).update({pay_status: parseInt(payStatus)});
+  }
+
+  /**
+   * 根据订单编号查找订单信息
+   * @param orderSn
+   * @returns {Promise.<Promise|Promise<any>|T|*>}
+   */
+  async getOrderByOrderSn(orderSn) {
+    if (think.isEmpty(orderSn)) {
+      return {};
+    }
+    return this.where({order_sn: orderSn}).find();
+  }
 };

+ 108 - 0
src/api/service/weixin.js

@@ -1,6 +1,14 @@
 const crypto = require('crypto');
+const md5 = require('md5');
 
 module.exports = class extends think.Service {
+  /**
+   * 解析微信登录用户数据
+   * @param sessionKey
+   * @param encryptedData
+   * @param iv
+   * @returns {Promise.<string>}
+   */
   async decryptUserInfoData(sessionKey, encryptedData, iv) {
     // base64 decode
     const _sessionKey = new Buffer(sessionKey, 'base64');
@@ -26,4 +34,104 @@ module.exports = class extends think.Service {
 
     return decoded;
   }
+
+  /**
+   * 统一下单
+   * @param payInfo
+   * @returns {Promise}
+   */
+  createUnifiedOrder(payInfo) {
+    const WeiXinPay = require('weixinpay');
+    const weixinpay = new WeiXinPay({
+      appid: think.config('weixin.appid'), // 微信小程序appid
+      openid: payInfo.openid, // 用户openid
+      mch_id: think.config('weixin.mch_id'), // 商户帐号ID
+      partner_key: think.config('weixin.partner_key') // 秘钥
+    });
+    return new Promise((resolve, reject) => {
+      weixinpay.createUnifiedOrder({
+        body: payInfo.body,
+        out_trade_no: payInfo.out_trade_no,
+        total_fee: payInfo.total_fee,
+        spbill_create_ip: payInfo.spbill_create_ip,
+        notify_url: think.config('weixin.notify_url'),
+        trade_type: 'JSAPI'
+      }, (res) => {
+        if (res.return_code === 'SUCCESS' && res.result_code === 'SUCCESS') {
+          const returnParams = {
+            'appid': res.appid,
+            'timeStamp': parseInt(Date.now() / 1000) + '',
+            'nonceStr': res.nonce_str,
+            'package': 'prepay_id=' + res.prepay_id,
+            'signType': 'MD5'
+          };
+          const paramStr = `appId=${returnParams.appid}&nonceStr=${returnParams.nonceStr}&package=${returnParams.package}&signType=${returnParams.signType}&timeStamp=${returnParams.timeStamp}&key=` + think.config('weixin.partner_key');
+          returnParams.paySign = md5(paramStr).toUpperCase();
+          resolve(returnParams);
+        } else {
+          reject(res);
+        }
+      });
+    });
+  }
+
+  /**
+   * 生成排序后的支付参数 query
+   * @param queryObj
+   * @returns {Promise.<string>}
+   */
+  buildQuery(queryObj) {
+    const sortPayOptions = {};
+    for (const key of Object.keys(queryObj).sort()) {
+      sortPayOptions[key] = queryObj[key];
+    }
+    let payOptionQuery = '';
+    for (const key of Object.keys(sortPayOptions).sort()) {
+      payOptionQuery += key + '=' + sortPayOptions[key] + '&';
+    }
+    payOptionQuery = payOptionQuery.substring(0, payOptionQuery.length - 1);
+    return payOptionQuery;
+  }
+
+  /**
+   * 对 query 进行签名
+   * @param queryStr
+   * @returns {Promise.<string>}
+   */
+  signQuery(queryStr) {
+    queryStr = queryStr + '&key=' + think.config('weixin.partner_key');
+    const md5 = require('md5');
+    const md5Sign = md5(queryStr);
+    return md5Sign.toUpperCase();
+  }
+
+  /**
+   * 处理微信支付回调
+   * @param notifyData
+   * @returns {{}}
+   */
+  payNotify(notifyData) {
+    if (think.isEmpty(notifyData)) {
+      return false;
+    }
+
+    const notifyObj = {};
+    let sign = '';
+    for (const key of Object.keys(notifyData)) {
+      if (key !== 'sign') {
+        notifyObj[key] = notifyData[key][0];
+      } else {
+        sign = notifyData[key][0];
+      }
+    }
+    if (notifyObj.return_code !== 'SUCCESS' || notifyObj.result_code !== 'SUCCESS') {
+      console.log('return_code false');
+      return false;
+    }
+    const signString = this.signQuery(this.buildQuery(notifyObj));
+    if (think.isEmpty(sign) || signString !== sign) {
+      return false;
+    }
+    return notifyObj;
+  }
 };

+ 5 - 2
src/common/config/config.js

@@ -2,7 +2,10 @@
 module.exports = {
   default_module: 'api',
   weixin: {
-    secret: '',
-    appid: ''
+    appid: '', // 小程序 appid
+    secret: '', // 小程序密钥
+    mch_id: '', // 商户帐号ID
+    partner_key: '', // 微信支付密钥
+    notify_url: '' // 微信异步通知,例:https://www.nideshop.com/api/pay/notify
   }
 };