前言

众所周知,在微信公众平台开发中,其实就是一系列的API请求和自身业务系统的集成,而在API请求中,AccessToken是优势一个必不可少的参数。

注:

  • 本文基于企业微信,故部分API请求可能和订阅号,服务号,小程序不太相同,但整体思路一致。
  • 本项目代码基于Java语言,SpringBoot框架。

在企业微信开发文档中有这样一段:

  • access_token的有效期通过返回的expires_in来传达,正常情况下为7200秒(2小时),有效期内重复获取返回相同结果,过期后获取会返回新的access_token。
  • 由于企业微信每个应用的access_token是彼此独立的,所以进行缓存时需要区分应用来进行存储。
  • access_token至少保留512字节的存储空间。
  • 企业微信可能会出于运营需要,提前使access_token失效,开发者应实现access_token失效时重新获取的逻辑。

需要注意的点:

  • 在有效期内,重复获取不会返回新的AccessToken,并且不会延长有消息。(即使不在同一个程序内获取)
  • AccessToken可能会提前过期。(两个小时获取一次可能会出现提前过期的问题)

问题描述

原始方案:
V0.1 定时器(schedule)
描述:在SpringBoot项目中,使用@Scheduled注解,每一小时获取一次AccessToken。
问题:在运行一段时间后,因网络波动导致某次请求失败,程序出错,定时器没有继续执行。
影响:程序无法进行任何微信相关的API请求。
改进:V0.2 定时器+异常捕获
V0.2 定时器+异常捕获
描述:在上述版本的情况下,增加异常捕获。
问题:运行一段时间再次出现异常,程序在获取AccessToken过程中出现阻塞,后续代码均未执行,定时器也无法执行。

影响:两次带来的影响都是致命的,犹如定时炸弹,完全不清楚下次会何时继续出现。

改进方案

java 企业微信获取客服新会话事件 企业微信已通过api接口_json

总结:

此方案的优点在于,程序无需通过定时器或线程去处理AccessToken,通过Redis的缓存,并设置过期时间,实现动态的管理,并能和其他程序共享AccessToken(对于多个程序需要使用同一个应用时,会存在此需求)。在过期也能自动获取,并不影响程序正常运行。
即使某次请求出现问题,不会影响之后的请求。


代码展示

图中设计的几个类实现如下:

  • WorkWXAPI类:
    定义了企业微信相关的API请求的URL地址,以及其他企业微信相关的常量等。
/**
 * @Author: geekfly
 * @Description:  企业微信相关API及常量定义
 * @Date: 2017/10/12
 * @Modified By:
 */
public class WorkWXAPI {

    public static String CORPID = ""; //企业ID
    public static Integer AGENTID = 1000017; //应用ID
    public static String AUTH_APP_SECRET = ""; //应用1Secret
    public static String CONTACTS_SECRET = ""; //应用2Secret
    public static String TOKEN = ""; //API接收Token
    public static String EncodingAESKey = "";//API接收EncodingAESKey

    public static String GET_ACCESS_TOKEN_URL = "https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid=%s&corpsecret=%s";
    public static String GET_USER_OPENID_URL = "https://qyapi.weixin.qq.com/cgi-bin/user/getuserinfo?access_token=%s&code=%s";
    public static String SEND_MESSAGE_URL = "https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token=%s";
    public static String GET_USER_INFO = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s";


    //标签相关
    public static String TAG_ADD_USERS_URL = "https://qyapi.weixin.qq.com/cgi-bin/tag/addtagusers?access_token=%s";
    public static String TAG_DELETE_USERS_URL = "https://qyapi.weixin.qq.com/cgi-bin/tag/deltagusers?access_token=%s";

