Eggjs笔记:接入微信支付相关流程与演示

概述

  • 准备

    • 个体工商户、企业、政府及事业单位
    • https://pay.weixin.qq.com/static/applyment_guide/applyment_detail_website.shtml
    • 需要获取内容:
      • appid:应用 APPID(必须配置,开户邮件中可查看)
      • MCHID:微信支付商户号(必须配置,开户邮件中可查看)
      • KEY:API 密钥,参考开户邮件设置(必须配置,登录商户平台自行设置)32位的密钥
  • 注册商户平台,申请微信支付需 1~5个工作日审核

    • 注册微信商户平台
    • 信息必须如实填写,以及销售商品的分类选择要和自己公司的匹配,不然容易审核失败。审核失败后,看看失败原因、修改重新提交申请。
    • 开户成功,登录商户平台进行验证
    • 资料审核通过后,商户信息会发到您的账户邮箱里面,请登录联系人邮箱查收商户号和密码,并登录商户平台填写财付通备付金打的小额资金数额,完成账户验证
    • https://kf.qq.com/faq/161220mQjmYj161220n6jYN7.html
    • 登录商户平台点击产品中心开通 Native 支付
    • 用微信给你发的商户号登陆对应的微信商户平台,获取API 密钥
      • 步骤:微信商户平台(pay.weixin.qq.com)–>账户设置–>API 安全–>密钥设置。
      • 设置地址:https://pay.weixin.qq.com/index.php/account/api_cert
      • 设置的时候可能会提示你安装证书
    • PC端用到的方式是Native支付,申请后,要进入商户平台-产品中心 查看Native是否开通,没有开通需要开通
    • APIkey设置在:商户平台-账户中心-API安全-设置密钥
  • 微信 pc 端网站支付流程

    • https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
    • 调用统一下单接口生成预支付交易信息,获取 code_url(二维码链接地址)
    • 用 code_url 生成二维码
    • 支付成功后监听服务器的异步通知,然后处理订单
  • 调用写好的模块生成 code_url

    • 安装模块 $ cnpm install request crypto xml2js --save
    • 引入写好的 wechatPay.js $ var wechatPay = require('./module/wechatPay');
    • 调用统一下单接口获取 code_url
    • 把 code_url 转化成二维码
    • 处理异步通知
  • 注意

    • 域名必须备案,不能是ip
    • 产品中心->Native->产品设置->扫码支付 扫码回调链接必须配置, 这个链接就是我们config中的wechat相关的notify_url,微信给我们post数据的链接

代码流程

eggjs中的config配置

支付配置如下:

// 支付相关配置:微信,支付宝
config.pay = {
    wechat: {
        config: {
            mch_id: '', // 商户id
            wxappid: '', // 微信appid
            wxpaykey: '' // 微信paykey
        },
        basicParams: {
            notify_url: 'http://127.0.0.1:7001/pay/wechat/notify' //注意回调地址必须在 微信商户平台配置 不能用127,用配置的域名
        }
    },
    // .... 当然这里还可以有其他支付,如支付宝等
}

关闭csrf验证如下:(注意如果不关闭,那么微信没法向我们的服务器post数据)

config.security = {
    csrf: {
        // 判断是否需要 ignore 的方法,请求上下文 context 作为第一个参数
        ignore: ctx => {
            // 屏蔽csrf验证的接口或路由
            let arr = [
                // ... 其他url
                '/pay/wechat/notify',
            ];
            let flag = false;
            // 进行匹配
            arr.some((item) => {
                if (ctx.request.url === item) {
                    // console.log(item);
                    flag = true;
                    return true;
                }
            });
            return flag;
        },
    },
};

eggjs中的路由配置

// 路由片段示例
router.get('/pay/wechat', controller.web.pay.wechat); // 微信支付
router.post('/pay/wechat/notify', controller.web.pay.wechatNotify); // 异步通知  注意关闭csrf验证

封装我们自己的微信支付相关的库 app/lib/wechatPay.js

