微信小程式內判斷是否關注公眾號(JAVA)

禿頭之巔俺鑽3 發表於 2021-10-15
Java

微信小程式內判斷是否關注公眾號(JAVA)

思路來源(第二種):
https://blog.csdn.net/Yanheeee/article/details/117295643
/**
 * 總體思路:
 * 1.微信公眾號和小程式都繫結到微信開放平臺, 所以會有一個共同的unionid, 每次使用者登入都會返回一個unionid
 * 2.獲取所有公眾號已關注使用者的資訊 : 通過微信介面獲取到一個已關注的使用者列表(公眾號的openid和unionid), 儲存到資料庫內(儲存openid, unionid)
 * 3.通過監聽關注/取關事件(使用者關注時, 微信會給我們傳送一條訊息[xml]), 來更新表的內容(關注->增加, 取消關注->刪除)
 * 4.登入小程式後, 通過使用者登入的unionid查表判斷是否已關注
 */

1. 微信開放平臺配置步驟

https://open.weixin.qq.com/

注意!!!!!!-----------------
配置好啟用後, 微信公眾號的自動回覆, 關鍵詞回覆, 底部選單欄會失效!!!
微信公眾號的自動回覆-關鍵詞回覆-底部選單欄配置程式碼:
https://www.cnblogs.com/Annago/p/15411110.html

微信小程式內判斷是否關注公眾號(JAVA)

微信小程式內判斷是否關注公眾號(JAVA)

1. 效驗程式碼
//如果有SpringSecurity, 需要開啟匿名訪問

	/**
	 * get用於微信校驗, post用來接收微信資訊(後面接收微信訊息時配置)
	 * 微信校驗
	 */
	@GetMapping("/weixinVerify")
	public void weixinVerify(WeixinCheckVo weixinCheckVo, HttpServletResponse response)  throws IOException {
		//通過檢驗signature對請求進行校驗,若校驗成功則原樣返回echostr,表示接入成功,否則接入失敗
		if (CheckUtil.checkSignature(weixinCheckVo.getSignature(), weixinCheckVo.getTimestamp(), weixinCheckVo.getNonce())) {
			response.getWriter().print(weixinCheckVo.getEchostr());
		}
	}
WeixinCheckVo
@Data
public class WeixinCheckVo {

    /**
     * 微信加密簽名
     */
    private String signature;

    /**
     * 時間戳
     */
    private String timestamp;

    /**
     * 隨機數
     */
    private String nonce;

    /**
     * 隨機字串
     */
    private String echostr;
}
CheckUtil
public class CheckUtil {

    /**
     * weixin繫結token資訊
     */
    private static String token = "MyToken";

    /**
     * 驗證簽名
     *
     * @param signature
     * @param timestamp
     * @param nonce
     * @return
     */
    public static boolean checkSignature(String signature, String timestamp, String nonce) {
        if(StringUtils.isBlank(signature) || StringUtils.isBlank(timestamp) || StringUtils.isBlank(nonce)){
            return false;
        }
        String[] arr = new String[] { token, timestamp, nonce };
        sort(arr);
        StringBuilder content = new StringBuilder();
        for (int i = 0; i < arr.length; i++) {
            content.append(arr[i]);
        }
        String tmpStr = null;
        try {
            MessageDigest md = MessageDigest.getInstance("SHA-1");
            byte[] digest = md.digest(content.toString().getBytes());
            tmpStr = byteToStr(digest);
        } catch (NoSuchAlgorithmException e) {
            e.printStackTrace();
        }
        return tmpStr != null ? tmpStr.equals(signature.toUpperCase()) : false;
    }

    /**
     * 將位元組陣列轉換為十六進位制字串
     *
     * @param byteArray
     * @return
     */
    private static String byteToStr(byte[] byteArray) {
        String strDigest = "";
        for (int i = 0; i < byteArray.length; i++) {
            strDigest += byteToHexStr(byteArray[i]);
        }
        return strDigest;
    }

    /**
     * 將位元組轉換為十六進位制字串
     *
     * @param mByte
     * @return
     */
    private static String byteToHexStr(byte mByte) {
        char[] Digit = { '0', '1', '2', '3', '4', '5', '6', '7', '8', '9', 'A', 'B', 'C', 'D', 'E', 'F' };
        char[] tempArr = new char[2];
        tempArr[0] = Digit[(mByte >>> 4) & 0X0F];
        tempArr[1] = Digit[mByte & 0X0F];
        String s = new String(tempArr);
        return s;
    }

