Springboot+微信小程序(2)——微信支付

  • 准备工作

在开发微信小程序支付接口之前,首先得为微信小程序申请接入微信支付,不麻烦,给个连接:微信支付接入指引

跟着指引走,很简单,目前,小程序开发的微信支付接口只支持企业级开发的小程序,需提交相关企业经营相关资质,个人暂时不支持开通支付,过程中要支付300¥,最终会拿到开发需要的需要的“商户号”、“商户api安全key”,这个key是自己在商户后台设置的的32位秘钥,生成方式自己选择,百度一堆;(这两个比较重要,放在后台,不可以放在小程序前台)。

除了获取到“商户号”和“api安全key”之外,我们还需要在商户后台获取到“API证书”,至于什么是“API证书”、如何获取的问题,百度一堆,这里给个参考:什么是API证书?如何获取API证书

最终我们会获取到3个文件,apiclient_cert.p12、apiclient_cert.pem、apiclient_key.pem,其中 apiclient_cert.p12 对我们是最重要的,将它放入到我们项目的资源文件目录下;

image.png

  • 业务流程

在进行开发之前,深刻理解业务流程很重要,这里可以参考官方的:微信小程序支付业务流程时序图

支付业务流程时序图.jpg

商户系统和微信支付系统主要交互:

1、小程序内调用登录接口,获取到用户的openid,api参见公共api【小程序登录API

2、商户server调用支付统一下单,api参见公共api【统一下单API

3、商户server调用再次签名,api参见公共api【再次签名

4、商户server接收支付通知,api参见公共api【支付结果通知API

5、商户server查询支付结果,api参见公共api【查询订单API

上面提到的交互中的第一条,登录接口,上一篇博文已经完成了,这里就可以不用做了,切勿重复调用登录接口,会刷新之前的session_key,造成用户登陆授权流程的混乱,引起不必要的麻烦。

  • 开发方式的选择

这里我选择的是使用微信官方提供的SDK开发,可以省去我们很多事情:官方SDK下载地址

image.png

可以看到官方java版SDK下载下来共有8个java文件,主要是一些封装好的工具类和配置文件,我们将这些java文件复制到我们的项目下,可以为其创建一个package包;

开发代码

首先,我们在sdk文件的同级目录,创建一个自己的配置类,MyConfig,继承sdk自带的WXPayConfig,重写其中的抽象方法,在构造函数中要读取我们的配置文件 apiclient_cert.p12 ,这个在上面讲过了;

MyConfig.java

package com.wx.video.wxpay.sdk;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import org.apache.commons.io.IOUtils;
import org.springframework.core.io.ClassPathResource;

/**   
 ** 功能描述:自定义的微信支付配置类
 * @Package: com.github.wxpay.sdk 
 * @author: jiguiquan   
 * @date: 2019年6月13日 上午11:08:35 
 */
public class MyConfig extends WXPayConfig {
    //读取证书文件返回的字节数组
    private byte[] certData;
	
    public MyConfig() throws Exception { 	
    	try {
            ClassPathResource classPathResource = new ClassPathResource("apiclient_cert.p12");
            //获取文件流
            InputStream certStream = classPathResource.getInputStream();
            this.certData = IOUtils.toByteArray(certStream);
            certStream.read(this.certData);
            certStream.close();
        } catch (IOException e) {
            e.printStackTrace();
        }
    }

    //我的appid
    @Override
    String getAppID() {
	return "wx0fb1335345abd2544";
    }

    //我的商户号
    @Override
    String getMchID() {
	return "15364354501";
    }

    //我的商户api安全key,在商户平台api安全目录下可设置
    @Override
    String getKey() {
	return "xKLPpyJOORlkVm35435545tx1PuxqqIx";
    }

    @Override
    InputStream getCertStream() {
	ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
    return certBis;
    }
	
    @Override
    public int getHttpConnectTimeoutMs() {
        return 8000;
    }

    @Override
    public int getHttpReadTimeoutMs() {
        return 10000;
    }

