概述
-
准备
- 个体工商户、企业、政府及事业单位
- 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">×</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>