    /**
     * 字典排序
     * @param array
     */
    public static void sort(String[] array) {
        for (int i = 0; i < array.length - 1; i++) {
            for (int j = i + 1; j < array.length; j++) {
                if (array[j].compareTo(array[i]) < 0) {
                    String temp = array[i];
                    array[i] = array[j];
                    array[j] = temp;
                }
            }
        }
    }
}

2. 登陸小程式判斷使用者是否關注

1. 需要的jar
	<!-- hutool工具集 -->
		<dependency>
		    <groupId>cn.hutool</groupId>
		    <artifactId>hutool-all</artifactId>
		    <version>5.3.9</version>
		</dependency>
            
        <!-- 阿里JSON解析器 -->
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>1.2.70</version>
		</dependency>
2. 獲取access_token
	/**
	 * 首先獲access_token(access_token是公眾號的全域性唯一介面呼叫憑據,公眾號呼叫各介面時都需要使用access_token)
	 * 需要用微信公眾號的appid和secret(小程式的和微信公眾號的不一樣!!!)
	 * 或者使用微信線上除錯工具 -> https://mp.weixin.qq.com/debug (基礎支援-獲取access_token介面 /token)
	 */
	public static JSONObject getAccess_token() {
		String url = "https://api.weixin.qq.com/cgi-bin/token";
		Map<String,Object> paramMap = new HashMap<>();
		paramMap.put("appid", "微信公眾號的appid");
		paramMap.put("secret", "微信公眾號的secret");
		paramMap.put("grant_type", "client_credential");
		String result = HttpRequest.get(url).charset(CharsetUtil.CHARSET_GBK)
				.header(Header.USER_AGENT, "Hutool http")//頭資訊,多個頭資訊多次呼叫此方法即可
				.form(paramMap)//表單內容
				.timeout(20000)//超時,毫秒
				.execute().body();
		JSONObject jsonObject = JSONObject.parseObject(result); //字串轉Map
		return jsonObject;
	}
3. 獲取已關注的使用者openid和unionid, 並建立已關注使用者表(自增id, unionid, openid)
	/**
	 * 獲取所有已關注的使用者openid(適用於 -> 關注人數小於1W人的公眾號)
	 */
	public static JSONArray getAllUserInfo(String access_token) {
		String url = "https://api.weixin.qq.com/cgi-bin/user/get";
		Map<String,Object> paramMap = new HashMap<>();
		paramMap.put("access_token", access_token);
		String result = HttpRequest.get(url).charset(CharsetUtil.CHARSET_GBK)
				.header(Header.USER_AGENT, "Hutool http")//頭資訊,多個頭資訊多次呼叫此方法即可
				.form(paramMap)//表單內容
				.timeout(20000)//超時,毫秒
				.execute().body();
		JSONObject jsonObject = JSONObject.parseObject(result); //字串轉Map
		JSONObject data = (JSONObject)jsonObject.get("data");
		JSONArray json = (JSONArray)data.get("openid");
        return json;
	}

	/**
	 * 通過使用者的openid(遍歷JSONArray陣列), 獲取每一位已關注的使用者unionid, 然後批量插入資料庫
	 */	
	public void insertUserOpenIdAndUinonId(JSONArray jsonArray) {
        //key放unionid, value放openid, 批量插入資料
        Map<String, String> mapList = new HashMap<>();
        for (Object openid : jsonArray) {
		String url = "https://api.weixin.qq.com/cgi-bin/user/info";
		Map<String,Object> paramMap = new HashMap<>();
		paramMap.put("access_token", access_token);
        paramMap.put("openid", openid.get(i));
		String result = HttpRequest.get(url).charset(CharsetUtil.CHARSET_GBK)
				.header(Header.USER_AGENT, "Hutool http")//頭資訊,多個頭資訊多次呼叫此方法即可
				.form(paramMap)//表單內容
				.timeout(20000)//超時,毫秒
				.execute().body();
		JSONObject jsonObject = JSONObject.parseObject(result); //字串轉Map
        mapList.put(jsonObject.getString("unionid"), jsonObject.getString("openid"));
        }
        //批量插入資料
        xxxMapper.batchInsert(mapList);
	}

