# 微信支付 **Repository Path**: wheres-cute/wechat_payment ## Basic Information - **Project Name**: 微信支付 - **Description**: 各个场景下微信支付,h5+小程序+云开发等等。通过这个demo,对微信sdk有了深入的了解。分享,转发,支付都进行了实践,微信小程序支付相对简单微信小程序对一些支付流程进行保护,h5支付相对比较复杂,对明码暗码的加密,加密算法的操作比较繁琐。不过都需要微信公众号的支持 - **Primary Language**: JavaScript - **License**: Not specified - **Default Branch**: master - **Homepage**: None - **GVP Project**: No ## Statistics - **Stars**: 0 - **Forks**: 4 - **Created**: 2020-09-04 - **Last Updated**: 2020-12-19 ## Categories & Tags **Categories**: Uncategorized **Tags**: None ## README # 微信支付 #### 介绍 各个场景下微信支付,h5+小程序+云开发等等 # node egg 微信支付服务 1. 初始化egg应用 ``` js npm init egg--type = simple npm i ``` 2. 配置微信支付用到的插件 plugin.js 必备插件(egg-wechat-pay可以选择别的插件代替) ``` js module.exports = { cors: { enable: true, package: 'egg-cors', }, wechatPay: { enable: true, package: 'egg-wechat-pay' }, }; ``` 3. 配置中间件 因为微信支付返回的数据都是xml格式数据 所以我们的解析xml数据 不然接收不到微信支付回调的请求数据 xmlparse.js (可以使用koa的其它中间件也行) ``` js module.exports = require('co-wechat-body'); ``` 4. 配置config.default.js ``` js //部分主要配置 config.security = { csrf: { enable: false, }, domainWhiteList: ['*'], } //跨域请求 const fs = require('fs') const path = require('path') config.mpWeixin = { appid: '', // 微信公众号或小程序号 AppSecret: '', // 密钥 Host: 'https://api.weixin.qq.com/', MsgToken: '', EncodingAESKey: '' } config.wechatPay = { client: { bodyPrefix: '', // appId: '', //微信公众号或小程序号 merchantId: '', //商户号 secret: '', //商户密钥 notifyUrl: '', //支付成功回调地址 REFUNDNotifyUrl: '', //退款成功回调地址 pfx: fs.readFileSync(path.join(__dirname, '../app/public/wxpaly/apiclient_cert.p12')) //退款证书地址 }, URLS: { UNIFIED_ORDER: 'https://api.mch.weixin.qq.com/pay/unifiedorder', ORDER_QUERY: 'https://api.mch.weixin.qq.com/pay/orderquery', REFUND: 'https://api.mch.weixin.qq.com/secapi/pay/refund', REFUND_QUERY: 'https://api.mch.weixin.qq.com/pay/refundquery', DOWNLOAD_BILL: 'https://api.mch.weixin.qq.com/pay/downloadbill', SHORT_URL: 'https://api.mch.weixin.qq.com/tools/shorturl', CLOSE_ORDER: 'https://api.mch.weixin.qq.com/pay/closeorder', REDPACK_SEND: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/sendredpack', REDPACK_QUERY: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gethbinfo', TRANSFERS: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/promotion/transfers', TRANSFERS_QUERY: 'https://api.mch.weixin.qq.com/mmpaymkttransfers/gettransferinfo', } } ``` 5. 工具函数util.js ``` js const crypto = require('crypto'); const fs = require('fs') const path = require('path') function randomString(len) { // isFinite 判断是否为有限数值 if (!Number.isFinite(len)) { throw new TypeError('Expected a finite number'); } return crypto.randomBytes(Math.ceil(len / 2)).toString('hex').slice(0, len); } function getSign(params, key, type = 'MD5') { //支付签名 const paramsArr = Object.keys(params); paramsArr.sort(); const stringArr = [] paramsArr.map(key => { if (key != 'sign' && params[key]) stringArr.push(key + '=' + params[key]); }) if (type == "MD5") { // 最后加上 商户Key stringArr.push("key=" + key) const string = stringArr.join('&'); const MD5 = crypto.createHash('md5'); return MD5.update(string).digest('hex').toUpperCase(); } else { return crypto.createHmac('sha256', key) .update(stringArr.join('&')) .digest('hex').toUpperCase(); } } function checkSign(params, key) { //验证签名是否正确 const { sign } = params; const Newsign = getSign(params, key) if (sign === Newsign) { return true } else { return false } } ``` 6. 编写Service Payment.js ``` js 'use strict'; const Service = require('egg').Service; const { randomString, ransign, getSign, checkSign, } = require('util.js') const moment = require('moment'); const Decimal = require('decimal.js'); class PaymentService extends Service { /** * H5支付或微信扫一扫支付 * * @param {*} body 费用说明 * @param {*} money 金额 * @returns * @memberof PaymentService */ async pay(PayType, body, money) { const { ctx, service, app } = this; let No = Date.now(); let OrderNo = moment().format('YYYYMMDDHHmm') + randomString(3) + No; let createTime = moment().format('YYYY-MM-DD HH:mm:ss'); let order = { //统一订单参数 device_info: 'WEB', body, out_trade_no: OrderNo, total_fee: new Decimal(money).mul(new Decimal(100)).toFixed(), attach: `${UserId}` }; if (PayType == 'H5') { order.scene_info = JSON.stringify({ h5_info: 'h5_info', type: 'Wap', wap_url: this.config.h5host, wap_name: '峰会报名付款' }) } const unifiedRes = await this.unifiedOrder(order, PayType == 'H5' ? 'MWEB' : 'NATIVE'); return unifiedRes } //支付回调 async payaction(params) { const { ctx, service, app } = this; const { appid, //公众账号ID bank_type, //付款银行 cash_fee, //现金支付金额 device_info, //设备号 fee_type, //货币种类 is_subscribe, //是否关注公众账号 mch_id, //商户号 nonce_str, //随机字符串 openid, //用户标识 out_trade_no, //商户订单号 result_code, //业务结果 return_code, //返回状态码 sign, //签名 time_end, //支付完成时间 total_fee, //订单金额 trade_type, //交易类型 transaction_id, //微信支付订单号 attach } = params; if (return_code == "SUCCESS") { if (checkSign(params, this.config.wechatPay.client.secret)) { //解析回调数据是否正确 //自己处理 } else { return { msg: '签名错误', status: 'error', } } } else { return { msg: '支付失败', status: 'error', } } } //退款 async refund(out_trade_no, refund_fee, refund_desc, PayPrice) { const { ctx, service, app } = this; let nonce_str = randomString(32); let out_refund_no = moment().format('YYYYMMDDHHmmss') + randomString(25); let data = { appid: this.config.wechatPay.client.appId, mch_id: this.config.wechatPay.client.merchantId, nonce_str, //随机字符串 out_trade_no, //订单号 out_refund_no, //退款订单号 total_fee: new Decimal(PayPrice).mul(new Decimal(100)).toFixed(), //总金额 refund_fee: new Decimal(refund_fee).mul(new Decimal(100)).toFixed(), //退款金额 refund_desc, //退款原因描述 notify_url: this.config.wechatPay.client.REFUNDNotifyUrl //退款成功回调地址 } let sign = getSign(data, this.config.wechatPay.client.secret); //签名 let xml = { ...data, sign }; try { const curlres = await ctx.curl( `${this.config.wechatPay.URLS.REFUND}` , { method: 'POST', pfx: this.config.wechatPay.client.pfx, //退款证书 passphrase: this.config.wechatPay.client.merchantId, // 直接发送原始 xml 数据,不需要 HttpClient 做特殊处理 content: this.app.wechatPay.stringify(xml), }); let rdata = await this.app.wechatPay.parse(curlres.data); //解析xml数据 return { res: rdata } } catch (error) { console.log(error, "error"); if (error == "invalid refund_fee") { return { msg: '退款金额不正确', status: 'error', } } else { return { msg: '退款失败', status: 'error', } } } } //微信内部浏览器或小程序 统一创建支付 async CreatePaymentInfo(money, openid) { const { ctx, service, app } = this; let No = Date.now(); let OrderNo = moment().format('YYYYMMDDHHmmss') + randomString(3) + No; const order = { //统一订单参数 device_info: 'WEB', body: "", 费用说明 out_trade_no: OrderNo, //订单编号 total_fee: new Decimal(money).mul(new Decimal(100)).toFixed(), //支付金额 分为单位 openid, //用户openid }; const res = await this.unifiedOrder(order); //请求生成订单信息 return { res } } async unifiedOrder(order, trade_type = 'JSAPI') { const { ctx, service } = this; let nonce_str = randomString(32); //随机字符串 let Data = { appid: this.config.wechatPay.client.appId, mch_id: this.config.wechatPay.client.merchantId, //商户号 nonce_str, sign_type: 'MD5', //签名方式 spbill_create_ip: ctx.ip, //用户ip trade_type, //支付类型 notify_url: this.config.wechatPay.client.notifyUrl, //支付成功回调地址 time_expire: moment().add(1, 'h').format('YYYYMMDDHHmmss'), ...order } let sign = getSign(Data, this.config.wechatPay.client.secret); //签名 let xml = { ...Data, sign }; const cur = await ctx.curl( `${this.config.wechatPay.URLS.UNIFIED_ORDER}` , { method: 'POST', // 直接发送原始 xml 数据,不需要 HttpClient 做特殊处理 content: this.app.wechatPay.stringify(xml), }); const parseData = await this.app.wechatPay.parse(cur.data); let res = { appId: Data.appid, nonceStr: nonce_str, package: `prepay_id=${parseData.prepay_id}` , timeStamp: moment().add(1, 'h').unix().toString(), signType: 'MD5', } if (trade_type == 'MWEB') { //手机浏览器H5支付 res.mweb_url = parseData.mweb_url } if (trade_type == 'NATIVE') { //可用于微信扫一扫支付 res.code_url = parseData.code_url } return { ...res, paySign: getSign(res, this.config.wechatPay.client.secret) } } } module.exports = PaymentService; ```