Vue+SpringBoot小程序版微信支付功能实现详细流程

Vue+SpringBoot小程序版微信支付

  • 前言
  • 一、项目背景和开发前准备
    • 1.项目背景
    • 2.下载SDK与DEMO
  • 二、小程序支付
    • 1.支付流程
    • 2.代码详情
  • 三、温馨提示

前言

令人“魂牵梦绕”小程序微信支付功能随着项目进入后期被提上日程,由于第一次有机会在实际开发的项目实现支付功能缺乏足够信心,因此提前也参考了很多相关文章,甚至看了跟微信支付相关的视频,最后终于参考着微信官方提供的操作文档,了解到了小程序微信的支付流程有哪些?理解了每个流程的作用是啥?就在这样准备的前提下,我开始整小程序版微信支付功能!

一、项目背景和开发前准备

1.项目背景

我所使用的项目框架是:小程序Vue(前端)+SpringBoot(后端)
事先准备:也就是开发微信支付你需要事先得到客户那边给你提供三个参数,即小程序号、商户号和密钥,至于如何获取这个参数,请在百度搜索”小程序开启微信支付功能的前提条件“等类似的问题。

2.下载SDK与DEMO

A.小程序支付模块没有提供对应的SDK与DEMO下载,但是和JSAPI支付类似,因此可以下载JSAPI支付模块的SDK与DEMO进行参考,主要就是引用里面的几个工具类。
SDK与DEMO下载参考链接
B.从上一步下载的SDK与DEMO,把IWXPayDomain.java、WXPayConfig.java、WXPayConstants.java、WXPayRequest.java、WXPayUtil.java、WXPayXmlUtil.java等几个工具类复制到自己的项目中,这几个类在完整的支付流程中会用到,当然你也可以把SDK与DEMO中所有的类都放进你的项目中。

二、小程序支付

1.支付流程

A.官方小程序支付流程图如下(官方流程链接):
Vue+SpringBoot小程序版微信支付功能实现详细流程_第1张图片
B.自己总结的主要流程:
一、前端页面用户操作发起支付,首先调取微信登录接口获取code,然后用获取的code调取后台方法获取微信用户的openId(相当于身份识别码)。
二、根据上一步拿到的openId和订单的相关信息参数作为参数调取后台微信支付的统一下单接口。
三、统一下单接口调取成功后会返回给前端一个prepay_id(微信生成的预支付会话标识,用于后续接口调用中使用),接着根据prepay_id商户server调用再次签名发起微信支付,此时页面上就会出现输入支付密码的弹出层(提示:开发阶段开发工具里此时出现的是一个二维码,扫了之后才能支付)。
四、用户在上一步输入密码成功支付后,前端会提示他支付成功,此时微信官方那边会在用户支付成功后,调用支付成功后的回调地址,用户会根据回调接口返回的数据,验证签名,核实金额,当没有问题之后,用户再进行相关的业务逻辑处理,例如:对交易表添加记录并修改订单表该条订单的支付状态。

2.代码详情

A.先调取微信内置登录接口拿到code,再根据code调取后台方法获取openId,前端页面再根据openId和相关订单信息调取后台统一下单接口,前后台代码如下:
前端代码:

import { getWXOpenIdByCode } from '@/api/user/wechat.js';
import { weiXinUndifiedOrder } from '@/api/order/pay.js';
methods: {
	//微信登录
	weiXinLogin(){
		let thatOrder = this;
		uni.login({
			provider: 'weixin',
			success: function (resCode) {
				if(resCode.errMsg == 'login:ok'){
					//获取 临时登录凭证code
					const weiXinCode = resCode.code;
					//根据code调取后台方法获取微信用户的openId(用户的唯一标识)
					getWXOpenIdByCode(weiXinCode).then(resOpenId=>{
						thatOrder.undifiedOrderObject.openId = resOpenId.msg;
						thatOrder.undifiedOrderObject.outTradeNo = thatOrder.orderNum;
						thatOrder.undifiedOrderObject.totalFee = thatOrder.order.totalAmount;
						thatOrder.undifiedOrderObject.body = thatOrder.list[0].productName;
						//调取统一下单接口
						weiXinUndifiedOrder(thatOrder.undifiedOrderObject).then(res=>{
							if(res.code == 200){
								//根据统一下单接口返回的prepay_id,商户server调用接口再次签名发起微信支付,
								thatOrder.requestPayment(res.msg);
							}
						});
					});
				}
			}
		})
	}
}

后端代码:

/**
 * 微信登录Controller
 * @author hc
 * @date 2020-06-30
 */
public class WeChetLogin {