    /*
       成员管理
     */
    //获取部门成员
    public static String CONTACTS_SIMPLE_LIST = "https://qyapi.weixin.qq.com/cgi-bin/user/simplelist?access_token=%s&department_id=%s&fetch_child=%s";
    //获取部门成员详情
    public static String CONTACTS_LIST = "https://qyapi.weixin.qq.com/cgi-bin/user/list?access_token=%s&department_id=%s&fetch_child=%s";
    //更新成员
    public static String CONTACTS_UPDATE= "https://qyapi.weixin.qq.com/cgi-bin/user/update?access_token=%s";
    //创建成员
    public static String CONTACTS_CREATE = "https://qyapi.weixin.qq.com/cgi-bin/user/create?access_token=%s";
    //读取成员
    public static String CONTACTS_GET = "https://qyapi.weixin.qq.com/cgi-bin/user/get?access_token=%s&userid=%s";


    /*
    部门管理
     */
    //创建部门
    public static String DEPARTMENT_CREATE = "https://qyapi.weixin.qq.com/cgi-bin/department/create?access_token=%s";
    //更新部门
    public static  String DEPARTMENT_UPDATE = "https://qyapi.weixin.qq.com/cgi-bin/department/update?access_token=%s";
    //获取部门列表
    public static String DEPARTMENT_LIST = "https://qyapi.weixin.qq.com/cgi-bin/department/list?access_token=%s&id=%s";

    //js api
    public static String JS_API = "https://qyapi.weixin.qq.com/cgi-bin/get_jsapi_ticket?access_token=%s";

    //认证通过
    public static String AUTH_SUCCESS = "https://qyapi.weixin.qq.com/cgi-bin/user/authsucc?access_token=%s&userid=%s";

    //不同身份人员所在部门ID
    public static Integer DEPARTMENT_JZG_ID_OTHER = 4658;    //教师(其他)
    public static Integer DEPARTMENT_JZG_ID = 2638;    //教师(新)

    public static Integer DEPARTMENT_NO_AUTH = 1889; //未认证用户部门
    public static Integer DEPARTMENT_ROOT = 1; //根部门ID

    //菜单ID
    public static String MENU_ID_USER_INFO = "user_info"; //个人信息
    public static String MENU_ID_USER_BIND = "user_bind"; //身份认证

}

注:代码使用String.format进行占位符替换,使用时需注意参数的顺序,以免因替换出错导致的传参错误。

  • WXAPIUtil类
    WXAPIUtil为封装的企业微信相关API操作的类,如:获取AccessToken获取,更新,删除用户信息,获取,创建,更新部门信息等。
/**
 * @Author geekfly
 * @Date 2018/1/17 9:28
 * @Desc 微信API帮助类
 */
public class WXAPIUtil {

    private final static Logger logger = LoggerFactory.getLogger(WXAPIUtil.class);

    private static String accessTokenAuth = "access_token:auth";

    private static String accessTokenContacts = "access_token:contacts";

    /**
     * 获取access token
     * @param key 根据此key获取
     * @return
     */
    public static Map<String, Object> getAccessToken(String key){
        String secret = "";
        if(key == accessTokenAuth){
            secret = WorkWXAPI.AUTH_APP_SECRET;
        }else if(key == accessTokenContacts){
            secret = WorkWXAPI.CONTACTS_SECRET;
        }
        String json = HttpClientUtil.get(String.format(WorkWXAPI.GET_ACCESS_TOKEN_URL, WorkWXAPI.CORPID, secret));
        Map<String, Object> data = JsonUtil.WXJsonToMap(json);
        if (Integer.parseInt(data.get("errcode").toString()) == 0) {
            return  data;
        } else {
            logger.info("{} get access error,msg:{}", key, data.toString());
        }
        return null;
    }

    /**
     * 根据code获取用户信息
     * @param code
     * @return
     */
    public static Map<String, Object> getOpenId(String code){

       return HttpClientUtil.wxRequest(accessTokenAuth, String.format(WorkWXAPI.GET_USER_OPENID_URL, accessTokenAuth, code)
                , null, HttpClientUtil.METHOD_GET);
    }

    /**
     * 根据UserId获取用户信息
     * @param userId
     * @return
     */
    public static Map<String, Object> getContact(String userId){

        return HttpClientUtil.wxRequest(accessTokenContacts, String.format(WorkWXAPI.CONTACTS_GET, accessTokenContacts, userId)
                , null, HttpClientUtil.METHOD_GET);
    }
 }

