小程序开发中常常会用到微信支付。微信支付里面的坑谁做谁知道。
先来个官方贴图谈一下支付的流程
对,支付是个异步的过程。
收集到用户的openid,支付金额,ip地址之后,我们生成随机串nonce_str和时间戳、订单号。当然商户号、商户秘钥、小程序的APPID、通知地址这些也是提前存好的。然后用这些信息按字典排序,它要求的是键值对,也就是key=value& 这样来拼接(具体的下面说)。拼接完成后尾处加上商户秘钥。最后用md5加密,提交到微信服务器生成预付单。拿到预付单之后我们传给小程序,小程序中用预付单的参数调起微信支付。然后支付结果通过我们前面传递的通知地址来通知我们(就是普通post请求传给你一个xml)。收到回调的xml后,我们去验证sign,如果验证通过则返回一个回馈给微信服务器(不然它还会持续发给你),然后我们执行支付成功的业务操作。
那我们现在从代码的角度来走一遍支付流程。
首先我们应该已知以下参数:appid(小程序appid)、mch_id(商户号)、key(商户秘钥)、trade_type(交易类型,JSAPI)、notify_url(支付回调地址,也就是通知我们的请求地址)
然后前端的提交一些信息,我们算出支付金额,获取支付用户的openid、支付用户客户端的IP地址、商品名称(其实可以固定写测试商品)。然后我们生成订单号,随机串和时间戳在后面生成。一个简单的controller如下:
/**
* 请求支付接口,当然参数不可能这么简单,其他业务参数我们就省去了
* @param openid 支付用户的openid
* @param request 用来获取支付用户的IP地址
*
* @return 预付单的信息 ,你也可以用其他类来进行封装,这里我直接用Map传回给前端了
*/
@RequestMapping("pay")
public Map insert(String openid,HttpServletRequest request) {
//我们假装计算出了支付金额,记住不能支付0元
//如果你不想用BigDecimal ,用其他也行。但是记住,最后提交的时候是以分为单位的
BigDecimal money = new BigDecimal(20);
//生成一个订单号,生成策略自己决定
String id = "123456";
//准备一个回调地址,为了给pay方法解耦,我们每种不同支付采用不同回调地址
String notifyUrl ="http://www.baidu.com/wxnotify";//要填能访问到的,别用本地localhost或者内网IP,尽量使用域名和公网IP
Map map = null;
//用异常包围起来,也许你想用事物来做
try {
map = pay(id,openid,money,request);
}catch (Exception e){
//处理一下异常
}
return map;
}
然后我们看一下这个pay方法。下面会调用StringUtil和HttpRequestor ,这两个是我自己准备的工具包,文末会贴上
/**
* 发起支付请求
* @param orderid 订单ID
* @param openid 用户ID
* @param money 支付金额
* @param request 请求体
* @param notifyUrl 支付结果回调通知地址
* @return 预付单参数
* @throws Exception
*/
public Map pay(String orderid, String openid, BigDecimal money,String notifyUrl, HttpServletRequest request) throws Exception {
//转化为分为单位
money = money.multiply(new BigDecimal(100));
//转换格式,最终还是转换成了long类型。。。。。但是绝对不要忘记是以分为单位
Long price = money.longValue();
//不能支付0元
if(price==0L){
return null;
}
// 生成的随机字符串,最长32位,uuid去掉“-”正好
String nonce_str = UUID.randomUUID().toString().replaceAll("-", "").toUpperCase();
// 商品名称,这里我写死了商品名称,如果你不想这样就改成参数吧
String productName = "测试商品";
// 获取终端IP
String ip = HttpRequestor.getIpFromRequest(request);
//GlobalPerties 是一个参数类,里面的所有变量都是static final的,方便访问。我用来存不会变动的参数
// 组装参数,用户生成统一下单接口的签名,这里用TreeMap 来就不需要自己排序了
SortedMap<String, String> packageParams = new TreeMap<String, String>();
packageParams.put("appid", GlobalProperties.APPID);// 小程序的aapid
packageParams.put("mch_id", GlobalProperties.MCHID);// 商户号
packageParams.put("nonce_str", nonce_str);// 随机字符串
packageParams.put("body", productName);// 商品名称
packageParams.put("out_trade_no", orderid);// 商户订单号
packageParams.put("total_fee", price + "");// 标价金额
packageParams.put("spbill_create_ip", ip);// 终端IP
packageParams.put("notify_url",notifyUrl);// 通知地址
packageParams.put("trade_type", "JSAPI");// 交易类型
packageParams.put("openid", openid);// 微信用户的openid
//参数map准备好之后我们进行拼接并加密。具体方法往后看
// MD5运算生成签名,这里是第一次签名,用于调用统一下单接口
String mysign = getSign(packageParams);
// 拼接统一下单接口使用的xml数据,要将上一步生成的签名一起拼接进去
String xml = "<xml>" + "<appid>" + GlobalProperties.APPID + "</appid>" + "<body><![CDATA[" + productName + "]]></body>"
+ "<mch_id>" + GlobalProperties.MCHID + "</mch_id>" + "<nonce_str>" + nonce_str + "</nonce_str>"
+ "<notify_url>" + notifyUrl + "</notify_url>" + "<openid>" + openid + "</openid>"
+ "<out_trade_no>" + orderid + "</out_trade_no>" + "<spbill_create_ip>" + ip + "</spbill_create_ip>"
+ "<total_fee>" + price + "</total_fee>" + "<trade_type>" + GlobalProperties.TRADETYPE + "</trade_type>"
+ "<sign>" + mysign + "</sign>" + "</xml>";
//打印一下日志方便调试,用了@Slf4j
log.info("调试模式_统一下单接口 请求XML数据:" + xml);
// 调用统一下单接口,并接受预付订单参数。这里我执行了一个post请求,这个请求工具类我放在文末
String result = HttpRequestor.doPost(GlobalProperties.PAYREQUEST_URL, xml);
//打印日志
log.info("调试模式_统一下单接口 返回XML数据:" + result);
// 将解析结果存储在HashMap中,xml转map 我是用了dom4j
Map<String, Object> map = StringUtil.doXMLParse(result);
/**
* 返回状态码
*/
String return_code = (String) map.get("return_code");
/**
* 准备返回给前端的map集合
*/
Map<String, Object> response = new HashMap<String, Object>(16);
// 判断一下预付订单是否成功的
if ("SUCCESS".equals(return_code)) {
/**
* 分析预付单信息
*/
String prepay_id = (String) map.get("prepay_id");
//前端需要的参数:nonceStr、package、timeStamp、paySign、appid,所以我们只需要关心这几个
response.put("nonceStr", nonce_str);
response.put("package", "prepay_id=" + prepay_id);
Long timeStamp = System.currentTimeMillis() / 1000;
/**
* 这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
*/
response.put("timeStamp", timeStamp + "");
// 拼接签名需要的参数
String stringSignTemp = "appId=" + GlobalProperties.APPID + "&nonceStr=" + nonce_str + "&package=prepay_id="