	/**
     * 小程序微信支付根据微信登录获取code查询该微信用户的openId
     * @param code
     * @return
     */
    @ApiOperation("小程序微信支付根据微信登录获取code查询该微信用户的openId")
    @PostMapping("/getWXOpenIdByCode")
    public AjaxResult getWXOpenIdByCode(@RequestBody String code){
	  //调取微信工具类型相关方法
      String openId = WeiXinUtil.getOpenIdByURL(code);
      if (openId != null && !openId.equals("")) {
    	  return AjaxResult.success(openId);
	  }else {
		  return AjaxResult.error("getFailed");
	  } 
    }
}

/**
 * 微信工具类
 * @author hc
 * @date 2020-06-30
 */
public class WeiXinUtil {

	public  final  static String auth_code2Session = "https://api.weixin.qq.com/sns/jscode2session?appid=APPID&secret=SECRET&js_code=JSCODE&grant_type=authorization_code";
    public final static String appid = PayConfig.APPID;
	public final static String appSecret = PayConfig.APP_SECRECT;
	
	/**
     * 根据微信登录获取code拿到该微信用户的openId
     * @param code
     * @return
     */
	public static String getOpenIdByURL(String code) {
		String reqUrl = auth_code2Session.replace("APPID", WeiXinUtil.appid);
		reqUrl = reqUrl.replace("SECRET", WeiXinUtil.appSecret);
		reqUrl = reqUrl.replace("JSCODE", code);

		//根据临时登录凭证code,调用auth.code2Session 接口,换取用户唯一标识 OpenID和会话密钥session_key
		JSONObject jsonUserInfo = CommonUtil.httpsRequest(reqUrl, "GET", null);
		if (jsonUserInfo != null && !jsonUserInfo.isEmpty()) {
			return jsonUserInfo.getString("openid");
		} else {
			return null;
		}
	}
}

/**
 * 微信支付配置类
 * @author hc
 * @date 2020-06-30
 */
public class PayConfig {
    //小程序号
    public final static String APPID = "**********";
	//密钥(商户Key)
    public final static String APP_KEY = "************************";
	//和小程序绑定的商户号
    public final static String MCH_ID = "**********";
	//支付成功后的回调地址
    public final static String NOTIFY_URL = "************************";
	//支付方式,小程序支付为:JSAPI
    public final static String TRADE_TYPE = "JSAPI";
}


/**
 * 微信支付Controller
 * @author hc
 * @date 2020-06-30
 */
public class PayController {
	/**
     *调取微信支付统一下单接口
     * @param uOrderObject
     * @return
     * @throws Exception 
     */
    @ApiOperation("调取微信支付统一下单接口")
    @PostMapping("/undifiedorder")
    public AjaxResult undifiedorder(@RequestBody UndifiedOrderObject uOrderObject, HttpServletRequest request) throws Exception{
    	//发起微信支付所属微信用户的身份识别码openId
    	String openId = uOrderObject.getOpenId();
    	//商品交易号
    	String outTradeNo = uOrderObject.getOutTradeNo();
    	//商品价格
    	String totalFee = uOrderObject.getTotalFee();
    	//商品详情
    	String body = uOrderObject.getBody();
    	//获取微信支付API的机器IP
    	String spbillCreateIp = IpUtils.getIpAddr(request);
    	System.out.println("spbillCreateIp:"+spbillCreateIp);
    	//格式化成所需的商品金额
    	NumberFormat format = NumberFormat.getInstance();
    	String formatTotalFee = String.valueOf(((format.parse(totalFee).doubleValue()) * 100));
        
    	//组装参数,用户生成统一下单接口的签名
		Map orderParams = new HashMap();
		//小程序ID
		orderParams.put("appid", PayConfig.APPID);
		//商品描述
		orderParams.put("body", body);
		//商户号
		orderParams.put("mch_id", PayConfig.MCH_ID);
		//随机字符串,WXPayUtil.generateNonceStr()生成随机字符串的方法
		orderParams.put("nonce_str", WXPayUtil.generateNonceStr());
		//支付成功后的回调地址
		orderParams.put("notify_url", PayConfig.NOTIFY_URL);
		//trade_type=JSAPI,此参数必传,用户在商户appid下的唯一标识。
		orderParams.put("openid", openId);
		//商品订单号
		orderParams.put("out_trade_no", outTradeNo);
		//支持IPV4和IPV6两种格式的IP地址。调用微信支付API的机器IP
		orderParams.put("spbill_create_ip", spbillCreateIp);
		//支付金额,这边需要转成字符串类型,否则后面的签名会失败
		orderParams.put("total_fee", formatTotalFee);
		//支付方式,小程序支付为:JSAPI
		orderParams.put("trade_type", PayConfig.TRADE_TYPE);
		
		//PayConfig.API_KEY,生成带有sign的 XML格式字符串
		String signXml = WXPayUtil.generateSignedXml(orderParams, PayConfig.API_KEY, WXPayConstants.SignType.MD5);
	    //System.out.println("signXml:"+signXml);
		//用sign的 XML格式字符串请求官方下单接口
		String resultXml = WXPayRequest.requestOnceBySignXml(signXml, "https://api.mch.weixin.qq.com/pay/unifiedorder");
	
		//拿到返回结果prepay_id微信生成的预支付会话标识,用于后续接口调用中使用,该值有效期为2小时
		Map resultMap = new HashMap();
		//XML格式字符串转换为Map
		resultMap = WXPayUtil.xmlToMap(resultXml);
		String resultCode = resultMap.get("return_code");
		String prepayId = "";
		//如果resultCode等于SUCCESS,说明调取成功,可拿到prepay_id
		if (resultCode.equals("SUCCESS")) {
			prepayId = resultMap.get("prepay_id");
			return AjaxResult.success(prepayId);
		}else {
			return AjaxResult.error("Failed");
		}
    }
}