注:因代码过长,此处只引入部门函数。

  • HttpClientUti类
    包括Get,Post,wxRequest方法,可执行普通的http请求。
package cn.edu.zut.wechat.utils;

import org.apache.http.HttpEntity;
import org.apache.http.HttpResponse;
import org.apache.http.client.HttpClient;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.DefaultHttpClient;
import org.apache.http.util.EntityUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletRequest;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.util.HashMap;
import java.util.Map;
import java.util.concurrent.TimeUnit;

/**
 * @Author: geekfly
 * @Description: HttpClient工具类,用于发送请求
 * @Date: 2017/10/12
 * @Modified By:
 */
@Component
public class HttpClientUtil {

    private static final Logger logger = LoggerFactory.getLogger(HttpClientUtil.class);

    public static String METHOD_GET = "Get";

    public static String METHOD_POST = "Post";

    private static StringRedisTemplate stringRedisTemplate;

    public static void setStringRedisTemplate(StringRedisTemplate template) {
        stringRedisTemplate = template;
    }

    /**
     * 执行企业微信接口方法
     * @param key AccessToken的key值
     * @param url 接口地址
     * @param json json数据
     * @param method 请求方式 Get or Post
     * @return
     */
    public static Map<String, Object> wxRequest(String key, String url, String json, String method){
        Map<String, Object> mapData = new HashMap<>();
        try{
            int num = 0;
            boolean flag = false;
            while(num < 3){
                num++;
                if(stringRedisTemplate.hasKey(key)){
                    url = url.replace(key, stringRedisTemplate.opsForValue().get(key));
                    String jsonData = null;
                    logger.info(stringRedisTemplate.opsForValue().get(key));
                    if(method.equals(METHOD_GET)){
                        jsonData = get(url);
                    }else if(method.equals(METHOD_POST)){
                        jsonData = post(url, json);
                    }
                    mapData = JsonUtil.toMap(jsonData);
                    int errCode = Integer.parseInt(mapData.get("errcode").toString());
                    if(errCode == 0){ //正常
                        return mapData;
                    }else if(errCode == 42001){ // access_token过期
                        stringRedisTemplate.delete(key); //删除key
                        logger.info("key:{},access token过期", key);
                    }else{
                        flag = true;
                        logger.info("key:{},url:{},json:{},method:{},msg:{}", key, url, json, method, jsonData);
                    }
                }
                if(flag == false){
                    mapData = WXAPIUtil.getAccessToken(key);
                    if(mapData != null){
                        stringRedisTemplate.opsForValue().set(key, mapData.get("access_token").toString(),
                                Integer.parseInt(mapData.get("expires_in").toString()), TimeUnit.SECONDS);
                    }
                }
            }
        }catch (Exception ex){
            ex.printStackTrace();
            logger.error("wxRequest error:{}", ex.getMessage());
        }
        mapData.put("errcode", -1000);
        mapData.put("errmsg", "未获取到信息");
        return mapData;
    }

    public static String get(String url){
        HttpClient httpClient = new DefaultHttpClient();
        HttpGet httpGet = new HttpGet(url);
        HttpResponse response = null;
        try{
            response = httpClient.execute(httpGet);
        }catch (Exception e) {}
        String temp="";
        try{
            HttpEntity entity = response.getEntity();
            temp= EntityUtils.toString(entity,"UTF-8");
        }catch (Exception e) {}

        return temp;
    }

    public static String post(String url, String json){
        HttpClient httpClient = null;
        HttpPost httpPost = null;
        String result = null;
        try{
            httpClient = new DefaultHttpClient();
            httpPost = new HttpPost(url);

            StringEntity entity = new StringEntity(json,"utf-8");//解决中文乱码问题
            entity.setContentEncoding("UTF-8");
            entity.setContentType("application/json");
            httpPost.setEntity(entity);
            HttpResponse response = httpClient.execute(httpPost);
            if(response != null){
                HttpEntity resEntity = response.getEntity();
                if(resEntity != null){
                    result = EntityUtils.toString(resEntity);
                }
            }
        }catch(Exception ex){
            ex.printStackTrace();
        }
        return result;
    }
}

