1. 获取 access_token 官方文档
-
说明
access_token
有效期目前为2小时
,且获取access_token
接口每天限制2000
次。因此需要保存,选择保存到redis
access_token
重复获取将导致上次获取的access_token
失效,但是公众平台后台会保证在5分钟内,新老access_token
都可用,这保证了第三方业务的平滑过渡。因此需要用分布式锁来限制刷新,选择redisson
实现
-
代码实现
@Override public String getAccessToken() { final String key = RedisConstant.WX_ACCESS_TOKEN_KEY + wxConfig.getAppID(); /* 1. 先获取 redis 上的 token */ Object o = valueOperations.get(key); String token = ""; if (Objects.nonNull(o) && StrUtil.isNotBlank(token = o.toString())) { return token; } // 锁 RLock lock = redisLock.getRLock(RedisConstant.PARAM_ACCESS_TOKEN); lock.lock(); try { // 防止不是第一个拿到锁 o = valueOperations.get(key); if (Objects.nonNull(o) && StrUtil.isNotBlank(token = o.toString())) { return token; } token = forceRefreshAccessToken(); } catch (Exception e) { log.error("微信公众号 {} 获取 access_token 失败", wxConfig.getAppID()); } finally { lock.unlock(); } return token; } @Override public String forceRefreshAccessToken() { final String key = RedisConstant.WX_ACCESS_TOKEN_KEY + wxConfig.getAppID(); String token = ""; try { /* 调用 微信公众号接口,根据 appId、appSecret 获取 access_token */ final String url = String.format(WxConstant.URL_ACCESS_TOKEN_GET, wxConfig.getAppID(), wxConfig.getAppSecret()); String result = HttpUtil.get(url); WxAccessTokenBean tokenBean = objectMapper.readValue(result, WxAccessTokenBean.class); Long expiresIn = tokenBean.getExpiresIn(); /* 保存到 redis */ if (StrUtil.isNotBlank(token = tokenBean.getAccessToken()) && Objects.nonNull(expiresIn)) { valueOperations.set(key, token, expiresIn, TimeUnit.SECONDS); } else { // access_token 异常 log.error("errCode: {}, errMsg: {}", tokenBean.getErrCode(), tokenBean.getErrMsg()); throw new RuntimeException("微信公众号 access_token 异常"); } } catch (JsonProcessingException e) { log.error("微信公众号 {} 获取 access_token 失败", wxConfig.getAppID()); } return token; }
2. 消息排重
-
说明
- 微信服务器在将用户的消息发给公众号的开发者服务器地址(开发者中心处配置)后,微信服务器在五秒内收不到响应会断掉连接,并且重新发起请求,总共重试三次。
- 当微信服务器重试时,那么有可能给用户回复多次信息,因此需要排重
-
实现
@Override public String msgHandle(WxInMsgBean bean, String signature, String timestamp, String nonce) { /* 验证是否是正常请求 */ if (!checkToken(signature, timestamp, nonce, wxConfig.getToken())) { throw new IllegalArgumentException("微信-非法请求,可能属于伪造请求"); } log.info("WxInMsgBean: {}", bean.toString()); String msg = WxConstant.MSG_SUCCESS; // 排重 if (isRepeat(bean.getMsgId(), bean.getFromUserName(), bean.getCreateTime())) { return msg; } /* 处理... */ return msg; } /** * 判断发送的消息是否是重复发送 * * @param msgId 信息id * @param fromUserName 发送者 * @param createTime 创建时间 * @return 重复 true */ private boolean isRepeat(Long msgId, String fromUserName, Long createTime) { /* 设置到 redis 进行排重 */ final String key = RedisConstant.DE_WEIGHT_KEY + wxConfig.getAppID() + ":" + (Objects.nonNull(msgId) ? msgId : fromUserName + createTime); // 第一次设置会返回 true,每5秒重试一次,共重试 3 次 // 为 null 或 false 代表 已存在,redis 不进行任何操作 return !Objects.equals(valueOperations.setIfAbsent(key, 1, 25, TimeUnit.SECONDS), true); }