|
|
@@ -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 + "×tamp=" + 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;
|
|
|
+ }
|
|
|
+
|
|
|
+}
|