void batchInsert(@Param("mapList") Map<String, String> mapList);

<insert id="batchInsert" parameterType="java.util.Map">
		insert into table (unionid, openid) values
        <foreach collection="mapList" item="value" index="key" separator=",">
                (#{key}, #{value})
		</foreach>
</insert>
4. 進入後小程式判斷使用者是否關注
//小程式登陸
public static JSONObject getWxMini(String code, String appid, String appSecret) {
		String url = "https://sz.api.weixin.qq.com/sns/jscode2session";
		Map<String,Object> paramMap = new HashMap<>();
		paramMap.put("appid", "小程式的adppid");
		paramMap.put("secret", "小程式的secret");
		paramMap.put("js_code", code);
		paramMap.put("grant_type", "authorization_code");
		String result = HttpRequest.get(url).charset(CharsetUtil.CHARSET_GBK)
				.header(Header.USER_AGENT, "Hutool http")//頭資訊,多個頭資訊多次呼叫此方法即可
				.form(paramMap)//表單內容
				.timeout(20000)//超時,毫秒
				.execute().body();
		JSONObject jsonObject = JSONObject.parseObject(result); //字串轉Map
		return jsonObject;
	}

//判斷使用者是否關注了公眾號
	if (jsonObject.getString("unionid") != null) {
		//判斷資料庫內是否有這個unionId, 如果有則已關注, 如果沒有則沒有關注
		ajax.put("subscribe", userService.checkUnionId(json.getString("unionid")));
		ajax.put("unionid", json.getString("unionid"));
		}

<select id="checkUnionId" parameterType="string" resultType="int">
        select count(-1) from table
        where unionid = #{unionid}
</select>

3. 微信監聽關注/取關事件

1. 接收微信的事件
	/**
	 * 事件型別:subscribe(訂閱)
	 */
	public static final String EVENT_TYPE_SUBSCRIBE = "subscribe";

	/**
	 * 事件型別:unsubscribe(取消訂閱)
	 */
	public static final String EVENT_TYPE_UNSUBSCRIBE = "unsubscribe";

	/**
	 * 接收微信的事件
	 */
	@PostMapping(value = "/weixinVerify", produces = "application/xml")
	public void weixinVerify(HttpServletRequest request, HttpServletResponse response)  throws Exception {
		response.setContentType("text/html; charset=utf-8");
		response.setContentType("application/xml; charset=utf-8");
		Map<String, String> map = new HashMap<>();
		String method = request.getMethod();
        
		if ("POST".equals(method)) {
			String xmlStr = "";
            String msgrsp = "";
			PrintWriter out = response.getWriter();
            
			try {
                //解析微信發來的XML
				xmlStr = XmlUtil.inputStream2StringNew(request.getInputStream());
				Map<String, String> requestMap = XmlUtil.parseXml(xmlStr);
				//傳送方帳號(公眾號的open_id)
				String openId = requestMap.get("fromUserName");

                //關注(subscribe)
				if (MessageUtil.EVENT_TYPE_SUBSCRIBE.equals(requestMap.get("event"))) {
					//通過openid獲取使用者unionid並新增到已關注表中(weixin_unionid_openid)
					JSONObject userInfo = GetReqUtil.getUserInfo(openId);
					map.put("openId", openId);
            		map.put("unionId", userInfo.getString("unionid"));
					//新增關注的使用者資訊
					userMapper.insertSubscribeInfo(map);
				}

				//取消關注(unsubscribe)
				if (MessageUtil.EVENT_TYPE_UNSUBSCRIBE.equals(requestMap.get("event"))) {
					//刪除取關的使用者資訊
					userMapper.deleteSubscribeInfo(openId);
				}
				out.print(msgrsp);
				out.close();
			} catch (Exception e) {
				logger.error("解析失敗:" + xmlStr);
				e.printStackTrace();
			}
		}
	}

<insert id="insertSubscribeInfo" parameterType="map">
      insert into table (openid, unionid) values (#{openId}, #{unionId})
</insert>
<delete id="deleteSubscribeInfo" parameterType="string">
       delete from table where openid = #{openId}
 </delete>
XmlUtil
public class XmlUtil {

    // 將輸入流使用指定編碼轉化為字串
    public static String inputStream2StringNew(InputStream inputStream) throws Exception {
        // 建立輸入流讀取類
        InputStreamReader reader = new InputStreamReader(inputStream);
        // 設定每次讀取字元個數
        char[] data = new char[512];
        int dataSize = 0;
        // 迴圈讀取
        StringBuilder stringBuilder = new StringBuilder();
        while ((dataSize = reader.read(data)) != -1) {
            stringBuilder.append(data, 0, dataSize);
        }
        return stringBuilder.toString();
    }

    // 將 xml 檔案解析為指定型別的實體物件。此方法只能解析簡單的只有一層的xml
    private static DocumentBuilderFactory documentBuilderFactory = null;

    //遮蔽某些編譯時的警告資訊(在強制型別轉換的時候編譯器會給出警告)
    public static Map<String, String> parseXml(String Str) throws Exception {
        // 將解析結果儲存在HashMap中
        Map<String, String> map = new HashMap<String, String>();

        // 從request中取得輸入流
        // 讀取輸入流
        SAXReader reader = new SAXReader();
        org.dom4j.Document document = reader.read(new ByteArrayInputStream(Str.getBytes()));
        // 得到xml根元素
        org.dom4j.Element root = document.getRootElement();
        // 得到根元素的所有子節點
        List<org.dom4j.Element> elementList = root.elements();

        // 遍歷所有子節點
        for (org.dom4j.Element e : elementList)
            map.put(getName(e.getName()), e.getText());
        return map;
    }

    private static String getName(String name) {
        if (StringUtils.isBlank(name)) {
            return null;
    }
        String frist = String.valueOf(name.charAt(0)).toLowerCase();
        return frist + name.substring(1, name.length());
    }
}
GetReqUtil
public class GetReqUtil {

	/**
	 * 獲取使用者資訊
	 * 1. 每次把獲取的access_token放入redis, 每天呼叫限制2000次
	 * 2. 如果別處呼叫access_token, 導致access_token失效, 則重新獲取
	 */
	public static JSONObject getUserInfo(String openId) {
        //靜態工具類之前注入出錯, 所以使用getBean(往期的Tools裡有SpringUtils)
		RedisCache redisCache = SpringUtils.getBean(RedisCache.class);
		Object key = redisCache.getCacheObject("weixin_accessToken");
		String access_token;

		//預設access_token過期時間為2小時, redis中儲存為110分鐘(每天介面呼叫上線為2000次), 每次呼叫都會產生新的access_token
		if (StringUtils.isEmpty(key)) {
			JSONObject aToken = GetReqUtil.getAccess_token();
			redisCache.setCacheObject("weixin_accessToken", aToken.getString("access_token"), 110, TimeUnit.MINUTES);
			access_token = aToken.getString("access_token");
		}else {
			access_token = key + "";
		}

		String url = "https://api.weixin.qq.com/cgi-bin/user/info";
		Map<String,Object> paramMap = new HashMap<>();
		paramMap.put("access_token", access_token);
		paramMap.put("openid", openId);

		String result = HttpRequest.get(url).charset(CharsetUtil.CHARSET_GBK)
				.header(Header.USER_AGENT, "Hutool http")//頭資訊,多個頭資訊多次呼叫此方法即可
				.form(paramMap)//表單內容
				.timeout(20000)//超時,毫秒
				.execute().body();
		JSONObject jsonObject = JSONObject.parseObject(result); //字串轉Map

		//如果別處呼叫access_token, 導致access_token失效, 則重新獲取
		if ("40001".equals(jsonObject.getString("errcode"))) {
			JSONObject aToken = GetReqUtil.getAccess_token();
			redisCache.setCacheObject("weixin_accessToken", aToken.getString("access_token"), 110, TimeUnit.MINUTES);

			paramMap.put("access_token", aToken.getString("access_token"));
			String result2 = HttpRequest.get(url).charset(CharsetUtil.CHARSET_GBK)
					.header(Header.USER_AGENT, "Hutool http")//頭資訊,多個頭資訊多次呼叫此方法即可
					.form(paramMap)//表單內容
					.timeout(20000)//超時,毫秒
					.execute().body();
			return JSONObject.parseObject(result2); //字串轉Map
		}
		return jsonObject;
	}
}