需求
最近一直在忙的项目主要是app开发 其中涉及到支付开通会员选项
之前一直是使用的支付宝 但是最近需求新增了微信支付的功能
其实对于我个人而言 支付宝的支付比较好对接 微信的接口有点抽象 而且还有v2和v3两个版本
v2的主要是通过xml格式传输信息 v3是使用json格式传输信息
v2是不需要加密 只要在微信支付后台有v2Key就行 v3的话需要加密 还要有证书 有v3key
我本意是使用v2版本 但是我没找到示例代码 看一些教程又比较笼统 实在是磨不出来
没办法 只能去使用v3版本 看了微信的v3示例代码 又看了微信的api接口 终于app把微信支付弄好了 本篇 主要讲解微信的v3接口对接的流程
依赖
<dependency>
<groupId>com.github.wechatpay-apiv3</groupId>
<artifactId>wechatpay-apache-httpclient</artifactId>
<version>0.4.9</version>
</dependency>
微信接口分析
微信v3版本 因为涉及加密 所以需要在微信支付平台里面申请证书 v3密钥 申请证书的同时还有证书序列号也需要记录 微信证书申请下面保存到本地的话 解压后内容如下
其中key.pem为我们需要的密钥 其他都可忽略
然后我们需要的资料在代码中都体现一下 方便调用 (示例代码 一切从简)
其中商户私钥的话就是key.pem文件 打开全复制 黏贴到字符串中就行
public interface WXPayConstants {
String NOTIFY_URL ="";//支付成功回调地址
String MCH_ID ="";// 商户号
String MCH_SERIAL_NO="";//商户证书序列号
String API_V3KEY ="";// api3密钥
String App_ID="";//appid
String PACKAGE ="sign=WXPay";//签名固定字符串(微信要求的)
//你的商户私钥
String PRIVATE_KEY="-----BEGIN PRIVATE KEY-----\n" +
"-----END PRIVATE KEY-----";
}
微信V3接口流程
微信和支付宝的支付有区分的 微信的话 是先去创建预支付下单 然后微信返回给你一个预支付订单号 然后再去跳转到微信 去根据你的预支付订单号查询相关信息(金额,商户等等)然后去完成支付 那就先去和微信交互 创建预支付订单 微信在每次交互的时候还会检查证书的有效期 一般证书的有效期为2个小时 意思就是两次交互的时间间隔为2小时 证书失效 要先更新证书 微信v3支付的新版本 解决了这个问题 我这里不想再去翻资料了 就每次请求更新一下证书 按照道理来说 连接的client还要有个线程池 一切从简了 我们哪app也没多少日活 没必要
预支付后端
public R createOrder(Double money,Integer userId) throws IOException {
PrivateKey merchantPrivateKey =
PemUtil.loadPrivateKey(WXPayConstants.PRIVATE_KEY);
//使用自动更新的签名验证器,不需要传入证书
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(WXPayConstants.MCH_ID, new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)),
WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8));
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(WXPayConstants.MCH_ID, WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
.build();
HttpPost httpPost = new HttpPost("https://api.mch.weixin.qq.com/v3/pay/transactions/app"); //app下单地址
httpPost.addHeader("Accept", "application/json");
httpPost.addHeader("Content-type","application/json; charset=utf-8");
ByteArrayOutputStream bos = new ByteArrayOutputStream();
ObjectMapper objectMapper = new ObjectMapper();
//我们系统自己的订单号 时间戳 + 三位随机数字
String order_id = System.currentTimeMillis() + RandomUtil.randomInteger(3);
ObjectNode rootNode = objectMapper.createObjectNode();
//发起预支付订单需要的信息 mchid appid 描述 回调地址 自己的订单号
//amount里面是支付的钱数(以分为单位 传入整数) 还可以写支付币种等等
//其他参数就需要看微信支付api接口 参数必填有就行了 其他想要也可以填
//这里支付1分
rootNode.put("mchid",WXPayConstants.MCH_ID)
.put("appid", WXPayConstants.App_ID)
.put("description", "会员充值")
.put("notify_url", WXPayConstants.NOTIFY_URL)
.put("out_trade_no", order_id);
rootNode.putObject("amount")
.put("total", 1);
objectMapper.writeValue(bos, rootNode);
httpPost.setEntity(new StringEntity(bos.toString("UTF-8"), "UTF-8"));
//交互
CloseableHttpResponse response = httpClient.execute(httpPost);
//取得返回值
String bodyAsString = EntityUtils.toString(response.getEntity());
JsonNode jsonNode = objectMapper.readTree(bodyAsString);
// 这个地方特别抽象 他取出来的值 就是用双引号包裹好的 所以需要去掉双引号
// 因为加密的数据不能携带引号
//微信加密需要 使用应用id 时间戳 随机字符串 微信订单id 且用换行符隔开 且最后一个也需要换行符
String prepayId = String.valueOf(jsonNode.get("prepay_id"));
String substring = prepayId.substring(1, prepayId.length() - 1);
String time_mill = (System.currentTimeMillis() /1000)+"";
//随机32字符串
String str = RandomUtil.random(32);
StringBuffer sb = new StringBuffer();
//应用id
sb.append( WXPayConstants.App_ID).append("\n");
//时间戳
sb.append(time_mill).append("\n");
//随机字符串
sb.append(str).append("\n");
//微信订单id
sb.append(substring).append("\n");
System.out.println(sb);
//加密 使用密钥加密
String sign = sign(sb.toString().getBytes(),merchantPrivateKey);
//数据库实例化(略) 生成订单信息
// 返回前端的内容
Map<String,String> map=new HashMap<>();
map.put("appid",WXPayConstants.App_ID);
map.put("partnerid",WXPayConstants.MCH_ID);
map.put("prepayid", substring);
map.put("package","Sign=WXPay");
map.put("noncestr",str);
map.put("timestamp",time_mill);
map.put("sign",sign);
// 记得关闭
httpClient.close();
return R.ok(map);
}
static String sign(byte[] message, PrivateKey merchantPrivateKey) throws SignatureException, NoSuchAlgorithmException, InvalidKeyException {
Signature sign = Signature.getInstance("SHA256withRSA");
sign.initSign(merchantPrivateKey);
sign.update(message);
return Base64.getEncoder().encodeToString(sign.sign());
}
异常
java.security.InvalidKeyException: Illegal key size 、
这个加密解密的地方部分jdk 可能会报这种错误 去百度百度 看看就可以解决了
前端处理
//订单对象,从服务器获取
var orderInfo = {
"appid": "wx499********7c70e", // 应用ID(AppID)
"partnerid": "148*****52", // 商户号(PartnerID)
"prepayid": "wx202254********************fbe90000", // 预支付交易会话ID
"package": "Sign=WXPay", // 固定值
"noncestr": "c5sEwbaNPiXAF3iv", // 随机字符串
"timestamp": 1597935292, // 时间戳(单位:秒)
"sign": "aaaaaaaaaaaa" // 签名,v2这里用的 MD5 签名 v3是SHA256withRSA 转base64
};
//获取支付渠道
var wxpaySev = null;
plus.payment.getChannels(function(channels){
for (var i in channels) {
var channel = channels[i];
if (channel.id === 'wxpay') {
wxpaySev = channel;
}
}
//发起支付
plus.payment.request(wxpaySev, orderInfo, function(result) {
var rawdata = JSON.parse(result.rawdata);
console.log("支付成功");
}, function(e) {
console.log("支付失败:" + JSON.stringify(e));
});
}, function(e){
console.log("获取支付渠道失败:" + JSON.stringify(e));
});
校验
这个地方的校验是指 预支付下单的时候 你也可以使用微信的工具进行校验 看看你原始数据和微信加密的数据对不对 如果校验通过的话 那就和后端没关系了
微信校验工具
https://pay.wechatpay.cn/wiki/doc/apiv3/wechatpay/download/Product_5.zip
使用第三个 选择你的密钥(key.pem)上面输入这些信息 每个信息后面有一个换行符 最后一个也有 别忘了 下面输入你sign的加密内容 校验就可以了
需要注意的是不同支付方式的需要的加密信息也不同 具体需要以微信官方的api示例
具体需要查询官方地址
产品介绍 - JSAPI支付 | 微信支付商户文档中心 (qq.com)
支付结果的查询
最后呢 当然是支付结果的查询 所有支付里面都是 回调的 不是立即给你返回支付成功的信息
目前有两种解决方案 我们在配置微信支付的相关信息里面 会有回调地址 这个地址就是支付成功是微信给你发送信息的地方 但是好像微信会对信息进行加密 然后还需要解密 而且微信的第一次通知为15s 这个老板不是很满意的 所以我选择了第二种 第二种简单 但是吃性能 适合日活少的app 写法简单 粗暴 就是主动去微信里面查询 我的订单的支付状态
@Scheduled(cron = "0/10 * * * * *")
@Transactional(rollbackFor = Exception.class)
public void order() throws URISyntaxException, IOException {
PrivateKey merchantPrivateKey = PemUtil.loadPrivateKey(WXPayConstants.PRIVATE_KEY);
//使用自动更新的签名验证器,不需要传入证书
AutoUpdateCertificatesVerifier verifier = new AutoUpdateCertificatesVerifier(
new WechatPay2Credentials(WXPayConstants.MCH_ID, new PrivateKeySigner(WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)),
WXPayConstants.API_V3KEY.getBytes(StandardCharsets.UTF_8));
CloseableHttpClient httpClient = WechatPayHttpClientBuilder.create()
.withMerchant(WXPayConstants.MCH_ID, WXPayConstants.MCH_SERIAL_NO, merchantPrivateKey)
.withValidator(new WechatPay2Validator(verifier))
.build();
// 去查询所有没有支付状态的订单 我们系统设计里面 预下单为1 即订单初始化
//2 为订单结束(没有支付) 3为支付成功
List<WxPay> wxPays = wxPayMapper.selectByStateIsOne();
for (WxPay wxpay:wxPays) {
URIBuilder uriBuilder = new URIBuilder("https://api.mch.weixin.qq.com/v3/pay/transactions/out-trade-no/"+wxpay.getTitle()+"?mchid=商户号(定值)");
HttpGet httpGet = new HttpGet(uriBuilder.build());
httpGet.addHeader("Accept", "application/json");
CloseableHttpResponse response = httpClient.execute(httpGet);
String bodyAsString = EntityUtils.toString(response.getEntity());
System.out.println(bodyAsString);
JSONObject jsonObject = JSONObject.parseObject(bodyAsString);
//这是支付结果 根据自己系统去处理就行了
String result = jsonObject.get("trade_state").toString();
//* SUCCESS:支付成功
//* REFUND:转入退款
//* NOTPAY:未支付
//* CLOSED:已关闭
}
}
当然 也需要写一个超时的订单转状态的定时任务 这样的话 主动询问微信的订单也不会变得很多
@Scheduled(cron = "0 0 0/2 * * ?")
public void wad() {
log.warn("进入订单未支付的修改异常状态 定时任务");
//当前时间
Calendar calendar = Calendar.getInstance();
//减去2小时
calendar.add(Calendar.HOUR_OF_DAY, -2);
SimpleDateFormat dateFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
String format = dateFormat.format(calendar.getTime());
//更新两小时之前订单未支付的
wxPayMapper.updateStateFromCreateTime(format);
}
嘿嘿 最后 我们的app里面没有退款的功能 所有不需要写那么多 但是我看了一下 基本接口都是万变不离其宗 主要就是看看微信的接口 注意一些参数
希望诸君顺利
最后 我还是想吐槽一下微信
为什么v2的demo 我都找不到
为什么微信支付的技术客服我排队一上午还都在排队 全靠自己摸索
而且v2和v3之间的割裂感太强了