/*
 * @Descrition : wechat 微信支付功能
 */

const url = require('url');
const queryString = require('querystring');
const crypto = require('crypto');
const request = require('request');
const xml2jsparseString = require('xml2js').parseString;

// wechat 支付类 (使用 es6 的语法)
class WechatPay {
    /*
     构造函数  
    */
    constructor(config) {
        this.config = config;
    }

    /**
     * 获取微信统一下单参数
     */
    getUnifiedorderXmlParams(obj) {
        let body = '<xml> ' +
            '<appid>' + this.config.wxappid + '</appid> ' +
            '<attach>' + obj.attach + '</attach> ' +
            '<body>' + obj.body + '</body> ' +
            '<mch_id>' + this.config.mch_id + '</mch_id> ' +
            '<nonce_str>' + obj.nonce_str + '</nonce_str> ' +
            '<notify_url>' + obj.notify_url + '</notify_url>' +
            '<openid>' + obj.openid + '</openid> ' +
            '<out_trade_no>' + obj.out_trade_no + '</out_trade_no>' +
            '<spbill_create_ip>' + obj.spbill_create_ip + '</spbill_create_ip> ' +
            '<total_fee>' + obj.total_fee + '</total_fee> ' +
            '<trade_type>' + obj.trade_type + '</trade_type> ' +
            '<sign>' + obj.sign + '</sign> ' +
            '</xml>';
        return body;
    }

    /**
     * 获取微信统一下单的接口数据
     */
    getPrepayId(obj) {
        let that = this;
        // 生成统一下单接口参数
        let UnifiedorderParams = {
            appid: this.config.wxappid,
            attach: obj.attach,
            body: obj.body,
            mch_id: this.config.mch_id,
            nonce_str: this.createNonceStr(),
            notify_url: obj.notify_url, // 微信付款后的回调地址
            openid: obj.openid, //改
            out_trade_no: obj.out_trade_no, //new Date().getTime(), //订单号
            spbill_create_ip: obj.spbill_create_ip,
            total_fee: obj.total_fee,
            // trade_type : 'JSAPI',
            trade_type: 'NATIVE',
            // sign : getSign(),
        };
        // 返回 promise 对象
        return new Promise(function(resolve, reject) {
            // 获取 sign 参数
            UnifiedorderParams.sign = that.getSign(UnifiedorderParams);
            let url = 'https://api.mch.weixin.qq.com/pay/unifiedorder';
            request.post({ url: url, body: JSON.stringify(that.getUnifiedorderXmlParams(UnifiedorderParams)) }, function(error, response, body) {
                // let prepay_id = '';
                if (!error && response.statusCode == 200) {
                    // 微信返回的数据为 xml 格式, 需要装换为 json 数据, 便于使用
                    xml2jsparseString(body, { async: true }, function(error, result) {
                        if (error) {
                            console.log(error);
                            reject(error);
                        } else {
                            // prepay_id = result.xml.prepay_id[0];   //小程序支付返回这个
                            console.log(result);
                            let code_url = result.xml.code_url[0];
                            resolve(code_url);
                        }
                    });
                } else {
                    console.log(body);
                    reject(body);
                }
            });
        })
    }

    /**
     * 获取微信支付的签名
     * @param payParams
     */
    getSign(signParams) {
        // 按 key 值的ascll 排序
        let keys = Object.keys(signParams);
        keys = keys.sort();
        let newArgs = {};
        keys.forEach(function(val, key) {
            if (signParams[val]) {
                newArgs[val] = signParams[val];
            }
        });
        let string = queryString.stringify(newArgs) + '&key=' + this.config.wxpaykey;
        // 生成签名
        return crypto.createHash('md5').update(queryString.unescape(string), 'utf8').digest("hex").toUpperCase();
    }