B.前端页面成功调取统一下单接口会拿到返回的prepay_id(预支付交易会话标识),前端根据拿到的prepay_id再次验证签名发起微信支付,此操作只有前端代码如下:
前端代码:

import { randomString} from '@/utils/mathutil.js';
import { md5 } from '@/utils/md5.js';
import config from '@/utils/config.js';
methods: {
	//根据prepayId发起微信支付
	requestPayment(prepayId) {
		//通过randomString方法获取32位随机字符串
		let nonceStr = randomString(false, 32);
		//获取当前时间戳
		let timedeal = String(Date.now());
		//获取预先存在config文件中的商户号,以下AppId 
		let m_id = config.m_id;
		// 参数按官方要求的顺序进行拼接
		let stringSignTemp = 'appId=' + config.AppId + '&nonceStr=' + nonceStr + '&package=prepay_id=' + prepayId + '&signType=' + 'MD5' + '&timeStamp=' + timedeal + '&key=' + config.key;
		//MD5签名方式
		let finalsign = md5(stringSignTemp).toUpperCase(); 
		//调用wx.requestPayment(OBJECT)发起微信支付
		uni.requestPayment({
			provider: 'wxpay',
			timeStamp: timedeal, 
			nonceStr: nonceStr, 
			package: 'prepay_id=' + prepayId, // 统一下单接口返回的 prepay_id 参数值,提交格式如:prepay_id=xx
			signType: 'MD5', 
			paySign: finalsign, // 签名
			success: function (success) {
				// 支付成功的回调中 创建成功
				uni.showModal({
					title: '支付成功!',
					showCancel: false,
					success: function (res) {
						if (res.confirm) {
							uni.navigateTo({
							    //成功后跳转至订单模块
								url: '/pages/mine/page/order/order'
							});
						} 
					}
				});
			},
			fail: function (err) {
				// 支付失败的回调中 用户未付款
				uni.showModal({
					title: '支付取消!',
					showCancel: false,
					success: function (res) {
						if (res.confirm) {
							uni.navigateTo({
							   //跳转至我的订单模块
								url: '/pages/mine/page/order/order'
							});
						} 
					}
				});
			},
			'complete':function(res){
				//console.log("complete:",res);
			}
		});
	}
}

C.用户在支付页面输入密码成功支付后,微信官方会在用户支付成功后调取用户先前在统一下单接口中设置的回调地址,此业务只有后端代码如下:
后端代码:

/**
 * 微信支付Controller
 * @author hc
 * @date 2020-06-30
 */
