wxpay.js 5.4 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127
  1. 'use strict';
  2. const { CrudService } = require('naf-framework-mongoose/lib/service');
  3. const { BusinessError, ErrorCode } = require('naf-core').Error;
  4. const _ = require('lodash');
  5. const assert = require('assert');
  6. const random = require('string-random');
  7. const moment = require('moment');
  8. const crypto = require('crypto');
  9. const xml2js = require('xml2js');
  10. const fs = require('fs');
  11. // wxpay
  12. class WxpayService extends CrudService {
  13. constructor(ctx) {
  14. super(ctx, 'wxpay');
  15. this.appInfo = this.app.config.appInfo;
  16. this.redis = this.app.redis;
  17. this.model = this.ctx.model.Card;
  18. this.cashModel = this.ctx.model.Cash;
  19. this.cashError = this.ctx.model.CashError;
  20. }
  21. async toAuth({ code, id }) {
  22. const token = await this.redis.get(id);
  23. if (token) return;
  24. const url = `https://api.weixin.qq.com/sns/jscode2session?appid=${this.appInfo.id}&secret=${this.appInfo.secret}&js_code=${code}&grant_type=authorization_code`;
  25. const res = await this.ctx.curl(url, {
  26. method: 'get',
  27. dataType: 'json',
  28. });
  29. const info = JSON.stringify(res.data);
  30. await this.redis.set(id, info, 'EX', 3600);
  31. }
  32. async cash({ id, money, points }) {
  33. money = parseInt(money);
  34. const token = await this.redis.get(id);
  35. if (!token) throw new BusinessError(ErrorCode.SERVICE_FAULT, '未找到用户的openid,请重新登陆小程序!');
  36. const { openid } = JSON.parse(token);
  37. if (!this.appInfo) throw new BusinessError(ErrorCode.FILE_FAULT, '未设置小程序相关设置');
  38. const user = await this.model.findById(id);
  39. if (!user) throw new BusinessError(ErrorCode.DATA_NOT_EXIST, '未找到用户信息!');
  40. const opoints = _.get(user, 'points', 0);
  41. const pres = _.subtract(opoints, points);
  42. if (pres < 0) throw new BusinessError(ErrorCode.BADPARAM, '积分不足');
  43. // check_name:校验真名,暂不校验
  44. const object = { amount: money, check_name: 'NO_CHECK', desc: '提现', mch_appid: this.appInfo.id, mchid: this.appInfo.store, nonce_str: random(16), openid, partner_trade_no: `cash${moment().format('YYYYMMDDHHmmss')}${random(12)}`, re_user_name: user.name };
  45. const clientIp = _.get(this.ctx.request, 'header.x-real-ip');
  46. if (clientIp) object.spbill_create_ip = clientIp;
  47. const sign = this.turnToSign(object);
  48. const xml = ` <xml>
  49. <mch_appid>${this.appInfo.id}</mch_appid>
  50. <mchid>${this.appInfo.store}</mchid>
  51. <nonce_str>${object.nonce_str}</nonce_str>
  52. <partner_trade_no>${object.partner_trade_no}</partner_trade_no>
  53. <openid>${openid}</openid>
  54. <check_name>NO_CHECK</check_name>
  55. <re_user_name>${user.name}</re_user_name>
  56. <amount>${money}</amount>
  57. <desc>提现</desc>
  58. ${clientIp ? '<spbill_create_ip></spbill_create_ip>' : ''}
  59. <sign>${sign}</sign>
  60. </xml>`;
  61. const url = 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers';
  62. const res = await this.ctx.curl(url, {
  63. method: 'post',
  64. data: xml,
  65. dataType: 'xml',
  66. key: fs.readFileSync('cert/apiclient_key.pem'),
  67. cert: fs.readFileSync('cert/apiclient_cert.pem'),
  68. });
  69. const data = res.data;
  70. const parse = new xml2js.Parser({ trim: true, explicitArray: false, explicitRoot: false });
  71. const result = await parse.parseStringPromise(data);
  72. // TODO 添加错误记录
  73. const { return_code, return_msg, result_code, err_code, err_code_des } = result;
  74. // 请求是否成功
  75. if (return_code !== 'SUCCESS') {
  76. this.cashError.create({ word: `${return_code}:${return_msg}`, create_time: moment().format('YYYY-MM-DD HH:mm:ss') });
  77. throw new BusinessError(ErrorCode.SERVICE_FAULT, `${return_code}:${return_msg}`);
  78. }
  79. // 请求失败具体问题
  80. if (result_code === 'FAIL') {
  81. this.cashError.create({ word: `${err_code}:${err_code_des}`, create_time: moment().format('YYYY-MM-DD HH:mm:ss') });
  82. throw new BusinessError(ErrorCode.SERVICE_FAULT, `${err_code}:${err_code_des}`);
  83. }
  84. // 成功,添加记录=>减去积分
  85. const cashRecord = { name: user.name, mobile: user.mobile, b_point: opoints, i_point: points, e_point: pres };
  86. try {
  87. await this.cashModel.create({ ...cashRecord, create_time: moment().format('YYYY-MM-DD HH:mm:ss') });
  88. } catch (error) {
  89. const word = '添加提现记录失败!但钱已转出.';
  90. this.cashError.create({ word, create_time: moment().format('YYYY-MM-DD HH:mm:ss') });
  91. throw new BusinessError(ErrorCode.SERVICE_FAULT, word);
  92. }
  93. user.points = pres;
  94. try {
  95. await user.save();
  96. } catch (error) {
  97. const word = '用户减掉已转出积分失败!钱已转出;提现记录已添加';
  98. this.cashError.create({ word, create_time: moment().format('YYYY-MM-DD HH:mm:ss') });
  99. throw new BusinessError(ErrorCode.SERVICE_FAULT, word);
  100. }
  101. let newUser = await this.model.findById(id);
  102. newUser = JSON.parse(JSON.stringify(newUser));
  103. const { meta, __v, ...userInfo } = newUser;
  104. return userInfo;
  105. }
  106. turnToSign(object) {
  107. const _arr = [];
  108. for (const key in object) {
  109. _arr.push(key + '=' + object[key]);
  110. }
  111. const str = `${_arr.join('&')}&key=${this.appInfo.storeKey}`;
  112. return crypto.createHash('md5').update(str, 'utf8').digest('hex')
  113. .toUpperCase();
  114. }
  115. NoticeCode(code) {
  116. const arr = [ 'NOTENOUGH', 'SYSTEMERROR', 'NAME_MISMATCH', 'SIGN_ERROR', 'FREQ_LIMIT', 'MONEY_LIMIT', 'CA_ERROR', 'V2_ACCOUNT_SIMPLE_BAN', 'PARAM_IS_NOT_UTF8', 'SENDNUM_LIMIT' ];
  117. return arr;
  118. }
  119. }
  120. module.exports = WxpayService;