前言
项目里要用微信支付,然后就开始研究怎么做呗,经历了一段时间,踩了一堆的坑,终于可以正常的支付了。刚开始网页扫码支付,然后又是APP支付,小程序支付,需要的参数还不一样,参数一样了,参数名称不一样。所以,记录一下吧。
微信支付是需要证书的,这个得去商户平台获取吧,应该会有人给,windows需要安装,linux不需要安装的。
项目环境:springboot 1.5.10
一、微信支付JAVA sdk
去微信支付官网下载java版本的sdk,https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1,下载后解压得到java_sdk_v3.0.9。然后导入IDE,我用的是Eclipse。前端时间说是微信支付有大BUG,这个版本修复了不用太担心。
官方READEME.md文档提供的示例是实现WXPayConfig,来配置证书路径和其他秘钥,但是用起来不好用,所以我就直接把WXPayConfig这个类修改为接口(interfafce)了。这样就可以直接实现这个类来配置需要的东西了。
package com.github.wxpay.sdk;
import java.io.InputStream;
public interface WXPayConfig {
/**
* 获取 App ID
*
* @return App ID
*/
String getAppID();
/**
* 获取 Mch ID
*
* @return Mch ID
*/
String getMchID();
/**
* 获取 API 密钥
*
* @return API密钥
*/
String getKey();
/**
* 获取商户证书内容
*
* @return 商户证书内容
*/
InputStream getCertStream();
/**
* HTTP(S) 连接超时时间,单位毫秒
*
* @return
*/
public int getHttpConnectTimeoutMs();
/*public int getHttpConnectTimeoutMs() {
return 6*1000;
}*/
/**
* HTTP(S) 读数据超时时间,单位毫秒
*
* @return
*/
public int getHttpReadTimeoutMs();
/*public int getHttpReadTimeoutMs() {
return 8*1000;
}*/
/**
* 获取WXPayDomain, 用于多域名容灾自动切换
* @return
*/
IWXPayDomain getWXPayDomain();
/**
* 是否自动上报。
* 若要关闭自动上报,子类中实现该函数返回 false 即可。
*
* @return
*/
public boolean shouldAutoReport();
/*public boolean shouldAutoReport() {
return true;
}*/
/**
* 进行健康上报的线程的数量
*
* @return
*/
public int getReportWorkerNum();
/*public int getReportWorkerNum() {
return 6;
}*/
/**
* 健康上报缓存消息的最大数量。会有线程去独立上报
* 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受
*
* @return
*/
public int getReportQueueMaxSize();
/*public int getReportQueueMaxSize() {
return 10000;
}*/
/**
* 批量上报,一次最多上报多个数据
*
* @return
*/
public int getReportBatchSize();
/*public int getReportBatchSize() {
return 10;
}*/
}
修改完之后,右键运行,maven install,jar就打入本地仓库了。使用时直接在pom文件引用就可以了。
<dependency>
<groupId>com.github.wxpay</groupId>
<artifactId>wxpay-sdk</artifactId>
<version>3.0.9</version>
</dependency>
二、配置类
1、新建PayConfig.java,用来存放自己的证书路径,秘钥等。
public class PayConfig {
/**扫码,app 公众账号ID */
public static final String APP_ID = "wx..";
/**商户号*/
public static final String MCH_ID = "";
/**API秘钥*/
public static final String KEY = "..";
/**小程序ID*/
public static final String JS_APP_ID = "wx...";
/**小程序秘钥,用于获取openid*/
public static final String JS_APP_SECRET = "...";
/***模式一回调URL*/
public static final String NOTIFY_URL="https://api.yourdomain/weixin/notifyurl";
/**证书路径*/
public static String CERT_PARTH = "E:\\wechatlicense\\cert01\\1516361741_20181106_cert.p12";
/**linux系统下微信支付证书路径*/
//public static String CERT_PARTH = "/var/ca/wppl/1516361741_20181106_cert.p12";
}
注意key是API秘钥,不是SecretKey
2、新建MyConfig.java 实现 WXPayConfig.java,该类主要用于web扫码支付和APP支付,其中的配置引用了Payconfig中配置的常量。
public class MyConfig implements WXPayConfig {
private byte[] certData;
public MyConfig() throws Exception {
String certPath = PayConfig.CERT_PARTH;
File file = new File(certPath);
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
}
@Override
public String getAppID() {
return PayConfig.APP_ID;
}
@Override
public String getMchID() {
return PayConfig.MCH_ID;
}
@Override
public String getKey() {
return PayConfig.KEY;
}
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
public int getHttpConnectTimeoutMs() {
return 8000;
}
public int getHttpReadTimeoutMs() {
return 10000;
}
public IWXPayDomain getWXPayDomain() {
return WXPayDomainSimpleImpl.instance();
}
public boolean shouldAutoReport() {
return false;
}
public int getReportWorkerNum() {
return 6;
}
public int getReportQueueMaxSize() {
return 10000;
}
public int getReportBatchSize() {
return 10;
}
}
3、新建MyJSConfig.java 实现 WXPayConfig,该类主要是用于小程序支付,和扫码支付、APP支付有几处配置不一样。为了省事就写了两个类,也可以判断下,不同条件赋值不同即可。主要不同在于AppID。
public class MyJSConfig implements WXPayConfig {
private byte[] certData;
public MyJSConfig() throws Exception {
String certPath = PayConfig.CERT_PARTH;
File file = new File(certPath);
InputStream certStream = new FileInputStream(file);
this.certData = new byte[(int) file.length()];
certStream.read(this.certData);
certStream.close();
}
@Override
public String getAppID() {
return PayConfig.JS_APP_ID;
}
@Override
public String getMchID() {
return PayConfig.MCH_ID;
}
@Override
public String getKey() {
return PayConfig.KEY;
}
@Override
public InputStream getCertStream() {
ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
return certBis;
}
public int getHttpConnectTimeoutMs() {
return 8000;
}
public int getHttpReadTimeoutMs() {
return 10000;
}
public IWXPayDomain getWXPayDomain() {
return WXPayDomainSimpleImpl.instance();
}
public boolean shouldAutoReport() {
return false;
}
public int getReportWorkerNum() {
return 6;
}
public int getReportQueueMaxSize() {
return 10000;
}
public int getReportBatchSize() {
return 10;
}
}
4、以上两个配置类都要返回一个IWXPayDomain ,这个在sdk中是一个接口,所以我们就需要实现一下它再返回。代码有点多,就不贴出来了,具体的可以看我的码云https://gitee.com/waynelee/weixinpay/blob/master/src/main/java/com/yuyi/lwq/wxpay/WXPayDomainSimpleImpl.java。这样所有的配置就基本完成了。
三、封装微信支付客户端
微信支付sdk中有一个类WXPay.java,用它就可以直接访问微信后台进行支付、退款等操作,所以就不用自己写工具类了。然后我在此基础上又封装了一层客户端用来处理不同客户端的支付请求。通过from参数来判断支付请求的来源,封装不同的WxPay请求微信后台。订单支付状态的查询,退款,退款查询都很简单,封装好请求参数,直接请求就可以了。
public class WXPayClient {
/**统一下单,模式
* @param from 支付来源
* @param openid 小程序专用
* @param out_trade_no 订单号
* @param body 商品
* @param total_fee 金额(分)
* @param attach 自定义字段
* @return
* @throws Exception
*/
public static Map<String, String> getOrder(Integer from,String openid,String out_trade_no,String body, String total_fee,String attach) throws Exception {
MyConfig config = new MyConfig();
WXPay wxpay = null;
//商户信息
Map<String,String> param=new HashMap<String, String>();
if (Objects.equals(from,2)) {
//app支付
param.put("trade_type","APP");//交易类型
wxpay = new WXPay(config);
}else if (Objects.equals(from,1)) {
//网页扫码支付
param.put("trade_type","NATIVE");//交易类型
wxpay = new WXPay(config);
}else if(Objects.equals(from,3)){
if (!StringUtils.isEmpty(openid)) {
//小程序支付
param.put("trade_type","JSAPI");//交易类型
param.put("openid",openid);
MyJSConfig jsConfig = new MyJSConfig();
wxpay = new WXPay(jsConfig);
}
}else{
throw new RuntimeException("支付来源错误");
}
param.put("body",body);//商品描述
param.put("out_trade_no",out_trade_no);//订单号
param.put("total_fee",total_fee);//价格
param.put("spbill_create_ip","127.0.0.1");//终端IP
param.put("notify_url", PayConfig.NOTIFY_URL);//通知地址
param.put("attach",attach);
Map<String, String> resp = wxpay.unifiedOrder(param);
return resp;
}
/**关闭订单
* @param out_trade_no
* @return
* @throws Exception
*/
public static Map<String, String> closeOrder(String out_trade_no) throws Exception{
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);
//商户信息
Map<String,String> param=new HashMap<String, String>();
param.put("out_trade_no",out_trade_no);//订单号
Map<String, String> resp = wxpay.closeOrder(param);
return resp;
}
/**查询订单
* @param out_trade_no
* @return
* @throws Exception
*/
public static Map<String, String> queryOrder(String out_trade_no) throws Exception{
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);
//商户信息
Map<String,String> param=new HashMap<String, String>();
param.put("out_trade_no",out_trade_no);//订单编号
Map<String, String> resp = wxpay.orderQuery(param);
return resp;
}
/**退款
* @param out_trade_no
* @param out_refund_no
* @param total_fee
* @param refund_fee
* @return
* @throws Exception
*/
public static Map<String, String> refund(String out_trade_no,String out_refund_no, String total_fee, String refund_fee) throws Exception{
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);
//商户信息
Map<String,String> param=new HashMap<String, String>();
param.put("out_trade_no",out_trade_no);//订单编号
param.put("out_refund_no",out_refund_no);//退款编号
param.put("total_fee",total_fee);//订单金额
param.put("refund_fee",refund_fee);//退款金额
param.put("notify_url", PayConfig.NOTIFY_URL);//通知地址
Map<String, String> resp = wxpay.refund(param);
return resp;
}
/**退款查询
* @param out_trade_no
* @return
* @throws Exception
*/
public static Map<String, String> refundQuery(String out_trade_no) throws Exception{
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);
//商户信息
Map<String,String> param=new HashMap<String, String>();
param.put("out_trade_no",out_trade_no);//商户订单号
Map<String, String> resp = wxpay.refundQuery(param);
return resp;
}
/**退款查询
* @param out_trade_no
* @return
* @throws Exception
*/
public static Map<String, String> refundQueryByRefund(String out_refund_no) throws Exception{
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);
//商户信息
Map<String,String> param=new HashMap<String, String>();
param.put("out_refund_no",out_refund_no);//商户退款号
Map<String, String> resp = wxpay.refundQuery(param);
return resp;
}
/**下载对账单
* @param bill_date
* @return
* @throws Exception
*/
public static Map<String, String> downloadBill (String bill_date) throws Exception{
MyConfig config = new MyConfig();
WXPay wxpay = new WXPay(config);
//商户信息
Map<String,String> param=new HashMap<String, String>();
//param.put("nonce_str",WXPayUtil.generateNonceStr());//随机字符串
param.put("bill_date",bill_date);
param.put("bill_type","ALL");
//param.put("tar_type", "GZIP");
Map<String, String> resp = wxpay.downloadBill(param);
return resp;
}
}
四、使用
在controller层直接调用就可以进行微信的支付了。from为支付来源参数,web扫码传1,APP传2,小程序传3。openid是小程序需要传的参数,这个后面会说明。ResultTool和ResultBO是自己封装的返回信息,和ResponseEnity差不多意思。
@PostMapping("/pay")
public ResultBO<Object> pay(@RequestParam Integer from,
@RequestParam(defaultValue="") String openid,
@RequestParam String out_trade_no,
@RequestParam String body,
@RequestParam String total_fee) throws Exception{
//对参数的限制逻辑
Map<String, String> order = WXPayClient.getOrder(from, openid, out_trade_no,body,total_fee,"attach");
log.info("{}",order);
String returnCode = order.get("return_code");
String resultCode = order.get("result_code");
String errCodeDes = order.get("err_code_des");
//请求微信后台失败
if (Objects.equals(returnCode,"FAIL")) {
log.info(errCodeDes);
//失败后的处理。
}
if (Objects.equals(resultCode, "FAIL")) {
//失败后的处理,
}
//请求成功
if (Objects.equals(resultCode,"SUCCESS")) {
//根据不同的客户端构造不同的返回参数
Map<String, String> param = getResponse(from, order);
return ResultTool.success(param);
}
return ResultTool.success();
}
当请求微信后台成功后就可以根据不同的客户端封装不同的返回参数了,也就是上面的getresponse方法。
/**
* 针对不同的客户端构造不同的返会信息
* @param from
* @param order
* @return
* @throws Exception
*/
public static Map<String, String> getResponse(Integer from,Map<String,String> order) throws Exception{
String js_app_id = PayConfig.JS_APP_ID;
String app_id = PayConfig.APP_ID;
String partnerid = PayConfig.MCH_ID;
String nonce_str = order.get("nonce_str");
String prepay_id = order.get("prepay_id");
String timeStamp = String.valueOf(new Date().getTime());
timeStamp = timeStamp.substring(0,timeStamp.length()-3);
if (Objects.equals(from, 2)) {
//app支付
Map<String,String> param = new HashMap<String, String>();
param.put("appid",app_id);
param.put("partnerid", partnerid);
param.put("prepayid", prepay_id);
param.put("noncestr", nonce_str);
param.put("timestamp", timeStamp);
param.put("package", "Sign=WXPay");
String sign = WXPayUtil.generateSignature(param, PayConfig.KEY, SignType.HMACSHA256);
param.put("sign", sign);
return param;
}else if(Objects.equals(from, 1)){
//扫码支付
String content = order.get("code_url");
Map<String,String> param = new HashMap<String, String>();
param.put("codeUrl", content);
return param;
}else if(Objects.equals(from, 3)){
//小程序支付
Map<String,String> param = new HashMap<String, String>();
param.put("appId", js_app_id);
param.put("timeStamp",timeStamp);
param.put("nonceStr",nonce_str);
param.put("package", "prepay_id="+prepay_id);
param.put("signType", "HMAC-SHA256");
String sign = WXPayUtil.generateSignature(param, PayConfig.KEY,SignType.HMACSHA256);
param.put("paySign", sign);
return param;
}
return null;
}
在这里,一定要看清楚微信支付官方文档对放回参数要求,以及生成签名的要求,尤其注意加密方式!!!
网页扫码支付,前端程序员根据codeUrl生成二维码让用户扫码支付就是了。APP和小程序支付,你只要返回了这些参数,他们就会自己发起微信客户端的支付了。
上面说到小程序支付还需要一个openid参数,这个参数是后台请求微信后台拿到的,返给小程序就是了,让他请求的时候带着这个参数。
@GetMapping("/openid")
public ResultBO<Object> getOpenId(@RequestParam String code)throws Exception{
JSONObject response = restTemplate.getForObject(
"https://api.weixin.qq.com/sns/jscode2session?appid="+PayConfig.JS_APP_ID
+"&secret="+PayConfig.JS_APP_SECRET+"&js_code="+code+"&grant_type=authorization_code",
JSONObject.class);
if (response.containsKey("openid")) {
String openid = response.getString("openid");
String session_key = response.getString("session_key");
JSONObject json = new JSONObject();
json.put("openid",openid);
json.put("session_key", session_key);
return ResultTool.success(json);
}
return ResultTool.success();
}
code参数是js_code,写小程序的程序员知道是什么,restTemplate是注入的RestTempalte,可自行百度使用方法。
至此,三种客户端的支付应该都可以了。但是后续的订单支付状态还要进行操作,微信支付提供了支付结果的异步通知,通知后台程序用户是否支付成功。但是,我们还是要先主动查询微信后台订单状态,支付成功后再提示用户后续操作。
具体流程是返给前端codeUrl生成二维码后,前端轮训后台订单查询API,当查询到订单支付成功后在返给前端成功,同时在成功时修改数据库订单状态,这里也可以只做订单支付装态的查询,不做数据库订单状态的修改,订单状态的修改放在异步通知里。
@PostMapping("/orderquery")
public ResultBO<Object> orderQuery(@RequestParam String out_trade_no) throws Exception{
Map<String, String> queryOrder = WXPayClient.queryOrder(out_trade_no);
String returnCode = queryOrder.get("return_code");
String resultCode = queryOrder.get("result_code");
String errCodeDes = queryOrder.get("err_code_des");
log.info("{}",queryOrder);
/**请求失败!!*/
if (Objects.equals(returnCode,"FAIL")) {
log.info(errCodeDes);
}
if (Objects.equals(resultCode,"FAIL")) {
log.info(errCodeDes);
}
//请求成功
if (Objects.equals(queryOrder.get("result_code"),"SUCCESS")) {
String state = queryOrder.get("trade_state");
/**1、订单已关闭*/
if (Objects.equals(state,"CLOSED")) {
//
}
if (Objects.equals(state,"REFUND")) {
//
}
if (Objects.equals(state,"NOTPAY")) {
//
}
if (Objects.equals(state,"SUCCESS")) {
//订单支付状态的修改
//数据库修改成功后告诉前端,支付成功了
return ResultTool.success();
}
}
return ResultTool.success();
}
异步通知的参数是数据流,先读取一下字节数组,转成String字符串,通过官方工具转为Map集合,然后再验证签名!!签名正确就可以调用service去修改数据库的订单状态了,注意,更新订单状态时一定要判断异步通知返回的金额和数据库的金额是否相等,不然可能会造成一毛买一千的东西哦。退款的异步通知的数据时加密,需要解密,具体的可以参考我的码云。
@RestController
@RequestMapping("/weixin")
public class NotifyController {
private Logger logger = LoggerFactory.getLogger(NotifyController.class);
@PostMapping("/notifyurl")
public String notifyUrl(HttpServletRequest request) throws Exception{
/**获取微信后台通知结果*/
InputStream inStream = request.getInputStream();
ByteArrayOutputStream outSteam = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = 0;
while ((len = inStream.read(buffer)) != -1) {
outSteam.write(buffer, 0, len);
}
outSteam.close();
inStream.close();
String result = new String(outSteam.toByteArray(),"utf-8");
Map<String, String> map = WXPayUtil.xmlToMap(result);
logger.info("异步通知:{}",map);
/**定义返给微信后台的成功字段*/
Map<String,String> res = new HashMap<String, String>();
res.put("return_code", "SUCCESS");
res.put("return_msg", "OK");
String resStr = WXPayUtil.mapToXml(res);
/**通过返回的信息里是否有“req_info”来判断是付款还是退款*/
String req_info = map.get("req_info");
/**1、没有req_info,则是付款*/
if (Objects.equals(req_info, null)) {
/**校验签名是否正确*/
boolean is = WXPayUtil.isSignatureValid(map,PayConfig.KEY,SignType.HMACSHA256);
if (Objects.equals(is,true)) {
logger.info("签名正确");
/**获取标识*/
//String attach = map.get("attach");
//订单状态更新
}else{
logger.info("签名不正确");
return resStr;
}
}
/**2、有req_info则是退款*/
if (!Objects.equals(req_info, null)) {
/*
String req_info_Str = map.get("req_info");
String key = EncryptAndDecryptTools.StrToBase64Str(WXPayUtil.MD5(PayConfig.KEY).toLowerCase());
String decrypt = EncryptAndDecryptTools.decrypt(req_info_Str, key);
Map<String, String> xmlToMap = WXPayUtil.xmlToMap(decrypt);
System.out.println(xmlToMap);*/
return resStr;
}
return resStr;
}
}
异步通知的路径也可以当做参数,设置多个,我为了省事,就写死了,通过自定义参数attach判断是什么订单支付,再调用相应的service修改订单状态。
退款,退款查询不是太困难,就不再赘述了,可以参考我的码云。
至此,微信支付完成。
写在最后
写这些代码,也不是几天的事情,都是一遍又一遍尝试的,希望对别人有点帮助,不要踩那么多的坑。