
简介企业微信会话内容存档 C# 调用接口源码是一套面向企业服务开发者的可直接嵌入业务系统的解决方案用于将企业微信聊天记录按合规要求自动同步并安全存储。代码完整覆盖 API 调用封装、身份验证与权限管理、定时任务增量同步、数据持久化以及异常处理和日志记录等关键环节且对文本、图片、语音、视频、链接、小程序等常见消息类型提供了对应解析模型替换企业微信后台配置即可运行。压缩包采用 7z 格式共 81 个文件以 49 个 C# 源码文件为核心包含解决方案与工程文件另有 WeWorkFinanceSdk 等 dll 动态库、VC 运行库一键安装 exe 及 config 配置文件便于快速搭建开发环境整体约 57.1MB。目前已有 209 人浏览/学习适合具备一定 C# 基础、希望快速落地合规存档能力的开发者与系统集成人员参考。1. 会话内容存档到底在存档什么C#接入前的三个认知企业微信的会话内容存档名义上是“存档”实际是一套完整的合规审计链路先由管理员在后台开启存档范围员工同意后服务端把聊天明文用随机密钥加密再把随机密钥用企业公钥加密最终把双层加密的密文推给调用方。C#调用接口时你拿到的永远是密文而不是明文。所以“企业微信会话内容存档C#调用接口源码”这个需求真正难的不是HTTP请求而是三层事第一层是管理后台的密钥配置第二层是C#对官方CSDK的封装第三层是解密后的消息体怎么按业务落库。适合的是做SCRM、客服质检、内部审计系统的开发者特别是公司已经买了企业微信认证版、需要把聊天记录拉回自己系统的团队。2. 企业微信会话内容存档的公钥体系为什么C#要先配私钥再调接口会话内容存档和普通API最大的区别在于它不是单纯的token鉴权而是引入了“非对称加密 会话密钥”的双层加密模型。企业微信服务端持久化每个人每条消息时会用一把随机会话密钥AES加密消息内容随后用你的企业公钥把会话密钥加密。调用方拿到密文后先用RSA私钥解出随机会话密钥再用会话密钥解出消息明文。这意味着你的C#代码里必须同时持有两部分用于OpenAPI鉴权的CorpId/Secret以及用于解密的RSA私钥。在动手写DllImport之前我一般会先把这套密钥体系跑通因为它决定后面所有报错的排查方向。C#代码本身没问题时多半是密钥格式、密钥用途混了。例如有人说“我拿的是API Secret怎么SDKInit一直失败”——因为SDKInit第三个参数要的是步长为1024的RSA私钥不是应用Secret。下面用一个最小示例说明密钥准备过程这部分建议放在接入文档的第一章。2.1 后台三个必配参数和它们各自的用途第一个是CorpId企业ID管理后台“我的企业”里能看到。它不是应用的AgentId别弄混。第二个是Secret在“管理工具-会话内容存档”里开通后申请用于换取access_token所有拉取类接口都基于它来做鉴权。第三个是RSA私钥由你自己生成或由企微提供公钥后配对成对的私钥私有存储不能泄露给前端。这三个参数分别对应SDKInit里三个入参。很多C#封装代码把Init函数写成SDKInit(corpId, secret, privateKey)如果不注意privateKey的格式返回结果永远是负值。常见做法是直接把RSA私钥的Base64内容读到一个字符串里传入不要带-----BEGIN PRIVATE KEY-----这些PEM头尾。顺序也要确认最早版本Sdk里还有第四参proxy等后来简化为三参具体按你拿到的官方头文件看。2.2 生成密钥对openssl一条命令和一对注意点密钥对可以由企业内部自己生成也可以由企微后台生成公钥后把对应的私钥下载给自己。自己生成时标准做法是用openssl生成1024位RSA密钥对# 生成1024位RSA私钥 openssl genrsa -out qy_private.pem 1024 # 导出公钥串 openssl rsa -in qy_private.pem -pubout -out qy_public.pem # 查看公钥内容把base64串复制到企微后台 cat qy_public.pem这段命令里genrsa -out指定私钥文件1024是位长度企业微信会话存档接口要求RSA长度为1024位不能用2048rsa -in -pubout从私钥中提取公钥。生成后后台填公钥串时只需要填cat输出的那一行Base64不需要填写BEGIN PUBLIC KEY部分。而C#侧使用的私钥需要把qy_private.pem里的Base64内容单独提取出来去掉PEM头尾后存到配置文件或环境变量。常见翻车点就是直接传了整个PEM内容导致CSDK解析失败。如果你后台显示“公钥已配置”但仍然解密失败优先检查这里。2.3 权限、可见范围和员工二次确认密钥配好只是第一步。会话内容存档的拉取有两个前置条件第一企业必须完成认证未认证企业无法开通第二管理员必须把需要存档的成员加入“可见范围”员工首次登录企微时会收到“是否同意存档”的确认拒绝后该员工的消息不会被拉取。这里的逻辑要理清C#接口本身没有权限开关但拉取结果为空、报权限错误多半是可见范围没配全。后台的配置路径是“管理工具-会话内容存档-成员管理”同步范围后一般要等1到2分钟生效。另有一个企业微信侧接口getagreeinfo可以检查某个用户在某个时间点是否同意存档我一般会在对接时先用它做一次“探活”确认账号状态再进入拉取流程。3. C#调用会话内容存档接口从DllImport到分页拉取企业微信没有提供官方的.NET SDK官方发布的是C动态库WeWorkFinanceSdk.dll。所以C#调用接口的源码本质是写一层P/Invoke封装把C的导出函数转换成C#能调用的方法。常见做法是把DllImport声明单独放一个类统一管理SDK的生命周期和数据交换结构体业务层只调用封装后的方法不直接接触IntPtr。这一章我按“初始化-拉取-游标处理-断点续传”的顺序展开每一段代码都是可以直接抄进工程里改参的。3.1 封装DllImportSDKInit为什么要在循环里做先定义SDK的导入函数using System; using System.Runtime.InteropServices; public class WeWorkFinanceSdk { [DllImport(WeWorkFinanceSdk.dll, CallingConvention CallingConvention.Cdecl)] public static extern IntPtr NewSdk(); [DllImport(WeWorkFinanceSdk.dll, CallingConvention CallingConvention.Cdecl)] public static extern int SDKInit(IntPtr sdk, string corpId, string secret, string privateKey); [DllImport(WeWorkFinanceSdk.dll, CallingConvention CallingConvention.Cdecl)] public static extern int GetChatData(IntPtr sdk, ulong seq, uint limit, string proxy, string passwd, ulong timeout, out IntPtr chatData); [DllImport(WeWorkFinanceSdk.dll, CallingConvention CallingConvention.Cdecl)] public static extern int DecryptData(string encryptKey, string encryptMsg, out string result); [DllImport(WeWorkFinanceSdk.dll, CallingConvention CallingConvention.Cdecl)] public static extern void DestroySdk(IntPtr sdk); }声明里需要注意NewSdk返回的IntPtr是SDK对象指针后续所有操作都依赖它SDKInit的第三个参数privateKey必须是不包含PEM头尾的Base64字符串GetChatData的proxy和passwd在直连场景传空字符串不需要可为空timeout单位是毫秒。如果你在调试时发现SDKInit返回非零常见原因是私钥字符串里混进了换行符建议传入前先做一次privateKey.Replace(\n, ).Replace(\r, )。初始化是一个容易被低估的步骤。我一般会在服务启动时执行一次SDKInit但同时要处理返回值IntPtr sdkHandle IntPtr.Zero; int initRet -1; // 最多重试3次每次都重建SDK对象 for (int i 0; i 3; i) { if (sdkHandle ! IntPtr.Zero) { WeWorkFinanceSdk.DestroySdk(sdkHandle); } sdkHandle WeWorkFinanceSdk.NewSdk(); initRet WeWorkFinanceSdk.SDKInit(sdkHandle, corpId, secret, privateKey); if (initRet 0) break; System.Threading.Thread.Sleep(1000 * (i 1)); }这段代码把失败重试和句柄清理放在一起。NewSdk()每次都重新申请对象DestroySdk用于释放旧句柄避免因初始化失败导致内存句柄泄漏。initRet为0代表初始化成功非0时可以根据错误码区分是密钥问题还是网络问题但错误码的具体含义官方头文件里有说明这里不展开。3.2 拉取单聊存档GetChatData的seq和limit怎么配合GetChatData的入参seq是游标而不是页码limit是本次拉取的最大条数。seq从0开始接口在返回数据的同时会返回NextSeq下次拉取必须使用NextSeq否则会出现消息漏拉或重复拉。limit建议在500到1000之间超过1000后CSDK内存申请会变大反而容易在32位进程中触发内存溢出。一个可用的拉取循环如下ulong currentSeq GetLastSeqFromStorage(); // 从本地存储读取上次游标 uint limit 1000; int retryCount 0; while (true) { IntPtr chatDataPtr; int ret WeWorkFinanceSdk.GetChatData(sdkHandle, currentSeq, limit, , , 5000, out chatDataPtr); if (ret ! 0) { if (retryCount 3) { System.Threading.Thread.Sleep(1000 * retryCount); continue; } break; } // 解析chatDataPtr指向的ChatData结构体 ChatData chatData Marshal.PtrToStructureChatData(chatDataPtr); if (chatData.records IntPtr.Zero || chatData.recordCount 0) { break; // 没有更多数据 } // 处理本批数据 ProcessChatRecords(chatData); currentSeq chatData.nextSeq; // 推进游标 SaveSeqToStorage(currentSeq); retryCount 0; }参数说明GetChatData返回后chatDataPtr指向一个包含recordCount和records的结构体每条record里是base64编码的加密消息和seq。这里最容易出现的误解是把record里的seq当成下一条游标实际上应该使用chatData结构体里的nextSeq字段。nextSeq只有在本次有数据时才有效如果recordCount为0说明当前游标已经到末尾可以直接停止。3.3 群聊、RoomId和消息类型的边界处理GetChatData的入参里有roomId传空字符串拉取单聊和群聊的混合数据传具体roomId则只拉取该群聊的数据。群聊拉取和单聊的差异在于群成员的存档同意权限是独立的群聊里某个成员未同意时该成员发送的消息会被过滤但其他人的消息正常返回。我用这个函数的经验是优先全量拉取再做业务侧分类。因为按roomId逐个拉取时需要实时维护群聊ID列表而这个列表本身也要靠接口去查绕一圈不如直接全量。全量拉取的数据里每条消息都带有roomId字段按需过滤即可。处理每批数据时建议把record的value字段先解码成字节数组保存到本地临时目录再进入解密流程。这是因为接口拉取和解密是两个耗时操作分开后可以通过查看日志快速定位到底哪一步丢数据。4. 解密会话存档数据C#侧把密文变成业务消息的三步拉取到的record.value是base64编码的二进制串它本身是“会话密钥加密后的消息密文”。想要拿到明文必须再调用SDK的DecryptData函数。这一章的三个步骤缺一不可获取随机密钥、调用SDK解密、解析消息头。4.1 理解DecryptData的encryptKey参数官方SDK的DecryptData函数签名通常是int DecryptData(string encryptKey, string encryptMsg, out string msg)。其中encryptKey不是你的RSA私钥而是从record里解析出来的随机会话密钥密文。也就是说在进入DecryptData之前C#代码要先从record的字段里取出encrypt_key字段把它作为encryptKey传入。这个过程里最容易踩的坑是把私钥字符串当成encryptKey传进去结果SDK内部对encryptKey做RSA私钥解密时失败返回负数或者空字符串。密钥的正确流转顺序是record.value里是base64编码的密文record里还有一个字段专门存放加密后的会话密钥SDK的DecryptData会用自己的RSA私钥解开会话密钥再用会话密钥解开消息。4.2 正确的解密调用模板下面是一段可在C#工程中直接落地的解密封装public class ArchiveMessage { public string MsgType { get; set; } public string Content { get; set; } public int Version { get; set; } public byte[] RawData { get; set; } } public ArchiveMessage DecryptRecord(Record record, IntPtr sdkHandle) { string encryptKey record.encryptKey; // 随机会话密钥密文 string encryptMsg record.value; // 消息密文 string plainText; int ret WeWorkFinanceSdk.DecryptData(encryptKey, encryptMsg, out plainText); if (ret ! 0) { throw new InvalidOperationException($DecryptData failed, code{ret}); } byte[] binaryData Convert.FromBase64String(plainText); ArchiveMessage result ParseArchiveMessage(binaryData); return result; }逻辑说明DecryptData的返回值是int0表示成功成功后plainText是Base64编码的明文二进制字符不是最终的UTF-8文本。所以要先把字符串转回字节数组再做消息头解析。注意Convert.FromBase64String可能因为字符串里有不可见字符而报错在调用前可以做一次plainText.Trim()。4.3 解析消息头Version、MAC和消息体分布解密后的二进制数据有四段固定结构4字节的Version4字节的MAC长度紧接着的MAC值最后是消息体。Version标记消息版本目前常用的是2对应企业微信消息结构MAC用于校验解密是否正确SDK内部已经校验过一次C#侧如果自己实现解密则必须校验MAC否则解密结果可能是乱码。看过太多自己用AesCbc实现的解密解密出来前几个字符是乱码然后抱怨SDK不兼容。实际情况是官方SDK里包含了MAC校验逻辑并且解密用的密钥不只一个而是先解开密钥块再解消息体。企业微信的加密是连哈希值一起加密的所以中间任意一位错误都会导致整个解密结果报废。所以我的建议很直接不要自己重写解密直接用官方SDK。C#侧只做数据搬运和结构化不做加密算法实现。除非你需要二进制级别的调试否则重写等于给自己找事。4.4 把明文二进制转成业务对象解出来的二进制里Version之后的字节就是实际的聊天消息序列化数据。消息体格式在不同版本里不一样常见的是protobuf结构。C#侧可以通过官方提供的proto文件生成对应的C#类再通过Serializer.Deserialize转成强类型对象。// 以protobuf-net为例实际类名和字段以官方proto定义为准 using ProtoBuf; [ProtoContract] public class ChatMessage { [ProtoMember(1)] public string msgtype { get; set; } [ProtoMember(2)] public string msgdata { get; set; } } public ArchiveMessage ParseArchiveMessage(byte[] rawData) { using (var ms new MemoryStream(rawData, 4, rawData.Length - 4)) { return Serializer.DeserializeArchiveMessage(ms); } }代码里new MemoryStream(rawData, 4, rawData.Length - 4)跳过了前4个Version字节。参数说明ProtoMember(1)和ProtoMember(2)的编号必须与官方proto一致否则会解析出默认值而不是报错这在排查时会很隐蔽。msgdata字段里是消息具体内容文本消息会有text字段图片消息会有image字段具体结构也由proto定义。5. C#会话存档接口的5个排查点从权限报错到解密失败5.1 SDKInit返回非0私钥里多了三个换行符现象SDKInit返回值一直不为0代码没有任何异常日志。 原因配置文件里保存的私钥是多行PEM格式字符串传入时带\n和\rCSDK内部解析失败。 解决读取私钥后做一次规整去除所有空白字符再传入。我用的是privateKey privateKey.Replace(-----BEGIN PRIVATE KEY-----, ) .Replace(-----END PRIVATE KEY-----, ) .Replace(\n, ).Replace(\r, );5.2 GetChatData返回60020可见范围没配或Secret用错现象拉取接口返回60020错误信息提示“not allowed”。 原因当前Secret对应的应用没有会话内容存档权限或可见范围里没有包含任何成员。 解决先确认Secret是在“会话内容存档”模块申请的而不是从“自建应用”里复制的再到后台检查“可见范围”配置后等1分钟再测。很多时候自建应用和会话存档共用一个CorpId但Secret是两套。5.3 拉取一直返回空数据seq永远从0开始现象接口返回0但recordCount一直是0后台明明有聊天记录。 原因每次服务重启后seq都从0开始拉取但企业微信只保留最近一段时间的数据过早的游标位置已经没有数据。另一种情况是拉取成功后没有持久化NextSeq下一次又回到0。 解决把currentSeq持久化到数据库或本地文件每次拉完立即写入NextSeq。服务重启后从存储读取游标而不是从0开始。这一步不做数据永远拉不完整。5.4 DecryptData解密成功但内容是乱码Version处理错误现象DecryptData返回0Convert.FromBase64String能转换但解析出来的消息字段全是乱码或高位字符。 原因解密后的明文前4字节是Version后面是MAC和消息体但C#代码没有跳过MAC长度直接对整个字节数组做了反序列化。 解决先读取第4到8字节的int值作为MAC长度然后接着跳过MAC部分从MAC结束位置开始解析消息体。这个错误在protobuf场景下特别容易踩因为protobuf对不确定前缀会直接报错或错位解析。5.5 拉取大文件时内存暴涨MediaData的缓冲区没释放现象程序运行几小时后内存持续上涨然后开始报OutOfMemory。 原因GetChatData返回的records、以及后续GetMediaData里申请的缓冲区官方SDK不会自动释放必须由调用方手动释放。C#里IntPtr指向的是非托管内存GC管不到。 解决每条record处理完后调用SDK的释放函数并把解析库封装成using模式或者try-finally。我一般会在封装层写一个Dispose方法统一释放record集合和mediaData缓冲区。6. 把加密存档存成本地文件按MsgId归档的一个小技巧到了这一步你已经能拉取、解密消息了。最后一个值得做的小优化是把每条消息按MsgId归档到本地文件文件名里带上用户ID和时间这样后续排查和审计都非常方便。我通常的做法是解密成功后用消息里的msgid作为文件名把原始密文和解密后的JSON都落盘。密文落盘的价值在于回溯——如果后续解密逻辑改版或发现解析错误还能拿原始密文重放不需要再回源拉取。明文JSON落盘则用于对接搜索和数据仓库。一个简单的归档结构可以是归档目录/日期/msgid_userid.json。日期按消息发送时间做第一级目录避免单目录文件过多。文件名里加userid是防止同一毫秒内有多条消息避免覆盖。代码上不需要额外引入框架一个静态类加两个方法就能搞定public static void ArchiveMessage(string baseDir, ChatMessage msg, byte[] rawDencrypted) { // 用消息时间生成日期目录 string dir Path.Combine(baseDir, msg.msgtime.ToString(yyyyMMdd)); Directory.CreateDirectory(dir); // 文件名msgid_userid.json string file Path.Combine(dir, ${msg.msgid}_{msg.userid}.json); // 明文结构化内容 File.WriteAllText(file, JsonSerializer.Serialize(msg)); // 原始密文单独存放方便回溯 string rawFile file .raw; File.WriteAllBytes(rawFile, rawDencrypted); }归档文件的具体格式可以按团队需求调整但“明文和密文都落盘按消息时间分目录”这个习惯我保持了很久它解决过一个很实际的问题后来发现有消息解析漏字段只需要找到对应raw文件重放就能验证新解析代码是否正确不用再等下一次拉取窗口。这也是这类接口调试里最省事的“后悔药”。另外一个验证办法是做个统计任务每天定时对比存档总数和昨天拉取时的recordCount对不上就说明漏拉了。你可以用一个定时器每小时执行一次把当前总消息数打到日志里超过1%偏差就告警。这个方法比人眼看日志可靠得多能尽早发现seq跳变或权限配置回滚。如果你正在做企业微信会话内容存档的C#接入建议在功能上线前先把这套统计加进去没有统计就上线将来出问题只能全量重查。希望这些从密钥配置到落盘归档的经验能帮到你。本文还有配套的精品资源点击获取