微信支付(公众号接入)-------------------------完整流程

花了几天时间终于把微信支付做完了,虽然只是调接口,但遇到的坑还是不少的,现在把整个流程梳理一下。

官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/jsapi.php?chapter=7_1

大概流程:后台调用统一下单接口,获取pre_pay_id ------> 将获取到的这个预支付标示和一些其他参数返回给前端  --------

---------->    前端接收参数利用微信浏览器内置对象(或者js api的发起支付接口)发出支付请求  --------------------->支付成功,微信服务器通知后台,后台处理相关业务逻辑。

下面上代码:

一、获取调用统一下单接口,获取pre_pay_id.

/**
	 * 微信统一下单
	 * @param userIp  客户端ip
	 * @param totalFee  费用
	 * @param order    订单
	 * @param openId    用户openId
	 * @return  prepay_id 预支付交易会话标识
	 * @throws Exception
	 */
	public static Map<String,String> unifiedorder(String userIp, String totalFee,
			String order, String openId)throws Exception{
		String url = "https://api.mch.weixin.qq.com/pay/unifiedorder";
		Map<String,String> requestMap = unifiedorderTemplate(userIp, totalFee, order, openId);
		String requestStr = WXPayUtil.mapToXml(requestMap); 
//		String requestStr = new String(WXPayUtil.mapToXml(requestMap).getBytes(),"utf-8");
		log.info("微信统一下单请求地址:" + url);
		log.info("微信统一下单请求参数:" + requestStr);
		String result = HttpUtil.sendXmlPost(url, requestStr);
		log.info("微信统一下单请求结果:" + result);
		Map<String,String> resultMap = WXPayUtil.xmlToMap(result);
		if(resultMap.containsKey("return_code")){
			if(resultMap.get("return_code").equals("SUCCESS")){
				if(resultMap.containsKey("result_code")){
					if(resultMap.get("result_code").equals("SUCCESS")){
						return resultMap;   //请求并且下单成功返回的参数里有pre_pay_id和一些其他参数,具体看官方文档
					}
				}
			}
		}
		return null;
	}
	
	/**
	 * 统一下单参数封装
	 * @param userIp
	 * @param totalFee
	 * @param order
	 * @param openId
	 * @return
	 * @throws Exception
	 */
	private static Map<String,String> unifiedorderTemplate(String userIp, String totalFee,
			String order, String openId)throws Exception{
		Map<String,String> requestMap = new HashMap<>();
		requestMap.put("appid", WxConstants.APP_ID);
		requestMap.put("mch_id", WxConstants.MCH_ID);
		requestMap.put("nonce_str", WXPayUtil.generateNonceStr());
		String body = "中文";
		requestMap.put("body", body);
		requestMap.put("out_trade_no",order);
		requestMap.put("total_fee", totalFee);
//		requestMap.put("spbill_create_ip", "118.114.230.35");
		requestMap.put("spbill_create_ip", userIp);
		requestMap.put("notify_url", WxConstants.NOTIFY_URL);
		requestMap.put("trade_type", "JSAPI");
		requestMap.put("openid", openId);
		requestMap.put("sign", WXPayUtil.generateSignature(requestMap, WxConstants.KEY));
		
		/*String xml = "<xml>" + "<appid><![CDATA[" + WxConstants.APP_ID + "]]></appid>"  
                + "<body><![CDATA[" + body + "]]></body>"  
                + "<mch_id><![CDATA[" +WxConstants.MCH_ID + "]]></mch_id>"  
                + "<nonce_str><![CDATA[" + requestMap.get("nonce_str") + "]]></nonce_str>"  
                + "<notify_url><![CDATA[" + requestMap.get("notify_url") + "]]></notify_url>"  
                + "<out_trade_no><![CDATA[" + requestMap.get("out_trade_no") + "]]></out_trade_no>"  
                + "<spbill_create_ip><![CDATA[" + requestMap.get("spbill_create_ip") + "]]></spbill_create_ip>"  
                + "<total_fee><![CDATA[" + totalFee + "]]></total_fee>"  
                + "<trade_type><![CDATA[" + "JSAPI" + "]]></trade_type>"
                + "<openid><![CDATA[" + openId + "]]></openid>"
                + "<sign><![CDATA[" + requestMap.get("sign") + "]]></sign>" + "</xml>";  
		requestMap.put("xml", xml);*/
		return requestMap;
	}

有几点需要注意:

1.参数较多,参数不能缺失,注意appid,mch_id,key的正确,这里的key在进行参数签名时会用到,是在商户平台设置的支付密钥,与公众号的appsecret不是同一个。

2.注意传值编码问题,如果签名错误,但是去官方提供的签名验证工具验证是正确的,那多半是设置的key不对或者传的参数里有中文发生乱码了。

