需求:公司想要拉取外部群客户发送的相关信息,用以进行跟进。
分析可行性方案:1.原来想用企业微信自带的机器人,但机器人目前不支持外部群。
2.然后使用了一个免费的第三方,不过只能拉取到文本信息也pass了
3.使用付费的第三方,但担心对方过两年跑路,并且有客户信息泄露的风险,故也放弃
4.故最后敲定使用企业微信提供的会话存档来实现(这个也是收费的,服务版450/年)
实现过程:首先你需要到公司对应的企业微信账号去开通会话存档功能,一开始有1个月的免费试用期(之后服务版的是450/年),需要配置响应的ip地址,信息接收回调接口(这里需要注意的是该接口需要get请求)等,企业微信有对应的官方文档,根据文档一步一步来,申请好后,我这里下载的C版的sdk(这里可以根据需要选择32,或者64位的,我选择的是64位的,这个只有一系列的环境也都需要相匹配哦)。
接下来准备工作都做好了,开始进入正题。
这里引用借鉴了大喵网站的相关文章,并加以改进,已适用自身业务。期间遇到的问题以及解决方案如下:
1.因为所给的libcurl-x64.dll等文件不用直接被vs直接引用,只需要放到项目的根路径就好,在使用DllImport来调用实现,这里FinanceAdapter.cs有封装好,可以自行查看。这里会遇到一个问题:
引用dll“找不到指定模块"
解决方案:方案1,1.鼠标右键属性(或者选中该DLL按下F4)—— 复制到输入目录 ——始终复制(默认情况下是不复制,但是不能选择不复制),2.并添加C++的相关环境(同理发布到服务器上时也需要)vc_redist.x64.exe
这里可能会遇到安装vc_redist.x64.exe不成功的可能,大多是因为已经安装了别的版本的redist原因,例如已经安装了32位的,64位就会弹窗提示,这里可以使用cmd强制安装或者卸载32位的,再安装64位。如果还是找不到,调用路径可以直接写成绝对路径,或者在 (x86系统)C:\Windows\System32或(x64系统)C:\Windows\SysWOW64目录下中放入WeWorkFinanceSdk相关的dll
2.引用成功后,代码跑起来,因为兼容性由出现了问题:
试图加载格式不正确的程序
解决方案:方案1(方案里的是32位的,64位的改为对应的即可),
1)允许的话把C#客户端项目平台修改为64位
2)客户端平台不允许修改,则选择AnyCPU,勾选首选64位,如图(在.netFramework4.5上,勾选64位才可以进行选择)
设置完这些,如果依旧报错,就需要修改VS的工具=>选项=>项目和解决方案=>Web项目=>对网站和项目使用IIS Express 的 64位版 打钩
3.RSAKEY密钥需要由string类型转换成xml才可以被FinanceAdapter.DecryptData调用
/// <summary>
/// /// 私钥转XML
/// /// </summary>
/// /// <param name="privateJavaKey"></param>
/// /// <returns></returns>
public static string ConvertToXmlPrivateKey(string privateJavaKey)
{
RsaPrivateCrtKeyParameters privateKeyParam = (RsaPrivateCrtKeyParameters)PrivateKeyFactory.CreateKey(Convert.FromBase64String(privateJavaKey));
return string.Format("<RSAKeyValue><Modulus>{0}</Modulus><Exponent>{1}</Exponent><P>{2}</P><Q>{3}</Q><DP>{4}</DP><DQ>{5}</DQ><InverseQ>{6}</InverseQ><D>{7}</D></RSAKeyValue>",
Convert.ToBase64String(privateKeyParam.Modulus.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.PublicExponent.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.P.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.Q.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.DP.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.DQ.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.QInv.ToByteArrayUnsigned()),
Convert.ToBase64String(privateKeyParam.Exponent.ToByteArrayUnsigned()));
}
4.会话存档成功搭建后,拉取信息时可能会遇到信息还在3天内,但依旧过期的情况,询问社区平台,还是待处理的情况,我这边的处理方式是,使用会话存档提供的会话回调,进行同步跟进,现将信息拉取到本地服务器,待之后进行处理使用。
以下是会用到的一些方法:
/// <summary>
/// 获取文本
/// </summary>
/// <param name="slice"></param>
/// <returns></returns>
public string GetContentFromSlice(long slice)
{
var length = FinanceAdapter.GetSliceLen(slice);
var bytes = new byte[length];
var ptr = FinanceAdapter.GetContentFromSlice(slice);
Marshal.Copy(ptr, bytes, 0, bytes.Length);
return Encoding.UTF8.GetString(bytes);
}
/// <summary>
/// 解密
/// </summary>
/// <param name="privateKey"></param>
/// <param name="text"></param>
/// <returns></returns>
public string Decrypt(string text)
{
string xml = ConvertToXmlPrivateKey(privateKeys);//Json字符串转xml字符串
var rsa = new RSACryptoServiceProvider();
var bytes = Convert.FromBase64String(text);
rsa.FromXmlString(xml);
var result = rsa.Decrypt(bytes, false);
return Encoding.UTF8.GetString(result);
}
/// <summary>
/// 拉取会话存档
/// </summary>
/// <param name="iSeq">从指定的seq开始拉取消息,注意的是返回的消息从seq+1开始返回,seq为之前接口返回的最大seq值。首次使用请使用seq:0</param>
/// <param name="iLimit">一次拉取的消息条数,最大值1000条,超过1000条会返回错误</param>
/// <param name="proxy">使用代理的请求,需要传入代理的链接。如:socks5://10.0.0.1:8081 或者 http://10.0.0.1:8081</param>
/// <param name="paswd">代理账号密码,需要传入代理的账号密码。如 user_name:passwd_123</param>
/// <param name="timeout">超时时间,单位秒</param>
/// <param name="echostr">返回本次拉取消息的数据,slice结构体.内容包括errcode/errmsg,以及每条消息内容。</param>
/// <returns></returns>
public int GetChatData(long iSeq, long iLimit, string proxy, string paswd, long timeout, ref long echostr)
{
try
{
DateTime timeTo = new DateTime();
DateTime timeFo = new DateTime();
string[] reser = new string[] { };
var seq = Convert.ToInt32(iSeq);
///NewSdk返回的sdk指针
var sdk = FinanceAdapter.NewSdk();
var ret = FinanceAdapter.Init(sdk, 企业微信corpId, 企业微信secret);
if (ret != 0)
{
//sdk需要主动释放
FinanceAdapter.DestroySdk(sdk);
return -1;
}
//拉取会话存档
//每次使用GetChatData拉取存档前需要调用NewSlice获取一个chatDatas,在使用完chatDatas中数据后,还需要调用FreeSlice释放。
var chatDatas = FinanceAdapter.NewSlice();
ret = FinanceAdapter.GetChatData(sdk, iSeq, iLimit, proxy, paswd, timeout, chatDatas);
echostr = chatDatas;
if (ret != 0)
{
FinanceAdapter.FreeSlice(chatDatas);
return -1;
}
FinanceAdapter.FreeSlice(chatDatas);
FinanceAdapter.DestroySdk(sdk);
return seq;
}
catch (Exception ex)
{
Logs.logSave("回滚报错"+ex);
throw;
}
}
/// <summary>
/// SDK解密会话存档内容
/// </summary>
/// <param name="encrypt_key">getchatdata返回的encrypt_random_key,使用企业自持对应版本秘钥RSA解密后的内容</param>
/// <param name="encrypt_msg">getchatdata返回的encrypt_chat_msg</param>
/// <param name="MsgsStr">消息明文,json格式</param>
/// <returns></returns>
public int GetDecryptData(string encrypt_key, string encrypt_msg, ref string MsgsStr)
{
var sdk = FinanceAdapter.NewSdk();
var ret = FinanceAdapter.Init(sdk, coupId, secret);
if (ret != 0)
{
//sdk需要主动释放
FinanceAdapter.DestroySdk(sdk);
return -1;
}
//解密会话存档内容
//sdk不会要求用户传入rsa私钥,保证用户会话存档数据只有自己能够解密。
//此处需要用户先用rsa私钥解密encrypt_random_key后,作为encrypt_key参数传入sdk来解密encrypt_chat_msg获取会话存档明文。
//每次使用DecryptData解密会话存档前需要调用NewSlice获取一个Msgs,在使用完Msgs中数据后,还需要调用FreeSlice释放。
var Msgs = FinanceAdapter.NewSlice();
ret = FinanceAdapter.DecryptData(encrypt_key, encrypt_msg, Msgs);
MsgsStr = GetContentFromSlice(Msgs);
FinanceAdapter.FreeSlice(Msgs);
FinanceAdapter.DestroySdk(sdk);
return ret;
}
/// <summary>
/// 获取媒体文件
/// </summary>
/// <param name="sdkFileid"></param>
/// <param name="proxy"></param>
/// <param name="passwd"></param>
/// <param name="timeout"></param>
/// <param name="echostr"></param>
/// <returns></returns>
public int GetMediaData(string sdkFileid, string proxy, string passwd, long timeout,string Title, ref string filepath)
{
var sdk = FinanceAdapter.NewSdk();
var ret = FinanceAdapter.Init(sdk, coupId, secret);
if (ret != 0)
{
//sdk需要主动释放
FinanceAdapter.DestroySdk(sdk);
return -1;
}
//拉取媒体文件
string index = "";
int isfinish = 0;
//媒体文件每次拉取的最大size为512k,因此超过512k的文件需要分片拉取。若该文件未拉取完整,mediaData中的is_finish会返回0,同时mediaData中的outindexbuf会返回下次拉取需要传入GetMediaData的indexbuf。
//indexbuf一般格式如右侧所示,”Range:bytes=524288-1048575“,表示这次拉取的是从524288到1048575的分片。单个文件首次拉取填写的indexbuf为空字符串,拉取后续分片时直接填入上次返回的indexbuf即可。
while (isfinish == 0)
{
//每次使用GetMediaData拉取存档前需要调用NewMediaData获取一个mediaData,在使用完mediaData中数据后,还需要调用FreeMediaData释放。
var mediaData = FinanceAdapter.NewMediaData();
ret = FinanceAdapter.GetMediaData(sdk, index, sdkFileid, proxy, passwd, timeout, mediaData);
if (ret != 0)
{
return -1;
}
string endpath = @"Excel\" + DateTime.Now.ToString("yyyyMMdd"); //打包的根路径
string uploadPath = HttpRuntime.AppDomainAppPath + endpath+"\\";
//var title = DateTime.Now.ToString("yyyyMMddHHmmss") + ".xlsx";
if (!Directory.Exists(uploadPath))
Directory.CreateDirectory(uploadPath);
filepath = uploadPath + Title;
byte[] bytes = new byte[FinanceAdapter.GetDataLen(mediaData)];
Marshal.Copy(FinanceAdapter.GetData(mediaData), bytes, 0, FinanceAdapter.GetDataLen(mediaData));
FileStream file = new FileStream(filepath, FileMode.Create, FileAccess.Write);
file.Write(bytes, 0, FinanceAdapter.GetDataLen(mediaData));
file.Close();
if (FinanceAdapter.IsMediaDataFinish(mediaData) == 1)
{
// need free media_data
FinanceAdapter.FreeMediaData(mediaData);
break;
}
else
{
index = FinanceAdapter.GetOutIndexBuf(mediaData);
// need free media_data
FinanceAdapter.FreeMediaData(mediaData);
}
}
FinanceAdapter.DestroySdk(sdk);
return ret;
}
最后,git上有一个使用.net对会话存档进行封装的源码项目。