    /**
     * 微信支付的所有参数
     * @param req 请求的资源, 获取必要的数据
     * @returns {{appId: string, timeStamp: Number, nonceStr: *, package: string, signType: string, paySign: *}}
     */
    getBrandWCPayParams(obj, callback) {
        let that = this;
        let prepay_id_promise = that.getPrepayId(obj);
        prepay_id_promise.then((prepay_id) => {
            let wcPayParams = {
                "appId": this.config.wxappid, //公众号名称,由商户传入
                "timeStamp": parseInt(new Date().getTime() / 1000).toString(), //时间戳,自1970年以来的秒数
                "nonceStr": that.createNonceStr(), //随机串                 
                // 通过统一下单接口获取
                // "package" : "prepay_id="+prepay_id,   //小程序支付用这个
                "code_url": prepay_id,
                "signType": "MD5", //微信签名方式:
            };
            wcPayParams.paySign = that.getSign(wcPayParams); //微信支付签名
            callback(null, wcPayParams);
        }, function(error) {
            callback(error);
        });
    }

    /**
     * 获取随机的NonceStr
     */
    createNonceStr() {
        return Math.random().toString(36).substr(2, 15);
    };

    //获取微信的 AccessToken   openid
    getAccessToken(code, cb) {
        let that = this;
        let getAccessTokenUrl = "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + this.config.wxappid + "&secret=" + this.config.wxappsecret + "&code=" + code + "&grant_type=authorization_code";
        request.post({ url: getAccessTokenUrl }, function(error, response, body) {
            if (!error && response.statusCode == 200) {
                if (40029 == body.errcode) {
                    cb(error, body);
                } else {
                    body = JSON.parse(body);

                    cb(null, body);
                }
            } else {
                cb(error);
            }
        });
    }

    /**
     * 创建订单
     */
    createOrder(obj, cb) {
        this.getBrandWCPayParams(obj, function(error, responseData) {
            if (error) {
                cb(error);
            } else {
                cb(null, responseData);
            }
        });
    }
}

module.exports = WechatPay;

编写service服务, 用于controller调用 app/service/pay.js

'use strict';

const Service = require('egg').Service;
const wechatPay = require('../lib/wechatPay.js'); // 引入自己封装的微信支付库

/**
 * 用于微信和支付宝相关的支付服务功能, 此处只展示支付宝代码
 */
class PayService extends Service {
    /* ************************ 微信支付相关服务 ************************ */
    async wechat(orderData) {
        return new Promise((resolve) => {
            let pay = new wechatPay(this.config.pay.wechat.config);
            let notify_url = this.config.pay.wechat.basicParams.notify_url;
            let out_trade_no = orderData.out_trade_no;
            let title = orderData.title;
            let price = orderData.price * 100; // 单位为分
            let ip = this.ctx.request.ip.replace(/::ffff:/, '');

            pay.createOrder({
                openid: '',
                notify_url: notify_url, //微信支付完成后的回调
                out_trade_no: out_trade_no, //订单号
                attach: title,
                body: title,
                total_fee: price.toString(), // 此处的额度为分
                spbill_create_ip: ip
            }, function(error, responseData) {
                console.log(responseData);
                if (error) {
                    console.log(error);
                }
                resolve(responseData.code_url)
            });
        })
    }

    /*params微信官方post给我们服务器的数据*/
    wechatNotify(params) {
        let pay = new wechatPay(this.config.pay.wechat.config);
        let notifyObj = params;
        let signObj = {};
        for (let attr in notifyObj) {
            // 去除微信post的sign字段
            if (attr != 'sign') {
                signObj[attr] = notifyObj[attr]
            }
        }
        let sign = pay.getSign(signObj);
        return sign;
    }
}

module.exports = PayService;

特别说明 关于微信支付中解析微信post给我们的xml文件

之前支付宝用到的koa-xml-body中间件不适用,我们直接使用原生的nodejs和xml2js来处理

关于微信支付url变二维码的相关服务 service/tools.js

关键代码