核心方法为:wxRequest

其中StringRedisTemplate在主函数中注入

public static void main(String[] args) {
		ConfigurableApplicationContext applicationContext = SpringApplication.run(WechatApplication.class, args);

		HttpClientUtil.setStringRedisTemplate(applicationContext.getBean(StringRedisTemplate.class));
	}

	@Bean
	StringRedisTemplate template(RedisConnectionFactory connectionFactory){
		return new StringRedisTemplate(connectionFactory);
	}

Controller中调用如下:

Map<String, Object>  jsonMap = WXAPIUtil.updateContact(JsonUtil.toJson(wxUser));
package cn.edu.xxx.wechat.utils;

import com.alibaba.fastjson.JSON;

import java.util.List;
import java.util.Map;

/**
 * @Author: hanyunfei
 * @Description: Json 工具类
 * @Date: 2017/10/12
 * @Modified By:
 */
public class JsonUtil {
	/**
	 * 对象转Json
	 * @param object
	 * @return 转化后的Json字符串
	 */
	public static String toJson(Object object){
		String string = null;
		try {
			string = JSON.toJSONString(object);
		} catch (Exception e) {
			e.printStackTrace();
		}
		return string;
	}
	
	/**
	 * 普通Json字符串转Map
	 * @param json
	 * @return 转化后的Map
	 */
	public static Map<String, Object> toMap(String json){
		return JSON.parseObject(json, Map.class);
	}

	/**
	 * 微信接口返回Json字符串转Map(考虑版本区别,需判断是否有errcode)
	 * @param json
	 * @return 转化后的Map
	 */
	public static Map<String, Object> WXJsonToMap(String json){
		Map<String, Object> map =  JSON.parseObject(json, Map.class);
		if(!map.containsKey("errcode")){ //如果不存在errcode键,则添加该键
			map.put("errcode", 0);
		}
		return map;
	}

	public static <T> T toBean(String text, Class<T> clazz) {
		return JSON.parseObject(text, clazz);
	}

	public static <T> List<T> toList(String text, Class<T> clazz) {
		return JSON.parseArray(text, clazz);
	}

	public static void main(String[] args) {
		String json = "{\"errcode\": 0,\"errmsg\": \"ok\",\"userid\": \"zhangsan\",\"name\": \"李四\",\"department\": [1, 2],\"order\": [1, 2],\"position\": \"后台工程师\",\"mobile\": \"15913215421\",\"gender\": \"1\",\"email\": \"zhangsan@gzdev.com\",\"isleader\": 1,\"avatar\": \"http://wx.qlogo.cn/mmopen/ajNVdqHZLLA3WJ6DSZUfiakYe37PKnQhBIeOQBO4czqrnZDS79FH5Wm5m4X69TBicnHFlhiafvDwklOpZeXYQQ2icg/0\",\"telephone\": \"020-123456\",\"english_name\": \"jackzhang\",\"extattr\": {\"attrs\":[{\"name\":\"爱好\",\"value\":\"旅游\"},{\"name\":\"卡号\",\"value\":\"1234567234\"}]},\"status\": 1,\"qr_code\":\"https://open.work.weixin.qq.com/wwopen/userQRCode?vcode=xxx\"\"external_profile\": {\"external_attr\": [{\"type\": 0,\"name\": \"文本名称\",\"text\": { \"value\": \"文本\"}},{\"type\": 1,\"name\": \"网页名称\",\"web\": { \"url\": \"http://www.test.com\", \"title\": \"标题\"}},{\"type\": 2,\"name\": \"测试app\",\"miniprogram\": { \"appid\": \"wx8bd80126147df384\", \"pagepath\": \"/index\", \"title\": \"my miniprogram\"}}]}";
		System.out.println(JsonUtil.toMap(json));

	}
}