其实微信支付这一块,真的是个翻来覆去讲烂了的话题,甚至我在去年也写过一个帖子用来记录。但是之所以还要写这篇文章,主要一来通过最近的使用对微信支付这块了解的更加深刻,其次也是这次用到了退款的功能,这个算是新鲜点,所以想来想去决定再从头整理一下。
结合实际说的话,我这次项目是小程序,所以支付是略有不同,我尽量一点点剥开了说。
第一步:看官网介绍
首先肯定是要从微信的开发文档说起,这里附上传送门:
微信支付开发文档——普通商户版
我个人是觉得微信的文档越来越好了,再也不是三年前一提起微信就脑壳痛的时候了。咱们继续这个页面说,虽然都是微信支付,但是也有不同的方式,我上面已经说了我在做的这个项目是小程序,所以毫不犹豫的选择小程序支付,其实各种支付方式大同小异,尤其是小程序支付走的类型也是JSAPI,所以两者没什么区别。而且有个小技巧(目前是2020/7/17,反正现在为止小程序付款是没有sdk的,但是别的付款方式是有的,所以可以把别的付款方式中的sdk下下来这里用)。
第二步:引入sdk
当然了,我习惯性的用maven直接引入依赖。
之前查了下资料,其实能直接引用的有好几种表现形式,我这里只列出我自己用的:
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>0.0.3</version>
</dependency>
说一下这个包,其实是这么引用还是把sdk中的几个工具类自己写出来我觉得都ok吧,我也特意和sdk中的代码对比了下,是一样的。


其实我看了一些网上的demo,我更愿意理解为时间太久了(有的哪怕是新发布的,但是可能代码年限很高了),居然自己写各种工具方法的占了一多半,挺费解的。所以我这里再次强调:微信已经有了非常好用的sdk,真没必要自己写!
第三步:逻辑流程
其实在有sdk的协助下,代码的实现变得很简单了。大概的步骤我简单说一下。主要也是要对这个支付,回调等流程心里有数。
微信的文档其实挺好的,就是有点复杂(可能是我有时候心烦意乱只想速成,所以没耐心一点点顺着图分析理解)。这里说一下JSAPI/小程序支付的大概流程:
-
预下单(有了下单的动作,计划好支付多少钱了。然后微信会照着已给的信息去生成那个支付页面,如下图)
支付页面
我简单说下,这个最上面的支付0.01元的页面,就是你先告诉微信,openId是XXX(你自己)的用户想给XXXX企业支付0.01元钱。你先把所有的信息告诉微信,微信觉得没问题了(其中检查你的openid,企业的情况啥的)就会生成这个弹窗页面。
所以说预下单其实可以理解为信息的提交与校验,没问题了就跳到微信页面(ps:这个页面就不属于前端的控制范围了,但是微信也提供一些接口可以关闭啥的。不过我们没做这个操作)。
-
回调
刚刚已经说了预下单之后,会弹出一个微信提供的页面,但是到底用户支付没支付要怎么判断呢?这里就用到了回调。
官网也有关于回调地址的详细要求,其实本质上就是提供一个接口,微信会把支付的情况告诉你,但是这个接口必须是外网的,必须是https的,这个回调地址可以在微信设置。这两个要求不复杂,但是让本地没办法测试了。反正代码简单,所以仔细点可以一次过。 -
退款
这个功能就是某单的钱到了商家的微信账户里,根据订单号或者微信流水号就可以退款了。当然也有一些限制的,比如微信账户要有钱,比如订单不能超过一年。当然了,因为我们这个业务需求都不会涉及这些,所以直接用订单号退款了。这个差不多就是秒退,如果有问题直接接口会报错,所以更简单了。一会儿我会附上代码。
第四步:代码实现
终于到了最重要的时候了,代码的实现。


而这个demo代码在readme里:

然后其实照着这个来做就行。简单说第一步:配置参数:
需要的参数有证书,appId,key,mchId。下面是我的配置类:
package com.dsyl.util;
import java.io.ByteArrayInputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.InputStream;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
import java.util.Map.Entry;
import com.github.wxpay.sdk.WXPay;
import com.github.wxpay.sdk.WXPayConfig;
public class PayConfig implements WXPayConfig{
private byte[] certData;
public PayConfig() throws Exception {
File file = new File("C:\\Program Files\\apache-tomcat-8.5.38\\cert\\apiclient_cert.p12");
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
}
public String getAppID() {
return "wx8888888888888";
}
public String getMchID() {
return "XXXXXXXXX";
}
public String getKey() {
return "xxxxxxxxxxxxxxxxxxxxxxxx";
}
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
public int getHttpConnectTimeoutMs() {
return 8000;
}
public int getHttpReadTimeoutMs() {
return 10000;
}
}
第二步就是调用了。首先说支付的调用(其实说真的,差不多就是readme百分之八十复制粘贴):
public R pay(String openId,Integer money){
try {
String no = (openId + "-" + money + "-" + System.currentTimeMillis()+"abcdefghigklmn").substring(0,32);
PayConfig config = new PayConfig();
WXPay wxpay = new WXPay(config);
Map<String, String> data = new HashMap<String, String>();
data.put("body", "支付");
data.put("out_trade_no", no);
data.put("openid", openId);
data.put("total_fee", (money*100)+"");
data.put("spbill_create_ip", "127.1.1.1");
data.put("notify_url", "https://www.hongchunyan.com/crowd-funding/wxpay/notify");
data.put("trade_type", "JSAPI");
Map<String, String> resp = wxpay.unifiedOrder(data);
String returnCode = resp.get("return_code");
String resultCode = resp.get("result_code");
String prepay_id = resp.get("prepay_id");
if (!"SUCCESS".equals(returnCode) || !"SUCCESS".equals(resultCode) || prepay_id == null) {
return R.error().put("code", 300).put("data", resp);
}
String packages = "prepay_id=" + prepay_id;
Map<String, String> wxPayMap = new HashMap<String, String>();
wxPayMap.put("appId", config.getAppID());
wxPayMap.put("timeStamp", String.valueOf(System.currentTimeMillis()).substring(0, 10));
wxPayMap.put("nonceStr", WXPayUtil.generateNonceStr());
wxPayMap.put("package", packages);
wxPayMap.put("signType", "MD5");
// 加密串中包括 appId timeStamp nonceStr package signType 5个参数, 通过sdk WXPayUtil类加密,
// 注意, 此处使用 MD5加密 方式
String sign = WXPayUtil.generateSignature(wxPayMap, config.getKey());
Map<String, Object> result = new HashMap<String, Object>();
result.put("prepay_id", prepay_id);
result.put("paySign", sign);
result.putAll(wxPayMap);
return R.ok().put("data", result);
} catch (Exception e) {
e.printStackTrace();
return R.error();
}
}
简单的说几点:
- 这里的out_trade_no要求是唯一的,其实用uuid也ok,我这里因为想用它存一些业务逻辑,所以是自己想要存的东西的拼接(用了时间戳保证唯一性)。
- 只有当支付方式是JSAPI(小程序支付也这么填)的时候才需要openId。不同的支付方式参数略有不同。这个稍微注意一下就行了。
- 我返回的这个R其实就是自己封装的一个返回对象,本质就是键值对。这个可以自由点。
- 这其中生成sign的方法是sdk里的,网上好多人自己写了,我比较喜欢拿现成的。
差不多预下单就这样了。
然后上面有个notify_url就是回调的接口地址了,必须是外网可访问的,必须是https的。
下面是回调接口,他的本质就是一个对外的接口,所以路径要对得上:
@RestController
@RequestMapping("/wxpay")
public class WXController {
@PostMapping("/notify")
public String wxnotify(HttpServletRequest request,HttpServletResponse response) {
String xmlBack = "";
try {
BufferedReader br = new BufferedReader(new InputStreamReader(request.getInputStream(),"UTF-8"));
String line = null;
StringBuilder sb = new StringBuilder();
while((line = br.readLine()) != null){
sb.append(line);
}
br.close();
//sb为微信返回的xml,微信sdk提供转化成map的方法
String notityXml = sb.toString();
System.out.println("接收到的报文:" + notityXml);
Map<String, String> map = WXPayUtil.xmlToMap(notityXml);
WXPay wxpay = new WXPay(new PayConfig());
if (wxpay.isPayResultNotifySignatureValid(map)) {//验证成功
String return_code = map.get("return_code");//状态
String out_trade_no = map.get("out_trade_no");//订单号
String result_code = map.get("result_code");
//其实这里验证两个code都是success就说明充值成功了。可以处理自己的业务逻辑了。
if (!"SUCCESS".equals(return_code) || !"SUCCESS".equals(result_code)) {
System.out.println("充值失败!");
//支付失败的逻辑
xmlBack = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
return xmlBack;
}
xmlBack = "<xml>" + "<return_code><![CDATA[SUCCESS]]></return_code>" + "<return_msg><![CDATA[SUCCESS]]></return_msg>" + "</xml> ";
return xmlBack;
} else {
System.out.println("<<<<<<<<<<<<<<<<<<<<<微信支付回调通知签名错误");
xmlBack = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
return xmlBack;
}
} catch (Exception e) {
System.err.println(e);
System.out.println("微信支付回调通知失败");
xmlBack = "<xml>" + "<return_code><![CDATA[FAIL]]></return_code>" + "<return_msg><![CDATA[报文为空]]></return_msg>" + "</xml> ";
return xmlBack;
}
}
}
这个给微信的返回值是为了防止重复回调。当你给微信返回了,微信就知道关于这个订单的回调你收到了,也就不会再给你发了(上面代码中涉及到业务逻辑的代码我都删了,因为删删减减的,所以不确定是不是多个括号少个括号啥的,如果报错了检查下代码吧)。
最后还有一个退款,因为当时就有结果了,所以比较简单,我直接附上代码:
public static Map<String, String> refund(String transactionId,Integer money) throws Exception{
PayConfig config = new PayConfig();
WXPay wxpay = new WXPay(config);
Map<String, String> params = new HashMap<String, String>();
params.put("appid", config.getAppID());
params.put("mch_id", config.getMchID());
params.put("nonce_str", System.currentTimeMillis()+"");
params.put("transaction_id",transactionId);
params.put("out_refund_no", System.currentTimeMillis()+"");
params.put("total_fee", (money*100)+"");
params.put("refund_fee", (money*100)+"");
String sign = WXPayUtil.generateSignature(params, config.getKey());
params.put("sign", sign);
Map<String, String> refund = wxpay.refund(params);
System.out.println(refund);
return refund;
}
注意因为我这里是退全款,所以两个fee是一样的。还有关于钱转化成分。我第一遍测试,因为一边乘100.一边忘记乘了,所以退款失败了,会报错金额不同。
微信退款页介绍
如果遇到什么退款失败可以去查查失败的原因,还是很容易理解的。
然后至此,我这两天这块关于微信支付的业务就写完了,比较简单,而且感觉对微信支付的了解也更深了一点。
本篇文章就记到这里,如果稍微帮到你了记得点个喜欢点个关注,如果有什么问题也欢迎私信或者留言交流。也祝大家工作顺顺利利!周末愉快!java技术交流群130031711,欢迎各位踊跃加入!
网友评论