二、将获取到的pre_pay_id和一些其他参数返回给前端。

    
        @Override
	public ReturnObject createOrder(String ip, String openId, String fee) throws Exception {
		ReturnObject ro = new ReturnObject();
		VipOrder order = new VipOrder();
		order.setFee(Integer.parseInt(fee));
		order.setOdrdeTime(System.currentTimeMillis());
		order.setOpenId(openId);
		order.setOutTradeNo(CommonUtil.makeOrderNumber(System.currentTimeMillis()));
		Map<String, String> resultMap = WxPay.unifiedorder(ip, fee, order.getOutTradeNo(), openId);
		if (resultMap != null) {
			String prePayId = resultMap.get("prepay_id");
			if (prePayId == null || prePayId.equals("")) {
				ro.setMessage("无法获取pre_pay_id");
			} else {
				order.setPrepayId(prePayId);
				orderDao.insertSelective(order);
				Map<String, String> jsMap = new HashMap<>();
				jsMap.put("appId", resultMap.get("appid"));
				jsMap.put("timeStamp", new Long(System.currentTimeMillis() / 1000).toString());
				jsMap.put("nonceStr", WXPayUtil.generateNonceStr());
				jsMap.put("package", "prepay_id=" + prePayId);
				jsMap.put("signType", "MD5");
				String paySign = WXPayUtil.generateSignature(jsMap, WxConstants.KEY);
				jsMap.put("paySign", paySign);
				log.info("返回前端:" + jsMap.toString());
				ro.setData(jsMap);
			}
		}
		return ro;
	}

这里只需要注意一点,因为调用统一下单成功后返回的参数里也有nonceStr,sign,timeStamp这些参数,我一开始以为就是把这些返回给前端,结果并没有关系,而且签名是根据js api调用需要的参数重新计算的,这里参与签名的有appId,timeStamp,nonceStr,package,signType计算出的,签名算法与统一下单签名算法一样,而且好像签名类型要一致,不过我没试,我签名都是采用的MD5。

三、微信js唤出支付框

<!DOCTYPE html>
<html xmlns="http://www.w3.org/1999/xhtml"
      xmlns:th="http://www.thymeleaf.org">
<head>
<meta charset="UTF-8">
<title>Insert title here</title>
</head>
<body>
	<button value="充值" id = "test">充值</button>
	<span th:text="${session.app}"></span>
</body>
<script type="text/javascript" src="/static/js/jquery-3.1.1.js"></script>
<script type="text/javascript" src="http://res.wx.qq.com/open/js/jweixin-1.2.0.js"></script>
<script type="text/javascript" th:inline="javascript">
 var a = [[${session.sign}]];
alert(a);
wx.config({
    debug: true, // 开启调试模式,调用的所有api的返回值会在客户端alert出来,若要查看传入的参数,可以在pc端打开,参数信息会通过log打出,仅在pc端时才会打印。
    appId: [[${session.sign.appId}]], // 必填,公众号的唯一标识
    timestamp: [[${session.sign.timestamp}]], // 必填,生成签名的时间戳
    nonceStr: [[${session.sign.nonceStr}]], // 必填,生成签名的随机串
    signature: [[${session.sign.signature}]],// 必填,签名,见附录1
    jsApiList: ['chooseWXPay'] // 必填,需要使用的JS接口列表,这里只写支付的
});

wx.ready(function(){
	     wx.hideOptionMenu();//隐藏右边的一些菜单
	});


	$("#test").click(function(){
		var openId = JSON.stringify("o9b1k0Yov9IRnX_MAB4suLydMvgA");
		$.ajax({
			contentType:"application/json",
			dataType:"json",
			type:"POST",
			url:"****",
			data:{
				"openId":openId
			},
			success:function(result){
				console.log(result);
				if(result.success == true){
//					pay(result.data);
					onBridgeReady(result.data);
				}
			}
		})
	})
	
	/* function pay(json){
		wx.chooseWXPay({
		    timestamp: json.timeStamp,
		    nonceStr: json.nonceStr, 
		    package: json.package,
		    signType: 'MD5',
		    paySign: json.paySign, 
		    success: function (res) {
		        alert("支付成功");
		    }
		});
	} */
	
 	function onBridgeReady(param){
   WeixinJSBridge.invoke(
       'getBrandWCPayRequest', {
           "appId":"",     //公众号名称,由商户传入     
           "timeStamp":param.timeStamp,         //时间戳,自1970年以来的秒数     
           "nonceStr":param.nonceStr, //随机串     
           "package":param.package,     
           "signType":"MD5",         //微信签名方式:     
           "paySign":param.paySign //微信签名 
       },
       function(res){     
           if(res.err_msg == "get_brand_wcpay_request:ok" ) {
        	   alert("支付成功");
           }     // 使用以上方式判断前端返回,微信团队郑重提示:res.err_msg将在用户支付成功后返回    ok,但并不保证它绝对可靠。 
       }
   ); 
} 
/* 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();
} */
</script>
</html>

