这几天公司微信端项目结束,把微信公众号支付做个整理方便有同样功能需求的同学。
先放上一份官方文档
https://pay.weixin.qq.com/wiki/doc/api/jsapi_sl.php?chapter=7_1,咱们既然要做支付肯定要先了解人家的规范,多读几遍这个文档能少走不少弯路。
微信这边的各种平台纷繁复杂,像:微信公众平台、微信开放平台、微信商户平台等,建议大家先把这些关系搞清楚,以便于后期开发的时候去对应各种key、秘钥、appId之类的。要是他们之间的概念含混不清保证让你在对应的时候懵逼。
以下名词解释摘自微信官方文档
https://pay.weixin.qq.com/wiki/doc/api/jsapi_sl.php?chapter=2_2
1、
微信公众平台
微信公众平台是微信公众账号申请入口和管理后台。商户可以在公众平台提交基本资料、业务资料、财务资料申请开通微信支付功能。
平台入口:http://mp.weixin.qq.com。
2、
微信开放平台
微信开放平台是商户APP接入微信支付开放接口的申请入口,通过此平台可申请微信APP支付。
平台入口:http://open.weixin.qq.com。
3、
微信商户平台
微信商户平台是微信支付相关的商户功能集合,包括参数配置、支付数据查询与统计、在线退款、代金券或立减优惠运营等功能。
平台入口:http://pay.weixin.qq.com。
4、
微信企业号
微信企业号是企业号的申请入口和管理后台,商户可以在企业号提交基本资料、业务资料、财务资料申请开通微信支付功能。
企业号入口:http://qy.weixin.qq.com。
5、
微信支付系统
微信支付系统是指完成微信支付流程中涉及的API接口、后台业务处理系统、账务系统、回调通知等系统的总称。
6、
商户收银系统
商户收银系统即商户的POS收银系统,是录入商品信息、生成订单、客户支付、打印小票等功能的系统。接入微信支付功能主要涉及到POS软件系统的开发和测试,所以在下文中提到的商户收银系统特指POS收银软件系统。
7、
商户后台系统
商户后台系统是商户后台处理业务系统的总称,例如:商户网站、收银系统、进销存系统、发货系统、客服系统等。
8、
扫码设备
一种输入设备,主要用于商户系统快速读取媒介上的图形编码信息。按读取码的类型不同,可分为条码扫码设备和二维码扫码设备。按读取物理原理可分为红外扫码设备、激光扫码设备。
9、
商户证书
商户证书是微信提供的二进制文件,商户系统发起与微信支付后台服务器通信请求的时候,作为微信支付后台识别商户真实身份的凭据。
10、
签名
商户后台和微信支付后台根据相同的密钥和算法生成一个结果,用于校验双方身份合法性。签名的算法由微信支付制定并公开,常用的签名方式有:MD5、SHA1、SHA256、HMAC等。
11、
JSAPI网页支付
JSAPI网页支付即前文说的公众号支付,可在微信公众号、朋友圈、聊天会话中点击页面链接,或者用微信“扫一扫”扫描页面地址二维码在微信中打开商户HTML5页面,在页面内下单完成支付。
12、
Native原生支付
Native原生支付即前文说的扫码支付,商户根据微信支付协议格式生成的二维码,用户通过微信“扫一扫”扫描二维码后即进入付款确认界面,输入密码即完成支付。
13、
支付密码
支付密码是用户开通微信支付时单独设置的密码,用于确认支付完成交易授权。该密码与微信登录密码不同。
14、
Openid
用户在公众号内的身份标识,不同公众号拥有不同的openid。商户后台系统通过登录授权、支付通知、查询订单等API可获取到用户的openid。主要用途是判断同一个用户,对用户发送客服消息、模版消息等。企业号用户需要使用企业号userid转openid接口将企业成员的userid转换成openid。
这里简单的做了一个图,无论是微信公众平台还是微信开放平台,涉及到支付的时候都在微信商户平台这个后台中去处理,前两者分别对应了一套商户平台的账号信息。尤其是这些账号信息分属不同部门管理时,在开发的时候给开发者带来不小的影响。
这是在支付中用到的几个重要配置参数,是申请的时候微信给发的邮件。
其实微信公众号支付与微信APP支付在后台组装参数上基本没啥区别。多了一个openid,支付方式为JSAPI。
这里的逻辑还是:
1、从前台获取到用户提交的订单参数,生成第一次签名。
2、调用微信统一下单接口URL https://api.mch.weixin.qq.com/pay/unifiedorder 生成预支付id:
prepayId。
3、对参数进行二次签名。
4、将所需的数据传给前台,调起微信支付。
5、微信将将支付通知给后台。
6、后台执行回调操作,完成整个支付流程。
后台代码是在原有基础上稍加修改的
/**
* 请求微信支付系统的接口,商户后台系统调用统一下单接口在微信支付服务后台生成预支付交易单
* @throws Exception
*/
@RequestMapping("/doWeiXinRequest")
@ApiOperation(value="微信支付生成订单号,App端再次需要请求的接口",httpMethod="POST",response=JsonUtil.class,notes="生成微信支付中需要的预支付id参数---pay/doWeiXinRequest")
public void doWeiXinRequest(@ApiParam(required=true,name="outTradeNo",value="提交订单返回的订单号")@RequestParam String outTradeNo) throws Exception{
//客户端需要传递的参数待定,先把下单接口的参数构造好
String body = "支付采购单";
String userId = (String) session.getAttribute("sysUserId");//确认是购买的用户进行付款操作
String openid = (String) session.getAttribute("openid");
System.out.println(openid+"~~~~~~~~~~~~~~~~~~");
try {
if(outTradeNo != null && !"".equals(outTradeNo)
&& userId != null && !"".equals(userId)
&& openid != null && !"".equals(openid)){
OrderCommCai orderCommCai = orderCommCaiService.get(outTradeNo);//根据综合采购单id来查找综合采购单
String totalFee = orderCommCai.getOrderZongCommPrices().multiply(new BigDecimal(100)).toString();
totalFee = totalFee.substring(0,totalFee.indexOf("."));
if(orderCommCai != null && !"".equals(orderCommCai)){
if("0".equals(orderCommCai.getOrderType())){//未支付
String currTime = TenpayUtil.getCurrTime();
//八位日期相关的字符串
String strTime = currTime.substring(8, currTime.length());
//四位随机数的字符串
String strRandom = TenpayUtil.buildRandom(4) + "";
//十位随机字符串
String nonceStr = strTime + strRandom;
/**
* 商品描述,订单号,订单总金额从手机端获取
*/
String spbillCreateIp = request.getRemoteAddr();
SortedMap requestParams = new TreeMap();
requestParams.put("appid", Constant.APP_ID);//公众账号id
requestParams.put("mch_id", Constant.PARTNER); //商户号
requestParams.put("nonce_str", nonceStr);//随机字符串
requestParams.put("body", body);//商品描述
requestParams.put("out_trade_no", outTradeNo);//订单号
requestParams.put("total_fee", String.valueOf(totalFee));//订单总金额
requestParams.put("spbill_create_ip", spbillCreateIp);//用户终端ip
requestParams.put("notify_url", Constant.NOTIFIY_URL);//通知地址
requestParams.put("openid", openid);//用户标识
requestParams.put("trade_type", "JSAPI");
RequestHandler reqHandler = new RequestHandler(request, response);
reqHandler.init(Constant.APP_ID, Constant.APP_SECRET, Constant.PARTNER_KEY);
// String requestSign = reqHandler.createSign(requestParams);//签名
String requestSign = WXPayUtil.generateSignature(requestParams, Constant.PARTNER_KEY,SignType.MD5);
System.out.println(requestParams);
//构造请求参数
String xml = ""
+ "" + Constant.APP_ID + ""
+ "" + body + ""
+ "" + Constant.PARTNER + ""
+ "" + nonceStr + ""
+ "" + Constant.NOTIFIY_URL + ""
+ "" + outTradeNo + ""
+ "" + requestSign + ""
+ "" + spbillCreateIp + ""
+ "" + totalFee + ""
+ "JSAPI "
+ "" + openid + ""
+ "";
System.out.println(xml);
new GetWxOrderNo();
String prepayId = GetWxOrderNo.getPayNo(Constant.PRE_URL, xml);//获得预支付单信息
boolean flag = WXPayUtil.isSignatureValid(xml, Constant.PARTNER_KEY);
/**
* 统一下单接口返回正常的prepay_id,再按签名规范重新生成签名后,将数据传输给APP。
* 参与签名的字段名为appId,partnerId,prepayId,nonceStr,timeStamp,package。
* 注意:package的值格式为Sign=WXPay
*/
SortedMap getParams = new TreeMap();
String timeStamp = Sha1Util.getTimeStamp();
String packages = "prepay_id="+prepayId;
getParams.put("appId", Constant.APP_ID);
getParams.put("timeStamp", timeStamp);
getParams.put("nonceStr", nonceStr);
getParams.put("package", packages);
getParams.put("signType", "MD5");
String getSign = reqHandler.createSign(getParams);
System.out.println("--微信支付 ---"+getSign);
map1.put("msg", "成功");//当统一下订单接口执行完成之后,返回调起支付接口所需要的所有参数给微信服务号前端
map2.put("appId", Constant.APP_ID);
map2.put("nonceStr", nonceStr);
map2.put("packages", "prepay_id="+prepayId);
map2.put("partnerId", Constant.PARTNER);
map2.put("timeStamp", timeStamp);
map2.put("sign", getSign);
}else{
map1.put("msg", "订单已支付");
}
}else{
map1.put("msg", "订单不存在");
}
}else{
map1.put("msg", "参数不全");
}
} catch (BusinessException e) {
e.printStackTrace();
map1.put("msg", "发生后台异常");
}
map.put("data", map2);
map.put("result", map1);
JsonUtil.writeJson(response, JsonUtil.objectToJson(map));
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @param signType 签名方式
* @return 签名
*/
public static String generateSignature(final Map data, String key, SignType signType) throws Exception {
Set keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals(WXPayConstants.FIELD_SIGN)) {
continue;
}
if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
sb.append("key=").append(key);
if (SignType.MD5.equals(signType)) {
System.out.println("-------MD5签名sign----------"+MD5(sb.toString()).toUpperCase());
return MD5(sb.toString()).toUpperCase();
}
else if (SignType.HMACSHA256.equals(signType)) {
return HMACSHA256(sb.toString(), key);
}
else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
public static String getPayNo(String url,String xmlParam){
DefaultHttpClient client = new DefaultHttpClient();
client.getParams().setParameter(ClientPNames.ALLOW_CIRCULAR_REDIRECTS, true);
HttpPost httpost= HttpClientConnectionManager.getPostMethod(url);
String prepay_id = "";
try {
httpost.setEntity(new StringEntity(xmlParam, "UTF-8"));
HttpResponse response = httpclient.execute(httpost);
String jsonStr = EntityUtils.toString(response.getEntity(), "UTF-8");
Map<String, Object> dataMap = new HashMap<String, Object>();
System.out.println(jsonStr);
if(jsonStr.indexOf("FAIL")!=-1){
return prepay_id;
}
Map map = doXMLParse(jsonStr);
String return_code = (String) map.get("return_code");
prepay_id = (String) map.get("prepay_id");
} catch (Exception e) {
e.printStackTrace();
}
return prepay_id;
}
/**
* 创建md5摘要,规则是:按参数名称a-z排序,遇到空值的参数不参加签名。
*/
public String createSign(SortedMap packageParams) {
StringBuffer sb = new StringBuffer();
Set es = packageParams.entrySet();
Iterator it = es.iterator();
while (it.hasNext()) {
Map.Entry entry = (Map.Entry) it.next();
String k = (String) entry.getKey();
String v = (String) entry.getValue();
if (null != v && !"".equals(v) && !"sign".equals(k)
&& !"key".equals(k)) {
sb.append(k + "=" + v + "&");
}
}
sb.append("key=" + this.getKey());
System.out.println("md5 sb:" + sb);
String sign = MD5Util.MD5Encode(sb.toString(), this.charset)
.toUpperCase();
System.out.println("packge签名:" + sign);
return sign;
}
1、下面这张图改动处是微信公众号支付的参数,openid是预先获取到的,交易类型是JSAPI。
2、所有参数成功获取以后,把参数传给前台,这里的packages跟微信APP支付略有不同,需要注意下。
3、第二次签名参数大小写问题需要注意(第二次签名失败的话,这里需要着重考虑。)
在微信APP支付方式中,第二次签名参数全为小写,而在微信公众号支付这里依旧是区分大小写的
//点击微信支付
function payByWeChat(outTradeNo){
$.ajax({
data:{outTradeNo:outTradeNo},
dataType:"json",
type:"post",
async: false,
url:"${ctxPath}/payWeiXin/doWeiXinRequest",
success:function(msg){
if(msg.result.msg == "成功"){
var content = msg.data;
var appId = content.appId;
var timeStamp = content.timeStamp;
var nonceStr = content.nonceStr;
var packages = content.packages;
var paySign = content.sign;
pay(appId,timeStamp,nonceStr,packages,paySign);
}
},
error:function(msg){
}
});
}
//h5唤起微信支付
function onBridgeReady(appId,timeStamp,nonceStr,packages,paySign){
WeixinJSBridge.invoke(
'getBrandWCPayRequest', {
"appId":appId, //公众号名称,由商户传入
"timeStamp":timeStamp, //时间戳,自1970年以来的秒数
"nonceStr":nonceStr, //随机串
"package":packages,
"signType":"MD5", //微信签名方式:
"paySign":paySign //微信签名
},
function(res){
console.log(res.err_code + res.err_desc);
console.log(res.err_msg);
alert(res.err_code + res.err_desc);
alert(res.err_msg);
if(res.err_msg == "get_brand_wcpay_request:ok" ) {
alert("支持成功");// 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回 ok,但并不保证它绝对可靠。
window.location.href = "${ctxPath}/jump/toOrderCommCai";
}else if (res.err_msg === 'get_brand_wcpay_request:cancel') {
alert("取消支付");
}
}
);
}
function pay(appId,timeStamp,nonceStr,packages,paySign){
if (typeof WeixinJSBridge == "undefined"){
if( document.addEventListener ){
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
}else if (document.attachEvent){
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}else{
onBridgeReady(appId,timeStamp,nonceStr,packages,paySign);
}
}
/**
* 获取微信支付回调的接口
*/
@RequestMapping("/wechatPayCallBack")
@ApiOperation(value="微信支付回调的接口",httpMethod="POST",response=JsonUtil.class,notes="微信支付用于回调的接口---pay/wechatPayCallBack")
public void wechatPayCallBack() throws Exception{
//后台服务器和微信服务器之间的交互,流操作
InputStream is = request.getInputStream();
ByteArrayOutputStream baos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = is.read(buffer)) != -1) {
baos.write(buffer, 0, len);
}
System.out.println("~~~~~~~~~~~~~~~~微信回调付款成功~~~~~~~~~");
baos.close();
is.close();
String result = new String(baos.toByteArray(),"utf-8");//获取微信调用notify_url的返回信息
Map
public static String setXML(String return_code, String return_msg) {
return "<xml><return_code>return_code><return_msg>return_msg>xml>";
}
重要事情说三遍:
一定要多看文档!一定要多看文档!一定要多看文档!