// 生成二维码 注意:const qr = require('qr-image');
qrImage(url) {
    let qrimg = qr.image(url, { type: 'png' });
    return qrimg;
}

编写支付相关的controller控制器 app/controller/web/pay.js

'use strict';

const Controller = require('egg').Controller;
const xml2js = require('xml2js').parseString;

class PayController extends Controller {
    /* ************************ 微信相关 ************************ */
    async wechat() {
        // 通过订单号查询订单
        let id = this.ctx.request.query.id;
        let orderResult = await this.ctx.model.Order.find({ "_id": id });
        if (!(orderResult && orderResult.length)) {
            return this.ctx.redirect('/order/confirm?id=' + id); // 定位到当前页面, 或返回错误信息
        }
        const data = {
            title: '微信支付', // 这里订单没有存标题,这里标题显示什么,是否需要查询所有订单商品order_item, 来拼凑product_title, 看需求,目前这样处理
            out_trade_no: id,
            price: orderResult[0].all_price
        }
        let code_url = await this.service.pay.wechat(data);
        // 调用方法生成二维码
        let qrImage = this.service.tools.qrImage(code_url);
        this.ctx.type = 'image/png';
        this.ctx.body = qrImage;
    }

    // 接收异步通知函数, koa-xml-body无法处理微信post的xml数据,用原生nodejs来接收,用xml2js来解析
    async wechatNotify() {
        let data = '';
        this.ctx.req.on('data', (chunk) => {
            data += chunk;
        });

        this.ctx.req.on('end', () => {
            xml2js(data, { explicitArray: false }, (err, json) => {
                console.log(json); //这里的json便是xml转为json的内容
                let mySign = this.service.pay.wechatNotify(json.xml);
                console.log(mySign);
                // console.log('-------------');
                // console.log(json.xml.sign);
                // 对比两者,如果签名一致,mySign === json.xml.sign 则进入后续操作 TODO 更新订单 等
            });
        });
    }
}

module.exports = PayController;

前端页面相关处理

也就是调用控制器中的wechat这个方法的页面,这里是确认订单页面,也就是点击使用微信支付页面,关键代码,仅作示例展示

<div class="section section-payment">
    <div class="cash-title" id="J_cashTitle">
        选择以下支付方式付款
    </div>

    <div class="payment-box ">
        <div class="payment-body">
            <ul class="clearfix payment-list J_paymentList J_linksign-customize">
                <li id="weixinPay"><img src="/public/web/image/weixinpay0701.png" alt="微信支付" style="margin-left: 0;"></li>
            </ul>
        </div>
    </div>
</div>

<div class="modal fade" id="weixinPayModel" tabindex="-1" role="dialog">
    <div class="modal-dialog" role="document">
        <div class="modal-content">
            <div class="modal-header">
                <button type="button" class="close" data-dismiss="modal" aria-label="Close"><span aria-hidden="true">&times;</span></button>
                <h4 class="modal-title" id="myModalLabel">微信支付</h4>
            </div>
            <div class="modal-body">
                <!-- 这里的id 传递的是订单号 这里是生成的二维码图片 -->
                <img class="lcode" src="/pay/wechat?id=<%=id%>" />
                <!-- 这是一张手机扫一扫图片,用于引导用户扫码 -->
                <img class="rphone" src="/public/web/image/phone.png" />
            </div>
        </div>
    </div>
</div>

<script>
// 这里使用jQuery,当前前提要引入jQuery
$("#weixinPay").click(function() {
    $('#weixinPayModel').modal('show');
});

// 这里有个轮询订单状态,支付成功后,跳转到用户订单页面,这里接口和路由都需要编写和配置,不做展示
var timer = setInterval(function() {
    $.get('/order/getOrderPayStatus?id=<%=id%>', function(response) {
        // console.log(response);
        if (response.success) {
            clearInterval(timer);
            location.href = '/user/order'
        }
    });
}, 5000);
</script>

猜你喜欢

转载自blog.csdn.net/Tyro_java/article/details/106531483