两种方式都试过,都可以成功唤出支付框,但是用js api调用接口的方式要麻烦一点,需要获取js_ticket,是调用接口用token获得的,然而和需要调用的url进行签名,然后返回前端进行wxConfig配置,如果只是支付的话用微信内置对象唤起支付就方便的多。

到这里就不得不吐槽一句了。作为一个很少做前端的后端狗,就是这里浪费了我至少开发微信支付的一半时间,难受。

四、处理支付回调

/**
	 * 微信支付通知
	 * @param req
	 * @param rep
	 */
	@RequestMapping(value = "/handlePayNotify", method = RequestMethod.POST)
	public void handlePayNotify(HttpServletRequest req, HttpServletResponse rep){
		try {
			req.setCharacterEncoding("UTF-8");
			rep.setCharacterEncoding("UTF-8");
			Map<String, String> map = XmlUtils.praseXml(req);
			wxService.handlePayNotify(req, rep, map);
		} catch (Exception e) {
			log.error(e.getMessage(),e);
		}
	}
@Override
	public synchronized void handlePayNotify(HttpServletRequest req, HttpServletResponse rep, Map<String, String> map)
			throws Exception {
		Map<String, String> returnMap = new HashMap<String, String>();
		String return_code = WxConstants.FAIL;
		String return_msg = "";
		if (map.containsKey("return_code") && map.containsKey("result_code") && map.get("return_code").equals("SUCCESS")
				&& map.get("result_code").equals("SUCCESS")) {
			// 验证签名
			String sign = map.get("sign");
			String mySign = WXPayUtil.generateSignature(map, WxConstants.KEY);
			if (!sign.equals(mySign)) {
				log.error("支付通知签名验证失败");
				log.error("sign:" + sign);
				log.error("mySign" + mySign);
				return_msg = "签名验证失败";
			}
			// 验证订单支付状态
			String outTradeNo = map.get("out_trade_no");
			VipOrder order = orderService.selectVipOrderByOutTradeNo(outTradeNo);
			if (order.getPayStatus() == 1) {
				return_code = WxConstants.SUCCESS;
			} else {
				// 验证金额
				String wxTotalFee = map.get("total_fee");
				if (order.getFee().toString().equals(wxTotalFee)) {
					order.setActualFee(Integer.parseInt(map.get("cash_fee")));
					order.setPayStatus((byte) 1);
					order.setPayTime(DateUtils.dateStrToTimestamp(map.get("time_end"), DateUtils.DATE_TYPE_4));
					order.setTransactionId(map.get("transaction_id"));
					if (orderDao.updateByPrimaryKeySelective(order) == 1) {
						// 添加会员记录
						VipRecord record = orderService.addVipRecord(order.getOpenId());
						if (record != null) {
							return_code = WxConstants.SUCCESS;
						}
					}
				} else {
					log.error("支付金额和订单金额不一致");
					return_msg = "支付金额与订单金额不一致";
				}
			}
			returnMap.put("return_code", return_code);
			returnMap.put("return_msg", return_msg);
			String returnStr = WXPayUtil.mapToXml(returnMap);
			rep.getWriter().write(returnStr);
		}
	}

这里的回调处理接口对应统一下单传的url参数。微信官方推荐针对通知为了安全最好验证签名与金额,并且要能正确处理微信的重复通知。下面是微信官方原话:

支付完成后,微信会把相关支付结果和用户信息发送给商户,商户需要接收处理,并返回应答。

对后台通知交互时,如果微信收到商户的应答不是成功或超时,微信认为通知失败,微信会通过一定的策略定期重新发起通知,尽可能提高通知的成功率,但微信不保证通知最终能成功。 (通知频率为15/15/30/180/1800/1800/1800/1800/3600,单位:秒)

注意:同样的通知可能会多次发送给商户系统。商户系统必须能够正确处理重复的通知。

推荐的做法是,当收到通知进行处理时,首先检查对应业务数据的状态,判断该通知是否已经处理过,如果没有处理过再进行处理,如果处理过直接返回结果成功。在对业务数据进行状态检查和处理之前,要采用数据锁进行并发控制,以避免函数重入造成的数据混乱。

特别提醒:商户系统对于支付结果通知的内容一定要做签名验证,并校验返回的订单金额是否与商户侧的订单金额一致,防止数据泄漏导致出现“假通知”,造成资金损失。

微信支付基本完成。额外提醒一个在设置支付授权目录时,设置的url应该是你调用微信支付的上一级,微信文档那里说的必须细化至二三级表述的有点问题。另外这是公众号支付,需要获取用户的openId,需要配置网页授权域名,域名必须经过ipc备案。

微信开发目前来说还是比较顺利的,就是需要各种配置有点麻烦。


评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值