经过不断的探索,终于在代码上完善了企业微信对接内部开发。由于企业微信token的管理机制比较特别,因此这里拎出来单独解释如何管理企业微信token(留意下方的粗体字)。
1.企业微信对接内部应用开发,基本上只能在获取到access_token后才能调用其他业务接口,因此access_token的管理非常重要,也是第一步要解决的问题。
2.access_token的管理机制不同于微信、公众号的token。根本原因在于企业微信access_token针对应用,而其他的token针对用户。显著的影响就是token的保存方式和代码与众不同。(钉钉对接内部应用开发用到的access_token同理)
3.access_token的过期时间由企业微信官方控制。可能某段时间内从企业微信官方获取的access_token都是同一个,无法由开发者强制更新。
4.根据官方文档的说明,access_token应在首次获取后由开发者保存。过期时间接近2小时,有可能顺延到接近4小时。经过测试发现,在请求gettoken接口获取tokenA后的2小时内,从某个时刻开始重复请求gettoken接口,获取的是tokenB。但此刻用tokenA调用业务接口仍然有效,并且tokenA的可用时间得到了很大延长,由此证明tokenA是有平滑过期时间,以保证某些使用tokenA的线程执行业务接口仍然有效。但这不妨碍更新本地缓存。缓存里只需要保存最新获取的tokenB就可以了。
5.可以使用缓存保存企业微信的access_token,不用担心项目是否需要在多个服务器部署。因为所有用户共用同一个access_token,企业微信会管理和平滑更新access_token。
6.代码上对于更新access_token的操作应进行加锁,防止在多线程情况下出现多次调用企业微信gettoken接口和写缓存的操作。
7.在外部应该通过委托调用业务方法而不是直接调用业务方法。在以下代码的示例中,调用顺序大概是“外部->DelegateHelper->ApiHelper->ATokenHelper”。因为调用业务接口失败可能由access_token过期引起,这时候不可能直接返回告诉用户操作失败,应该更新凭证然后重新执行业务逻辑,这需要委托的帮助。在外部直接调用ApiHelper会使得外部表现臃肿并且难以维护。
8.敲黑板划重点:开发者不要过多理会access_token什么时候过期,而是注重调用业务接口失败后应该怎么做!!!!!!!!!!! 代码中设置的缓存过期时间主要是为了按时释放缓存。
注:demo中涉及到json解析、http模拟请求的代码,就不提供这部分源码了。demo里有大量注释,可根据注释自己实现对应的功能。
1.ATokenHelper.cs
/// <summary> /// 企业微信access_token的管理中心 /// 官方文档:https://work.weixin.qq.com/api/doc/90000/90135/91039 /// 全局错误码参考:https://work.weixin.qq.com/api/doc/90000/90139/90313 /// </summary> public sealed class ATokenHelper { private static readonly object _Object = new object(); private static readonly string _CacheName = "access_token"; /// <summary> /// 尝试从缓存获取access_token /// </summary> /// <returns>返回企业微信的token</returns> public static string GetAToken() { string accessToken; try { //1.【尝试从缓存中读取token】 accessToken = Convert.ToString(CacheHelper.GetCache(_CacheName)); //2.若缓存里没有token,则请求企业微信接口获取token if (string.IsNullOrEmpty(accessToken)) { accessToken = RequestForToken(); } return accessToken; } catch { return string.Empty; } } /// <summary> /// 重新获取token /// </summary> public static string RequestForToken() { string accessToken; try { //1.尝试从缓存里获取当前token string strTokenOld = Convert.ToString(CacheHelper.GetCache(_CacheName)); lock (_Object) { //2.再次从缓存里获取当前token string strTokenNew = Convert.ToString(CacheHelper.GetCache(_CacheName)); //3.判断锁前和锁后从缓存里读取的值:若一致,说明目前没人操作 if (strTokenOld == strTokenNew) { //【拼接企业微信消息推送接口的地址】 string getATokenUrl = string.Format("https://qyapi.weixin.qq.com/cgi-bin/gettoken?corpid={0}&corpsecret={1}", ConfigurationManager.AppSettings["QYWX.CorpID"], ConfigurationManager.AppSettings["QYWX.CorpSecret"]); //【发起Get请求】 string strResp = getATokenUrl.Get(); //【从返回的json里获取状态码节点】 string strErrorCode = strResp.JsonCutApart("errcode"); //【企业微信的状态码不为0则表示请求失败】 if (strErrorCode != "0") { return string.Empty; } //【从返回的json里获取过期时间节点】 int iExpireTime = Convert.ToInt32(strResp.JsonCutApart("expires_in")); //【从返回的json里获取企业微信token accessToken = strResp.JsonCutApart(_CacheName); //【保存到缓存中,并且设置过期时间】 CacheHelper.SetCache(_CacheName, accessToken, iExpireTime); } else { //4.锁前和锁后的值不一致,说明已经写入了新值 accessToken = strTokenNew; } } return accessToken; } catch { return string.Empty; } } /// <summary> /// 判断接口调用失败是否由access_token过期引起 /// </summary> /// <param name="_ErrorCode">全局错误码</param> /// <returns>返回结果</returns> public static bool CheckATokenExpire(string _ErrorCode) { //全局错误码42001表示token过期 if (_ErrorCode == "42001") { //更新token RequestForToken(); return true; } return false; } }
2.ApiHelper.cs
/// <summary> /// 针对代用企业微信业务接口的方法封装 /// </summary> public class ApiHelper { /// <summary> /// 发送文本消息 /// </summary> /// <param name="_UserId">接收者</param> /// <param name="_Msg">消息</param> /// <param name="_RespCode">返回状态码</param> public static void SendMsg(string _ToUser, string _Msg, out string _RespCode) { _RespCode = string.Empty; try { var jsonData = new { touser = _ToUser, toparty = "", totag = "", msgtype = "text", agentid = ConfigurationManager.AppSettings["QYWX.AgentID"], text = new { content = _Msg }, safe = 1, enable_id_trans = 0, enable_duplicate_check = 0, duplicate_check_interval = 1800 }; string token = ATokenHelper.GetAToken(); //【拼接请求接口的地址】 string strSendUrl = string.Format("https://qyapi.weixin.qq.com/cgi-bin/message/send?access_token={0}", token); //【发起Post请求】 string strResp = strSendUrl.Post(jsonData.ToJson(), ContentType.ApplicationJson); //【从返回的json里获取状态码节点】 _RespCode = strResp.JsonCutApart("errcode"); } catch { } } }
3.DelegateHelper.cs
public delegate void Action(string _Param1, string _Param2, out string _RespCode); public class DelegateHelper { /// <summary> /// 调用示例:DelegateHelper.Do(ApiHelper.SendMsg, "Liuyu", "您有一条新的消息") /// </summary> /// <returns>返回调用结果 true:成功 false:失败</returns> public static bool Do(Action _Action, string _Param1, string _Param2) { string respCode = string.Empty; if (_Action != null) { _Action(_Param1, _Param2, out respCode); //检查access_token是否过期,若过期则更新,并且重新执行业务方法 if (ATokenHelper.CheckATokenExpire(respCode)) { _Action(_Param1, _Param2, out respCode); } } return respCode == "0"; } }
4.调用示例Program.cs
class Program { static void Main(string[] args) { bool result = DelegateHelper.Do(ApiHelper.SendMsg, "Liu.Yu", "您收到了一条测试信息"); Console.WriteLine("处理结果:{0}", result); } }