配置pom.xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<groupId>com.zhangye</groupId>
<artifactId>wxpay</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>wxpay</name>
<packaging>jar</packaging>
<description>Demo project for Spring Boot</description>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.0.3.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
<commons-lang3.version>3.7</commons-lang3.version>
<commons-collections.version>3.2.2</commons-collections.version>
<com.google.zxing.version>3.3.3</com.google.zxing.version>
<fastjson.version>1.2.46</fastjson.version>
</properties>
<dependencies>
<!-- mvc支持-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-thymeleaf</artifactId>
</dependency>
<!-- 热部署模块 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-devtools</artifactId>
<optional>true</optional>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<!-- Commons utils begin -->
<dependency>
<groupId>org.apache.commons</groupId>
<artifactId>commons-lang3</artifactId>
<version>${commons-lang3.version}</version>
</dependency>
<dependency>
<groupId>commons-collections</groupId>
<artifactId>commons-collections</artifactId>
<version>${commons-collections.version}</version>
</dependency>
<!-- Commons utils end -->
<!-- google 生成二维码 begin-->
<dependency>
<groupId>com.google.zxing</groupId>
<artifactId>javase</artifactId>
<version>${com.google.zxing.version}</version>
</dependency>
<!-- google 生成二维码 end-->
<!-- JSONObject JSONArray begin -->
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version>${fastjson.version}</version>
</dependency>
<!-- JSONObject JSONArray end -->
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<fork>true</fork>
</configuration>
</plugin>
</plugins>
</build>
</project>
WxPayController
package com.zhangye.wxpay.modules.controller;
import com.alibaba.fastjson.JSONObject;
import com.zhangye.wxpay.modules.common.wx.WxConfig;
import com.zhangye.wxpay.modules.common.wx.WxConstants;
import com.zhangye.wxpay.modules.common.wx.WxUtil;
import com.zhangye.wxpay.modules.model.Order;
import com.zhangye.wxpay.modules.service.WxMenuService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.Map;
/**
* @author zhangye
* @version 1.0
* @description 微信扫码支付接口
* @date 2019/12/19
* <p>
* 微信支付接口官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
* 本 demo 使用的支付方式为: 模式二
* <p>
* 微信扫码支付流程说明:
* 1.需要商户生成订单
* 2.商户调用微信统一下单接口获取二维码链接 code_url (请求参数请见官方文档)
* 请求参数中的 notify_url 为用户支付成功后, 微信服务端回调商户的接口地址
* 3.商户根据 code_url 生成二维码
* 4.用户使用微信扫码进行支付
* 5.支付成功后, 微信服务端会调用 notify_url 通知商户支付结果
* 6.商户接到通知后, 执行业务操作(修改订单状态等)并告知微信服务端接收通知成功
* <p>
* 查询微信支付订单、关闭微信支付订单流程较为简单,请自行查阅官方文档
*/
@Controller
public class WxPayController {
@Autowired
private WxMenuService wxMenuService;
/**
* 二维码首页 测试用
*/
@RequestMapping(value = {"/"}, method = RequestMethod.GET)
public String wxPayList(Model model) {
//商户订单号
model.addAttribute("outTradeNo", WxUtil.mchOrderNo());
return "/wxPayList";
}
/**
* 获取订单流水号 测试用
*/
@RequestMapping(value = {"/wxPay/outTradeNo"})
@ResponseBody
public String getOutTradeNo(Model model) {
//商户订单号
return WxUtil.mchOrderNo();
}
/**
* 默认 signType 为 md5
*/
final private String signType = WxConstants.SING_MD5;
/**
* 微信支付统一下单-生成二维码
* 1.请求微信预下单接口
* 2.根据预下单返回的 code_url 生成二维码
* 3.将二维码 write 到前台页面
*/
@RequestMapping(value = {"/wxPay/payUrl"})
public void payUrl(HttpServletRequest request, HttpServletResponse response,
@RequestParam(value = "totalFee") int totalFee,
@RequestParam(value = "outTradeNo") String outTradeNo,
@RequestParam(value = "productId") String productId) throws Exception {
//模拟测试订单信息
Order order = new Order();
order.setClintIp("123.12.12.123");
order.setOrderNo(outTradeNo);
order.setProductId(productId);
order.setSubject("ESM365充值卡");
order.setTotalFee(totalFee);
//获取二维码链接
String codeUrl = wxMenuService.wxPayUrl(order, signType);
if (!StringUtils.isNotBlank(codeUrl)) {
System.out.println("----生成二维码失败----");
WxConfig.setPayMap(outTradeNo, "CODE_URL_ERROR");
} else {
//根据链接生成二维码
WxUtil.writerPayImage(response, codeUrl);
}
}
/**
* 微信支付统一下单-通知链接
* 1.用户支付成功后
* 2.微信回调该方法
* 3.商户最终通知微信已经收到结果
*/
@RequestMapping(value = {"/wxPay/unifiedorderNotify"})
public void unifiedorderNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
//商户订单号
String outTradeNo = null;
String xmlContent = "<xml>" +
"<return_code><![CDATA[FAIL]]></return_code>" +
"<return_msg><![CDATA[签名失败]]></return_msg>" +
"</xml>";
try {
String requestXml = WxUtil.getStreamString(request.getInputStream());
System.out.println("requestXml : " + requestXml);
Map<String, String> map = WxUtil.xmlToMap(requestXml);
String returnCode = map.get(WxConstants.RETURN_CODE);
//校验一下 ,判断是否已经支付成功
if (StringUtils.isNotBlank(returnCode) && StringUtils.equals(returnCode, "SUCCESS") && WxUtil.isSignatureValid(map, WxConfig.key, signType)) {
//商户订单号
outTradeNo = map.get("out_trade_no");
System.out.println("outTradeNo : " + outTradeNo);
//微信支付订单号
String transactionId = map.get("transaction_id");
System.out.println("transactionId : " + transactionId);
//支付完成时间
SimpleDateFormat payFormat = new SimpleDateFormat("yyyyMMddHHmmss");
Date payDate = payFormat.parse(map.get("time_end"));
SimpleDateFormat systemFormat = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss");
System.out.println("支付时间:" + systemFormat.format(payDate));
//临时缓存
WxConfig.setPayMap(outTradeNo, "SUCCESS");
//根据支付结果修改数据库订单状态
//其他操作
//......
//给微信的应答 xml, 通过 response 回写
xmlContent = "<xml>" +
"<return_code><![CDATA[SUCCESS]]></return_code>" +
"<return_msg><![CDATA[OK]]></return_msg>" +
"</xml>";
}
} catch (Exception e) {
e.printStackTrace();
}
WxUtil.responsePrint(response, xmlContent);
}
/**
* 前台页面定时器查询是否已支付
* 1.前台页面轮询
* 2.查询订单支付状态
*/
@RequestMapping(value = {"/wxPay/payStatus"})
@ResponseBody
public String payStatus(@RequestParam(value = "outTradeNo") String outTradeNo) {
JSONObject responseObject = new JSONObject();
//从临时缓存中取
String outTradeNoValue = WxConfig.getPayMap(outTradeNo);
String status = "200";
//判断是否已经支付成功
if (StringUtils.isNotBlank(outTradeNoValue)) {
if (StringUtils.equals(outTradeNoValue, "SUCCESS")) {
status = "0";
} else if (StringUtils.equals(outTradeNoValue, "CODE_URL_ERROR")) {
//生成二维码失败
status = "1";
}
} else {
//如果临时缓存中没有 去数据库读取
//......
}
responseObject.put("status", status);
return responseObject.toJSONString();
}
/**
* 微信支付订单查询
* 1.如果由于网络通信问题 导致微信没有通知到商户支付结果
* 2.商户主动去查询支付结果 而后执行其他业务操作
*/
@RequestMapping(value = {"/wxPay/orderQuery"})
@ResponseBody
public String orderQuery(@RequestParam(value = "orderNo") String orderNo) throws Exception {
String result = wxMenuService.wxOrderQuery(orderNo, signType);
return result;
}
/**
* 关闭微信支付订单
* 1.商户订单支付失败需要生成新单号重新发起支付,要对原订单号调用关单,避免重复支付
* 2.系统下单后,用户支付超时,系统退出不再受理,避免用户继续,请调用关单接口
*/
@RequestMapping(value = {"/wxPay/closeOrder"})
@ResponseBody
public String closeOrder(@RequestParam(value = "orderNo") String orderNo) throws Exception {
String result = wxMenuService.wxCloseOrder(orderNo, signType);
return result;
}
//申请退款
//查询退款
}
WxMenuService
package com.zhangye.wxpay.modules.service;
import com.zhangye.wxpay.modules.model.Order;
/**
* @author zhangye
* @version 1.0
* @description 微信支付接口类
* @date 2019/12/19
*/
public interface WxMenuService {
/**
* 生成支付二维码URL
*
* @param order 订单类
* @param signType 签名类型
* @throws Exception
*/
String wxPayUrl(Order order, String signType) throws Exception;
/**
* 查询微信订单
*
* @param orderNo 订单号
* @param signType 签名类型
* @return
*/
String wxOrderQuery(String orderNo, String signType) throws Exception;
/**
* 关闭微信支付订单
*
* @param orderNo 订单号
* @param signType 签名类型
* @return
*/
String wxCloseOrder(String orderNo, String signType) throws Exception;
}
WxMenuServiceImpl
package com.zhangye.wxpay.modules.service.impl;
import com.zhangye.wxpay.modules.common.http.HttpsClient;
import com.zhangye.wxpay.modules.common.wx.WxConfig;
import com.zhangye.wxpay.modules.common.wx.WxConstants;
import com.zhangye.wxpay.modules.common.wx.WxUtil;
import com.zhangye.wxpay.modules.model.Order;
import com.zhangye.wxpay.modules.service.WxMenuService;
import org.springframework.stereotype.Service;
import java.util.HashMap;
import java.util.Map;
/**
* @author zhangye
* @version 1.0
* @description 微信支付实现类
* @date 2019/12/19
*/
@Service("wxMenuService")
public class WxMenuServiceImpl implements WxMenuService {
@Override
public String wxPayUrl(Order order, String signType) throws Exception {
HashMap<String, String> data = new HashMap<String, String>();
//公众账号ID
data.put("appid", WxConfig.appID);
//商户号
data.put("mch_id", WxConfig.mchID);
//随机字符串
data.put("nonce_str", WxUtil.getNonceStr());
//商品描述
data.put("body", order.getSubject());
//商户订单号
data.put("out_trade_no", order.getOrderNo());
//标价币种
data.put("fee_type", "CNY");
//标价金额
data.put("total_fee", String.valueOf(order.getTotalFee()));
//用户的IP
data.put("spbill_create_ip", order.getClintIp());
//通知地址
data.put("notify_url", WxConfig.unifiedorderNotifyUrl);
//交易类型
data.put("trade_type", "NATIVE");
//签名类型
data.put("sign_type", signType);
//商品id
data.put("product_id", order.getProductId());
//签名 签名中加入key
data.put("sign", WxUtil.getSignature(data, WxConfig.key, signType));
String requestXML = WxUtil.mapToXml(data);
String responseString = HttpsClient.httpsRequestReturnString(WxConstants.PAY_UNIFIEDORDER, HttpsClient.METHOD_POST, requestXML);
//解析返回的xml
Map<String, String> resultMap = WxUtil.processResponseXml(responseString, signType);
if (resultMap.get(WxConstants.RETURN_CODE).equals("SUCCESS")) {
return resultMap.get("code_url");
}
return null;
}
@Override
public String wxOrderQuery(String orderNo, String signType) throws Exception {
HashMap<String, String> data = new HashMap<String, String>();
//公众账号ID
data.put("appid", WxConfig.appID);
//商户号
data.put("mch_id", WxConfig.mchID);
//随机字符串
data.put("nonce_str", WxUtil.getNonceStr());
//商户订单号
data.put("out_trade_no", orderNo);
//签名类型
data.put("sign_type", signType);
//签名 签名中加入key
data.put("sign", WxUtil.getSignature(data, WxConfig.key, signType));
String requestXML = WxUtil.mapToXml(data);
String responseString = HttpsClient.httpsRequestReturnString(WxConstants.PAY_ORDERQUERY, HttpsClient.METHOD_POST, requestXML);
//解析返回的xml
Map<String, String> resultMap = WxUtil.processResponseXml(responseString, signType);
if (resultMap.get(WxConstants.RETURN_CODE).equals("SUCCESS")) {
/**
* 订单支付状态
* SUCCESS—支付成功
* REFUND—转入退款
* NOTPAY—未支付
* CLOSED—已关闭
* REVOKED—已撤销(刷卡支付)
* USERPAYING--用户支付中
* PAYERROR--支付失败(其他原因,如银行返回失败)
*/
return resultMap.get("trade_state");
}
return null;
}
@Override
public String wxCloseOrder(String orderNo, String signType) throws Exception {
HashMap<String, String> data = new HashMap<String, String>();
//公众账号ID
data.put("appid", WxConfig.appID);
//商户号
data.put("mch_id", WxConfig.mchID);
//随机字符串
data.put("nonce_str", WxUtil.getNonceStr());
//商户订单号
data.put("out_trade_no", orderNo);
//签名类型
data.put("sign_type", signType);
//签名 签名中加入key
data.put("sign", WxUtil.getSignature(data, WxConfig.key, signType));
String requestXML = WxUtil.mapToXml(data);
String responseString = HttpsClient.httpsRequestReturnString(WxConstants.PAY_CLOSEORDER, HttpsClient.METHOD_POST, requestXML);
//解析返回的xml
Map<String, String> resultMap = WxUtil.processResponseXml(responseString, signType);
if (resultMap.get(WxConstants.RETURN_CODE).equals("SUCCESS")) {
/**
* 关闭订单状态
* SUCCESS—关闭成功
* FAIL—关闭失败
*/
return resultMap.get("result_code");
}
return null;
}
}
HttpsClient
package com.zhangye.wxpay.modules.common.http;
import com.alibaba.fastjson.JSONObject;
import com.zhangye.wxpay.modules.common.wx.WxConfig;
import com.zhangye.wxpay.modules.common.wx.WxConstants;
import com.zhangye.wxpay.modules.common.wx.WxUtil;
import org.apache.commons.lang3.StringUtils;
import javax.net.ssl.HttpsURLConnection;
import javax.net.ssl.SSLContext;
import javax.net.ssl.SSLSocketFactory;
import javax.net.ssl.TrustManager;
import java.io.OutputStream;
import java.net.URL;
/**
* @author zhangye
* @version 1.0
* @description HttpsClient类
* @date 2019/12/19
*/
public class HttpsClient {
/**
* GET请求方式
*/
public static final String METHOD_GET = "GET";
/**
* POST请求方式
*/
public static final String METHOD_POST = "POST";
/**
* 连接超时时间
*/
private static Integer CONNECTION_TIMEOUT = WxConfig.connectionTimeout;
/**
* 请求超时时间
*/
private static Integer READ_TIMEOUT = WxConfig.readTimeout;
/**
* 发起https请求
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(Get或者post)
* @param postData 提交数据
* @return JSONObject
*/
public static JSONObject httpsRequestReturnJSONObject(String requestUrl, String requestMethod, String postData) throws Exception {
JSONObject jsonObject = JSONObject.parseObject(HttpsClient.httpsRequestReturnString(requestUrl, requestMethod, postData));
System.out.println("jsonObjectDate: " + jsonObject);
return jsonObject;
}
/**
* 发起https请求
*
* @param requestUrl 请求地址
* @param requestMethod 请求方式(Get或者post)
* @param postData 提交数据
* @return String
*/
public static String httpsRequestReturnString(String requestUrl, String requestMethod, String postData) throws Exception {
String response;
HttpsURLConnection httpsUrlConnection = null;
try {
//创建https请求证书
TrustManager[] tm = {new MyX509TrustManager()};
//创建SSLContext管理器对像,使用我们指定的信任管理器初始化
SSLContext sslContext = SSLContext.getInstance("SSL", "SunJSSE");
sslContext.init(null, tm, new java.security.SecureRandom());
SSLSocketFactory ssf = sslContext.getSocketFactory();
// 创建URL对象
URL url = new URL(requestUrl);
// 创建HttpsURLConnection对象,并设置其SSLSocketFactory对象
httpsUrlConnection = (HttpsURLConnection) url.openConnection();
//设置ssl证书
httpsUrlConnection.setSSLSocketFactory(ssf);
//设置header信息
httpsUrlConnection.setRequestProperty("Content-Type", "application/x-www-form-urlencoded");
//设置User-Agent信息
httpsUrlConnection.setRequestProperty("User-Agent", "Mozilla/5.0 (Windows NT 6.3; WOW64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/33.0.1750.146 Safari/537.36");
//设置可接受信息
httpsUrlConnection.setDoOutput(true);
//设置可输入信息
httpsUrlConnection.setDoInput(true);
//不使用缓存
httpsUrlConnection.setUseCaches(false);
//设置请求方式(GET/POST)
httpsUrlConnection.setRequestMethod(requestMethod);
//设置连接超时时间
if (CONNECTION_TIMEOUT > 0) {
httpsUrlConnection.setConnectTimeout(CONNECTION_TIMEOUT);
} else {
//默认10秒超时
httpsUrlConnection.setConnectTimeout(10000);
}
//设置请求超时
if (READ_TIMEOUT > 0) {
httpsUrlConnection.setReadTimeout(READ_TIMEOUT);
} else {
//默认10秒超时
httpsUrlConnection.setReadTimeout(10000);
}
//设置编码
httpsUrlConnection.setRequestProperty("Charsert", WxConstants.DEFAULT_CHARSET);
//判断是否需要提交数据
if (StringUtils.equals(requestMethod, HttpsClient.METHOD_POST) && StringUtils.isNotBlank(postData)) {
//讲参数转换为字节提交
byte[] bytes = postData.getBytes(WxConstants.DEFAULT_CHARSET);
//设置头信息
httpsUrlConnection.setRequestProperty("Content-Length", Integer.toString(bytes.length));
//开始连接
httpsUrlConnection.connect();
//防止中文乱码
OutputStream outputStream = httpsUrlConnection.getOutputStream();
outputStream.write(postData.getBytes(WxConstants.DEFAULT_CHARSET));
outputStream.flush();
outputStream.close();
} else {
//开始连接
httpsUrlConnection.connect();
}
response = WxUtil.getStreamString(httpsUrlConnection.getInputStream());
} catch (Exception e) {
throw new Exception();
} finally {
if (httpsUrlConnection != null) {
// 关闭连接
httpsUrlConnection.disconnect();
}
}
return response;
}
}
MyX509TrustManager
package com.zhangye.wxpay.modules.common.http;
import javax.net.ssl.X509TrustManager;
import java.security.cert.CertificateException;
import java.security.cert.X509Certificate;
/**
* @author zhangye
* @version 1.0
* @description X509TrustManager用于实现SSL证书的安全校验
* @date 2019/12/19
*/
public class MyX509TrustManager implements X509TrustManager {
@Override
public void checkClientTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public void checkServerTrusted(X509Certificate[] chain, String authType) throws CertificateException {
}
@Override
public X509Certificate[] getAcceptedIssuers() {
return null;
}
}
SHA1
package com.zhangye.wxpay.modules.common.util;
import java.security.MessageDigest;
/**
* @author zhangye
* @version 1.0
* @description 微信SHA1算法
* @date 2019/12/19
*/
public final class SHA1 {
private static final char[] HEX_DIGITS = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'a', 'b', 'c', 'd', 'e', 'f'};
/**
* 将字节并格式化
*
* @param bytes 原始字节
* @return 格式化字节
*/
private static String getFormattedText(byte[] bytes) {
int len = bytes.length;
StringBuilder buf = new StringBuilder(len * 2);
// 把密文转换成十六进制的字符串形式
for (int j = 0; j < len; j++) {
buf.append(HEX_DIGITS[(bytes[j] >> 4) & 0x0f]);
buf.append(HEX_DIGITS[bytes[j] & 0x0f]);
}
return buf.toString();
}
public static String encode(String str) {
if (str == null) {
return null;
}
try {
MessageDigest messageDigest = MessageDigest.getInstance("SHA1");
messageDigest.update(str.getBytes());
return getFormattedText(messageDigest.digest());
} catch (Exception e) {
throw new RuntimeException(e);
}
}
}
WxConfig
package com.zhangye.wxpay.modules.common.wx;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
import java.util.HashMap;
/**
* @author zhangye
* @version 1.0
* @description 微信公众号开发配置类
* @date 2019/12/19
*/
@Component
public class WxConfig {
/**
* 开发者ID
*/
public static String appID;
@Value("${wx.appID}")
public void setAppID(String appID) {
this.appID = appID;
}
/**
* 开发者密码
*/
public static String appSecret;
@Value("${wx.appSecret}")
public void setAppSecret(String appSecret) {
this.appSecret = appSecret;
}
/**
* 商户号
*/
public static String mchID;
@Value("${wx.mchID}")
public void setMchID(String mchID) {
this.mchID = mchID;
}
/**
* API密钥
*/
public static String key;
@Value("${wx.key}")
public void setKey(String key) {
this.key = key;
}
/**
* 统一下单-通知链接
*/
public static String unifiedorderNotifyUrl;
@Value("${wx.unifiedorder.notifyUrl}")
public void setUnifiedorderNotifyUrl(String unifiedorderNotifyUrl) {
this.unifiedorderNotifyUrl = unifiedorderNotifyUrl;
}
/**
* 连接超时时间
*/
public static Integer connectionTimeout;
@Value("${https.connectionTimeout}")
public void setConnectionTimeout(Integer connectionTimeout) {
this.connectionTimeout = connectionTimeout;
}
/**
* 连接超时时间
*/
public static Integer readTimeout;
@Value("${https.readTimeout}")
public void setReadTimeout(Integer readTimeout) {
this.readTimeout = readTimeout;
}
//支付map缓存处理
private static HashMap<String,String> payMap = new HashMap<String,String>();
public static String getPayMap(String key) {
return payMap.get(key);
}
public static void setPayMap(String key,String value) {
payMap.put(key,value);
}
}
WxConstants
package com.zhangye.wxpay.modules.common.wx;
/**
* @author zhangye
* @version 1.0
* @description 微信公众号常量类
* @date 2019/12/19
*/
public class WxConstants {
/**
* 默认编码
*/
public static final String DEFAULT_CHARSET = "UTF-8";
/**
* 统一下单-扫描支付
*/
public static String PAY_UNIFIEDORDER = "https://api.mch.weixin.qq.com/pay/unifiedorder";
/**
* 统一下单-查询订单
*/
public static String PAY_ORDERQUERY = "https://api.mch.weixin.qq.com/pay/orderquery";
/**
* 统一下单-关闭订单
*/
public static String PAY_CLOSEORDER = "https://api.mch.weixin.qq.com/pay/closeorder";
/**
* 请求成功返回码
*/
public final static String ERRCODE_OK_CODE = "0";
/**
* 错误的返回码的Key
*/
public final static String ERRCODE = "errcode";
/**
* 返回状态码
*/
public final static String RETURN_CODE = "return_code";
/**
* access_token 字符串
*/
public final static String ACCESS_TOKEN = "access_token";
/**
* 签名类型 MD5
*/
public final static String SING_MD5 = "MD5";
/**
* 签名类型 HMAC-SHA256
*/
public final static String SING_HMACSHA256 = "HMAC-SHA256";
}
WxUtil
package com.zhangye.wxpay.modules.common.wx;
import com.google.zxing.BarcodeFormat;
import com.google.zxing.EncodeHintType;
import com.google.zxing.MultiFormatWriter;
import com.google.zxing.client.j2se.MatrixToImageWriter;
import com.google.zxing.common.BitMatrix;
import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
import com.zhangye.wxpay.modules.common.util.SHA1;
import org.w3c.dom.Node;
import org.w3c.dom.NodeList;
import javax.crypto.Mac;
import javax.crypto.spec.SecretKeySpec;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.transform.OutputKeys;
import javax.xml.transform.Transformer;
import javax.xml.transform.TransformerFactory;
import javax.xml.transform.dom.DOMSource;
import javax.xml.transform.stream.StreamResult;
import java.io.*;
import java.security.MessageDigest;
import java.text.SimpleDateFormat;
import java.util.*;
/**
* @author zhangye
* @version 1.0
* @description 微信公众号接口工具类
* 在微信提供的 skk 中的 WXPayUtil 基础上根据自己的需求做出了一些修改
* 微信 sdk 下载地址: https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=11_1
* @date 2019/12/19
*/
public class WxUtil {
/**
* 加密/校验流程如下:
* 1. 将token、timestamp、nonce 三个参数进行字典序排序
* 2. 将三个参数字符串拼接成一个字符串进行 sha1 加密
* 3. 开发者获得加密后的字符串可与 signature 对比,标识该请求来源于微信
*
* @param token Token验证密钥
* @param signature 微信加密签名,signature 结合了开发者填写的 token 参数和请求中的 timestamp 参数,nonce 参数
* @param timestamp 时间戳
* @param nonce 随机数
* @return 验证成功返回:true, 失败返回:false
*/
public static boolean checkSignature(String token, String signature, String timestamp, String nonce) {
List<String> params = new ArrayList<String>();
params.add(token);
params.add(timestamp);
params.add(nonce);
//1. 将token、timestamp、nonce三个参数进行字典序排序
Collections.sort(params, new Comparator<String>() {
@Override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});
//2. 将三个参数字符串拼接成一个字符串进行sha1加密
String temp = SHA1.encode(params.get(0) + params.get(1) + params.get(2));
//3. 开发者获得加密后的字符串可与signature对比,标识该请求来源于微信
return temp.equals(signature);
}
/**
* 输入流转化为字符串
*
* @param inputStream 流
* @return String 字符串
* @throws Exception
*/
public static String getStreamString(InputStream inputStream) throws Exception {
StringBuffer buffer = new StringBuffer();
InputStreamReader inputStreamReader = null;
BufferedReader bufferedReader = null;
try {
inputStreamReader = new InputStreamReader(inputStream, WxConstants.DEFAULT_CHARSET);
bufferedReader = new BufferedReader(inputStreamReader);
String line;
while ((line = bufferedReader.readLine()) != null) {
buffer.append(line);
}
} catch (Exception e) {
throw new Exception();
} finally {
if (bufferedReader != null) {
bufferedReader.close();
}
if (inputStreamReader != null) {
inputStreamReader.close();
}
if (inputStream != null) {
inputStream.close();
}
}
return buffer.toString();
}
/**
* 获取随机字符串 Nonce Str
*
* @return String 随机字符串
*/
public static String getNonceStr() {
return UUID.randomUUID().toString().replaceAll("-", "").substring(0, 32);
}
/**
* 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
*
* @param data 待签名数据
* @param key API密钥
* @return 签名
*/
public static String getSignature(final Map<String, String> data, String key, String signType) throws Exception {
Set<String> keySet = data.keySet();
String[] keyArray = keySet.toArray(new String[keySet.size()]);
Arrays.sort(keyArray);
StringBuilder sb = new StringBuilder();
for (String k : keyArray) {
if (k.equals("sign")) {
continue;
}
//参数值为空,则不参与签名
if (data.get(k).trim().length() > 0) {
sb.append(k).append("=").append(data.get(k).trim()).append("&");
}
}
sb.append("key=").append(key);//加上key 再生成签名
if (signType.equals(WxConstants.SING_MD5)) {
return MD5(sb.toString()).toUpperCase();
} else if (signType.equals(WxConstants.SING_HMACSHA256)) {
return HMACSHA256(sb.toString(), key);
} else {
throw new Exception(String.format("Invalid sign_type: %s", signType));
}
}
/**
* 生成 MD5
*
* @param data 待处理数据
* @return MD5结果
*/
public static String MD5(String data) throws Exception {
MessageDigest md = MessageDigest.getInstance("MD5");
byte[] array = md.digest(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 生成 HMACSHA256
*
* @param data 待处理数据
* @param key 密钥
* @return 加密结果
* @throws Exception
*/
public static String HMACSHA256(String data, String key) throws Exception {
Mac sha256_HMAC = Mac.getInstance("HmacSHA256");
SecretKeySpec secret_key = new SecretKeySpec(key.getBytes("UTF-8"), "HmacSHA256");
sha256_HMAC.init(secret_key);
byte[] array = sha256_HMAC.doFinal(data.getBytes("UTF-8"));
StringBuilder sb = new StringBuilder();
for (byte item : array) {
sb.append(Integer.toHexString((item & 0xFF) | 0x100).substring(1, 3));
}
return sb.toString().toUpperCase();
}
/**
* 将Map转换为XML格式的字符串
*
* @param data Map类型数据
* @return XML格式的字符串
* @throws Exception
*/
public static String mapToXml(Map<String, String> data) throws Exception {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
org.w3c.dom.Document document = documentBuilder.newDocument();
org.w3c.dom.Element root = document.createElement("xml");
document.appendChild(root);
for (String key : data.keySet()) {
String value = data.get(key);
if (value == null) {
value = "";
}
value = value.trim();
org.w3c.dom.Element filed = document.createElement(key);
filed.appendChild(document.createTextNode(value));
root.appendChild(filed);
}
TransformerFactory tf = TransformerFactory.newInstance();
Transformer transformer = tf.newTransformer();
DOMSource source = new DOMSource(document);
transformer.setOutputProperty(OutputKeys.ENCODING, "UTF-8");
transformer.setOutputProperty(OutputKeys.INDENT, "yes");
StringWriter writer = new StringWriter();
StreamResult result = new StreamResult(writer);
transformer.transform(source, result);
String output = writer.getBuffer().toString(); //.replaceAll("\n|\r", "");
try {
writer.close();
} catch (Exception ex) {
}
return output;
}
/**
* 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。
*
* @param xmlStr API返回的XML格式数据
* @return Map类型数据
* @throws Exception
*/
public static Map<String, String> processResponseXml(String xmlStr, String signType) throws Exception {
String RETURN_CODE = WxConstants.RETURN_CODE;
String return_code;
Map<String, String> respData = xmlToMap(xmlStr);
if (respData.containsKey(RETURN_CODE)) {
return_code = respData.get(RETURN_CODE);
} else {
throw new Exception(String.format("No `return_code` in XML: %s", xmlStr));
}
if (return_code.equals("FAIL")) {
return respData;
} else if (return_code.equals("SUCCESS")) {
//如果通信正常 验证签名
if (isResponseSignatureValid(respData, signType)) {
return respData;
} else {
throw new Exception(String.format("Invalid sign value in XML: %s", xmlStr));
}
} else {
throw new Exception(String.format("return_code value %s is invalid in XML: %s", return_code, xmlStr));
}
}
/**
* XML格式字符串转换为Map
*
* @param strXML XML字符串
* @return XML数据转换后的Map
* @throws Exception
*/
public static Map<String, String> xmlToMap(String strXML) throws Exception {
try {
Map<String, String> data = new HashMap<String, String>();
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
String FEATURE = "http://apache.org/xml/features/disallow-doctype-decl";
documentBuilderFactory.setFeature(FEATURE, true);
FEATURE = "http://xml.org/sax/features/external-general-entities";
documentBuilderFactory.setFeature(FEATURE, false);
FEATURE = "http://xml.org/sax/features/external-parameter-entities";
documentBuilderFactory.setFeature(FEATURE, false);
FEATURE = "http://apache.org/xml/features/nonvalidating/load-external-dtd";
documentBuilderFactory.setFeature(FEATURE, false);
documentBuilderFactory.setXIncludeAware(false);
documentBuilderFactory.setExpandEntityReferences(false);
DocumentBuilder documentBuilder = documentBuilderFactory.newDocumentBuilder();
InputStream stream = new ByteArrayInputStream(strXML.getBytes("UTF-8"));
org.w3c.dom.Document doc = documentBuilder.parse(stream);
doc.getDocumentElement().normalize();
NodeList nodeList = doc.getDocumentElement().getChildNodes();
for (int idx = 0; idx < nodeList.getLength(); ++idx) {
Node node = nodeList.item(idx);
if (node.getNodeType() == Node.ELEMENT_NODE) {
org.w3c.dom.Element element = (org.w3c.dom.Element) node;
data.put(element.getNodeName(), element.getTextContent());
}
}
try {
stream.close();
} catch (Exception ex) {
// do nothing
}
return data;
} catch (Exception ex) {
throw ex;
}
}
/**
* 判断xml数据的sign是否有效,必须包含sign字段,否则返回false。
*
* @param reqData 向wxpay post的请求数据
* @return 签名是否有效
* @throws Exception
*/
private static boolean isResponseSignatureValid(final Map<String, String> reqData, String signType) throws Exception {
// 返回数据的签名方式和请求中给定的签名方式是一致的 由于签名的时候加上了key 所以验证的时候也需要
return isSignatureValid(reqData, WxConfig.key, signType);
}
/**
* 判断签名是否正确,必须包含sign字段,否则返回false。
*
* @param data Map类型数据
* @param key API密钥
* @param signType 签名方式
* @return 签名是否正确
* @throws Exception
*/
public static boolean isSignatureValid(Map<String, String> data, String key, String signType) throws Exception {
if (!data.containsKey("sign")) {
return false;
}
String sign = data.get("sign");
return getSignature(data, key, signType).equals(sign);
}
/**
* 生成支付二维码
*
* @param response 响应
* @param contents url链接
* @throws Exception
*/
public static void writerPayImage(HttpServletResponse response, String contents) throws Exception {
ServletOutputStream out = response.getOutputStream();
try {
Map<EncodeHintType, Object> hints = new HashMap<EncodeHintType, Object>();
hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.L);
hints.put(EncodeHintType.MARGIN, 0);
BitMatrix bitMatrix = new MultiFormatWriter().encode(contents, BarcodeFormat.QR_CODE, 300, 300, hints);
MatrixToImageWriter.writeToStream(bitMatrix, "jpg", out);
} catch (Exception e) {
throw new Exception("生成二维码失败!");
} finally {
if (out != null) {
out.flush();
out.close();
}
}
}
/**
* 生成商户订单号
* 1.此方法只用在 demo 中生成假订单号
* 2.生产环境中需要根据自己的业务做调整
*
* @return 测试用的订单号
*/
public static String mchOrderNo() {
SimpleDateFormat sdf = new SimpleDateFormat("yyyyMMddHHmmss");
String date = sdf.format(new Date());
Random random = new Random();
String fourRandom = String.valueOf(random.nextInt(10000));
int randLength = fourRandom.length();
//不足4位继续补充
if (randLength < 4) {
for (int remain = 1; remain <= 4 - randLength; remain++) {
fourRandom += random.nextInt(10);
}
}
return date + fourRandom;
}
/**
* 返回信息给微信 商户已经接收到回调
*
* @param response
* @param content 内容
* @throws Exception
*/
public static void responsePrint(HttpServletResponse response, String content) throws Exception {
response.setCharacterEncoding("UTF-8");
response.setContentType("text/xml");
response.getWriter().print(content);
response.getWriter().flush();
response.getWriter().close();
}
}
Order
package com.zhangye.wxpay.modules.model;
import java.io.Serializable;
/**
* @author zhangye
* @version 1.0
* @description 商户订单实体类(测试)
* @date 2019/12/19
*/
public class Order implements Serializable {
private String productId;//商品id
private String subject;//商品名称
private String orderNo;//订单号
private String clintIp;//客户端ip
private int totalFee;//订单金额 以分为单位
public String getProductId() {
return productId;
}
public void setProductId(String productId) {
this.productId = productId;
}
public String getSubject() {
return subject;
}
public void setSubject(String subject) {
this.subject = subject;
}
public String getOrderNo() {
return orderNo;
}
public void setOrderNo(String orderNo) {
this.orderNo = orderNo;
}
public String getClintIp() {
return clintIp;
}
public void setClintIp(String clintIp) {
this.clintIp = clintIp;
}
public int getTotalFee() {
return totalFee;
}
public void setTotalFee(int totalFee) {
this.totalFee = totalFee;
}
}
application.properties
# ---微信扫码支付开始
#开发者ID
wx.appID=wxab8acb865bb1637e
#开发者密码
wx.appSecret=86ae4a77893342f7568947e243c84d9aa
#商户号
wx.mchID=11473623
#API密钥,key设置路径:微信商户平台(pay.weixin.qq.com)-->账户设置-->API安全-->密钥设置
wx.key=2ab9071b06b9f739b950ddb41db2690d
#内网穿透的链接(由于测试demo没有外网地址及域名,所以使用工具穿透)
#穿透工具使用方法请见 /resources/natapp/readme.md
#生产环境下将此链接修改为正确的域名即可
intranet.penetrateUrl=http://vaiiak.natappfree.cc
#统一下单-通知链接
wx.unifiedorder.notifyUrl=${intranet.penetrateUrl}/wxPay/unifiedorderNotify
# ---微信扫码支付结束
#连接超时时间
https.connectionTimeout=15000
#请求超时时间
https.readTimeout=15000
spring.mvc.view.prefix=/templates
spring.mvc.view.suffix=.html
spring.mvc.static-path-pattern=/**
#禁止thymeleaf缓存(建议:开发环境设置为false,生成环境设置为true)
spring.thymeleaf.cache=false
wxPayList.html
<html>
<head>
<title>微信支付测试DEMO</title>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8"/>
<script type="text/javascript" src="/js/jquery/jquery-3.3.1.min.js"></script>
<script type="text/javascript" src="/js/jquery/jquery.timers-1.2.js"></script>
<script type='text/javascript'>
$(function () {
getOutTradeNo();
});
function save() {
var outTradeNo = $("#outTradeNo").val(); //订单号
var productId = "00001"; //商品id
var totalFee = $("#totalFee").val(); //订单金额 单位为分
//生成二维码
$("#payImg").attr("src", '/wxPay/payUrl' + "?totalFee=" + totalFee + "&outTradeNo=" + outTradeNo + "&productId=" + productId);
//轮询获取支付状态
$('body').everyTime('2s', 'payStatusTimer', function () {
$.ajax({
type: "POST",
url: '/wxPay/payStatus?outTradeNo=' + outTradeNo + "&random=" + new Date().getTime(),
contentType: "application/json",
dataType: "json",
async: "false",
success: function (json) {
if (json != null && json.status == 0) {
alert("支付成功!");
$('body').stopTime('payStatusTimer');
return false;
} else if (json.status == 1) {
alert("生成二维码失败!")
$('body').stopTime('payStatusTimer');
return false;
}
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("服务器错误!状态码:" + json.status);
// 状态
console.log(json.readyState);
// 错误信息
console.log(json.statusText);
return false;
}
})
});
}
//获取测试订单流水号
function getOutTradeNo() {
$.ajax({
type: "POST",
url: '/wxPay/outTradeNo',
success: function (json) {
if (json != null) {
$("h3").html(json);
$("#outTradeNo").val(json);
} else {
alert("获取流水号失败!");
}
return false;
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("服务器错误!状态码:" + XMLHttpRequest.status);
// 状态
console.log(XMLHttpRequest.readyState);
// 错误信息
console.log(textStatus);
return false;
}
});
}
//查询订单
function queryOrder() {
var orderNo = $("#orderNo").val();
$.ajax({
type: "POST",
url: '/wxPay/orderQuery?orderNo=' + orderNo,
success: function (data) {
alert(data);
return false;
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("服务器错误!状态码:" + XMLHttpRequest.status);
// 状态
console.log(XMLHttpRequest.readyState);
// 错误信息
console.log(textStatus);
return false;
}
});
}
//关闭订单
function closeOrder() {
var orderNo = $("#orderNo2").val();
$.ajax({
type: "POST",
url: '/wxPay/closeOrder?orderNo=' + orderNo,
success: function (data) {
alert(data);
return false;
},
error: function (XMLHttpRequest, textStatus, errorThrown) {
alert("服务器错误!状态码:" + XMLHttpRequest.status);
// 状态
console.log(XMLHttpRequest.readyState);
// 错误信息
console.log(textStatus);
return false;
}
});
}
</script>
</head>
<body>
<p>订单流水号:
<h3></h3></p>
支付金额:<input id="totalFee" type="text" value="1"/> 分
<button type="button" onclick="save();">生成二维码</button>
<input id="outTradeNo" type="hidden" value="${outTradeNo}"/>
<img id="payImg" width="300" height="300">
<br/><br/><br/>
<p>查询订单:
订单号<input id="orderNo" type="text" value=""/>
<button type="button" onclick="queryOrder();">查询订单</button>
<br/>
<p>关闭订单:
订单号<input id="orderNo2" type="text" value=""/>
<button type="button" onclick="closeOrder();">关闭订单</button>
</body>
</html>