微信扫码支付java版完整demo
示例说明:
微信支付接口官方文档地址:https://pay.weixin.qq.com/wiki/doc/api/native.php?chapter=6_5
本 demo 使用的支付方式为: 模式二
文章最下方有可以直接运行的demo的百度云下载地址
项目结构:
项目代码:
pom文件
<?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>
controller--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; } //申请退款 //查询退款 }
service--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; }
service--impl--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; } }
common--http--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; } }
common--http--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; } }
common--util--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); } } }
common--wx--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); } }
common--wx--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"; }
common--wx--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(); } }
common--model--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; } }
resources--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
resources--templates--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>
其他内容为jquery文件和natapp的使用方法:(如果不需要使用natapp测试,上面的代码基本上是全部的demo实现代码了)
最后附上整个demo百度云下载的地址:
链接:https://pan.baidu.com/s/1C-hi_TTxAxpiWF_5T_PDIw
提取码:p1yb