java开发微信JSAPI:Springboot+Angular
去年开始做的一个微信挂号支付项目,涉及到微信支付接口调用,由于之前没有接触过微信接口导致实际做的时候陷入了巨大的坑中,自己几度陷入抓狂的状态接口调用出错、签名出错、字符集......导致本来可以一周完成的工作拖了十几天,这其中的苦不必多说。
不过我也想吐槽一下微信的接口做的真的烂!
好了不多说,开始
首先我们开发之前,需要准备以下必要的参数
- appid ,微信公众号appid;
- mch_id ,商户号;
- appsecret ,应用密钥;
- openid ,用户微信号与微信公众号交互,该微信公众号下微信的唯一标示符;
- 商户Key 商户支付密匙;
当然这其中就是openid需要我们自行获取,其他参数都是配置参数,(由于我们项目的微信公众号是客户的,所以以上这些数据没有实际配置过,不过可以参考官方文档)
https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_3
我们先来获取openid
1:用户同意授权,获取code
关于网页授权的两种scope的区别说明
1.1、以snsapi_base为scope发起的网页授权,是用来获取进入页面的用户的openid的,并且是静默授权并自动跳转到回调页的。用户感知的就是直接进入了回调页(不需要用户授权可以直接获取,但是不能用来获取用户信息)。
1.2、以snsapi_userinfo为scope发起的网页授权,是用来获取用户的基本信息的。但这种授权需要用户手动同意,并且由于用户同意过,所以无须关注,就可在授权后获取该用户的基本信息(会弹出授权框需要用户授权)。
1.3调用接口https://open.weixin.qq.com/connect/oauth2/authorize?appid=APPID&redirect_uri=url&response_type=code&scope=上面介绍的两个参数&state=自定义参数#wechat_redirect
#wechat_redirect 是无论直接打开还是做页面302重定向时候,必须带此参数
如果没有问题页面将跳转至 xxxx/?code=CODE&state=STATE
获取openid
String url = "https://api.weixin.qq.com/sns/oauth2/access_token?appid="+appid+"&secret="+secret+"&code="+code+"&grant_type=authorization_code";
ResponseEntity<String> responseEntity = restTemplate.getForEntity(url,String.class);
String str = responseEntity.getBody();
JSONObject jsonObject = JSONObject.parseObject(str);
// 获取openid
String opid = jsonObject.getString("openid");
到此为止我们的参数基本已经搞定
现在准备统一下单(是不是很激动!!)
我们先来看一下微信官方文档中统一下单需要那些必要参数把
说一下这些参数中需要注意的地方和(坑)
1:商户订单号 out_trade_no 不能重复,重新发起一笔支付要使用原订单号(这个其实不用多说,反正需要注意一下)。
下面说两个 特别特别特别 需要注意的地方
大家应该看明白这个回答的意思了(两次签名必须使用同一个随机字符串)!!!
这是我当时报错在微信开放社区看到的官方回答,这在微信支付的文档中从来没有说起过(坑).
还有一个
看官方文档中这个签名类型参数不是必须的,但是我后来从网上查了一下,这个好像是在什么沙箱模式下才是默认MD5签名方式,不是全部默认的。
所以如果你不加可能会出问题,所以建议大家在统一下单的时候加上这个参数。
这里是统一下单代码
Map<String,String> map = new HashMap<>();
String randomstr = WXPayUtil.generateNonceStr(); // 随机字符串
map.put("appid",appid);// appid
map.put("mch_id",mch_id);// 商户号
map.put("sign_type","MD5"); // 加密方式
map.put("nonce_str",randomstr);// 随机字符串
map.put("body","body");// 商品名称
map.put("notify_url","xxxx"); // 回调地址
map.put("out_trade_no",GetRandom.getRandomStringByLength(32));// 订单号
map.put("spbill_create_ip","spbill_create_ip");// 终端ip(调用微信支付API的机器IP)
map.put("total_fee","1");// 订单金额 现在默认写死,money
map.put("trade_type","JSAPI"); // 交易类型交易类型
map.put("openid",openid);// openid
//生成签名 将集合M内非空参数值的参数按照参数名ASCII码从小到大排序
String orderstr = GroupWeuxin.formatUrlMap(map,false,false);
// 拼接商户key
String SignTemp = orderstr+"&key="+payhospital.getKeyy();
// MD5加密 并且转换为大写 获得签名
String sign = Md5Util.getMD5String(SignTemp).toUpperCase();
map.put("sign",sign);
String xml = WXPayUtil.mapToXml(map);
String xmlstr = new String(xml.getBytes("UTF-8"), "ISO-8859-1");
// 调用统一下单接口
String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
ResponseEntity<String> responseEntity = restTemplate.postForEntity(url,xmlstr,String.class);
// 获取统一下单返回数据,并将数据转换为Map集合 到此统一下单完成
Map map1 = WXPayUtil.xmlToMap(responseEntity.getBody());
这里我说一下我的签名生成算法是自己写的,微信官方文档中提供了Util工具类
官方Util:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=11_1
当然这里我也把我用到的类给大家看一下
// 生成订单(也就是随机字符串)
public class GetRandom {
public static String getRandomStringByLength(int length) {
String base = "ABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
Random random = new Random();
StringBuffer sb = new StringBuffer();
for (int i = 0; i < length; i++) {
int number = random.nextInt(base.length());
sb.append(base.charAt(number));
}
return sb.toString();
}
}
/**
* 方法用途: 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序),并且生成url参数串<br>
* 实现步骤: <br>
*
* @param paraMap
* 要排序的Map对象
* @param urlEncode
* 是否需要URLENCODE
* @param keyToLower
* 是否需要将Key转换为全小写 true:key 转化成小写,false:不转化
* @return
*/
// 生成签名(我也是在网上找的)
public class GroupWeuxin {
public static String formatUrlMap(Map<String, String> paraMap, boolean urlEncode, boolean keyToLower) {
String buff = "";
try {
List<Map.Entry<String, String>> infoIds = new ArrayList<Map.Entry<String, String>>(paraMap.entrySet());
// 对所有传入参数按照字段名的 ASCII 码从小到大排序(字典序)
Collections.sort(infoIds, new Comparator<Map.Entry<String, String>>() {
@Override
public int compare(Map.Entry<String, String> o1, Map.Entry<String, String> o2) {
return (o1.getKey()).toString().compareTo(o2.getKey());
}
});
// 构造URL 键值对的格式
StringBuilder buf = new StringBuilder();
for (Map.Entry<String, String> item : infoIds) {
if (StringUtils.isNotBlank(item.getKey())) {
String key = item.getKey();
String val = item.getValue();
if (urlEncode) {
val = URLEncoder.encode(val, "utf-8");
}
if (keyToLower) {
buf.append(key.toLowerCase() + "=" + val);
} else {
buf.append(key + "=" + val);
}
buf.append("&");
}
}
buff = buf.toString();
if (buff.isEmpty() == false) {
buff = buff.substring(0, buff.length() - 1);
}
} catch (Exception e) {
return null;
}
return buff;
}
}
// 生成一个MD5加密计算摘要
public class Md5Util {
public static String getMD5String(String str) {
try {
MessageDigest md = MessageDigest.getInstance("MD5");
md.update(str.getBytes("utf-8"));
return new BigInteger(1, md.digest()).toString(16);
} catch (Exception e) {
e.printStackTrace();
return null;
}
}
}
别的没有列出的就是微信提供的了,文档说明也很详细
到这里统一下单基本就是这样了
如果没问题就会收到这样的一个XML
我们废了半天的劲就是为了获取prepay_id这个东西,现在我们拿到了就可以进行下一步了
微信内H5调起支付
还是一样先来看看这里需要什么参数
这里注意哦,这个appId中的i是大写I,上面统一下单时候用的是小写(千万不要搞错“后果很严重”),还有就是package这个参数的格式一定要注意,还有就是之前说的这里的随机字符串和统一下单用一个(是不是有点啰嗦了…)
下面还是和上面一样生成签名
Map<String,String> map2 = new HashMap<>();
// 获取统一下单返回的prepay_id值
String prepay_id = (String)map1.get("prepay_id");
map2.put("appId",appId);//appId注意大小写
map2.put("timeStamp",String.valueOf(WXPayUtil.getCurrentTimestamp())); // 时间戳微信Util中有
map2.put("nonceStr",randomstr);// 随机字符串
map2.put("package","prepay_id="+prepay_id);// 这里千万千万注意格式
map2.put("signType","MD5");// 签名方式
//生成签名 将集合内非空参数值的参数按照参数名ASCII码从小到大排序
String paixu2 = GroupWeuxin.formatUrlMap(map2,false,false);
// 拼接key MD5加密 并且转换为大写
String SignTemp2 = paixu2+"&key="+payhospital.getKeyy();
String sign2 = Md5Util.getMD5String(SignTemp2).toUpperCase();
map2.put("paySign",sign2);
return JSON.toJSONString(map2);// 直接转为JSON
这里给大家说一下我在这里坑的地方(当然大家应该不会这么笨),就是微信Util中提供了一个判断签名是否正确的方法,我在统一下单的时候用了那个方法测试了一下签名是否正确(当然也说明我的签名没毛病),然后我在第二次签名的时候我又用了一下那个方法准备测试一下我的签名,结果…死活就是返回false,我就找哪里错了找了整整一个周,期间还问了项目主管、项目经理他们都不知到为什么,就这样搞的我心态都炸了,期间我对照参数字母大小写(我连复制粘贴都不信了),不管怎么搞都是不行后来我就在网上求助,找了一些人问了他们后来才发现那个签名检验方法只是给统一下单时生的成签名校验的,在哪里用只有可能返回false…就这样(和一个不存在的错误斗智斗勇,还把自己搞奔溃了)我的项目进度延期了一周左右啊。
这里我非常感谢帮助我的那些朋友,再次感谢
到这里我们需要准备的参也就完成了,接下来就是传给前台调起支付(很激动)
这是我用的Angular的前台代码
export class Payweixin {
public static dategap(date: any) {
function onBridgeReady() {
// @ts-ignore
window.WeixinJSBridge.invoke(
'getBrandWCPayRequest', date,
function (res) {
if (res.err_msg === 'get_brand_wcpay_request:ok') {
swal('充值成功', '', 'success');
// 使用以上方式判断前端返回,微信团队郑重提示:
// res.err_msg将在用户支付成功后返回ok,但并不保证它绝对可靠。
} else {
swal('充值失败请稍后再试', '', 'info');
}
});
}
// @ts-ignore
if (typeof window.WeixinJSBridge === 'undefined') {
if (document.addEventListener) {
document.addEventListener('WeixinJSBridgeReady', onBridgeReady, false);
} else {
// @ts-ignore
if (document.attachEvent) {
// @ts-ignore
document.attachEvent('WeixinJSBridgeReady', onBridgeReady);
// @ts-ignore
document.attachEvent('onWeixinJSBridgeReady', onBridgeReady);
}
}
} else {
onBridgeReady();
}
}
}
由于我在之前已经转为了JSON所以在这里直接,给出参数就可以了,到此所有的代码就完成了,