diff --git a/server.keystore b/server.keystore
new file mode 100644
index 00000000..9543579d
Binary files /dev/null and b/server.keystore differ
diff --git a/smart-admin-service/smart-admin-api/pom.xml b/smart-admin-service/smart-admin-api/pom.xml
index d14150a3..b5066d62 100644
--- a/smart-admin-service/smart-admin-api/pom.xml
+++ b/smart-admin-service/smart-admin-api/pom.xml
@@ -227,8 +227,29 @@
provided
+
+
+ com.google.zxing
+ core
+ 3.3.3
+
+
+
+ com.google.zxing
+ javase
+ 3.3.3
+
+
+
+
+ wxpay
+
+ https://pay.weixin.qq.com/wiki/doc/api/wxa/wxa_api.php?chapter=11_1
+
+
+
compile
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/constant/SwaggerTagConst.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/constant/SwaggerTagConst.java
index 6fc3cd82..9fae6781 100644
--- a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/constant/SwaggerTagConst.java
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/constant/SwaggerTagConst.java
@@ -50,6 +50,8 @@ public class SwaggerTagConst {
public static final String MANAGER_HEART_BEAT = "通用-心跳服务";
public static final String MANAGER_MALL_API = "皇家API-接口封装";
+
+ public static final String MANAGER_MALL_PAY_API = "皇家API-支付接口";
}
/**
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/qrcode/QRCodeUtil.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/qrcode/QRCodeUtil.java
new file mode 100644
index 00000000..724c1e28
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/qrcode/QRCodeUtil.java
@@ -0,0 +1,247 @@
+package net.lab1024.smartadmin.module.system.qrcode;
+
+import com.google.zxing.*;
+import com.google.zxing.client.j2se.BufferedImageLuminanceSource;
+import com.google.zxing.common.BitMatrix;
+import com.google.zxing.common.HybridBinarizer;
+import com.google.zxing.qrcode.decoder.ErrorCorrectionLevel;
+import lombok.extern.slf4j.Slf4j;
+import org.springframework.stereotype.Component;
+
+import javax.imageio.ImageIO;
+import javax.swing.filechooser.FileSystemView;
+import java.awt.image.BufferedImage;
+import java.io.File;
+import java.io.IOException;
+import java.io.OutputStream;
+import java.net.URL;
+import java.util.Date;
+import java.util.HashMap;
+import java.util.Hashtable;
+import java.util.Map;
+
+@Component
+@Slf4j
+public class QRCodeUtil {
+
+ /**
+ * CODE_WIDTH:二维码宽度,单位像素
+ * CODE_HEIGHT:二维码高度,单位像素
+ * FRONT_COLOR:二维码前景色,0x000000 表示黑色
+ * BACKGROUND_COLOR:二维码背景色,0xFFFFFF 表示白色
+ * 演示用 16 进制表示,和前端页面 CSS 的取色是一样的,注意前后景颜色应该对比明显,如常见的黑白
+ */
+ private static final int CODE_WIDTH = 400;
+ private static final int CODE_HEIGHT = 400;
+ private static final int FRONT_COLOR = 0x000000;
+ private static final int BACKGROUND_COLOR = 0xFFFFFF;
+
+ /**
+ * @param codeContent 二维码参数内容,如果是一个网页地址,如 https://www.baidu.com/ 则 微信扫一扫会直接进入此地址, 如果是一些参数,如
+ * 1541656080837,则微信扫一扫会直接回显这些参数值
+ * @param codeImgFileSaveDir 二维码图片保存的目录,如 D:/codes
+ * @param fileName 二维码图片文件名称,带格式,如 123.png
+ */
+ public static void createCodeToFile(String codeContent, File codeImgFileSaveDir, String fileName) {
+ try {
+ if (codeContent == null || "".equals(codeContent)) {
+ log.info("二维码内容为空,不进行操作...");
+ return;
+ }
+ codeContent = codeContent.trim();
+ if (codeImgFileSaveDir == null || codeImgFileSaveDir.isFile()) {
+ codeImgFileSaveDir = FileSystemView.getFileSystemView().getHomeDirectory();
+ log.info("二维码图片存在目录为空,默认放在桌面...");
+ }
+ if (!codeImgFileSaveDir.exists()) {
+ codeImgFileSaveDir.mkdirs();
+ log.info("二维码图片存在目录不存在,开始创建...");
+ }
+ if (fileName == null || "".equals(fileName)) {
+ fileName = new Date().getTime() + ".png";
+ log.info("二维码图片文件名为空,随机生成 png 格式图片...");
+ }
+
+ BufferedImage bufferedImage = getBufferedImage(codeContent);
+
+ /*
+ * javax.imageio.ImageIO:java扩展的图像IO
+ * write(RenderedImage im, String formatName, File output)
+ * im:待写入的图像, formatName:图像写入的格式,output:写入的图像文件,文件不存在时会自动创建
+ */
+ File codeImgFile = new File(codeImgFileSaveDir, fileName);
+ ImageIO.write(bufferedImage, "png", codeImgFile);
+
+ log.info("二维码图片生成成功:" + codeImgFile.getPath());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+ /**
+ * 生成二维码并输出到输出流, 通常用于输出到网页上进行显示
+ * 输出到网页与输出到磁盘上的文件中,区别在于最后一句 ImageIO.write
+ * write(RenderedImage im,String formatName,File output):写到文件中
+ * write(RenderedImage im,String formatName,OutputStream output):输出到输出流中
+ *
+ * @param codeContent :二维码内容
+ * @param outputStream :输出流,比如 HttpServletResponse 的 getOutputStream
+ */
+ public static void createCodeToOutputStream(String codeContent, OutputStream outputStream) {
+ try {
+ if (codeContent == null || "".equals(codeContent.trim())) {
+ log.info("二维码内容为空,不进行操作...");
+ return;
+ }
+ codeContent = codeContent.trim();
+
+ BufferedImage bufferedImage = getBufferedImage(codeContent);
+ /*
+ * 区别就是以一句,输出到输出流中,如果第三个参数是 File,则输出到文件中
+ */
+ ImageIO.write(bufferedImage, "png", outputStream);
+ log.info("二维码图片生成到输出流成功...");
+ } catch (Exception e) {
+ e.printStackTrace();
+ log.error("发生错误: {}!", e.getMessage());
+ }
+ }
+
+ private static BufferedImage getBufferedImage(String codeContent) throws WriterException {
+ /*
+ * com.google.zxing.EncodeHintType:编码提示类型,枚举类型
+ * EncodeHintType.CHARACTER_SET:设置字符编码类型
+ * EncodeHintType.ERROR_CORRECTION:设置误差校正
+ * ErrorCorrectionLevel:误差校正等级,L = ~7% correction、M = ~15% correction、Q = ~25% correction、H = ~30% correction
+ * 不设置时,默认为 L 等级,等级不一样,生成的图案不同,但扫描的结果是一样的
+ * EncodeHintType.MARGIN:设置二维码边距,单位像素,值越小,二维码距离四周越近
+ */
+ Map hints = new HashMap();
+ hints.put(EncodeHintType.CHARACTER_SET, "UTF-8");
+ hints.put(EncodeHintType.ERROR_CORRECTION, ErrorCorrectionLevel.M);
+ hints.put(EncodeHintType.MARGIN, 1);
+
+ /*
+ * MultiFormatWriter:多格式写入,这是一个工厂类,里面重载了两个 encode 方法,用于写入条形码或二维码
+ * encode(String contents,BarcodeFormat format,int width, int height,Map hints)
+ * contents:条形码/二维码内容
+ * format:编码类型,如 条形码,二维码 等
+ * width:码的宽度
+ * height:码的高度
+ * hints:码内容的编码类型
+ * BarcodeFormat:枚举该程序包已知的条形码格式,即创建何种码,如 1 维的条形码,2 维的二维码 等
+ * BitMatrix:位(比特)矩阵或叫2D矩阵,也就是需要的二维码
+ */
+ MultiFormatWriter multiFormatWriter = new MultiFormatWriter();
+ BitMatrix bitMatrix = multiFormatWriter.encode(codeContent, BarcodeFormat.QR_CODE, CODE_WIDTH, CODE_HEIGHT, hints);
+
+ /*
+ * java.awt.image.BufferedImage:具有图像数据的可访问缓冲图像,实现了 RenderedImage 接口
+ * BitMatrix 的 get(int x, int y) 获取比特矩阵内容,指定位置有值,则返回true,将其设置为前景色,否则设置为背景色
+ * BufferedImage 的 setRGB(int x, int y, int rgb) 方法设置图像像素
+ * x:像素位置的横坐标,即列
+ * y:像素位置的纵坐标,即行
+ * rgb:像素的值,采用 16 进制,如 0xFFFFFF 白色
+ */
+ BufferedImage bufferedImage = new BufferedImage(CODE_WIDTH, CODE_HEIGHT, BufferedImage.TYPE_INT_BGR);
+ for (int x = 0; x < CODE_WIDTH; x++) {
+ for (int y = 0; y < CODE_HEIGHT; y++) {
+ bufferedImage.setRGB(x, y, bitMatrix.get(x, y) ? FRONT_COLOR : BACKGROUND_COLOR);
+ }
+ }
+ return bufferedImage;
+ }
+
+
+ /**
+ * 根据本地二维码图片解析二维码内容 注:图片必须是二维码图片,但也可以是微信用户二维码名片,上面有名称、头像也是可以的)
+ *
+ * @param file 本地二维码图片文件,如 E:\\logs\\2.jpg
+ * @return
+ * @throws Exception
+ */
+ public static String parseQRCodeByFile(File file) {
+ String resultStr = null;
+ if (file == null || file.isDirectory() || !file.exists()) {
+ return resultStr;
+ }
+ try {
+ /*
+ * ImageIO的BufferedImage read(URL input)方法用于读取网络图片文件转为内存缓冲图像
+ * 同理还有:read(File input)、read(InputStream input)、、read(ImageInputStream stream)
+ */
+ BufferedImage bufferedImage = ImageIO.read(file);
+ /*
+ * com.google.zxing.client.j2se.BufferedImageLuminanceSource:缓冲图像亮度源
+ * 将 java.awt.image.BufferedImage 转为 zxing 的 缓冲图像亮度源
+ * 关键就是下面这几句:HybridBinarizer 用于读取二维码图像数据,BinaryBitmap 二进制位图
+ */
+ BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+ Hashtable hints = new Hashtable();
+ hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
+ /*
+ * 如果图片不是二维码图片,则 decode 抛异常:com.google.zxing.NotFoundException
+ * MultiFormatWriter 的 encode 用于对内容进行编码成 2D 矩阵
+ * MultiFormatReader 的 decode 用于读取二进制位图数据
+ */
+ Result result = new MultiFormatReader().decode(bitmap, hints);
+ resultStr = result.getText();
+ } catch (IOException e) {
+ e.printStackTrace();
+ } catch (NotFoundException e) {
+ e.printStackTrace();
+ log.error("图片非二维码图片, 路径是: {}!", file.getPath());
+ }
+ return resultStr;
+ }
+
+ /**
+ * 根据网络二维码图片解析二维码内容, 区别仅仅在于 ImageIO.read(url); 这一个重载的方法)
+ *
+ * @param url 二维码图片网络地址,如 https://res.wx.qq.com/mpres/htmledition/images/mp_qrcode3a7b38.gif
+ * @return
+ * @throws Exception
+ */
+ public static String parseQRCodeByUrl(URL url) {
+ String resultStr = null;
+ if (url == null) {
+ return resultStr;
+ }
+ try {
+ /*
+ * ImageIO 的 BufferedImage read(URL input) 方法用于读取网络图片文件转为内存缓冲图像
+ * 同理还有:read(File input)、read(InputStream input)、、read(ImageInputStream stream)
+ * 如果图片网络地址错误,比如不能访问,则 read 抛异常:javax.imageio.IIOException: Can't get input stream from URL!
+ */
+ BufferedImage bufferedImage = ImageIO.read(url);
+ /*
+ * com.google.zxing.client.j2se.BufferedImageLuminanceSource:缓冲图像亮度源
+ * 将 java.awt.image.BufferedImage 转为 zxing 的 缓冲图像亮度源
+ * 关键就是下面这几句:HybridBinarizer 用于读取二维码图像数据,BinaryBitmap 二进制位图
+ */
+ BufferedImageLuminanceSource source = new BufferedImageLuminanceSource(bufferedImage);
+ BinaryBitmap bitmap = new BinaryBitmap(new HybridBinarizer(source));
+ Hashtable hints = new Hashtable();
+ /*
+ * 如果内容包含中文,则解码的字符集格式应该和编码时一致
+ */
+ hints.put(DecodeHintType.CHARACTER_SET, "UTF-8");
+ /*
+ * 如果图片不是二维码图片,则 decode 抛异常:com.google.zxing.NotFoundException
+ * MultiFormatWriter 的 encode 用于对内容进行编码成 2D 矩阵
+ * MultiFormatReader 的 decode 用于读取二进制位图数据
+ */
+ Result result = new MultiFormatReader().decode(bitmap, hints);
+ resultStr = result.getText();
+ } catch (IOException e) {
+ e.printStackTrace();
+ log.error("二维码图片地址错误, 地址是: {}!", url);
+ } catch (NotFoundException e) {
+ e.printStackTrace();
+ log.error("图片非二维码图片, 地址是: {}!", url);
+ }
+ return resultStr;
+ }
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/MyConfig.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/MyConfig.java
new file mode 100644
index 00000000..41570972
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/MyConfig.java
@@ -0,0 +1,79 @@
+package net.lab1024.smartadmin.module.system.wxpay;
+
+
+import net.lab1024.smartadmin.module.system.wxpay.sdk.IWXPayDomain;
+import net.lab1024.smartadmin.module.system.wxpay.sdk.WXPayConfig;
+import net.lab1024.smartadmin.module.system.wxpay.sdk.WXPayConstants;
+
+import java.io.*;
+
+public class MyConfig implements WXPayConfig {
+
+ private byte[] certData;
+
+ public MyConfig() throws Exception {
+ String certPath = "C:/Users/Administrator/IdeaProjects/smart-admin/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_cert.p12";
+ File file = new File(certPath);
+ InputStream certStream = new FileInputStream(file);
+ this.certData = new byte[(int) file.length()];
+ certStream.read(this.certData);
+ certStream.close();
+ }
+ public String getAppID() {
+ return "wx3c51c14272f63a64";
+ }
+
+ public String getMchID() {
+ return "1315161001";
+ }
+
+ public String getKey() {
+ return "b69497999e8fd1f8f1f0a9591b24eb72";
+ }
+
+ public InputStream getCertStream() {
+ ByteArrayInputStream certBis = new ByteArrayInputStream(this.certData);
+ return certBis;
+ }
+
+ public int getHttpConnectTimeoutMs() {
+ return 8000;
+ }
+
+ public int getHttpReadTimeoutMs() {
+ return 10000;
+ }
+
+ @Override
+ public IWXPayDomain getWXPayDomain() {
+ // 这个方法需要这样实现, 否则无法正常初始化WXPay
+ IWXPayDomain iwxPayDomain = new IWXPayDomain() {
+
+ public void report(String domain, long elapsedTimeMillis, Exception ex) {
+
+ }
+
+ public DomainInfo getDomain(WXPayConfig config) {
+ return new IWXPayDomain.DomainInfo(WXPayConstants.DOMAIN_API, true);
+ }
+ };
+ return iwxPayDomain;
+ }
+
+ @Override
+ public boolean shouldAutoReport() {
+ return false;
+ }
+
+ public int getReportWorkerNum() {
+ return 6;
+ }
+
+ public int getReportQueueMaxSize() {
+ return 10000;
+ }
+
+ public int getReportBatchSize() {
+ return 10;
+ }
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/WxpayController.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/WxpayController.java
new file mode 100644
index 00000000..a24d08f0
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/WxpayController.java
@@ -0,0 +1,99 @@
+package net.lab1024.smartadmin.module.system.wxpay;
+
+import net.lab1024.smartadmin.module.system.wxpay.sdk.WXPay;
+import io.swagger.annotations.Api;
+import lombok.extern.slf4j.Slf4j;
+import net.lab1024.smartadmin.common.anno.OperateLog;
+import net.lab1024.smartadmin.constant.SwaggerTagConst;
+import net.lab1024.smartadmin.module.system.qrcode.QRCodeUtil;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import javax.servlet.http.HttpServletResponse;
+import java.io.IOException;
+import java.util.HashMap;
+import java.util.Map;
+
+@Api(tags = {SwaggerTagConst.Admin.MANAGER_MALL_PAY_API})
+@OperateLog
+@Slf4j
+@RestController
+public class WxpayController {
+
+
+
+ @GetMapping("royalcanin/qrCode")
+ public void getQRCode(String codeContent, HttpServletResponse response) {
+ System.out.println("codeContent=" + codeContent);
+ try {
+ /*
+ * 调用工具类生成二维码并输出到输出流中
+ */
+ QRCodeUtil.createCodeToOutputStream(codeContent, response.getOutputStream());
+ log.info("成功生成二维码!");
+
+ } catch (IOException e) {
+ log.error("发生错误, 错误信息是:{}!", e.getMessage());
+ }
+ }
+
+
+ @GetMapping("royalcanin/unifiedOrder")
+ public void unifiedOrder(String codeContent, HttpServletResponse response) throws Exception {
+ MyConfig config = new MyConfig();
+ WXPay wxpay = new WXPay(config);
+
+ Map data = new HashMap();
+ data.put("body", "腾讯充值中心-QQ会员充值");
+ data.put("out_trade_no", "2016090910595900000012");
+ data.put("device_info", "");
+ data.put("fee_type", "CNY");
+ data.put("total_fee", "1");
+ data.put("spbill_create_ip", "123.12.12.123");
+ data.put("notify_url", "http://www.example.com/wxpay/notify");
+ data.put("trade_type", "NATIVE"); // 此处指定为扫码支付
+ data.put("product_id", "12");
+
+ try {
+ Map resp = wxpay.unifiedOrder(data);
+ QRCodeUtil.createCodeToOutputStream(resp.toString(), response.getOutputStream());
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+ @GetMapping("royalcanin/orderQuery")
+ public String orderQuery(String codeContent, HttpServletResponse response) throws Exception {
+ MyConfig config = new MyConfig();
+ WXPay wxpay = new WXPay(config);
+
+ Map data = new HashMap();
+ data.put("out_trade_no", "2016090910595900000012");
+
+ try {
+ Map resp = wxpay.orderQuery(data);
+ return resp.toString();
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ return "null";
+ }
+
+ @GetMapping("royalcanin/refundQuery")
+ public void refundQuery(String codeContent, HttpServletResponse response) throws Exception {
+ MyConfig config = new MyConfig();
+ WXPay wxpay = new WXPay(config);
+
+ Map data = new HashMap();
+ data.put("out_trade_no", "2016090910595900000012");
+
+ try {
+ Map resp = wxpay.refundQuery(data);
+ System.out.println(resp);
+ } catch (Exception e) {
+ e.printStackTrace();
+ }
+ }
+
+
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/IWXPayDomain.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/IWXPayDomain.java
new file mode 100644
index 00000000..9fabe358
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/IWXPayDomain.java
@@ -0,0 +1,42 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+/**
+ * 域名管理,实现主备域名自动切换
+ */
+public abstract interface IWXPayDomain {
+ /**
+ * 上报域名网络状况
+ * @param domain 域名。 比如:api.mch.weixin.qq.com
+ * @param elapsedTimeMillis 耗时
+ * @param ex 网络请求中出现的异常。
+ * null表示没有异常
+ * ConnectTimeoutException,表示建立网络连接异常
+ * UnknownHostException, 表示dns解析异常
+ */
+ abstract void report(final String domain, long elapsedTimeMillis, final Exception ex);
+
+ /**
+ * 获取域名
+ * @param config 配置
+ * @return 域名
+ */
+ abstract DomainInfo getDomain(final WXPayConfig config);
+
+ static class DomainInfo{
+ public String domain; //域名
+ public boolean primaryDomain; //该域名是否为主域名。例如:api.mch.weixin.qq.com为主域名
+ public DomainInfo(String domain, boolean primaryDomain) {
+ this.domain = domain;
+ this.primaryDomain = primaryDomain;
+ }
+
+ @Override
+ public String toString() {
+ return "DomainInfo{" +
+ "domain='" + domain + '\'' +
+ ", primaryDomain=" + primaryDomain +
+ '}';
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPay.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPay.java
new file mode 100644
index 00000000..732816b8
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPay.java
@@ -0,0 +1,690 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+import net.lab1024.smartadmin.module.system.wxpay.sdk.WXPayConstants.SignType;
+import net.lab1024.smartadmin.module.system.wxpay.sdk.WXPayRequest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+public class WXPay {
+
+ private WXPayConfig config;
+ private SignType signType;
+ private boolean autoReport;
+ private boolean useSandbox;
+ private String notifyUrl;
+ private WXPayRequest wxPayRequest;
+
+ public WXPay(final WXPayConfig config) throws Exception {
+ this(config, null, true, false);
+ }
+
+ public WXPay(final WXPayConfig config, final boolean autoReport) throws Exception {
+ this(config, null, autoReport, false);
+ }
+
+
+ public WXPay(final WXPayConfig config, final boolean autoReport, final boolean useSandbox) throws Exception{
+ this(config, null, autoReport, useSandbox);
+ }
+
+ public WXPay(final WXPayConfig config, final String notifyUrl) throws Exception {
+ this(config, notifyUrl, true, false);
+ }
+
+ public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport) throws Exception {
+ this(config, notifyUrl, autoReport, false);
+ }
+
+ public WXPay(final WXPayConfig config, final String notifyUrl, final boolean autoReport, final boolean useSandbox) throws Exception {
+ this.config = config;
+ this.notifyUrl = notifyUrl;
+ this.autoReport = autoReport;
+ this.useSandbox = useSandbox;
+ if (useSandbox) {
+ this.signType = SignType.MD5; // 沙箱环境
+ }
+ else {
+ this.signType = SignType.HMACSHA256;
+ }
+ this.wxPayRequest = new WXPayRequest(config);
+ }
+
+ private void checkWXPayConfig() throws Exception {
+ if (this.config == null) {
+ throw new Exception("config is null");
+ }
+ if (this.config.getAppID() == null || this.config.getAppID().trim().length() == 0) {
+ throw new Exception("appid in config is empty");
+ }
+ if (this.config.getMchID() == null || this.config.getMchID().trim().length() == 0) {
+ throw new Exception("appid in config is empty");
+ }
+ if (this.config.getCertStream() == null) {
+ throw new Exception("cert stream in config is empty");
+ }
+ if (this.config.getWXPayDomain() == null){
+ throw new Exception("config.getWXPayDomain() is null");
+ }
+
+ if (this.config.getHttpConnectTimeoutMs() < 10) {
+ throw new Exception("http connect timeout is too small");
+ }
+ if (this.config.getHttpReadTimeoutMs() < 10) {
+ throw new Exception("http read timeout is too small");
+ }
+
+ }
+
+ /**
+ * 向 Map 中添加 appid、mch_id、nonce_str、sign_type、sign
+ * 该函数适用于商户适用于统一下单等接口,不适用于红包、代金券接口
+ *
+ * @param reqData
+ * @return
+ * @throws Exception
+ */
+ public Map fillRequestData(Map reqData) throws Exception {
+ reqData.put("appid", config.getAppID());
+ reqData.put("mch_id", config.getMchID());
+ reqData.put("nonce_str", WXPayUtil.generateNonceStr());
+ if (SignType.MD5.equals(this.signType)) {
+ reqData.put("sign_type", WXPayConstants.MD5);
+ }
+ else if (SignType.HMACSHA256.equals(this.signType)) {
+ reqData.put("sign_type", WXPayConstants.HMACSHA256);
+ }
+ reqData.put("sign", WXPayUtil.generateSignature(reqData, config.getKey(), this.signType));
+ return reqData;
+ }
+
+ /**
+ * 判断xml数据的sign是否有效,必须包含sign字段,否则返回false。
+ *
+ * @param reqData 向wxpay post的请求数据
+ * @return 签名是否有效
+ * @throws Exception
+ */
+ public boolean isResponseSignatureValid(Map reqData) throws Exception {
+ // 返回数据的签名方式和请求中给定的签名方式是一致的
+ return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), this.signType);
+ }
+
+ /**
+ * 判断支付结果通知中的sign是否有效
+ *
+ * @param reqData 向wxpay post的请求数据
+ * @return 签名是否有效
+ * @throws Exception
+ */
+ public boolean isPayResultNotifySignatureValid(Map reqData) throws Exception {
+ String signTypeInData = reqData.get(WXPayConstants.FIELD_SIGN_TYPE);
+ SignType signType;
+ if (signTypeInData == null) {
+ signType = SignType.MD5;
+ }
+ else {
+ signTypeInData = signTypeInData.trim();
+ if (signTypeInData.length() == 0) {
+ signType = SignType.MD5;
+ }
+ else if (WXPayConstants.MD5.equals(signTypeInData)) {
+ signType = SignType.MD5;
+ }
+ else if (WXPayConstants.HMACSHA256.equals(signTypeInData)) {
+ signType = SignType.HMACSHA256;
+ }
+ else {
+ throw new Exception(String.format("Unsupported sign_type: %s", signTypeInData));
+ }
+ }
+ return WXPayUtil.isSignatureValid(reqData, this.config.getKey(), signType);
+ }
+
+
+ /**
+ * 不需要证书的请求
+ * @param urlSuffix String
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 超时时间,单位是毫秒
+ * @param readTimeoutMs 超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public String requestWithoutCert(String urlSuffix, Map reqData,
+ int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String msgUUID = reqData.get("nonce_str");
+ String reqBody = WXPayUtil.mapToXml(reqData);
+
+ String resp = this.wxPayRequest.requestWithoutCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, autoReport);
+ return resp;
+ }
+
+
+ /**
+ * 需要证书的请求
+ * @param urlSuffix String
+ * @param reqData 向wxpay post的请求数据 Map
+ * @param connectTimeoutMs 超时时间,单位是毫秒
+ * @param readTimeoutMs 超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public String requestWithCert(String urlSuffix, Map reqData,
+ int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String msgUUID= reqData.get("nonce_str");
+ String reqBody = WXPayUtil.mapToXml(reqData);
+
+ String resp = this.wxPayRequest.requestWithCert(urlSuffix, msgUUID, reqBody, connectTimeoutMs, readTimeoutMs, this.autoReport);
+ return resp;
+ }
+
+ /**
+ * 处理 HTTPS API返回数据,转换成Map对象。return_code为SUCCESS时,验证签名。
+ * @param xmlStr API返回的XML格式数据
+ * @return Map类型数据
+ * @throws Exception
+ */
+ public Map processResponseXml(String xmlStr) throws Exception {
+ String RETURN_CODE = "return_code";
+ String return_code;
+ Map respData = WXPayUtil.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(WXPayConstants.FAIL)) {
+ return respData;
+ }
+ else if (return_code.equals(WXPayConstants.SUCCESS)) {
+ if (this.isResponseSignatureValid(respData)) {
+ 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));
+ }
+ }
+
+ /**
+ * 作用:提交刷卡支付
+ * 场景:刷卡支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map microPay(Map reqData) throws Exception {
+ return this.microPay(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:提交刷卡支付
+ * 场景:刷卡支付
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map microPay(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_MICROPAY_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.MICROPAY_URL_SUFFIX;
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+ /**
+ * 提交刷卡支付,针对软POS,尽可能做成功
+ * 内置重试机制,最多60s
+ * @param reqData
+ * @return
+ * @throws Exception
+ */
+ public Map microPayWithPos(Map reqData) throws Exception {
+ return this.microPayWithPos(reqData, this.config.getHttpConnectTimeoutMs());
+ }
+
+ /**
+ * 提交刷卡支付,针对软POS,尽可能做成功
+ * 内置重试机制,最多60s
+ * @param reqData
+ * @param connectTimeoutMs
+ * @return
+ * @throws Exception
+ */
+ public Map microPayWithPos(Map reqData, int connectTimeoutMs) throws Exception {
+ int remainingTimeMs = 60*1000;
+ long startTimestampMs = 0;
+ Map lastResult = null;
+ Exception lastException = null;
+
+ while (true) {
+ startTimestampMs = WXPayUtil.getCurrentTimestampMs();
+ int readTimeoutMs = remainingTimeMs - connectTimeoutMs;
+ if (readTimeoutMs > 1000) {
+ try {
+ lastResult = this.microPay(reqData, connectTimeoutMs, readTimeoutMs);
+ String returnCode = lastResult.get("return_code");
+ if (returnCode.equals("SUCCESS")) {
+ String resultCode = lastResult.get("result_code");
+ String errCode = lastResult.get("err_code");
+ if (resultCode.equals("SUCCESS")) {
+ break;
+ }
+ else {
+ // 看错误码,若支付结果未知,则重试提交刷卡支付
+ if (errCode.equals("SYSTEMERROR") || errCode.equals("BANKERROR") || errCode.equals("USERPAYING")) {
+ remainingTimeMs = remainingTimeMs - (int)(WXPayUtil.getCurrentTimestampMs() - startTimestampMs);
+ if (remainingTimeMs <= 100) {
+ break;
+ }
+ else {
+ WXPayUtil.getLogger().info("microPayWithPos: try micropay again");
+ if (remainingTimeMs > 5*1000) {
+ Thread.sleep(5*1000);
+ }
+ else {
+ Thread.sleep(1*1000);
+ }
+ continue;
+ }
+ }
+ else {
+ break;
+ }
+ }
+ }
+ else {
+ break;
+ }
+ }
+ catch (Exception ex) {
+ lastResult = null;
+ lastException = ex;
+ }
+ }
+ else {
+ break;
+ }
+ }
+
+ if (lastResult == null) {
+ throw lastException;
+ }
+ else {
+ return lastResult;
+ }
+ }
+
+
+
+ /**
+ * 作用:统一下单
+ * 场景:公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map unifiedOrder(Map reqData) throws Exception {
+ return this.unifiedOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:统一下单
+ * 场景:公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map unifiedOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_UNIFIEDORDER_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.UNIFIEDORDER_URL_SUFFIX;
+ }
+ if(this.notifyUrl != null) {
+ reqData.put("notify_url", this.notifyUrl);
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+ /**
+ * 作用:查询订单
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map orderQuery(Map reqData) throws Exception {
+ return this.orderQuery(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:查询订单
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据 int
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map orderQuery(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_ORDERQUERY_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.ORDERQUERY_URL_SUFFIX;
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+ /**
+ * 作用:撤销订单
+ * 场景:刷卡支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map reverse(Map reqData) throws Exception {
+ return this.reverse(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:撤销订单
+ * 场景:刷卡支付
+ * 其他:需要证书
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map reverse(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_REVERSE_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.REVERSE_URL_SUFFIX;
+ }
+ String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+ /**
+ * 作用:关闭订单
+ * 场景:公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map closeOrder(Map reqData) throws Exception {
+ return this.closeOrder(reqData, config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:关闭订单
+ * 场景:公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map closeOrder(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_CLOSEORDER_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.CLOSEORDER_URL_SUFFIX;
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+ /**
+ * 作用:申请退款
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map refund(Map reqData) throws Exception {
+ return this.refund(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:申请退款
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * 其他:需要证书
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map refund(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_REFUND_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.REFUND_URL_SUFFIX;
+ }
+ String respXml = this.requestWithCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+ /**
+ * 作用:退款查询
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map refundQuery(Map reqData) throws Exception {
+ return this.refundQuery(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:退款查询
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map refundQuery(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_REFUNDQUERY_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.REFUNDQUERY_URL_SUFFIX;
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+ /**
+ * 作用:对账单下载(成功时返回对账单数据,失败时返回XML格式数据)
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map downloadBill(Map reqData) throws Exception {
+ return this.downloadBill(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:对账单下载
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * 其他:无论是否成功都返回Map。若成功,返回的Map中含有return_code、return_msg、data,
+ * 其中return_code为`SUCCESS`,data为对账单数据。
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return 经过封装的API返回数据
+ * @throws Exception
+ */
+ public Map downloadBill(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_DOWNLOADBILL_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.DOWNLOADBILL_URL_SUFFIX;
+ }
+ String respStr = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs).trim();
+ Map ret;
+ // 出现错误,返回XML数据
+ if (respStr.indexOf("<") == 0) {
+ ret = WXPayUtil.xmlToMap(respStr);
+ }
+ else {
+ // 正常返回csv数据
+ ret = new HashMap();
+ ret.put("return_code", WXPayConstants.SUCCESS);
+ ret.put("return_msg", "ok");
+ ret.put("data", respStr);
+ }
+ return ret;
+ }
+
+
+ /**
+ * 作用:交易保障
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map report(Map reqData) throws Exception {
+ return this.report(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:交易保障
+ * 场景:刷卡支付、公共号支付、扫码支付、APP支付
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map report(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_REPORT_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.REPORT_URL_SUFFIX;
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return WXPayUtil.xmlToMap(respXml);
+ }
+
+
+ /**
+ * 作用:转换短链接
+ * 场景:刷卡支付、扫码支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map shortUrl(Map reqData) throws Exception {
+ return this.shortUrl(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:转换短链接
+ * 场景:刷卡支付、扫码支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map shortUrl(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_SHORTURL_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.SHORTURL_URL_SUFFIX;
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+ /**
+ * 作用:授权码查询OPENID接口
+ * 场景:刷卡支付
+ * @param reqData 向wxpay post的请求数据
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map authCodeToOpenid(Map reqData) throws Exception {
+ return this.authCodeToOpenid(reqData, this.config.getHttpConnectTimeoutMs(), this.config.getHttpReadTimeoutMs());
+ }
+
+
+ /**
+ * 作用:授权码查询OPENID接口
+ * 场景:刷卡支付
+ * @param reqData 向wxpay post的请求数据
+ * @param connectTimeoutMs 连接超时时间,单位是毫秒
+ * @param readTimeoutMs 读超时时间,单位是毫秒
+ * @return API返回数据
+ * @throws Exception
+ */
+ public Map authCodeToOpenid(Map reqData, int connectTimeoutMs, int readTimeoutMs) throws Exception {
+ String url;
+ if (this.useSandbox) {
+ url = WXPayConstants.SANDBOX_AUTHCODETOOPENID_URL_SUFFIX;
+ }
+ else {
+ url = WXPayConstants.AUTHCODETOOPENID_URL_SUFFIX;
+ }
+ String respXml = this.requestWithoutCert(url, this.fillRequestData(reqData), connectTimeoutMs, readTimeoutMs);
+ return this.processResponseXml(respXml);
+ }
+
+
+} // end class
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayConfig.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayConfig.java
new file mode 100644
index 00000000..6e6cdd2a
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayConfig.java
@@ -0,0 +1,92 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+
+import java.io.InputStream;
+
+public interface WXPayConfig {
+
+
+
+ /**
+ * 获取 App ID
+ *
+ * @return App ID
+ */
+ public abstract String getAppID();
+
+
+ /**
+ * 获取 Mch ID
+ *
+ * @return Mch ID
+ */
+ public abstract String getMchID();
+
+
+ /**
+ * 获取 API 密钥
+ *
+ * @return API密钥
+ */
+ public abstract String getKey();
+
+
+ /**
+ * 获取商户证书内容
+ *
+ * @return 商户证书内容
+ */
+ public abstract InputStream getCertStream();
+
+ /**
+ * HTTP(S) 连接超时时间,单位毫秒
+ *
+ * @return
+ */
+ public int getHttpConnectTimeoutMs() ;
+
+ /**
+ * HTTP(S) 读数据超时时间,单位毫秒
+ *
+ * @return
+ */
+ public int getHttpReadTimeoutMs() ;
+
+ /**
+ * 获取WXPayDomain, 用于多域名容灾自动切换
+ * @return
+ */
+ public abstract IWXPayDomain getWXPayDomain();
+
+ /**
+ * 是否自动上报。
+ * 若要关闭自动上报,子类中实现该函数返回 false 即可。
+ *
+ * @return
+ */
+ public boolean shouldAutoReport() ;
+
+ /**
+ * 进行健康上报的线程的数量
+ *
+ * @return
+ */
+ public int getReportWorkerNum() ;
+
+
+ /**
+ * 健康上报缓存消息的最大数量。会有线程去独立上报
+ * 粗略计算:加入一条消息200B,10000消息占用空间 2000 KB,约为2MB,可以接受
+ *
+ * @return
+ */
+ public int getReportQueueMaxSize() ;
+
+ /**
+ * 批量上报,一次最多上报多个数据
+ *
+ * @return
+ */
+ public int getReportBatchSize() ;
+
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayConstants.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayConstants.java
new file mode 100644
index 00000000..ab3c2ca1
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayConstants.java
@@ -0,0 +1,59 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+import org.apache.http.client.HttpClient;
+
+/**
+ * 常量
+ */
+public class WXPayConstants {
+
+ public enum SignType {
+ MD5, HMACSHA256
+ }
+
+ public static final String DOMAIN_API = "api.mch.weixin.qq.com";
+ public static final String DOMAIN_API2 = "api2.mch.weixin.qq.com";
+ public static final String DOMAIN_APIHK = "apihk.mch.weixin.qq.com";
+ public static final String DOMAIN_APIUS = "apius.mch.weixin.qq.com";
+
+
+ public static final String FAIL = "FAIL";
+ public static final String SUCCESS = "SUCCESS";
+ public static final String HMACSHA256 = "HMAC-SHA256";
+ public static final String MD5 = "MD5";
+
+ public static final String FIELD_SIGN = "sign";
+ public static final String FIELD_SIGN_TYPE = "sign_type";
+
+ public static final String WXPAYSDK_VERSION = "WXPaySDK/3.0.9";
+ public static final String USER_AGENT = WXPAYSDK_VERSION +
+ " (" + System.getProperty("os.arch") + " " + System.getProperty("os.name") + " " + System.getProperty("os.version") +
+ ") Java/" + System.getProperty("java.version") + " HttpClient/" + HttpClient.class.getPackage().getImplementationVersion();
+
+ public static final String MICROPAY_URL_SUFFIX = "/pay/micropay";
+ public static final String UNIFIEDORDER_URL_SUFFIX = "/pay/unifiedorder";
+ public static final String ORDERQUERY_URL_SUFFIX = "/pay/orderquery";
+ public static final String REVERSE_URL_SUFFIX = "/secapi/pay/reverse";
+ public static final String CLOSEORDER_URL_SUFFIX = "/pay/closeorder";
+ public static final String REFUND_URL_SUFFIX = "/secapi/pay/refund";
+ public static final String REFUNDQUERY_URL_SUFFIX = "/pay/refundquery";
+ public static final String DOWNLOADBILL_URL_SUFFIX = "/pay/downloadbill";
+ public static final String REPORT_URL_SUFFIX = "/payitil/report";
+ public static final String SHORTURL_URL_SUFFIX = "/tools/shorturl";
+ public static final String AUTHCODETOOPENID_URL_SUFFIX = "/tools/authcodetoopenid";
+
+ // sandbox
+ public static final String SANDBOX_MICROPAY_URL_SUFFIX = "/sandboxnew/pay/micropay";
+ public static final String SANDBOX_UNIFIEDORDER_URL_SUFFIX = "/sandboxnew/pay/unifiedorder";
+ public static final String SANDBOX_ORDERQUERY_URL_SUFFIX = "/sandboxnew/pay/orderquery";
+ public static final String SANDBOX_REVERSE_URL_SUFFIX = "/sandboxnew/secapi/pay/reverse";
+ public static final String SANDBOX_CLOSEORDER_URL_SUFFIX = "/sandboxnew/pay/closeorder";
+ public static final String SANDBOX_REFUND_URL_SUFFIX = "/sandboxnew/secapi/pay/refund";
+ public static final String SANDBOX_REFUNDQUERY_URL_SUFFIX = "/sandboxnew/pay/refundquery";
+ public static final String SANDBOX_DOWNLOADBILL_URL_SUFFIX = "/sandboxnew/pay/downloadbill";
+ public static final String SANDBOX_REPORT_URL_SUFFIX = "/sandboxnew/payitil/report";
+ public static final String SANDBOX_SHORTURL_URL_SUFFIX = "/sandboxnew/tools/shorturl";
+ public static final String SANDBOX_AUTHCODETOOPENID_URL_SUFFIX = "/sandboxnew/tools/authcodetoopenid";
+
+}
+
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayReport.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayReport.java
new file mode 100644
index 00000000..dba76414
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayReport.java
@@ -0,0 +1,265 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+
+import java.util.concurrent.ExecutorService;
+import java.util.concurrent.Executors;
+import java.util.concurrent.LinkedBlockingQueue;
+import java.util.concurrent.ThreadFactory;
+
+/**
+ * 交易保障
+ */
+public class WXPayReport {
+
+ public static class ReportInfo {
+
+ /**
+ * 布尔变量使用int。0为false, 1为true。
+ */
+
+ // 基本信息
+ private String version = "v1";
+ private String sdk = WXPayConstants.WXPAYSDK_VERSION;
+ private String uuid; // 交易的标识
+ private long timestamp; // 上报时的时间戳,单位秒
+ private long elapsedTimeMillis; // 耗时,单位 毫秒
+
+ // 针对主域名
+ private String firstDomain; // 第1次请求的域名
+ private boolean primaryDomain; //是否主域名
+ private int firstConnectTimeoutMillis; // 第1次请求设置的连接超时时间,单位 毫秒
+ private int firstReadTimeoutMillis; // 第1次请求设置的读写超时时间,单位 毫秒
+ private int firstHasDnsError; // 第1次请求是否出现dns问题
+ private int firstHasConnectTimeout; // 第1次请求是否出现连接超时
+ private int firstHasReadTimeout; // 第1次请求是否出现连接超时
+
+ public ReportInfo(String uuid, long timestamp, long elapsedTimeMillis, String firstDomain, boolean primaryDomain, int firstConnectTimeoutMillis, int firstReadTimeoutMillis, boolean firstHasDnsError, boolean firstHasConnectTimeout, boolean firstHasReadTimeout) {
+ this.uuid = uuid;
+ this.timestamp = timestamp;
+ this.elapsedTimeMillis = elapsedTimeMillis;
+ this.firstDomain = firstDomain;
+ this.primaryDomain = primaryDomain;
+ this.firstConnectTimeoutMillis = firstConnectTimeoutMillis;
+ this.firstReadTimeoutMillis = firstReadTimeoutMillis;
+ this.firstHasDnsError = firstHasDnsError?1:0;
+ this.firstHasConnectTimeout = firstHasConnectTimeout?1:0;
+ this.firstHasReadTimeout = firstHasReadTimeout?1:0;
+ }
+
+ @Override
+ public String toString() {
+ return "ReportInfo{" +
+ "version='" + version + '\'' +
+ ", sdk='" + sdk + '\'' +
+ ", uuid='" + uuid + '\'' +
+ ", timestamp=" + timestamp +
+ ", elapsedTimeMillis=" + elapsedTimeMillis +
+ ", firstDomain='" + firstDomain + '\'' +
+ ", primaryDomain=" + primaryDomain +
+ ", firstConnectTimeoutMillis=" + firstConnectTimeoutMillis +
+ ", firstReadTimeoutMillis=" + firstReadTimeoutMillis +
+ ", firstHasDnsError=" + firstHasDnsError +
+ ", firstHasConnectTimeout=" + firstHasConnectTimeout +
+ ", firstHasReadTimeout=" + firstHasReadTimeout +
+ '}';
+ }
+
+ /**
+ * 转换成 csv 格式
+ *
+ * @return
+ */
+ public String toLineString(String key) {
+ String separator = ",";
+ Object[] objects = new Object[] {
+ version, sdk, uuid, timestamp, elapsedTimeMillis,
+ firstDomain, primaryDomain, firstConnectTimeoutMillis, firstReadTimeoutMillis,
+ firstHasDnsError, firstHasConnectTimeout, firstHasReadTimeout
+ };
+ StringBuffer sb = new StringBuffer();
+ for(Object obj: objects) {
+ sb.append(obj).append(separator);
+ }
+ try {
+ String sign = WXPayUtil.HMACSHA256(sb.toString(), key);
+ sb.append(sign);
+ return sb.toString();
+ }
+ catch (Exception ex) {
+ return null;
+ }
+
+ }
+
+ }
+
+ private static final String REPORT_URL = "http://report.mch.weixin.qq.com/wxpay/report/default";
+ // private static final String REPORT_URL = "http://127.0.0.1:5000/test";
+
+
+ private static final int DEFAULT_CONNECT_TIMEOUT_MS = 6*1000;
+ private static final int DEFAULT_READ_TIMEOUT_MS = 8*1000;
+
+ private LinkedBlockingQueue reportMsgQueue = null;
+ private WXPayConfig config;
+ private ExecutorService executorService;
+
+ private volatile static WXPayReport INSTANCE;
+
+ private WXPayReport(final WXPayConfig config) {
+ this.config = config;
+ reportMsgQueue = new LinkedBlockingQueue(config.getReportQueueMaxSize());
+
+ // 添加处理线程
+ executorService = Executors.newFixedThreadPool(config.getReportWorkerNum(), new ThreadFactory() {
+ public Thread newThread(Runnable r) {
+ Thread t = Executors.defaultThreadFactory().newThread(r);
+ t.setDaemon(true);
+ return t;
+ }
+ });
+
+ if (config.shouldAutoReport()) {
+ WXPayUtil.getLogger().info("report worker num: {}", config.getReportWorkerNum());
+ for (int i = 0; i < config.getReportWorkerNum(); ++i) {
+ executorService.execute(new Runnable() {
+ public void run() {
+ while (true) {
+ // 先用 take 获取数据
+ try {
+ StringBuffer sb = new StringBuffer();
+ String firstMsg = reportMsgQueue.take();
+ WXPayUtil.getLogger().info("get first report msg: {}", firstMsg);
+ String msg = null;
+ sb.append(firstMsg); //会阻塞至有消息
+ int remainNum = config.getReportBatchSize() - 1;
+ for (int j=0; jcreate()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", SSLConnectionSocketFactory.getSocketFactory())
+ .build(),
+ null,
+ null,
+ null
+ );
+ HttpClient httpClient = HttpClientBuilder.create()
+ .setConnectionManager(connManager)
+ .build();
+
+ HttpPost httpPost = new HttpPost(REPORT_URL);
+
+ RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build();
+ httpPost.setConfig(requestConfig);
+
+ StringEntity postEntity = new StringEntity(data, "UTF-8");
+ httpPost.addHeader("Content-Type", "text/xml");
+ httpPost.addHeader("User-Agent", WXPayConstants.USER_AGENT);
+ httpPost.setEntity(postEntity);
+
+ HttpResponse httpResponse = httpClient.execute(httpPost);
+ HttpEntity httpEntity = httpResponse.getEntity();
+ return EntityUtils.toString(httpEntity, "UTF-8");
+ }
+
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayRequest.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayRequest.java
new file mode 100644
index 00000000..b69072f1
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayRequest.java
@@ -0,0 +1,258 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+import org.apache.http.HttpEntity;
+import org.apache.http.HttpResponse;
+import org.apache.http.client.HttpClient;
+import org.apache.http.client.config.RequestConfig;
+import org.apache.http.client.methods.HttpPost;
+import org.apache.http.config.RegistryBuilder;
+import org.apache.http.conn.ConnectTimeoutException;
+import org.apache.http.conn.socket.ConnectionSocketFactory;
+import org.apache.http.conn.socket.PlainConnectionSocketFactory;
+import org.apache.http.conn.ssl.DefaultHostnameVerifier;
+import org.apache.http.conn.ssl.SSLConnectionSocketFactory;
+import org.apache.http.entity.StringEntity;
+import org.apache.http.impl.client.HttpClientBuilder;
+import org.apache.http.impl.conn.BasicHttpClientConnectionManager;
+import org.apache.http.util.EntityUtils;
+
+import javax.net.ssl.KeyManagerFactory;
+import javax.net.ssl.SSLContext;
+import java.io.InputStream;
+import java.net.SocketTimeoutException;
+import java.net.UnknownHostException;
+import java.security.KeyStore;
+import java.security.SecureRandom;
+
+import static net.lab1024.smartadmin.module.system.wxpay.sdk.WXPayConstants.USER_AGENT;
+
+public class WXPayRequest {
+ private WXPayConfig config;
+ public WXPayRequest(WXPayConfig config) throws Exception{
+
+ this.config = config;
+ }
+
+ /**
+ * 请求,只请求一次,不做重试
+ * @param domain
+ * @param urlSuffix
+ * @param uuid
+ * @param data
+ * @param connectTimeoutMs
+ * @param readTimeoutMs
+ * @param useCert 是否使用证书,针对退款、撤销等操作
+ * @return
+ * @throws Exception
+ */
+ private String requestOnce(final String domain, String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert) throws Exception {
+ BasicHttpClientConnectionManager connManager;
+ if (useCert) {
+ // 证书
+ char[] password = config.getMchID().toCharArray();
+ InputStream certStream = config.getCertStream();
+ KeyStore ks = KeyStore.getInstance("PKCS12");
+ ks.load(certStream, password);
+
+ // 实例化密钥库 & 初始化密钥工厂
+ KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm());
+ kmf.init(ks, password);
+
+ // 创建 SSLContext
+ SSLContext sslContext = SSLContext.getInstance("TLS");
+ sslContext.init(kmf.getKeyManagers(), null, new SecureRandom());
+
+ SSLConnectionSocketFactory sslConnectionSocketFactory = new SSLConnectionSocketFactory(
+ sslContext,
+ new String[]{"TLSv1"},
+ null,
+ new DefaultHostnameVerifier());
+
+ connManager = new BasicHttpClientConnectionManager(
+ RegistryBuilder.create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", sslConnectionSocketFactory)
+ .build(),
+ null,
+ null,
+ null
+ );
+ }
+ else {
+ connManager = new BasicHttpClientConnectionManager(
+ RegistryBuilder.create()
+ .register("http", PlainConnectionSocketFactory.getSocketFactory())
+ .register("https", SSLConnectionSocketFactory.getSocketFactory())
+ .build(),
+ null,
+ null,
+ null
+ );
+ }
+
+ HttpClient httpClient = HttpClientBuilder.create()
+ .setConnectionManager(connManager)
+ .build();
+
+ String url = "https://" + domain + urlSuffix;
+ HttpPost httpPost = new HttpPost(url);
+
+ RequestConfig requestConfig = RequestConfig.custom().setSocketTimeout(readTimeoutMs).setConnectTimeout(connectTimeoutMs).build();
+ httpPost.setConfig(requestConfig);
+
+ StringEntity postEntity = new StringEntity(data, "UTF-8");
+ httpPost.addHeader("Content-Type", "text/xml");
+ httpPost.addHeader("User-Agent", USER_AGENT + " " + config.getMchID());
+ httpPost.setEntity(postEntity);
+
+ HttpResponse httpResponse = httpClient.execute(httpPost);
+ HttpEntity httpEntity = httpResponse.getEntity();
+ return EntityUtils.toString(httpEntity, "UTF-8");
+
+ }
+
+
+ private String request(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean useCert, boolean autoReport) throws Exception {
+ Exception exception = null;
+ long elapsedTimeMillis = 0;
+ long startTimestampMs = WXPayUtil.getCurrentTimestampMs();
+ boolean firstHasDnsErr = false;
+ boolean firstHasConnectTimeout = false;
+ boolean firstHasReadTimeout = false;
+ IWXPayDomain.DomainInfo domainInfo = config.getWXPayDomain().getDomain(config);
+ if(domainInfo == null){
+ throw new Exception("WXPayConfig.getWXPayDomain().getDomain() is empty or null");
+ }
+ try {
+ String result = requestOnce(domainInfo.domain, urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, useCert);
+ elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
+ config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, null);
+ WXPayReport.getInstance(config).report(
+ uuid,
+ elapsedTimeMillis,
+ domainInfo.domain,
+ domainInfo.primaryDomain,
+ connectTimeoutMs,
+ readTimeoutMs,
+ firstHasDnsErr,
+ firstHasConnectTimeout,
+ firstHasReadTimeout);
+ return result;
+ }
+ catch (UnknownHostException ex) { // dns 解析错误,或域名不存在
+ exception = ex;
+ firstHasDnsErr = true;
+ elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
+ WXPayUtil.getLogger().warn("UnknownHostException for domainInfo {}", domainInfo);
+ WXPayReport.getInstance(config).report(
+ uuid,
+ elapsedTimeMillis,
+ domainInfo.domain,
+ domainInfo.primaryDomain,
+ connectTimeoutMs,
+ readTimeoutMs,
+ firstHasDnsErr,
+ firstHasConnectTimeout,
+ firstHasReadTimeout
+ );
+ }
+ catch (ConnectTimeoutException ex) {
+ exception = ex;
+ firstHasConnectTimeout = true;
+ elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
+ WXPayUtil.getLogger().warn("connect timeout happened for domainInfo {}", domainInfo);
+ WXPayReport.getInstance(config).report(
+ uuid,
+ elapsedTimeMillis,
+ domainInfo.domain,
+ domainInfo.primaryDomain,
+ connectTimeoutMs,
+ readTimeoutMs,
+ firstHasDnsErr,
+ firstHasConnectTimeout,
+ firstHasReadTimeout
+ );
+ }
+ catch (SocketTimeoutException ex) {
+ exception = ex;
+ firstHasReadTimeout = true;
+ elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
+ WXPayUtil.getLogger().warn("timeout happened for domainInfo {}", domainInfo);
+ WXPayReport.getInstance(config).report(
+ uuid,
+ elapsedTimeMillis,
+ domainInfo.domain,
+ domainInfo.primaryDomain,
+ connectTimeoutMs,
+ readTimeoutMs,
+ firstHasDnsErr,
+ firstHasConnectTimeout,
+ firstHasReadTimeout);
+ }
+ catch (Exception ex) {
+ exception = ex;
+ elapsedTimeMillis = WXPayUtil.getCurrentTimestampMs()-startTimestampMs;
+ WXPayReport.getInstance(config).report(
+ uuid,
+ elapsedTimeMillis,
+ domainInfo.domain,
+ domainInfo.primaryDomain,
+ connectTimeoutMs,
+ readTimeoutMs,
+ firstHasDnsErr,
+ firstHasConnectTimeout,
+ firstHasReadTimeout);
+ }
+ config.getWXPayDomain().report(domainInfo.domain, elapsedTimeMillis, exception);
+ throw exception;
+ }
+
+
+ /**
+ * 可重试的,非双向认证的请求
+ * @param urlSuffix
+ * @param uuid
+ * @param data
+ * @return
+ */
+ public String requestWithoutCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception {
+ return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), false, autoReport);
+ }
+
+ /**
+ * 可重试的,非双向认证的请求
+ * @param urlSuffix
+ * @param uuid
+ * @param data
+ * @param connectTimeoutMs
+ * @param readTimeoutMs
+ * @return
+ */
+ public String requestWithoutCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean autoReport) throws Exception {
+ return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, false, autoReport);
+ }
+
+ /**
+ * 可重试的,双向认证的请求
+ * @param urlSuffix
+ * @param uuid
+ * @param data
+ * @return
+ */
+ public String requestWithCert(String urlSuffix, String uuid, String data, boolean autoReport) throws Exception {
+ return this.request(urlSuffix, uuid, data, config.getHttpConnectTimeoutMs(), config.getHttpReadTimeoutMs(), true, autoReport);
+ }
+
+ /**
+ * 可重试的,双向认证的请求
+ * @param urlSuffix
+ * @param uuid
+ * @param data
+ * @param connectTimeoutMs
+ * @param readTimeoutMs
+ * @return
+ */
+ public String requestWithCert(String urlSuffix, String uuid, String data, int connectTimeoutMs, int readTimeoutMs, boolean autoReport) throws Exception {
+ return this.request(urlSuffix, uuid, data, connectTimeoutMs, readTimeoutMs, true, autoReport);
+ }
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayUtil.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayUtil.java
new file mode 100644
index 00000000..83b4f4c9
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayUtil.java
@@ -0,0 +1,295 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+import net.lab1024.smartadmin.module.system.wxpay.sdk.WXPayConstants.SignType;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.w3c.dom.Node;
+import org.w3c.dom.NodeList;
+
+import javax.crypto.Mac;
+import javax.crypto.spec.SecretKeySpec;
+import javax.xml.parsers.DocumentBuilder;
+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.ByteArrayInputStream;
+import java.io.InputStream;
+import java.io.StringWriter;
+import java.security.MessageDigest;
+import java.security.SecureRandom;
+import java.util.*;
+
+
+public class WXPayUtil {
+
+ private static final String SYMBOLS = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
+
+ private static final Random RANDOM = new SecureRandom();
+
+ /**
+ * XML格式字符串转换为Map
+ *
+ * @param strXML XML字符串
+ * @return XML数据转换后的Map
+ * @throws Exception
+ */
+ public static Map xmlToMap(String strXML) throws Exception {
+ try {
+ Map data = new HashMap();
+ DocumentBuilder documentBuilder = WXPayXmlUtil.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) {
+ WXPayUtil.getLogger().warn("Invalid XML, can not convert to map. Error message: {}. XML content: {}", ex.getMessage(), strXML);
+ throw ex;
+ }
+
+ }
+
+ /**
+ * 将Map转换为XML格式的字符串
+ *
+ * @param data Map类型数据
+ * @return XML格式的字符串
+ * @throws Exception
+ */
+ public static String mapToXml(Map data) throws Exception {
+ org.w3c.dom.Document document = WXPayXmlUtil.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;
+ }
+
+
+ /**
+ * 生成带有 sign 的 XML 格式字符串
+ *
+ * @param data Map类型数据
+ * @param key API密钥
+ * @return 含有sign字段的XML
+ */
+ public static String generateSignedXml(final Map data, String key) throws Exception {
+ return generateSignedXml(data, key, SignType.MD5);
+ }
+
+ /**
+ * 生成带有 sign 的 XML 格式字符串
+ *
+ * @param data Map类型数据
+ * @param key API密钥
+ * @param signType 签名类型
+ * @return 含有sign字段的XML
+ */
+ public static String generateSignedXml(final Map data, String key, SignType signType) throws Exception {
+ String sign = generateSignature(data, key, signType);
+ data.put(WXPayConstants.FIELD_SIGN, sign);
+ return mapToXml(data);
+ }
+
+
+ /**
+ * 判断签名是否正确
+ *
+ * @param xmlStr XML格式数据
+ * @param key API密钥
+ * @return 签名是否正确
+ * @throws Exception
+ */
+ public static boolean isSignatureValid(String xmlStr, String key) throws Exception {
+ Map data = xmlToMap(xmlStr);
+ if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
+ return false;
+ }
+ String sign = data.get(WXPayConstants.FIELD_SIGN);
+ return generateSignature(data, key).equals(sign);
+ }
+
+ /**
+ * 判断签名是否正确,必须包含sign字段,否则返回false。使用MD5签名。
+ *
+ * @param data Map类型数据
+ * @param key API密钥
+ * @return 签名是否正确
+ * @throws Exception
+ */
+ public static boolean isSignatureValid(Map data, String key) throws Exception {
+ return isSignatureValid(data, key, SignType.MD5);
+ }
+
+ /**
+ * 判断签名是否正确,必须包含sign字段,否则返回false。
+ *
+ * @param data Map类型数据
+ * @param key API密钥
+ * @param signType 签名方式
+ * @return 签名是否正确
+ * @throws Exception
+ */
+ public static boolean isSignatureValid(Map data, String key, SignType signType) throws Exception {
+ if (!data.containsKey(WXPayConstants.FIELD_SIGN) ) {
+ return false;
+ }
+ String sign = data.get(WXPayConstants.FIELD_SIGN);
+ return generateSignature(data, key, signType).equals(sign);
+ }
+
+ /**
+ * 生成签名
+ *
+ * @param data 待签名数据
+ * @param key API密钥
+ * @return 签名
+ */
+ public static String generateSignature(final Map data, String key) throws Exception {
+ return generateSignature(data, key, SignType.MD5);
+ }
+
+ /**
+ * 生成签名. 注意,若含有sign_type字段,必须和signType参数保持一致。
+ *
+ * @param data 待签名数据
+ * @param key API密钥
+ * @param signType 签名方式
+ * @return 签名
+ */
+ public static String generateSignature(final Map data, String key, SignType signType) throws Exception {
+ Set 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(WXPayConstants.FIELD_SIGN)) {
+ continue;
+ }
+ if (data.get(k).trim().length() > 0) // 参数值为空,则不参与签名
+ sb.append(k).append("=").append(data.get(k).trim()).append("&");
+ }
+ sb.append("key=").append(key);
+ if (SignType.MD5.equals(signType)) {
+ return MD5(sb.toString()).toUpperCase();
+ }
+ else if (SignType.HMACSHA256.equals(signType)) {
+ return HMACSHA256(sb.toString(), key);
+ }
+ else {
+ throw new Exception(String.format("Invalid sign_type: %s", signType));
+ }
+ }
+
+
+ /**
+ * 获取随机字符串 Nonce Str
+ *
+ * @return String 随机字符串
+ */
+ public static String generateNonceStr() {
+ char[] nonceChars = new char[32];
+ for (int index = 0; index < nonceChars.length; ++index) {
+ nonceChars[index] = SYMBOLS.charAt(RANDOM.nextInt(SYMBOLS.length()));
+ }
+ return new String(nonceChars);
+ }
+
+
+ /**
+ * 生成 MD5
+ *
+ * @param data 待处理数据
+ * @return MD5结果
+ */
+ public static String MD5(String data) throws Exception {
+ java.security.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();
+ }
+
+ /**
+ * 日志
+ * @return
+ */
+ public static Logger getLogger() {
+ Logger logger = LoggerFactory.getLogger("wxpay java sdk");
+ return logger;
+ }
+
+ /**
+ * 获取当前时间戳,单位秒
+ * @return
+ */
+ public static long getCurrentTimestamp() {
+ return System.currentTimeMillis()/1000;
+ }
+
+ /**
+ * 获取当前时间戳,单位毫秒
+ * @return
+ */
+ public static long getCurrentTimestampMs() {
+ return System.currentTimeMillis();
+ }
+
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayXmlUtil.java b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayXmlUtil.java
new file mode 100644
index 00000000..9ead2c53
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/java/net/lab1024/smartadmin/module/system/wxpay/sdk/WXPayXmlUtil.java
@@ -0,0 +1,30 @@
+package net.lab1024.smartadmin.module.system.wxpay.sdk;
+
+import org.w3c.dom.Document;
+
+import javax.xml.XMLConstants;
+import javax.xml.parsers.DocumentBuilder;
+import javax.xml.parsers.DocumentBuilderFactory;
+import javax.xml.parsers.ParserConfigurationException;
+
+/**
+ * 2018/7/3
+ */
+public final class WXPayXmlUtil {
+ public static DocumentBuilder newDocumentBuilder() throws ParserConfigurationException {
+ DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
+ documentBuilderFactory.setFeature("http://apache.org/xml/features/disallow-doctype-decl", true);
+ documentBuilderFactory.setFeature("http://xml.org/sax/features/external-general-entities", false);
+ documentBuilderFactory.setFeature("http://xml.org/sax/features/external-parameter-entities", false);
+ documentBuilderFactory.setFeature("http://apache.org/xml/features/nonvalidating/load-external-dtd", false);
+ documentBuilderFactory.setFeature(XMLConstants.FEATURE_SECURE_PROCESSING, true);
+ documentBuilderFactory.setXIncludeAware(false);
+ documentBuilderFactory.setExpandEntityReferences(false);
+
+ return documentBuilderFactory.newDocumentBuilder();
+ }
+
+ public static Document newDocument() throws ParserConfigurationException {
+ return newDocumentBuilder().newDocument();
+ }
+}
diff --git a/smart-admin-service/smart-admin-api/src/main/resources/sql/smart-admin.sql b/smart-admin-service/smart-admin-api/src/main/resources/sql/smart-admin.sql
index 2d15d341..4149c002 100644
--- a/smart-admin-service/smart-admin-api/src/main/resources/sql/smart-admin.sql
+++ b/smart-admin-service/smart-admin-api/src/main/resources/sql/smart-admin.sql
@@ -1598,11 +1598,11 @@ CREATE TABLE IF NOT EXISTS `t_royalcanin_operate_log` (
`params` varchar(500) COLLATE utf8mb4_unicode_ci NOT NULL,
`start_time` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`elapsed_time` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
+ `accept_time` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`code` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
`msg` varchar(50) COLLATE utf8mb4_unicode_ci NOT NULL,
PRIMARY KEY (`id`)
-) ENGINE=MyISAM DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
-COMMIT;
+) ENGINE=MyISAM AUTO_INCREMENT=51 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
/*!40101 SET CHARACTER_SET_CLIENT=@OLD_CHARACTER_SET_CLIENT */;
/*!40101 SET CHARACTER_SET_RESULTS=@OLD_CHARACTER_SET_RESULTS */;
diff --git a/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_cert.p12 b/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_cert.p12
new file mode 100644
index 00000000..3bd999f4
Binary files /dev/null and b/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_cert.p12 differ
diff --git a/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_cert.pem b/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_cert.pem
new file mode 100644
index 00000000..0e0a2b58
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_cert.pem
@@ -0,0 +1,24 @@
+-----BEGIN CERTIFICATE-----
+MIID8DCCAtigAwIBAgIUNXr3yeDUjmouRUJS1REpfuJb5fgwDQYJKoZIhvcNAQEL
+BQAwXjELMAkGA1UEBhMCQ04xEzARBgNVBAoTClRlbnBheS5jb20xHTAbBgNVBAsT
+FFRlbnBheS5jb20gQ0EgQ2VudGVyMRswGQYDVQQDExJUZW5wYXkuY29tIFJvb3Qg
+Q0EwHhcNMjExMjA5MDgwMTMwWhcNMjYxMjA4MDgwMTMwWjCBgTETMBEGA1UEAwwK
+MTMxNTE2MTAwMTEbMBkGA1UECgwS5b6u5L+h5ZWG5oi357O757ufMS0wKwYDVQQL
+DCTlub/lt57pn6bmgankv6Hmga/np5HmioDmnInpmZDlhazlj7gxCzAJBgNVBAYM
+AkNOMREwDwYDVQQHDAhTaGVuWmhlbjCCASIwDQYJKoZIhvcNAQEBBQADggEPADCC
+AQoCggEBALcuHNWXYS6901JqhZkzAZOTZF04J/9yC5VFqutFh27LKdwUNlJ087gO
+18SNXLUCPD9DGLBQRY7FivBAHLtxN/ZBR7wqRUVIVuiYFw2vu8jzTNERwsx+WXgX
+sPlbFQ8PWX6JFHY8FjSyXdDJwt7syiWqZp2E6eYucybbm/rLoygEOpkHDdtvlXI+
+V3+XDicQNdvOlNq6Rv85StaianNgUMVOqsDVsPT/QbAcGZKhY5Ff8yPtHtsIh7EN
+myGtRfz5EBPKt2PVa5Ycw9aozpeEtZWKr/5Nnb+XB5XD3l5smSap4BwZKoCz57MP
+sG+f2GbuK0iugrIWuiEbDZY7ved9HQECAwEAAaOBgTB/MAkGA1UdEwQCMAAwCwYD
+VR0PBAQDAgTwMGUGA1UdHwReMFwwWqBYoFaGVGh0dHA6Ly9ldmNhLml0cnVzLmNv
+bS5jbi9wdWJsaWMvaXRydXNjcmw/Q0E9MUJENDIyMEU1MERCQzA0QjA2QUQzOTc1
+NDk4NDZDMDFDM0U4RUJEMjANBgkqhkiG9w0BAQsFAAOCAQEAs/vNxUSMLc7sWc8/
+uVeia6KLO0LQO/D8cv/ycchRhVRvyqz2xPyiOaCvl4nZ4f3gaGIFp321cvzJEOey
+Tuv7kdklhxPUeBkTW5u6vclLSOjvjEsWUnsn7qpJCQBq+7WRiXdTZsfnOuLbIQ89
+vyX0BKoUyTxtQfWftjtCgtfX7qdcJ3lHNTOw8tmSeyu4/uLB347BEK3Xm8kwuKm1
+Gwh86UftSxKKX/0/6xH9Ptzf6VnBRTnXY6CbVf0rUccBW9EQ6cYYWxqSfQpVrXxE
+kVw41i7Ug7xHHVxCw9w7984/7NSRw2ImaRogU51ZUdvITH3bheK66wkZrGar3pQQ
+wFFyIA==
+-----END CERTIFICATE-----
diff --git a/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_key.pem b/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_key.pem
new file mode 100644
index 00000000..664bd73a
--- /dev/null
+++ b/smart-admin-service/smart-admin-api/src/main/resources/wxpay/apiclient_key.pem
@@ -0,0 +1,28 @@
+-----BEGIN PRIVATE KEY-----
+MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQC3LhzVl2EuvdNS
+aoWZMwGTk2RdOCf/cguVRarrRYduyyncFDZSdPO4DtfEjVy1Ajw/QxiwUEWOxYrw
+QBy7cTf2QUe8KkVFSFbomBcNr7vI80zREcLMfll4F7D5WxUPD1l+iRR2PBY0sl3Q
+ycLe7MolqmadhOnmLnMm25v6y6MoBDqZBw3bb5VyPld/lw4nEDXbzpTaukb/OUrW
+ompzYFDFTqrA1bD0/0GwHBmSoWORX/Mj7R7bCIexDZshrUX8+RATyrdj1WuWHMPW
+qM6XhLWViq/+TZ2/lweVw95ebJkmqeAcGSqAs+ezD7Bvn9hm7itIroKyFrohGw2W
+O73nfR0BAgMBAAECggEAMqkZJmVIZlHveNKnAjieDf9seW+HeFlFJNJws8K18wlT
+GV+pqmJX6SaJu/1Qj2r/2Iso79IzeondpBZHG/vy+zMQsNSL9d5M0uUFMy7P44KO
+Ux/zXPTwr4r10fDDwTeAi8NDmnOBvK87zPuP3nFkcshOuu/AdH2SMUP5PoJFLFKP
+PFCgBzJurjLajUx0CYseJMIopxlcYfmRUTNZEyYWDmlf+Y+PGaq8obWTazKn/pOs
+KZV2nWvri2p4VMHJbaEJ9NeMeFSaJT1CbEdkxA4zcBatPdVJ3NQSZsMYR7822qyR
+JfzpfgvEAIshrHe/8WIVoCKCJ7Ih6aySL+Y9FyS2RQKBgQDgGY70qyBIeKXf0YC7
+AzvKQUe5dvAJNhojOaEGgG1fZmuQztxH8YJT20i5BjzUNVbaWTjlGNM40BdA3Ksg
+szeJKiF+bfP2CEIrpQYNgOFI5fm/EKA3L5LlrbMa9Q2fdV43ZIOeNv1q8v/8DJol
+/YCptrMUIYJhPyXDU0/wwX75gwKBgQDRQWVAf6qzID7sin6KwsfXE3BzRvLFVIqx
+Gdbb14Dm07dEyN2P7CSig+CYdHcopT7in0NxCsjdDC6VmhOO39F+PgDUguUjjTZr
+mKyHC8cCqnrYp+zEDG/E7f2YpV0Eip4h5PbUCgYBafVuQILRHabASt1/ripdfWbO
+yBPUTc+8KwKBgFWb7eyx4udMj7FtC0xl89VUEt5a/V/Hdzy1qzocnauwKK9tFuG0
+9SCryKBGLb6zkbZgF0gNr8MoW3m/0u/vBuwzWBBAWM5IYV7i7+kArw92K24+H7Z1
+ShzLunw12TptT8zBbTZns2/D3N6gwdJ5gUlULh0mn/Qed4KitQs4IBWZAoGBAKkC
+LIk/k9tBPBFIepQQZd+sb7Jrp0EB21LXD9ZX5GCKtwbzRSAtFhm8cfYwet11OalI
+AOyYTUtbv3RNEWbgFw/jfEAnyHJCZ6XXleEs/fuFPdsnJe/iK5PetGhJhytX2gh+
+KW1w+xDBR91YdU/4punAjc3GGamQ0yxIJNvgTNwpAoGAWRcng9vvselZN2gYw967
+bHRKURo+91WtjvB6PL3fSltLg3xkqFZHx6LTysITTtm/7D+CRemSAKhsNE0S7Qvb
+D/S+dSXtdHNHO4pPJF0g1sstkubFgXf7mDwjKlVJPohlafk7t7sMsCnRQU37F1Q4
+2ndfutSk2sUoqYUz3HeRZkE=
+-----END PRIVATE KEY-----