    @Override
    IWXPayDomain getWXPayDomain() {
	return new IWXPayDomain() {
            @Override
            public void report(String domain, long elapsedTimeMillis, Exception ex) {
            }

            @Override
            public DomainInfo getDomain(WXPayConfig config) {
                return new DomainInfo("api.mch.weixin.qq.com", false);
            }
        };
    }
}

做完上面的一系列工作后,就可以在Controller中上手开发我们的统一微信支付接口啦,当然更优的做法是将方法都封装起来,或者提取到service层,但是我这里为了直观,就直接将逻辑全部写到controller层了;

    /**
	 ** 微信支付接口
	 * 
	 * @param order   实际业务订单详情
	 * @param request
	 * @return
	 */
	@RequestMapping(value = "/wxPay", method = RequestMethod.POST)
	@ResponseBody
	public JsonResult wxPay(@RequestBody Vorder order, HttpServletRequest request) {
		// 从request中解析出当前用户的个人信息
		Claims claims = jwtUtils.getUserClaim(request);
		String uid = claims.get("uid").toString();
		String openid = claims.get("openid").toString();

		// 判断是否重复购买
		List<Integer> list = vorderService.myOrderVidList(Integer.parseInt(uid));
		System.out.println(list);
		if (list.contains(order.getVid())) {
			return JsonResult.error("当前课程你已经购买过,无需重复购买");
		}

		// 没有购买过的情况下,则记得在数据库中插入订单信息,为了后面的模板消息推送中能获取prepay_id,我需要等到第一次签名结束后,得到prepay_id后,才向数据库新增这条订单数据
		Map resultMap = new HashMap(); // 这个resultMap是用来封装最终返回给小程序的数据的

		// 开始进入构建微信支付环境
		MyConfig config = null;
		WXPay wxpay = null;
		try {
			config = new MyConfig();
			wxpay = new WXPay(config);
		} catch (Exception e) {
			e.printStackTrace();
		}

		// 使用官方sdk工具生成商户订单号
		String out_trade_no = WXPayUtil.generateNonceStr();
		// 生成的随机字符串
		String nonce_str = WXPayUtil.generateNonceStr();
		// 获取客户端的ip地址
		// 获取本机的ip地址
		InetAddress addr = null;
		try {
			addr = InetAddress.getLocalHost();
		} catch (UnknownHostException e) {
			e.printStackTrace();
		}
		String spbill_create_ip = addr.getHostAddress();
		// 支付金额,需要转成字符串类型,否则后面的签名会失败
		int total_fee = (int) (order.getOprice() * 100);
		System.out.println("付款金额为=" + total_fee);
		// 商品描述
		String body = order.getDescription();

		// 统一下单接口参数
		HashMap<String, String> data = new HashMap<String, String>();
		data.put("appid", "wx0fb1142123bd2544");
		data.put("mch_id", "15364312101");
		data.put("nonce_str", nonce_str);
		data.put("body", body);
		data.put("out_trade_no", out_trade_no);
		data.put("total_fee", String.valueOf(total_fee));
		data.put("spbill_create_ip", spbill_create_ip);
		data.put("notify_url", "https://www.testtuan.com/wxvideo/api/order/notify");
		data.put("trade_type", "JSAPI");
		data.put("openid", openid);
		try {
			Map<String, String> rMap = wxpay.unifiedOrder(data);     //这里是在调用统一下单API,已经被官方的SDK封装好了;
			System.out.println("统一下单接口返回: " + rMap);
			String return_code = (String) rMap.get("return_code");
			String result_code = (String) rMap.get("result_code");
			String nonceStr = WXPayUtil.generateNonceStr();
			resultMap.put("nonceStr", nonceStr);
			Long timeStamp = System.currentTimeMillis() / 1000;
			if ("SUCCESS".equals(return_code) && return_code.equals(result_code)) {
				String prepayid = rMap.get("prepay_id");
				// 在第一次签名之后,再进行预付订单的创建,这样可以同时将prepay_id一并存入数据库
				order.setOutTradeNo(out_trade_no);
				order.setUid(Integer.parseInt(uid));
				order.setOpenid(openid);
				order.setStatus("0");
				order.setOtime(new Date());
				order.setOtype("wxPay");
				order.setPrepayId(prepayid);
				int count = vorderService.save(order);
				if (count != 1) {
					return JsonResult.error("新增订单失败");
				}

				System.out.println("新增订单成功");

				// 开始进行第二次签名
				resultMap.put("package", "prepay_id=" + prepayid);
				resultMap.put("signType", "HMAC-SHA256");
				// 这边要将返回的时间戳转化成字符串,不然小程序端调用wx.requestPayment方法会报签名错误
				resultMap.put("timeStamp", timeStamp + "");
				// 再次签名,这个签名用于小程序端调用wx.requesetPayment方法
				resultMap.put("appId", "wx0fb1dssfsf2544");
				String sign = WXPayUtil.generateSignature(resultMap, "xKLPpyJsdffjovptx1PuxqqIx");
				resultMap.put("paySign", sign);
				System.out.println("生成的签名paySign : " + sign);

				System.out.println("返回结果为" + resultMap);
				return JsonResult.successs(resultMap);
			} else {
				return JsonResult.error("失败了");
			}
		} catch (Exception e) {
			e.printStackTrace();
			return JsonResult.error("出错了");
		}
	}