public class PayController {
    /**
     * 接收微信官方返回觉得支付结果通知
     * @return
     * @throws Exception
     */
    @ApiOperation("接收支付结果通知的接口")
    @PostMapping("/getNotifyUrl")
    @ResponseBody
    public String weiXinPayCallBack(HttpServletRequest request) throws Exception{
    	Map rMap = new HashMap();
    	//接收微信官方返回的支付结果
    	InputStream inputStream = request.getInputStream();
    	BufferedReader bufferedReader = new BufferedReader(new InputStreamReader(inputStream, "UTF-8"));
    	String temp;
    	StringBuilder stringBuilder = new StringBuilder();
    	while ((temp = bufferedReader.readLine()) != null) {
    		stringBuilder.append(temp);
		}
		//关闭流,先打开后关,后打开先关
    	bufferedReader.close();
    	inputStream.close();
    	Map resultMap = WXPayUtil.xmlToMap(stringBuilder.toString());
        //判断是不是来自微信官方,进行签名验证
    	boolean flag = WXPayUtil.isSignatureValid(resultMap, PayConfig.API_KEY, WXPayConstants.SignType.MD5);
    	if (flag) {
			//获取支付结果中的result_code,根据此值判断是否进行自身业务实现
    		String resultCode = resultMap.get("result_code");
    		if (resultCode.equals("SUCCESS")) {
				//获取微信返回的交易号
    			String tradeNo = resultMap.get("transaction_id");
    			//商户订单号
    			String orderNo = resultMap.get("out_trade_no");
    			//支付状态判断和自身业务实现
    			UserOrder userOrder = userOrderService.getById(orderNo);
    			if(userOrder != null) {
    				//格式化成所需的商品金额
    				double TotalFeeDouble = userOrder.getTotalAmount() * 100;
    				System.out.println("TotalFeeDouble:"+TotalFeeDouble);
    				System.out.println("Totalee:"+TotalFeeDouble);
    				//回调接口和实际订单进行比较
    				if (TotalFeeDouble == Double.parseDouble(resultMap.get("total_fee"))) {
    					LambdaUpdateWrapper uOrderQueryWrapper = new LambdaUpdateWrapper();
    					//支付状态改为1(已支付); 订单状态改为2(已完成):
    					uOrderQueryWrapper.eq(UserOrder::getId, userOrder.getId())
    					                  .set(UserOrder::getPayStatus, 1)
    					                  .set(UserOrder::getOrderStatus, 2);
    					//修改订单支付状态
    					boolean bStatus = userOrderService.update(uOrderQueryWrapper);
    					if (bStatus) {
    						SimpleDateFormat simpleDateFormat = new SimpleDateFormat("yyyyMMddHHmmss");
    						Date speicDate = simpleDateFormat.parse(resultMap.get("time_end"));
    						SimpleDateFormat df = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 
    						Date payTime = df.parse(df.format(speicDate));
    						//新建交易实例,设置相关属性值
    						OrderTrade orderTrade = new OrderTrade();
    						orderTrade.setTradeNo(tradeNo);
    						orderTrade.setOrderId(Long.parseLong(orderNo));
    						orderTrade.setOrderPrice(userOrder.getTotalAmount());
    						orderTrade.setPayTime(payTime);
    						orderTrade.setCreateTime(new Date());
    						//交易表新增一条交易记录
    						iOrderTradeService.save(orderTrade);   
    		    			rMap.put("return_code", "SUCCESS"); 
    						rMap.put("return_msg", "已收到");
						}
					}
    			}else {
    				rMap.put("return_code", "FAIL"); 
    				rMap.put("return_msg", "已收到"); 
    			}	
			}else {
				rMap.put("return_code", "FAIL"); 
				rMap.put("return_msg", "已收到"); 
			}
		}else {
			rMap.put("return_code", "FAIL"); 
			rMap.put("return_msg", "已收到"); 
		}
		//以xml格式给微信官方返回是否成功的应答
        return WXPayUtil.mapToXml(rMap);
    }
}

三、温馨提示

在实现整个微信支付的过程中,我有以下几点需要提醒初次接触微信支付功能的小伙伴们:
1.参考官方小程序支付整个流程很重要,尽量熟悉。
2.百度先找几篇和你类似架构环境下微信支付的文章,多看几篇有利于整个支付流程的熟悉,以及了解每个流程所要达到的目的。
3.调取统一下单接口以及获取到prepay_id再次签名发起微信支付等接口时,参数名称要严格安装官方文档提供的名称,不能有错,而且拼接字符串得按参数首字母由小到大顺序进行拼接排列,否则会报签名错误!
4.成功之后用于微信官方回调地址,需互联网能够访问,要不然调不通。由于我是用的若依框架,那么访问方法是需要token的,所以这个回调方法要在相关的权限配置文件(SecurityConfig.java)中放开,也就是不能加token验证,不同的若依框架权限配置文件也不同,不过大同小异!
5.我自己做的时候虽然项目互联网能访问但支付成功后,一直不走我的后台回调方法,最后发现由于我用的是RuoYi-Vue分离版框架,访问后台的方法都需要有prod-api字段,我的项目部署使用了Nginx的反向代理,使用了两个locahost,一个直接访问前端打包的静态页面,一个加prod-api字段代理访问后台项目打的jar包,所以回调地址上也得加prod-api才能正常回调,如果不加的话,那就是访问的前端,示例写法如下:

 public final static String NOTIFY_URL = "https://备案且绑定服务器IP地址的域名/prod-api/pay/getNotifyUrl";

你可能感兴趣的:(微信支付)