经过不断的探索,终于在代码上完善了企业微信对接内部开发。由于企业微信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);
}
}