由于上一个项目的小程序支付模块的历练,让我意识到支付确实是一个复杂且测试起来需要的配置特别复杂的模块,这么说吧,学生想要实打实的测试微信支付太难了,它需要你有企业的相关证明,营业执照呀,公众号商户号,不论资质是否满足办理条件,单论开户费用就是几百块的收,但是微信和支付宝都提供了沙箱环境,对于沙箱这个东西,我真的没什么话说!bug很多,大家经历过的可以喷一喷。所幸甲方提供了开发所需要的一些参数与配置(商户号的一些信息,Apv3密钥与商户id,商户证书等)。那么现在不喷别的了,我们来看一些微信支付的开发文档:
一、首先需要选取商户的类型(区别最大的就是资金流的流动):
1、直连商户(即资金流与信息流直接与我们的个体商户进行流动)
2、服务商(类似于加盟形式,资金流先中转到中间的服务商商户再定时转入加盟的子商户)
需要注意的是不同的商户类型开发的模式也是有一定的区别,特别是参数需求的不同,如果误以为二者的开发模式一样,可能会导致后续的支付返回信息是参数校验不正确
二、选取好类型后我们来配置参数(这里我们以直连商户开聊):
主要的参数为:mchid(商户号),mchSerialNo(商户证书序列号),apiV3Key(商户配置的apiv3密钥),PrivateKey(证书私钥),服务器以及配置好的域名(微信后台是需要频繁与服务器进行数据交换的)。
下面是微信官方给出的文档供大家参考:
JSAPI支付-接入前准备 | 微信支付商户平台文档中心 (qq.com)https://pay.weixin.qq.com/wiki/doc/apiv3/open/pay/chapter2_1.shtml
三、了解流程并分析哪些事情是服务器端需要做的?哪些是小程序需要做的?
先看一下时序图(来源:微信开发者社区):
分析流程(注意这里我们用服务器来代表后端,序号不代表处理顺序):
1、小程序对服务器发起下单请求(携带业务参数如商品信息以及由wx.login()得到的临时凭证code)
2、服务器接收参数,生成业务订单插入数据库记录并带着code去请求微信登录API得到openid(openid是每一个微信用户使用小程序时获取的唯一身份标识,永久唯一且在登录小程序之后不变)
3、服务器根据商品信息结合openid以微信规定的参数格式生成用户订单
4、带着订单数据进行一次签名的验证之后请求支付统一下单API:
https://api.mch.weixin.qq.com/v3/pay/transactions/jsapi
获取统一预支付会话prepay_id
5、服务器将获得的prepay_id根据微信需要的数据格式将它与其他参数加载签名之后得到sign并再次封装时间戳,sign,随机字串,小程序appid,包含prepay_id的签名,将封装之后的数据发送给小程序。
6、小程序鉴权支付之后,微信后台会发送一条支付成功的信息给服务器确认处理。
7、服务器端更新业务订单的状态。
四、设计关键点的处理思路,以及根据微信官方给的文档设计签名验证,请求发送,封装数据,生成签名,获取随机字符串等工具类。
这里我处理的关键点是openid的获取与prepay_id的获取:因为两次都需要请求微信后台并实现服务器端与微信后台的交互,其次服务器端的二次签名如果出现问题会产生小程序支付时参数验证错误的报错。
(1)看一下获取openid我的处理代码:
/**
* 为保护APPSecret信息,需要将这次请求放在后台进行
* @param AppId 小程序Id
* @param code 换取的临时凭证
* @return 带有openId的通用对象
*/
@RequestMapping("/WxLogin")
@ResponseBody
public R WeChatLogin(@RequestParam("AppId")String AppId,@RequestParam("code")String code){
try{
System.out.println("---------入库查询小程序secret---------");
String servet = appInfoService.findAppServetByAppId(AppId);
System.out.println("---------封装访问路由获取小程序唯一openId-------");
String Url = "https://api.weixin.qq.com/sns/jscode2session" +
"?appid="+AppId+"&" +
"secret="+servet+"&" +
"js_code="+code+"&grant_type=authorization_code";
System.out.println("封装路由为"+Url);
BasicHttpClientConnectionManager connectionManager;
connectionManager = new BasicHttpClientConnectionManager(
RegistryBuilder.create().
register("http", PlainConnectionSocketFactory.getSocketFactory())
.register("https", SSLConnectionSocketFactory.getSocketFactory())
.build(),null,null,null
);
CloseableHttpClient httpClient = HttpClientBuilder.create().
setConnectionManager(connectionManager)
.build();
HttpGet httpGet = new HttpGet(Url);
try{
System.out.println("客户端连接成功,执行请求----------");
HttpResponse httpResponse = httpClient.execute(httpGet);
HttpEntity entity = httpResponse.getEntity();
String s = EntityUtils.toString(entity, "UTF-8");
System.out.println("请求执行结束,结果为"+ s);
JSONObject jsonObject = new JSONObject(s);
String openid = jsonObject.get("openid").toString();
R r= new R<>();
r.setData(openid);
r.setCode(1);
r.setMsg("登录验证");
return r;
}catch (Exception e){
e.printStackTrace();
}
}catch (Exception e){
return R.error("登录异常");
}
return R.error("登录错误");
}
不难发现我将敏感的关键信息都放在了服务器处理,将openid返回给小程序,后面为了能够不需要在支付时再发起请求获取一次openid,我将openid存储在小程序的全局参数里以供本次会话范围内调用。(当然为了之后的登录不再访问微信后台获取openid,我也会将openid存储在数据库该用户的表里)
(2)看一下获取prepay_id我的处理代码:
/**
* 用户普通订单支付后回调
* 修改设备信息以及在订单记录中插入一条记录
*
* @param receipt 支付金额
* @param deviceId 设备编号
* @param userId 用户编号
* @param useTime 使用自习室时长
* @return 统一响应对象
*/
@RequestMapping("/insertUserDevice")
@ResponseBody
/**设置事务回滚(一般用于多表操作,单表使用异常捕获就可以实现)*/
@Transactional(rollbackFor = Exception.class)
public R