基于需求:微信公共号给指定关注着发送模板消息
功能包括:获取微信用户信息(支持静默授权和非静默授权)、发送模板消息、获取微信token、获取unionid和openid的映射(token每日有获取次数、不可用情况有过期和失效)
代码中的核心就是调微信的api,加了重试机制、redis缓存,以提高可用性及token复用率,加redission分布式锁以应对多实例部署下的并发场景,可根据实际情况适当调整
目前代码中的token刷新机制设计不是很好,有更好的设计思路打在评论区
微信工具类:
package com.mkhr.applets.utils;
import com.alibaba.fastjson.JSONArray;
import com.alibaba.fastjson.JSONObject;
import com.google.common.collect.Maps;
import com.mkhr.applets.dto.OpenIdEntity;
import org.redisson.Redisson;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.util.*;
import java.util.concurrent.*;
/**
* @author xzg
*/
@Component
public class WeChatUtil {
private static final String WECHAT_TOKEN_KEY = "weChatToken";
private static final String WECHAT_TOKEN_KEY_TEMP = "weChatTokenTemp";
private static final String REDIS_LOCK_TOKEN = "weChatTokenLock";
private static final String WECHAT_USER_INFO = "weChatUserInfo";
private static final String REDIS_LOCK_USER_INFO = "weChatUserInfoLock";
@Value("${wechat.appId}")
private String appId;
@Value("${wechat.secret}")
private String appSecret;
@Resource
private RedisTemplate<String, Object> redisTemplate;
@Resource
private Redisson redisson;
/**
* 获取微信token
*
* @return java.lang.String
* @author xzg
*/
public String getWeChatToken() {
Object token = redisTemplate.opsForValue().get(WECHAT_TOKEN_KEY);
if (null != token) {
return token.toString();
}
RLock redissonLock = redisson.getLock(REDIS_LOCK_TOKEN);
redissonLock.lock();
token = redisTemplate.opsForValue().get(WECHAT_TOKEN_KEY);
if (null != token) {
return token.toString();
}
try {
token = HttpUtil.doWeChatRequest("get", "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret, Maps.newHashMap()).getString("access_token");
redisTemplate.opsForValue().set(WECHAT_TOKEN_KEY, token, 119, TimeUnit.MINUTES);
return token.toString();
} finally {
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
}
/**
* 根据unionId获取关注公共号用户的openId
* 过渡方法:现没直接授权获取关注公共号的用户信息,基于现有的小程序能获取unionId间接获取公共号openId
* 用的小程序的openId获取的unionId,获取公共号下的openId,该openId再获取unionId并绑定它俩的关系,通过绑定关系获取指定用户openId
*
* @param unionId 微信用户的unionId
* @return java.lang.String
* @author xzg
*/
public String getOpenId(String unionId) {
Object o = redisTemplate.opsForHash().get(WECHAT_USER_INFO, unionId);
if (null != o) {
return o.toString();
}
RLock redissonLock = redisson.getLock(REDIS_LOCK_USER_INFO);
redissonLock.lock();
o = redisTemplate.opsForHash().get(WECHAT_USER_INFO, unionId);
if (null != o) {
return o.toString();
}
ExecutorService executorService = Executors.newFixedThreadPool(10);
List<Future<Map<String, String>>> futures = new ArrayList<>();
try {
List<List<OpenIdEntity>> lists = splitJSONArray(getOpenIds());
for (List<OpenIdEntity> list : lists) {
Map<String, Object> map = new HashMap<>();
map.put("user_list", list);
Callable<Map<String, String>> task = () -> getUnionIdAndOpenIdMap(map);
futures.add(executorService.submit(task));
}
Map<String, String> map = new HashMap<>();
for (Future<Map<String, String>> future : futures) {
map.putAll(future.get());
}
o = map.get(unionId);
if (null != o) {
return o.toString();
}
} catch (ExecutionException | InterruptedException e) {
e.printStackTrace();
} finally {
executorService.shutdown();
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
return "unionId有误或用户取消关注公共号";
}
/**
* 通过公共号发消息时的openId理想状态是直接从微信获取,和小程序解耦
* 官网:https://developers.weixin.qq.com/doc/offiaccount/OA_Web_Apps/Wechat_webpage_authorization.html
* 从官网第二步开始的,第一步获取code可能需要前端去做
*
* @param code 用户同意授权后的code
* @param type 只获取openid还是既获取openId也获取unionId
* @return com.alibaba.fastjson.JSONObject
* @author xzg
*/
public JSONObject getWeChatUserInfo(String code, int type) {
JSONObject jsonObject = HttpUtil.doWeChatRequest("get", "https://api.weixin.qq.com/sns/oauth2/access_token?appid=" + appId + "&secret=" + appSecret + "&code=" + code + "&grant_type=authorization_code", Maps.newHashMap());
if (!jsonObject.containsKey("access_token")) {
throw new RuntimeException("获取用户信息失败");
}
if (0 == type) {
String openid = jsonObject.get("openid").toString();
//根据openid获取unionid
JSONObject jsonObject1 = doWeChatRequest("get", "https://api.weixin.qq.com/cgi-bin/user/info?openid=" + openid + "&lang=zh_CN&access_token=", Maps.newHashMap());
if (!jsonObject1.containsKey("openid")) {
throw new RuntimeException("获取openId和unionId失败");
}
jsonObject.put("unionid", jsonObject1.get("unionid"));
return jsonObject;
}
jsonObject = HttpUtil.doWeChatRequest("get", "https://api.weixin.qq.com/sns/userinfo?access_token=" + jsonObject.get("access_token") + "&openid=" + jsonObject.get("openid") + "&lang=zh_CN", Maps.newHashMap());
if (!jsonObject.containsKey("openid")) {
throw new RuntimeException("获取用户信息失败");
}
return jsonObject;
}
/**
* 发送模板消息
*
* @param paramMap 模板消息参数
* @return boolean
* @author xzg
*/
public boolean sendTemplateMessage(Map<String, Object> paramMap) {
//参数格式
/*
{
//用户的openid——必传
"touser":"",
//模板id——必传
"template_id":"",
//点击消息所跳转的地址
"url":"",
//点击消息所跳转到的小程序
"miniprogram":{
"appid":"",
"pagepath":""
},
//模板里所需的参数
"data":{
//消息的标题
"first": {
"value":""
},
//模板第一个参数
//依次类推 keyword2。。。keyword5
"keyword1":{
"value":""
},
//消息的备注
"remark":{
"value":""
}
}
}
*/
JSONObject jsonObject = doWeChatRequest("post", "https://api.weixin.qq.com/cgi-bin/message/template/send?access_token=", paramMap);
if (0 == (Integer) jsonObject.get("errcode")) {
System.out.println("发送消息成功 -> " + paramMap.get("touser"));
return true;
} else {
//取消关注公共号也会导致发送失败
System.out.println("发送消息失败 -> " + paramMap.get("touser"));
System.out.println(jsonObject);
return false;
}
}
/**
* 根据openId获取unionId
*
* @param openIdEntities openId列表
* @return java.util.Map<java.lang.String, java.lang.String>
* @author xzg
*/
public Map<String, String> getUnionIdAndOpenIdMap(Map<String, Object> openIdEntities) {
Map<String, String> result = new HashMap<>();
JSONObject jsonObject = doWeChatRequest("post", "https://api.weixin.qq.com/cgi-bin/user/info/batchget?access_token=", openIdEntities);
System.out.println(jsonObject);
if (!jsonObject.containsKey("user_info_list")) {
throw new RuntimeException("获取unionid失败 -> " + openIdEntities.get("user_list"));
}
JSONArray userInfoList = jsonObject.getJSONArray("user_info_list");
for (int i = 0, size = userInfoList.size(); i < size; i++) {
result.put(userInfoList.getJSONObject(i).getString("unionid"), userInfoList.getJSONObject(i).getString("openid"));
}
redisTemplate.opsForHash().putAll(WECHAT_USER_INFO, result);
return result;
}
//---------------------------------------------------------------------
/**
* 刷新微信token
*
* @author xzg
*/
private void refreshWeChatToken() {
Object token = redisTemplate.opsForValue().get(WECHAT_TOKEN_KEY);
Object tempToken = redisTemplate.opsForValue().get(WECHAT_TOKEN_KEY_TEMP);
if (token != null && token.equals(tempToken)) {
return;
}
RLock redissonLock = redisson.getLock(REDIS_LOCK_TOKEN);
redissonLock.lock();
token = redisTemplate.opsForValue().get(WECHAT_TOKEN_KEY);
tempToken = redisTemplate.opsForValue().get(WECHAT_TOKEN_KEY_TEMP);
if (token != null && token.equals(tempToken)) {
return;
}
redisTemplate.delete(WECHAT_TOKEN_KEY);
try {
token = HttpUtil.doWeChatRequest("get", "https://api.weixin.qq.com/cgi-bin/token?grant_type=client_credential&appid=" + appId + "&secret=" + appSecret, Maps.newHashMap()).getString("access_token");
redisTemplate.opsForValue().set(WECHAT_TOKEN_KEY, token, 119, TimeUnit.MINUTES);
redisTemplate.opsForValue().set(WECHAT_TOKEN_KEY_TEMP, token, 30, TimeUnit.MINUTES);
} finally {
if (redissonLock.isLocked() && redissonLock.isHeldByCurrentThread()) {
redissonLock.unlock();
}
}
}
/**
* 封装调微信接口刷新token
*
* @param requestType 请求类型
* @param url 地址,路径参数中把token放在最后
* @param paramMap 参数
* @return com.alibaba.fastjson.JSONObject
* @author xzg
*/
private JSONObject doWeChatRequest(String requestType, String url, Map<String, Object> paramMap) {
JSONObject jsonObject = HttpUtil.doWeChatRequest(requestType, url + getWeChatToken(), paramMap);
if (jsonObject.containsKey("errcode") && (40001 == (Integer) jsonObject.get("errcode") || 42001 == (Integer) jsonObject.get("errcode"))) {
//刷新token
try {
refreshWeChatToken();
jsonObject = HttpUtil.doWeChatRequest(requestType, url + getWeChatToken(), paramMap);
} finally {
redisTemplate.delete(WECHAT_TOKEN_KEY_TEMP);
}
}
return jsonObject;
}
/**
* 拉取关注公共号的openId
*
* @return com.alibaba.fastjson.JSONArray
* @author xzg
*/
private JSONArray getOpenIds() {
JSONObject openIds = doWeChatRequest("get", "https://api.weixin.qq.com/cgi-bin/user/get?access_token=", Maps.newHashMap());
int total = (Integer) openIds.get("total");
JSONArray result1 = openIds.getJSONObject("data").getJSONArray("openid");
if (10000 >= total) {
return result1;
}
JSONArray result2 = new JSONArray();
result2.addAll(result1);
for (int i = 0; i < total / 10000; i++) {
String nextOpenId = openIds.get("next_openid").toString();
openIds = doWeChatRequest("get", "https://api.weixin.qq.com/cgi-bin/user/get?next_openid=" + nextOpenId + "&access_token=", Maps.newHashMap());
result2.addAll(openIds.getJSONObject("data").getJSONArray("openid"));
}
return result2;
}
/**
* 拆分openId列表为100个一组
*
* @param jsonArray openId
* @return java.util.List<java.util.List < com.mkhr.applets.dto.OpenIdEntity>>
* @author xzg
*/
private List<List<OpenIdEntity>> splitJSONArray(JSONArray jsonArray) {
List<List<OpenIdEntity>> result = new ArrayList<>();
int size = jsonArray.size();
if (100 >= size) {
List<OpenIdEntity> temp = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
temp.add(new OpenIdEntity().setOpenid(jsonArray.get(i).toString()));
}
result.add(temp);
return result;
}
for (int i = 0, count = size / 100; i < count; i++) {
List<OpenIdEntity> temp = new ArrayList<>(100);
for (int j = 0; j < 100; j++) {
temp.add(new OpenIdEntity().setOpenid(jsonArray.get(0).toString()));
jsonArray.remove(0);
}
result.add(temp);
}
if (!jsonArray.isEmpty()) {
size = jsonArray.size();
List<OpenIdEntity> temp = new ArrayList<>(size);
for (int i = 0; i < size; i++) {
temp.add(new OpenIdEntity().setOpenid(jsonArray.get(0).toString()));
jsonArray.remove(0);
}
result.add(temp);
}
return result;
}
}
使用案例:
package com.mkhr.applets.WeChat;
import com.mkhr.applets.dto.HttpResultBean;
import com.mkhr.applets.dto.OpenIdEntity;
import com.mkhr.applets.enums.RetCodeEnum;
import com.mkhr.applets.utils.WeChatUtil;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
@Api(tags = "微信API")
@RestController
@RequestMapping("/weChat")
public class WeChatController {
@Resource
private WeChatUtil weChatUtil;
@ApiOperation("获取微信用户信息")
@PostMapping("/getWeChatUserInfo/{code}/{type}")
public HttpResultBean getWeChatUserInfo(@PathVariable("code") String code, @PathVariable("type") int type) {
return HttpResultBean.result(RetCodeEnum.SUCCESS, weChatUtil.getWeChatUserInfo(code, type));
}
@ApiOperation("发送模板消息")
@PostMapping("/sendTemplateMessage")
public HttpResultBean sendTemplateMessage(@RequestBody Map<String, Object> paramMap) {
return HttpResultBean.result(RetCodeEnum.SUCCESS, weChatUtil.sendTemplateMessage(paramMap));
}
//-----------------------------------辅助接口-----------------------------------------
@ApiOperation("获取微信token")
@PostMapping("/getWeChatToken")
public HttpResultBean getWeChatToken() {
return HttpResultBean.result(RetCodeEnum.SUCCESS, weChatUtil.getWeChatToken());
}
@ApiOperation("根据unionId获取openId")
@PostMapping("/getOpenIdByUnionId/{unionId}")
public HttpResultBean getOpenIdByUnionId(@PathVariable("unionId") String unionId) {
return HttpResultBean.result(RetCodeEnum.SUCCESS, weChatUtil.getOpenId(unionId));
}
@ApiOperation("补偿获取unionId和openId映射时出错")
@PostMapping("/getUnionIdAndOpenIdMap")
public HttpResultBean getUnionIdAndOpenIdMap(@RequestBody List<OpenIdEntity> openIdEntities) {
Map<String, Object> map = new HashMap<>(openIdEntities.size());
map.put("user_list", openIdEntities);
return HttpResultBean.result(RetCodeEnum.SUCCESS, weChatUtil.getUnionIdAndOpenIdMap(map));
}
}
HTTP请求工具:
package com.mkhr.applets.utils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import org.apache.http.ParseException;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.methods.*;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClientBuilder;
import org.apache.http.util.EntityUtils;
import java.io.IOException;
import java.util.Map;
/**
* http工具
* hutool工具类http工具地址:https://www.hutool.cn/docs/#/http/概述
*
* @author xzg
* @since 2022/4/2 上午 11:16
*/
public class HttpUtil {
private static final RequestConfig requestConfig = RequestConfig.custom()
// 设置连接超时时间(单位毫秒)
.setConnectTimeout(5000)
// 设置请求超时时间(单位毫秒)
.setConnectionRequestTimeout(5000)
// socket读写超时时间(单位毫秒)
.setSocketTimeout(5000)
// 设置是否允许重定向(默认为true)
.setRedirectsEnabled(true).build();
private HttpUtil() {
}
public static JSONObject doWeChatRequest(String requestType, String url, Map<String, Object> paramMap) {
JSONObject jsonObject = doRequest(requestType, url, paramMap);
if (jsonObject.containsKey("errcode") && -1 == (Integer) jsonObject.get("errcode")) {
int i = 0;
while (i < 3 || jsonObject.containsKey("errcode")) {
i++;
try {
Thread.sleep(3);
} catch (InterruptedException e) {
e.printStackTrace();
}
jsonObject = doRequest(requestType, url, paramMap);
}
if (jsonObject.containsKey("errcode") && -1 == (Integer) jsonObject.get("errcode")) {
throw new RuntimeException("调微信接口失败,请检查网络");
}
}
return jsonObject;
}
public static JSONObject doRequest(String requestType, String url, Map<String, Object> paramMap) {
switch (requestType.toLowerCase()) {
case "get":
return httpRequest(httpGet(url));
case "post":
return httpRequest(httpPost(url, paramMap));
case "put":
return httpRequest(httpPut(url, paramMap));
default:
throw new RuntimeException("未知的请求方式");
}
}
//------------根据需要去HttpRequestBase基类或子类修改、完善--------------------
private static JSONObject httpRequest(HttpRequestBase httpRequest) {
// 获得Http客户端(可以理解为:你得先有一个浏览器;注意:实际上HttpClient与浏览器是不一样的)
CloseableHttpClient httpClient = HttpClientBuilder.create().build();
// 响应模型
CloseableHttpResponse response = null;
//返回结果
JSONObject jsonObject = null;
try {
// 由客户端执行(发送)请求
response = httpClient.execute(httpRequest);
// 从响应模型中获取响应实体
jsonObject = JSONObject.parseObject(EntityUtils.toString(response.getEntity()));
} catch (ParseException | IOException e) {
e.printStackTrace();
} finally {
try {
// 释放资源
if (httpClient != null) {
httpClient.close();
}
if (response != null) {
response.close();
}
} catch (IOException e) {
e.printStackTrace();
}
}
return jsonObject;
}
private static HttpPost httpPost(String url, Map<String, Object> paramMap) {
// 创建Post请求
HttpPost httpPost = new HttpPost(url);
//将上面的配置信息 运用到这个Post请求里
httpPost.setConfig(requestConfig);
//设置请求头
httpPost.setHeader("Content-Type", "application/json;charset=utf-8");
//设置请求参数
//注意:此处对入参进行了JSON格式转换,可根据实际需要调整此处代码
httpPost.setEntity(new StringEntity(JSON.toJSONString(paramMap), "utf-8"));
return httpPost;
}
private static HttpGet httpGet(String url) {
// 创建Get请求
HttpGet httpGet = new HttpGet(url);
// 将上面的配置信息 运用到这个Get请求里
httpGet.setConfig(requestConfig);
return httpGet;
}
private static HttpPut httpPut(String url, Map<String, Object> paramMap) {
HttpPut httpPut = new HttpPut(url);
httpPut.setConfig(requestConfig);
httpPut.setHeader("Content-Type", "application/json;charset=utf-8");
httpPut.setEntity(new StringEntity(JSON.toJSONString(paramMap), "utf-8"));
return httpPut;
}
}