Parcourir la source

微信公众号对话消息

zuy il y a 2 ans
Parent
commit
ef49400948

+ 406 - 0
common/src/main/java/com/kym/common/utils/EncryptUtil.java

@@ -0,0 +1,406 @@
+package com.kym.common.utils;
+
+import org.apache.commons.codec.digest.DigestUtils;
+
+import javax.crypto.BadPaddingException;
+import javax.crypto.Cipher;
+import javax.crypto.CipherInputStream;
+import javax.crypto.CipherOutputStream;
+import javax.crypto.IllegalBlockSizeException;
+import javax.crypto.KeyGenerator;
+import javax.crypto.Mac;
+import javax.crypto.NoSuchPaddingException;
+import javax.crypto.SecretKey;
+import javax.crypto.SecretKeyFactory;
+import javax.crypto.spec.DESKeySpec;
+import javax.crypto.spec.IvParameterSpec;
+import javax.crypto.spec.SecretKeySpec;
+import java.io.File;
+import java.io.FileInputStream;
+import java.io.FileOutputStream;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.nio.charset.StandardCharsets;
+import java.security.InvalidKeyException;
+import java.security.Key;
+import java.security.MessageDigest;
+import java.security.NoSuchAlgorithmException;
+import java.security.SecureRandom;
+import java.util.Base64;
+
+
+/**
+ * MD5加密算法类
+ *
+ * @author yaopeng
+ * @date 2015年5月29日 上午11:22:56
+ */
+public class EncryptUtil {
+    /**
+     * md5盐值
+     */
+    private static final String SALT = "M33ab";
+    /**
+     * 十六进制下数字到字符的映射数组
+     */
+    private final static String[] HEX_DIGITS = {"0", "1", "2", "3", "4", "5", "6", "7", "8", "9", "a", "b", "c", "d", "e", "f"};
+
+    /**
+     * 8位偏移量
+     */
+    private static final String IV = "97812367";
+
+    /**
+     * aes des加密key
+     */
+    public static final String DEFAULT_KEY = "MTIzNzg5YWNk";
+
+    private EncryptUtil() {
+    }
+
+    /**
+     * 对字符串进行MD5加密
+     */
+    public static String encodeByMD5(String content) {
+        try {
+            if (null != content) {
+                return DigestUtils.md5Hex(content);
+          /*      //创建具有指定算法名称的信息摘要
+                MessageDigest md = MessageDigest.getInstance("MD5");
+                //使用指定的字节数组对摘要进行最后更新,然后完成摘要计算
+                byte[] results = md.digest(content.getBytes(StandardCharsets.UTF_8));
+                //将得到的字节数组变成字符串返回
+                String resultString = byteArrayToHexString(results);
+                return resultString.toUpperCase();*/
+            }
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 对字符串进行HmacSHA1加密
+     *
+     */
+    public static String encodeByHmacSHA1(String content) {
+        try {
+            if (null != content) {
+                SecretKey secretKey = new SecretKeySpec(content.getBytes(), "HmacSHA1");
+                Mac mac = Mac.getInstance(secretKey.getAlgorithm());
+                mac.init(secretKey);
+                byte[] results = mac.doFinal();
+                //将得到的字节数组变成字符串返回
+                String resultString = byteArrayToHexString(results);
+                return resultString.toUpperCase();
+            }
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 对字符串进行HmacSHA256加密
+     *
+     */
+    public static String encodeByHmacSHA256(String content) {
+        try {
+            if (null != content) {
+                SecretKey secretKey = new SecretKeySpec(content.getBytes(), "HmacSHA256");
+                Mac mac = Mac.getInstance(secretKey.getAlgorithm());
+                mac.init(secretKey);
+                byte[] results = mac.doFinal();
+                //将得到的字节数组变成字符串返回
+                String resultString = byteArrayToHexString(results);
+                return resultString.toUpperCase();
+            }
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 对字符串进行HmacMD5加密
+     *
+     */
+    public static String encodeByHmacMD5(String content) {
+        try {
+            if (null != content) {
+                SecretKey secretKey = new SecretKeySpec(content.getBytes(), "HmacMD5");
+                Mac mac = Mac.getInstance(secretKey.getAlgorithm());
+                mac.init(secretKey);
+                byte[] results = mac.doFinal();
+                //将得到的字节数组变成字符串返回
+                String resultString = byteArrayToHexString(results);
+                return resultString.toUpperCase();
+            }
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 对字符串进行SHA1加密
+     */
+    public static String encodeBySHA1(String content) {
+        try {
+            if (null != content) {
+                //创建具有指定算法名称的信息摘要
+                MessageDigest md = MessageDigest.getInstance("SHA-1");
+                md.reset();
+                //使用指定的字节数组对摘要进行最后更新,然后完成摘要计算
+                byte[] results = md.digest(content.getBytes(StandardCharsets.UTF_8));
+                //将得到的字节数组变成字符串返回
+                String resultString = byteArrayToHexString(results);
+                return resultString.toUpperCase();
+            }
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * 字符串SHA-256加密
+     */
+    public static String encodeBySHA256(String content) {
+        MessageDigest messageDigest = null;
+        try {
+            if (null != content) {
+                messageDigest = MessageDigest.getInstance("SHA-256");
+                byte[] bytes = content.getBytes();
+                messageDigest.update(bytes);
+                byte[] disestBytes = messageDigest.digest();
+                char[] hexDigits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F'};
+                char[] chars = new char[disestBytes.length * 2];
+                int index = 0;
+                for (byte b : disestBytes) {
+                    chars[index++] = hexDigits[b >>> 4 & 0xf];
+                    chars[index++] = hexDigits[b & 0xf];
+                }
+                return new String(chars);
+            }
+        } catch (NoSuchAlgorithmException e) {
+            e.printStackTrace();
+        }
+        return null;
+    }
+
+    /**
+     * aes加密
+     *
+     * @param key     密钥
+     */
+    public static String encodeByAes(String content, String key) {
+        if (null != content) {
+            KeyGenerator kgen = null;
+            try {
+                kgen = KeyGenerator.getInstance("AES");
+
+
+                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+                random.setSeed(key.getBytes());
+                kgen.init(128, random);
+
+                SecretKey secretKey = kgen.generateKey();
+                byte[] enCodeFormat = secretKey.getEncoded();
+                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
+                Cipher cipher = Cipher.getInstance("AES");
+                byte[] byteContent = content.getBytes(StandardCharsets.UTF_8);
+                cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec);
+                byte[] bytes = cipher.doFinal(byteContent);
+                return byteArrayToHexString(bytes);
+            } catch (NoSuchAlgorithmException | InvalidKeyException | NoSuchPaddingException | BadPaddingException | IllegalBlockSizeException e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+    /**
+     * aes 解密
+     *
+     * @param key     密钥
+     */
+    public static String decodeByAes(String content, String key) {
+        if (null != content) {
+            try {
+                byte[] byteRresult = new byte[content.length() / 2];
+                for (int i = 0; i < content.length() / 2; i++) {
+                    int high = Integer.parseInt(content.substring(i * 2, i * 2 + 1), 16);
+                    int low = Integer.parseInt(content.substring(i * 2 + 1, i * 2 + 2), 16);
+                    byteRresult[i] = (byte) (high * 16 + low);
+                }
+                KeyGenerator kgen = KeyGenerator.getInstance("AES");
+                SecureRandom random = SecureRandom.getInstance("SHA1PRNG");
+                random.setSeed(key.getBytes());
+                kgen.init(128, random);
+
+                SecretKey secretKey = kgen.generateKey();
+                byte[] enCodeFormat = secretKey.getEncoded();
+                SecretKeySpec secretKeySpec = new SecretKeySpec(enCodeFormat, "AES");
+                Cipher cipher = Cipher.getInstance("AES");
+                cipher.init(Cipher.DECRYPT_MODE, secretKeySpec);
+                byte[] result = cipher.doFinal(byteRresult);
+                return new String(result);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+
+    public static String encodeByDes(String content, String key) {
+        if (null != content) {
+            if (key.length() < 8) {
+                throw new IllegalArgumentException("des encrypt key length less than 8!");
+            }
+            try {
+                DESKeySpec dks = new DESKeySpec(key.getBytes(StandardCharsets.UTF_8));
+                SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+                Key secretKey = keyFactory.generateSecret(dks);
+                //加密算法模式
+                Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
+                IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
+                cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
+                byte[] bytes = cipher.doFinal(content.getBytes(StandardCharsets.UTF_8));
+                return Base64.getEncoder().encodeToString(bytes);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+
+    public static String decodeByDes(String content, String key) {
+        if (null != content) {
+            if (key.length() < 8) {
+                throw new IllegalArgumentException("des decrypt key length less than 8!");
+            }
+            try {
+                DESKeySpec dks = new DESKeySpec(key.getBytes(StandardCharsets.UTF_8));
+                SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+                Key secretKey = keyFactory.generateSecret(dks);
+                Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
+                IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
+                cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
+                return new String(cipher.doFinal(Base64.getDecoder().decode(content.getBytes(StandardCharsets.UTF_8))), StandardCharsets.UTF_8);
+            } catch (Exception e) {
+                e.printStackTrace();
+            }
+        }
+        return null;
+    }
+
+
+    /**
+     * DES加密文件
+     *
+     * @param srcFile  待加密的文件
+     * @param destFile 加密后存放的文件路径
+     * @return 加密后的文件路径
+     */
+    public static void encodeFileByDes(String key, String srcFile, String destFile) {
+        if (key.length() < 8) {
+            throw new IllegalArgumentException("des encrypt key length less than 8!");
+        }
+        try {
+            DESKeySpec dks = new DESKeySpec(key.getBytes(StandardCharsets.UTF_8));
+            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+            Key secretKey = keyFactory.generateSecret(dks);
+            IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
+            Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
+            cipher.init(Cipher.ENCRYPT_MODE, secretKey, iv);
+            InputStream is = new FileInputStream(srcFile);
+            OutputStream out = new FileOutputStream(destFile);
+            CipherInputStream cis = new CipherInputStream(is, cipher);
+            byte[] buffer = new byte[1024];
+            int r;
+            while ((r = cis.read(buffer)) > 0) {
+                out.write(buffer, 0, r);
+            }
+            cis.close();
+            is.close();
+            out.close();
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+    }
+
+
+    /**
+     * DES解密文件
+     *
+     * @param srcFile  已加密的文件
+     * @param destFile 解密后存放的文件路径
+     * @return 解密后的文件路径
+     */
+    public static void decodeFileByDes(String key, String srcFile, String destFile) {
+        if (key.length() < 8) {
+            throw new IllegalArgumentException("des encrypt key length less than 8!");
+        }
+        try {
+            File file = new File(destFile);
+            if (!file.exists()) {
+                file.getParentFile().mkdirs();
+                file.createNewFile();
+            }
+            DESKeySpec dks = new DESKeySpec(key.getBytes(StandardCharsets.UTF_8));
+            SecretKeyFactory keyFactory = SecretKeyFactory.getInstance("DES");
+            Key secretKey = keyFactory.generateSecret(dks);
+            IvParameterSpec iv = new IvParameterSpec(IV.getBytes(StandardCharsets.UTF_8));
+            Cipher cipher = Cipher.getInstance("DES/CBC/PKCS5Padding");
+            cipher.init(Cipher.DECRYPT_MODE, secretKey, iv);
+            InputStream is = new FileInputStream(srcFile);
+            OutputStream out = new FileOutputStream(destFile);
+            CipherOutputStream cos = new CipherOutputStream(out, cipher);
+            byte[] buffer = new byte[1024];
+            int r;
+            while ((r = is.read(buffer)) >= 0) {
+                cos.write(buffer, 0, r);
+            }
+            cos.close();
+            is.close();
+            out.close();
+        } catch (Exception ex) {
+            ex.printStackTrace();
+        }
+    }
+
+    /**
+     * 转换字节数组为十六进制字符串
+     *
+     * @param b 字节数组
+     */
+    private static String byteArrayToHexString(byte[] b) {
+        StringBuilder sbr = new StringBuilder();
+        for (byte value : b) {
+            sbr.append(byteToHexString(value));
+        }
+        return sbr.toString();
+    }
+
+    /**
+     * 将一个字节转化成十六进制形式的字符串
+     *
+     * @param b 字节数
+     */
+    private static String byteToHexString(byte b) {
+        int n = b;
+        if (n < 0) {
+            n = 256 + n;
+        }
+        int d1 = n / 16;
+        int d2 = n % 16;
+        return HEX_DIGITS[d1] + HEX_DIGITS[d2];
+    }
+
+    public static String encryptPassword(String pwd) {
+        return encodeByMD5(encodeByMD5(pwd) + SALT);
+    }
+}

+ 20 - 0
common/src/main/java/com/kym/common/utils/wx/WxAccess.java

@@ -0,0 +1,20 @@
+package com.kym.common.utils.wx;
+
+/**
+ * 接口访问token业务类
+ * create by yaop at 2019/4/6 0:22
+ */
+public class WxAccess {
+
+    public String accessToken;
+    public String refreshToken;
+    public int expire;
+    public long expireAt;
+    public String jsApiTicket;
+    public String appId;
+    public String openId;
+    public String unionId;
+
+
+
+}

+ 75 - 0
common/src/main/java/com/kym/common/utils/wx/WxPbConfig.java

@@ -0,0 +1,75 @@
+package com.kym.common.utils.wx;
+
+
+
+
+/**
+ * 微信公众开发配置类
+ * create by yaop at 2019/4/6 0:05
+ */
+public class WxPbConfig {
+
+
+  //TODO
+//    static String TOKEN = ConfigUtil.getProperty("wx.pb.token");
+//    //微信支付分配的公众账号ID(企业号corpid即为此appId)
+//  public  static final String PUBLIC_APP_ID = ConfigUtil.getProperty("wx.pb.appid");
+//    public static final String PUBLIC_APP_SECRET = ConfigUtil.getProperty("wx.pb.appsecret");
+
+    static String TOKEN = "";
+    //微信支付分配的公众账号ID(企业号corpid即为此appId)
+  public  static final String PUBLIC_APP_ID = "";
+    public static final String PUBLIC_APP_SECRET = "";
+
+    //场景值
+    static final int SCENE_订阅=100;
+    static final int SCENE_取消订阅=101;
+
+
+    /**
+     * 获取微信接口授权token
+     */
+    static String API_ACCESS_TOKEN = "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=%s&secret=%s";
+    /**
+     * 刷新微信接口授权token
+     */
+    public static String API_REFRESH_TOKEN = "https://api.weixin.qq.com/sns/oauth2/refresh_token?grant_type=refresh_token&appid=%s&refresh_token=%s";
+    /**
+     * 查询用户关注状态
+     */
+    static String API_SUBSCRIBE = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s";
+    /**
+     * 获取微信api_ticket
+     */
+    static String API_JS_APITICKET = "https://api.weixin.qq.com/cgi-bin/ticket/getticket?type=jsapi&access_token=%s";
+    /**
+     * 素材文件下载
+     */
+    static String API_MEDIAFILE = "https://file.api.weixin.qq.com/cgi-bin/media/get?media_id=%s&access_token=%s";
+    /**
+     * 获取openId、unionId
+     */
+    static String API_OPENID = "https://api.weixin.qq.com/sns/oauth2/access_token?grant_type=authorization_code&appid=%s&secret=%s&code=%s";
+    /**
+     * 获取用户基本信息
+     */
+    static String API_USER_INFO = "https://api.weixin.qq.com/cgi-bin/user/info?access_token=%s&openid=%s&lang=zh_CN";
+    /**
+     * 推广二维码
+     */
+    static String API_QR_CODE = "https://api.weixin.qq.com/cgi-bin/qrcode/create?access_token=%s";
+    /**
+     * 获取公众号模板列表
+     */
+    static String API_TEMPLATE_LIST = "https://api.weixin.qq.com/cgi-bin/template/get_all_private_template?access_token=%s";
+    /**
+     * 发送公众号模板消息
+     */
+    static String API_SEND_TEMPLATE_MSG = "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=%s";
+
+    /**
+     * 发送客服消息
+     */
+    static String API_SEND_KF_MSG = "https://https://api.weixin.qq.com/cgi-bin/message/mass/send?access_token=%s";
+
+}

+ 406 - 0
common/src/main/java/com/kym/common/utils/wx/WxPbUtil.java

@@ -0,0 +1,406 @@
+package com.kym.common.utils.wx;
+
+
+import cn.hutool.json.JSONUtil;
+import com.alibaba.fastjson2.JSON;
+import com.kym.common.exception.BusinessException;
+import com.kym.common.utils.CommUtil;
+import com.kym.common.utils.EncryptUtil;
+import com.kym.common.utils.HttpUtil;
+import org.apache.http.Consts;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.util.Arrays;
+import java.util.HashMap;
+import java.util.Map;
+import java.util.UUID;
+
+/**
+ * 微信公众开发工具类
+ * create by yaop at 2019/4/6 0:03
+ *
+ * @author yaopeng
+ */
+public class WxPbUtil {
+    
+    private static final  Logger logger = LoggerFactory.getLogger(WxPbUtil.class);
+
+    /**
+     * 工具类禁用公有构造方法
+     */
+    private WxPbUtil() {
+    }
+
+    private static WxAccess config = null;
+
+    /**
+     * 发送客服消息
+     *
+     * @param openId   接收客户openId
+     * @param content  回复消息内容
+     * @param miniPath 小程序跳转地址
+     */
+    public static void sendCustomMessage(String openId, String content, String miniPath) {
+        String accessToken = getAccessToken();
+        String url = String.format(WxPbConfig.API_SEND_KF_MSG, accessToken);
+        try {
+            Map<String, Object> param = new HashMap<>(4);
+            param.put("touser", openId);
+            param.put("msgtype", "text");
+            Map<String, String> text = new HashMap<>(2);
+            if (CommUtil.isNotEmptyAndNull(miniPath)) {
+                String xcxAppId =WxPbConfig.PUBLIC_APP_ID;
+                if (CommUtil.isNotEmptyAndNull(xcxAppId)) {
+                    // 哈哈哈{appid},跳转
+                    content =content.replaceFirst("\\{}",xcxAppId);
+//                    StringUtils.replaceOnce(content, "{}", xcxAppId);
+                }
+            }
+            text.put("content", content);
+            param.put("text", text);
+            String data = JSON.toJSONString(param);
+            Map<String, Object> result = sslRequest(url, "post", data);
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.sendCustomMessage error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("服务异常");
+            }
+        } catch (Exception e) {
+            logger.error("WxPbUtil.sendCustomMessage error ", e);
+        }
+    }
+
+
+    /**
+     * jsapi 签名
+     */
+    public static Map<String, Object> signJsApi(String url) {
+        Map<String, Object> ret = new HashMap<>(4);
+        String jsapiTicket = WxPbUtil.config.jsApiTicket;
+        String nonceStr = UUID.randomUUID().toString();
+        String timestamp = Long.toString(System.currentTimeMillis() / 1000);
+        String string1;
+        String signature = "";
+        // 注意这里参数名必须全部小写,且必须有序
+        string1 = "jsapi_ticket=" + jsapiTicket + "&noncestr=" + nonceStr + "&timestamp=" + timestamp + "&url=" + url;
+        try {
+            signature = EncryptUtil.encodeBySHA1(string1);
+            ret.put("appId", WxPbConfig.PUBLIC_APP_ID);
+            ret.put("timestamp", Long.valueOf(timestamp));
+            ret.put("nonceStr", nonceStr);
+            ret.put("signature", signature);
+        } catch (Exception e) {
+            logger.error("WxPbUtil.signJsApi error ", e);
+        }
+        return ret;
+    }
+
+    /**
+     * 获取消息模板列表
+     *
+     * @return
+     */
+    public static Map<String, Object> getTemplateList() {
+        String accessToken = getAccessToken();
+        String url = String.format(WxPbConfig.API_TEMPLATE_LIST, accessToken);
+        try {
+            Map<String, Object> result = sslRequest(url, "post", null);
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.getTemplateList error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("服务异常");
+            }
+            return result;
+        } catch (Exception e) {
+            logger.error("WxPbUtil.getTemplateList error ", e);
+        }
+        return null;
+    }
+
+
+    /**
+     * 发送公众号消息
+     *
+     * @param openId     接收者openId
+     * @param templateId
+     * @param vars
+     * @param url        模板跳转链接
+     * @param xcxUrl     跳转小程序url
+     */
+    public static void sendPublicTemplateMessage(String openId, String templateId, Map<String, String> vars, String url, String xcxUrl) {
+        String accessToken = getAccessToken();
+        Map<String, Object> param = new HashMap<String, Object>();
+        param.put("touser", openId);
+        param.put("template_id", templateId);
+        param.put("url", url);
+        param.put("topcolor", "#FF0000");
+        //跳转小程序
+        if (CommUtil.isNotEmptyAndNull(xcxUrl)) {
+//            String xcxAppId = WxMpConfig.XCX_APPID;
+            String xcxAppId = "";
+            if (CommUtil.isNotEmptyAndNull(xcxAppId)) {
+                Map<String, String> xcxParam = new HashMap<>();
+                xcxParam.put("appid", xcxAppId);
+                xcxParam.put("pagepath", xcxUrl);
+                param.put("miniprogram", xcxParam);
+            }
+        }
+        Map<String, Object> varsMap = new HashMap<>();
+        vars.forEach((k, v) -> {
+            Map<String, String> varValueMap = new HashMap<>();
+            varValueMap.put("value", v);
+            varValueMap.put("color", CommUtil.isEmptyOrNull(v) ? "#2b2b2b" : v);
+            varsMap.put(k, varValueMap);
+        });
+        //
+        param.put("data", varsMap);
+        String data = JSON.toJSONString(param);
+        String reqUrl = String.format(WxPbConfig.API_SEND_TEMPLATE_MSG, accessToken);
+        try {
+            Map<String, Object> result = sslRequest(reqUrl, "post", data);
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.sendPublicTemplateMessage error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("服务异常");
+            }
+        } catch (Exception e) {
+            logger.error("WxPbUtil.sendPublicTemplateMessage error ", e);
+        }
+    }
+
+
+    /**
+     * 获取推广二维码链接(临时)<br>
+     * 1.如果用户还未关注公众号,则用户可以关注公众号,关注后微信会将带场景值关注事件推送给开发者。<br>
+     * 2.如果用户已经关注公众号,在用户扫描后会自动进入会话,微信也会将带场景值扫描事件推送给开发者。
+     *
+     * @param sceneId 场景值ID,临时二维码时为32位非0整型,永久二维码时最大值为100000(目前参数只支持1--100000)
+     */
+    public String spreadQrCode(int sceneId) {
+        //有效期最大30天
+        int expireSeconds = 2592000;
+        String accessToken = getAccessToken();
+        Map<String, String> wrap = new HashMap<>(2);
+        wrap.put("access_token", accessToken);
+        Map<String, Object> actionWrap = new HashMap<>(4);
+        actionWrap.put("expire_seconds", expireSeconds);
+        actionWrap.put("action_name", "QR_SCENE");
+        //
+        Map<String, Object> actionInfo = new HashMap<>(2);
+        Map<String, Object> sceneInfo = new HashMap<>(2);
+        sceneInfo.put("scene_id", sceneId);
+        actionInfo.put("scene", sceneInfo);
+        actionWrap.put("action_info", actionInfo);
+        //
+        String data = JSON.toJSONString(actionWrap);
+        String url = String.format(WxPbConfig.API_QR_CODE, accessToken);
+        try {
+            Map<String, Object> result = sslRequest(url, "post", data);
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.spreadQrCode error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("服务异常");
+            }
+            return (String) result.get("url");
+        } catch (Exception e) {
+            logger.error("WxPbUtil.spreadQrCode error ", e);
+        }
+        return null;
+    }
+
+    /**
+     * 获取用户基本信息
+     */
+    public static Map<String, Object> getUserInfo(String openId) {
+        String accessToken = getAccessToken();
+        String url = String.format(WxPbConfig.API_USER_INFO, accessToken, openId);
+        try {
+            Map<String, Object> result = sslRequest(url, "post", null);
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.getUserInfo error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("服务异常");
+            }
+            return result;
+        } catch (Exception e) {
+            logger.error("WxPbUtil.getUserInfo error ", e);
+        }
+        return null;
+    }
+
+
+    /**
+     * <p>https://mp.weixin.qq.com/wiki?t=resource/res_main&id=mp1421140842</p>
+     * <p>
+     * 网页授权获取openId
+     */
+    public static WxAccess getOpenId(String code, String appId, String appSecret) {
+        String url = String.format(WxPbConfig.API_OPENID, appId, appSecret, code);
+        try {
+            Map<String, Object> result = sslRequest(url, "post", null);
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.getOpenId error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("服务异常");
+            }
+            WxAccess accessToken = new WxAccess();
+            accessToken.accessToken = (String) result.get("access_token");
+            accessToken.openId = (String) result.get("openid");
+            accessToken.unionId = (String) result.get("unionId");
+            accessToken.refreshToken = (String) result.get("refresh_token");
+            config = accessToken;
+            return accessToken;
+        } catch (Exception e) {
+            logger.error("WxPbUtil.getOpenId error ", e);
+        }
+        return null;
+    }
+
+    /**
+     * 获取多媒体素材
+     */
+    public static byte[] getMediaFile(String mediaId) {
+        String accessToken = getAccessToken();
+        try {
+            String url = String.format(WxPbConfig.API_MEDIAFILE, mediaId, accessToken);
+            Map<String, Object> result = sslRequest(url, "post", mediaId);
+            String str = (String) result.get("result");
+            return str.getBytes();
+        } catch (Exception e) {
+            logger.error("WxPbUtil.getMediaFile error ", e);
+        }
+        return null;
+    }
+
+    /**
+     * 获取jsapi 凭证
+     */
+    public static String getJsApiTicket() {
+        try {
+            String accessToken = getAccessToken();
+            String url = String.format(WxPbConfig.API_JS_APITICKET, accessToken);
+            Map<String, Object> result = sslRequest(url, "get", null);
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.getJsApiTicket error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("服务异常");
+            }
+            String jsApiTicket = (String) result.get("ticket");
+            config.jsApiTicket = jsApiTicket;
+            return jsApiTicket;
+        } catch (Exception e) {
+            logger.error("WxPbUtil.getJsApiTicket error ", e);
+        }
+        return null;
+    }
+
+    /**
+     * 普通获取访问access_token须和网页授权access_token区分
+     */
+    public static WxAccess reqAccessToken(String appId, String appSecret) {
+        WxAccess accessToken = null;
+        try {
+            String url = String.format(WxPbConfig.API_ACCESS_TOKEN, appId, appSecret);
+            Map<String, Object> result = sslRequest(url, "get", null);
+            accessToken = new WxAccess();
+            accessToken.accessToken = (String) result.get("access_token");
+            accessToken.expireAt = System.currentTimeMillis() + accessToken.expire * 1000;
+            accessToken.expire = (int) result.get("expires_in");
+        } catch (Exception e) {
+            logger.error("WxPbUtil.reqAccessToken error ", e);
+            throw new IllegalStateException("can't get access token from weixin server",e);
+        }
+
+        return accessToken;
+    }
+
+    /**
+     * 刷新access_token
+     */
+    public static void refreshAccessToken(String appId) {
+        try {
+            String url = String.format(WxPbConfig.API_REFRESH_TOKEN, appId, config.accessToken);
+            Map<String, Object> result = sslRequest(url, "get", null);
+
+            if (!checkResp(result)) {
+                logger.error("WxPbUtil.refreshAccessToken error msg:{}", JSON.toJSONString(result));
+                throw new BusinessException("token异常");
+            }
+            WxAccess accessToken = new WxAccess();
+            accessToken.accessToken = (String) result.get("access_token");
+            accessToken.expire = (int) result.get("expires_in");
+            accessToken.expireAt = System.currentTimeMillis() + accessToken.expire * 1000;
+            config = accessToken;
+        } catch (Exception e) {
+            logger.error("WxPbUtil.refreshAccessToken error ", e);
+        }
+    }
+
+    /**
+     * 查询用户关注公众号状态
+     *
+     * @return 1-绑定微信并关注
+     */
+    public static Integer getSubscribeState(String openId) {
+        String accessToken = getAccessToken();
+        String url = String.format(WxPbConfig.API_SUBSCRIBE, accessToken, openId);
+        Map<String, Object> result = sslRequest(url, "get", null);
+        if (!checkResp(result)) {
+            logger.error("WxPbUtil.getSubscribeState error,openId:{},content:{}");
+            throw new BusinessException("服务异常");
+        }
+        return (Integer) result.get("subscribe");
+    }
+
+    private synchronized static String getAccessToken() {
+        //accesstoken过期或不存在
+        if (null == config || config.expireAt < System.currentTimeMillis()) {
+            config = reqAccessToken(WxPbConfig.PUBLIC_APP_ID, WxPbConfig.PUBLIC_APP_SECRET);
+        }
+        return config.accessToken;
+    }
+
+
+    private static Map<String, Object> sslRequest(String reqUrl, String method, String body) {
+        logger.debug("WxPbUtil.sslRequest req url:{},method:{},body:{}", reqUrl, method, body);
+        long startTime = System.currentTimeMillis();
+        try {
+            String result = null;
+            if ("get".equals(method)) {
+                result = HttpUtil.get(reqUrl);
+            } else {
+                result = HttpUtil.post(reqUrl, body);
+            }
+            logger.info("WxPbUtil.sslRequest resp cost:{}ms,content:{}", (System.currentTimeMillis() - startTime), result);
+            try {
+                return  JSON.parseObject(result, Map.class);
+            } catch (Exception e) {
+                return Map.of("result",result);
+            }
+        } catch (Exception e) {
+            logger.error("WxPbUtil.sslRequest error,", e);
+        }
+
+        return null;
+    }
+
+    private static boolean checkResp(Map<String, Object> resp) {
+        return null != resp && "0".equals(resp.get("errcode"));
+    }
+
+
+    /**
+     * 校验公众接口签名
+     */
+    public static boolean checkSign(String signature, String timestamp, String nonce) {
+        logger.info("signature:{},timestamp:{},nonce:{}",signature,timestamp,nonce);
+        String[] arr = new String[]{WxPbConfig.TOKEN, timestamp, nonce};
+        Arrays.sort(arr);
+        StringBuilder content = new StringBuilder();
+        for (String s : arr) {
+            content.append(s);
+        }
+        try {
+            String sign = EncryptUtil.encodeBySHA1(content.toString());
+            return signature.equalsIgnoreCase(sign);
+        } catch (Exception e) {
+            logger.error("WxPbUtil.checkSign error", e);
+        }
+        return false;
+    }
+
+}

+ 96 - 0
miniapp/src/main/java/com/kym/miniapp/controller/WeixinController.java

@@ -0,0 +1,96 @@
+package com.kym.miniapp.controller;
+
+
+import cn.hutool.core.io.IoUtil;
+import com.kym.common.annotation.SysLog;
+import com.kym.common.utils.CommUtil;
+import com.kym.common.utils.wx.WxPbUtil;
+import jakarta.servlet.http.HttpServletRequest;
+import jakarta.servlet.http.HttpServletResponse;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+import org.springframework.http.MediaType;
+import org.springframework.web.bind.annotation.GetMapping;
+import org.springframework.web.bind.annotation.PostMapping;
+import org.springframework.web.bind.annotation.RequestMapping;
+import org.springframework.web.bind.annotation.RestController;
+
+import java.io.IOException;
+import java.io.PrintWriter;
+import java.util.Map;
+
+
+@RestController
+@RequestMapping("wx")
+public class WeixinController {
+    private Logger logger = LoggerFactory.getLogger(WeixinController.class);
+
+
+    /**
+     * 微信服务器token验证
+     */
+    @GetMapping(value = "check")
+    @SysLog("微信服务器token验证")
+    public void checkSign(HttpServletResponse response, String signature, String timestamp, String nonce, String echostr) {
+        String responseVal = echostr;
+        try {
+
+            boolean resp = WxPbUtil.checkSign(signature, timestamp, nonce);
+            if (!resp) {
+                responseVal = "failure";
+                logger.error("wxpb check sign ERR!!!!");
+            }
+        } catch (Exception e) {
+            responseVal = "failure";
+        } finally {
+            try {
+                PrintWriter writer = response.getWriter();
+                writer.print(responseVal);
+                writer.flush();
+                writer.close();
+            } catch (Exception e) {
+                logger.error(e.getMessage(), e);
+            }
+        }
+    }
+
+
+/*
+
+
+    */
+/**
+     * 消息通知(订阅、用户消息)
+     *
+     * @param request
+     *//*
+
+    @PostMapping(value = "check", produces = MediaType.TEXT_XML_VALUE)
+    @SysLog("微信服务器推送消息")
+    public void handleMessage(HttpServletRequest request, HttpServletResponse response) {
+        String result = "";
+        try {
+            String xml = IoUtil.getContent(request.getInputStream());
+            logger.info("wx pb receive message:\n{}", xml);
+            Map<String, Object> ret = wxPbService.handleMessage(xml);
+            if (!CommUtil.isEmptyOrNull(ret)) {
+                response.setContentType("text/html");
+                result = XmlUtil.map2xml(ret);
+            }
+        } catch (Exception e) {
+            logger.error("WxPbController.handleMessage ERR", e);
+        } finally {
+            try {
+                logger.info("WxPbController.handleMessage response: \n{}",result);
+                PrintWriter writer = response.getWriter();
+                writer.print(result);
+                writer.close();
+            } catch (IOException e) {
+                logger.error(e.getMessage(),e);
+            }
+        }
+    }
+*/
+
+
+}