到这一步,如果第一次签名(统一下单API)和第二次签名都成功的话,小程序前端就可以收到我们返回的5和参数啦,然后调用wx.requestPayment()接口就可以发起支付啦:

示例代码:

wx.requestPayment(
{
 'timeStamp': '',
 'nonceStr': '',
 'package': '',
 'signType': 'MD5',
 'paySign': '',
 'success':function(res){},
 'fail':function(res){},
 'complete':function(res){}
 })

然后用户界面会弹出支付确认,用户确认后,则支付流程就成功啦;这里可参考:小程序调起支付API

注意

1、上面所有的常量,我都是直接写在它应该出现的地方的,实际开发中,注意使用常量类统一管理,这些值都是我复制上来后,随手敲的假值,所以上下文有可能对应不上;

2、支付后微信会异步通知我们支付结果,通知的地址就是我们在统一下单时候,put进去的参数notify_url,这个url必须为外网可访问的url,不难理解;

3、我在支付这里被卡了很久,无论后台代码检查还是前台代码检查都没有问题,用户确认支付后一直显示,“支付签名验证失败”,网上百度很好几天,各种别人遇到的问题都排查了,比如“appid大小写问题”,“二次签名顺序问题”等等,最后不报希望地更换了一下加密算法(官方推荐,网上教程,默认全都是MD5算法),其实还有另外一种加密算法,即“HMAC-SHA256”加密算法,最后竟然生效了,支付过程顺利完成,所以如果有人跟我遇到相同的问题,在百度上面的方法都无效的时候,可以尝试我的方法,将加密算法修改为“HMAC-SHA256”,记住,除了自己的代码里,SDK中也需要修改,小程序前端发起支付时候的signType也需要跟着修改;

4、另外提醒一下:虽然小程序官方要求授权的域名必须是带https证书的,但是在开发过程中,忽略了https域名选项是完全可以支持开发和完成支付的,刚开始我遇到前面失败时候,各种方法尝试无果,一度怀疑是不是https域名没有落实的原因,实际正常,并不是,如果你也遇到了此问题,不要怀疑。

有了上面的代码,小程序微信支付环境就完成啦;由于篇幅原因,至于“异步回调通知”我就留着下篇博文讲述了,如果篇幅不长,我会将异步回调通知和“模板消息”在一篇博文里面讲述。

此致敬礼

jiguiquan@163.com

文章作者信息...

1 Comment

  • 跟着楼主的教程顺利搞定了,非常感激,想让楼主请我吃饭

留下你的评论

*评论支持代码高亮<pre class="prettyprint linenums">代码</pre>

相关推荐