简介:面向企业应用开发者的企业微信会话内容存档C#调用接口源码包,聚焦合规监管、质量控制和数据统计场景下的聊天记录合法存档需求。方案覆盖企业微信API调用方式、身份验证与权限管理、数据同步与存储、定时任务设计、异常处理及日志记录等技术要点,适合具备C#基础并希望快速集成会话存档能力的开发者学习使用。7z压缩包内共81个文件,以cs业务源码为主,辅以dll动态库、exe工具(含VC运行库一键安装包)、config配置与xml文档、sln解决方案等,整体压缩后57.1MB,目录结构便于按VS工程直接打开调试。作者将整套接口调用逻辑封装为辅助类与多类型消息解析模块,并给出环境依赖处理与部署维护思路,嵌入企业应用时可直接复用。目前已有209人学习浏览,可作为企业微信二次开发与数据合规领域的实用参考资料。
1. 会话内容存档的 C# 接入:先解密钥,再谈调用接口
做客服质检或合规留痕,迟早碰到企业微信的会话内容存档。开通后它不会把聊天记录自动送给你,而是给一个拉取接口:传游标 seq,返回加密消息包,每包带 encrypt 和 encrypt_random_key。要看明文,得先用私钥解出 AES key 再解消息体——门槛不在调用接口,而在解密和游标推进。
这份 C# 源码做的事,就是把链路从「能调通」做到「能落库可查询」:access_token 缓存、get_archive_data 增量拉取、双层解密、消息反序列化、授权查询、媒体文件下载校验。适合做审计、风控、质检的 C# 开发,也适合给客户做企微对接的交付工程师。
以为存档就是把返回 JSON 直接存库?后面每一步都会踩坑。下面按我实际拆包、联调、上线的顺序写,类名和源码包一一对应,改完配置就能跑。
2. 前置条件与密钥准备:corpid、secret、私钥三件套怎么配
接入企微存档,头一件事不是写代码,而是把后台的凭证理清楚。存档对凭证的绑定比普通自建应用严格,不少项目卡在第一步:调 gettoken 正常,拉取接口却一直返回空,最后发现 secret 拿的是聊天应用而不是存档应用。这章把三件套的来源、授权范围和环境依赖一次讲完,源码包里的配置类 AppSettings.cs 就是按这三项设计的。
2.1 三类凭证从哪拿:别把自建应用的 secret 当存档 secret 用
| 凭证 | 获取位置 | 用途 |
|---|---|---|
| corpid | 管理后台 → 我的企业 → 企业信息 | 所有企微接口的企业标识 |
| secret | 管理工具 → 会话内容存档 → API 接口 | 换取存档链路专用的 access_token |
| 私钥 | 管理工具 → 会话内容存档 → 密钥 | RSA 解密消息包中的 encrypt_random_key |
corpid 好找,在后台「我的企业」里就有,整段链路的 URL 都要带它。secret 是最容易拿错的一个:会话存档有自己的独立 secret,从「管理工具 → 会话内容存档 → API 接口」页面生成,它跟你自建应用的 app secret 不是同一个,哪怕你把应用的可见范围设得再大,用错 secret 也拉不到存档数据。企微对这个 secret 的权限隔离做得很死,我见过有人在文档里反复核对接口参数,最后才发现是 secret 串了。
密钥这边,后台会同时生成一对 public.pem 和 private.pem。public.pem 是企微侧用来加密的,整个解密过程用不到;真正要保管好的是 private.pem。它要放到生产服务器的受控目录里,千万别提交进 Git 仓库,否则等于把聊天记录的解密权公开了。源码包里默认从环境变量读取私钥路径,部署时单独注入,配置示例里留的是占位符。
注意:如果你们是给第三方客户做对接,走的是服务商模式,那么 access_token 的获取逻辑要换成服务商 token 体系,secret 也不再是客户后台那个。源码先把企业自建应用这条最通用的链路写好,服务商模式在 ArchiveApiClient 里预留了 TokenProvider 接口,换实现类即可,不用动拉取和解密代码。
2.2 授权范围与可信 IP:决定你能拉到谁的消息
凭证齐了不代表能拉到数据。会话存档对「人」的范围有两层限制,第一层是员工授权。企业开启存档后,私聊消息需要员工本人在聊天界面同意存档通知,只有点了同意的成员,他的私聊才会进入拉取流;群聊则以群成员是否同意为准。企微提供 getagreeinfo 接口查询某个成员的同意状态,联调时空数据先调它看一眼,能省很多排查时间。
第二层是管理后台分配的可见范围。管理员在「会话内容存档」页面里设置哪些部门、哪些成员允许被存档查看,不在范围内的员工,即使点击了同意,他们的消息也不会出现在 get_archive_data 的返回里。这两层是「与」的关系,缺一层都拿不到数据。生产环境里员工入职离职频繁,我的习惯是在入职同步流程里顺手把存档授权状态查一遍,离职前把关键会话先拉全,免得人一走,授权一撤,历史窗口期内的数据就再也补不回来。
可信 IP 也要提前配。存档 secret 换取 access_token 时会校验来源 IP,后台未加白名单的出口地址会直接被拒。公司网络如果有多条出口,比如办公室和机房各一个公网 IP,要把所有出口都加进去。本地调试最方便的做法是给开发机配一个固定公网 IP,写入后台白名单,不然每次换网络都要去后台改一遍。
2.3 开发环境与源码包结构:.NET 版本和三个核心类
源码基于 .NET 6+ 写,核心原因是用 RSA.ImportFromPem 可以直接吃 PEM 字符串,不用再手动剥头尾或者转 XML。如果你还在 .NET Framework 4.8 上,老版本的 RSA 不支持直接导入 PEM,源码包里附带了一个 PemToXml 转换方法,把这个坑填上了。第三方依赖只有 Newtonsoft.Json,处理企微返回的嵌套字符串比较省事,数据库访问用 Dapper,其余都是 BCL 自带。
整个源码包按三条职责拆成三个类,后面章节的代码都对应这三个类:
- ArchiveApiClient:access_token 缓存、get_archive_data 拉取、get_media_data 下载;
- ArchiveCrypto:私钥加载、RSA 解随机密钥、AES 解消息体;
- ArchiveRepo:原始消息与明文消息的落库、游标存储。
配置集中在 AppSettings.cs,包括 corpid、secret、私钥路径、数据库连接串。拿到源码第一件事是把这个文件里的占位符换掉,然后跑一遍第 6 章的冒烟用例,再谈改业务。这三个类之间没有循环依赖,ArchiveApiClient 只依赖 TokenProvider 接口,换服务商模式时不用动 ArchiveCrypto,这是我拆过几个企微项目后固定的拆分方式。
3. 核心链路:拉取、RSA 解密、AES 解密的 C# 落地
前置条件配好之后,真正的主干就是一条管道:定时持 seq 去拉数据,拉回来先 RSA 后 AES,解完再按消息类型反序列化。这一章把三段的代码贴出来,配合参数说明,源码包里的 ArchiveApiClient 和 ArchiveCrypto 就是按这三段实现的。
3.1 get_archive_data 请求参数与 seq 游标语义
拉取接口路径是 /cgi-bin/msgaudit/get_archive_data,GET 请求,三个参数:
| 参数 | 说明 |
|---|---|
| access_token | 用存档 secret 换取,有效期 7200 秒,必须缓存 |
| seq | 游标,从服务端返回的 next_seq 取,初始为 0 |
| limit | 单次最大 1000 条,取小也能用,但会放大请求次数 |
核心代码:
public class ArchiveApiClient { private readonly AccessTokenCache _tokenCache; public async Task<(List<ArchiveRecord> Records, long NextSeq)> PullAsync(long seq, int limit = 1000) { string token = await _tokenCache.GetAsync(); // 走缓存,不要每次现调 gettoken string url = $"https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_archive_data" + $"?access_token={token}&seq={seq}&limit={limit}"; using var http = new HttpClient(); string json = await http.GetStringAsync(url); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; if (root.GetProperty("errcode").GetInt32() != 0) { throw new WeComApiException( root.GetProperty("errcode").GetInt32(), root.GetProperty("errmsg").GetString()); } var records = new List<ArchiveRecord>(); foreach (var item in root.GetProperty("data").EnumerateArray()) { records.Add(new ArchiveRecord { Seq = item.GetProperty("seq").GetInt64(), MsgId = item.GetProperty("msgid").GetString(), Encrypt = item.GetProperty("encrypt").GetString(), EncryptRandomKey = item.GetProperty("encrypt_random_key").GetString() }); } return (records, root.GetProperty("next_seq").GetInt64()); } }response 主体里,data 是一个数组,数组里每个元素就是一条消息包,包含 seq、msgid、encrypt、encrypt_random_key 四个字段;next_seq 是下一次请求要用的游标。seq 本身不保证连续,别拿它当业务序号,它只是企微侧的数据定位游标。
access_token 这里强调一下:存档接口对 token 的获取频率有限制,每次拉消息都去调 gettoken 必被限流。源码包里 AccessTokenCache 用双重锁缓存,默认 7000 秒刷新一次,同时处理了并发下多个请求同时过期的情况。token 失效会返回 40014 之类的错误码,缓存要主动清掉再重试一次,不能把异常直接抛给业务。
3.2 C# 实现双层解密:从 PEM 私钥到明文 JSON
消息包里的两个加密字段分工不同:encrypt_random_key 是 RSA 加密的随机密钥,encrypt 是用这个随机密钥做 AES-256-CBC 加密的消息体。解密顺序必须严格:先用私钥 RSA 解出随机密钥,再用随机密钥解消息体。代码如下:
public class ArchiveCrypto { private readonly RSA _rsa; public ArchiveCrypto(string pemPrivateKey) { _rsa = RSA.Create(); _rsa.ImportFromPem(pemPrivateKey); // .NET 6+ 直接吃 PEM } public string DecryptRecord(ArchiveRecord record) { // 第一步:RSA/ECB/PKCS1 解出随机密钥,企微用的是 PKCS1,不是 OAEP byte[] rsaOut = _rsa.Decrypt( Convert.FromBase64String(record.EncryptRandomKey), RSAEncryptionPadding.Pkcs1); string randomKeyText = Encoding.UTF8.GetString(rsaOut); byte[] aesKey = randomKeyText.Length == 64 ? Convert.FromHexString(randomKeyText) // 64 位 hex 字符串 : rsaOut; // 部分环境直接给 32 字节裸密钥 // 第二步:AES-256-CBC,IV 取 AES key 前 16 字节,PKCS7 填充 using var aes = Aes.Create(); aes.Key = aesKey; aes.IV = aesKey.Take(16).ToArray(); aes.Mode = CipherMode.CBC; aes.Padding = PaddingMode.PKCS7; using var cipher = new MemoryStream(Convert.FromBase64String(record.Encrypt)); using var transform = aes.CreateDecryptor(); using var plain = new CryptoStream(cipher, transform, CryptoStreamMode.Read); using var reader = new StreamReader(plain, Encoding.UTF8); return reader.ReadToEnd(); } }RSA 解密这一步最常见的问题是填充模式。C# 里 RSA.Decrypt 支持 PKCS1 或 OAEP,企微存档这边用的是 PKCS1,选错会直接抛 CryptographicException,提示信息还特别隐晦。AES 侧 IV 不是另外传的,就取密钥前 16 字节,这是企微系加密的固定约定,跟一般 AES 用法不一样,照常规写法会踩坑。
私钥加载用 ImportFromPem 的前提是传给它的字符串必须是完整的 PEM 内容,包括 BEGIN/END 行。从配置文件读出来时记得 Trim(),结尾多一个换行符在某些平台上也会导致导入失败。源码包里 ArchiveCrypto 对 hex 和裸字节两种随机密钥都做了探测,主要是有客户从 Java 版 SDK 迁过来,两边对 random_key 的处理口径不完全一致。
3.3 消息模型映射:msgtype 决定 content 的嵌套结构
解密出来的明文直接是一段 JSON,顶层字段是固定的:
{ "msgid": "1234567890_abcdef", "action": "send", "from": "zhangsan", "tolist": ["lisi"], "roomid": "", "msgtime": 1712345678, "msgtype": "text", "content": "{\"content\":\"你好,这条消息来自存档测试\"}" }对应 C# 模型:
public class ArchiveMessage { public string MsgId { get; set; } public string Action { get; set; } // send / recv 等动作类型 public string From { get; set; } public List<string> Tolist { get; set; } public string RoomId { get; set; } // 空字符串表示私聊 public long MsgTime { get; set; } // Unix 秒 public string MsgType { get; set; } public string Content { get; set; } // 注意:这里是嵌套 JSON 字符串 }最容易理解错的是 content 字段。它不是直接的内容字符串,而是一段按 msgtype 区分的嵌套 JSON。比如 text 类型解出来是 {"content":"..."},image 类型解出来是 {"image":{"md5":"...","size":...}},voice、video、file 的结构又各自不同。所以反序列化要分两层:先读顶层拿 msgtype,再按 msgtype 把 content 字符串反序列化成对应类型。
源码包里按 text、image、voice、video、file、emoji、link 等常见类型各建了模型,新增类型不需要改拉取和解密代码,只加一个解析分支即可。msgtime 是 Unix 秒,落库时直接存整数,展示层再转本地时间;不要在入库前转成字符串,排序和范围查询都不方便。
4. 增量轮询与存储:把数据窗口用满,别在半夜补拉
会话存档的数据在企微侧保留时间有限,官方文档口径在不同版本里不完全一致,有说三天的,也有更长的说法,但共识是:不能囤到第二天再批量补拉。生产环境里我一般把轮询周期设在 30 秒到 5 分钟之间,理由很简单,拉得越勤,单批数据越小,解密失败重试的范围也越小。这一章讲轮询怎么写、落库怎么设计、媒体文件怎么处理。
4.1 轮询调度与游标推进:用 next_seq 而不是自己数
主循环的代码很短,但是游标推进的细节决定数据完整性:
public async Task RunArchiveLoopAsync(CancellationToken ct) { long seq = await _repo.GetLastSeqAsync(); // 从数据库恢复上次游标 while (!ct.IsCancellationRequested) { var (records, nextSeq) = await _api.PullAsync(seq); foreach (var rec in records) { await _repo.SaveRawAsync(rec); // 先落原始密文,保证不丢 try { string plain = _crypto.DecryptRecord(rec); await _repo.SavePlainAsync(rec.Seq, plain); } catch (Exception ex) { await _repo.MarkFailedAsync(rec.Seq, ex.Message); // 失败进重试表 } } // 游标取服务端返回的 next_seq,不是自己数 records 的最大值 seq = nextSeq > seq ? nextSeq : seq; await Task.Delay(TimeSpan.FromSeconds(30), ct); } }这里有一个很多人写错的点:游标推进必须用服务端返回的 next_seq,而不是自己取这批记录里最大的 seq。企微返回的 seq 本身不连续,中间有跳号,自己数最大值可能往回退,导致同一批消息反复拉取;反过来,如果 next_seq 比 records 最大值更大,不用它又会丢数据。
每批先写原始密文再解密,是血泪经验换来的顺序。解密是整条链路里唯一可能失败的环节,如果先解密后落库,一条坏消息会让后面的消息全部卡住;先落原始密文,解密失败只影响单条,后续可以用后台任务补解,原始数据永远在。重试表 MarkFailed 时把异常信息记下来,排查时直接看库就知道哪条、什么错误,不用翻日志。
轮询进程重启是常态。把 seq 存数据库而不是内存,就是为了重启后能接着跑。生产环境如果起了多个实例,一定要加分布式锁,否则两个进程会同时拉同一段 seq,配合上面的游标逻辑会产生重复或断档。我没有用太重的东西,Redis 的 SETNX 加 60 秒过期就够。
4.2 原始表与明文表的双写设计
两张表的 DDL 如下:
CREATE TABLE archive_raw ( seq BIGINT PRIMARY KEY, msgid VARCHAR(64) NOT NULL, encrypt MEDIUMTEXT NOT NULL, encrypt_random_key VARCHAR(512) NOT NULL, pull_time DATETIME NOT NULL, status TINYINT NOT NULL DEFAULT 0, INDEX idx_pull_time (pull_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4; CREATE TABLE archive_msg ( seq BIGINT PRIMARY KEY, msgid VARCHAR(64) NOT NULL, roomid VARCHAR(64) NOT NULL DEFAULT '', sender VARCHAR(64) NOT NULL, receivers TEXT, msgtime INT NOT NULL, msgtype VARCHAR(16) NOT NULL, content_json MEDIUMTEXT, INDEX idx_roomid_msgtime (roomid, msgtime), INDEX idx_sender (sender), INDEX idx_msgtype (msgtype) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;archive_raw 存的是接口返回的原始密文,作用有两个:一是解密失败可重试,二是将来想换业务解析逻辑时原始数据还在,不用回头找企微重新拉。status 字段 0 表示待解密、1 表示成功、2 表示解密失败,后台补解任务只处理 2。
archive_msg 存解密后的明文。receivers 字段直接存 tolist 的 JSON 数组,不要拆成多行,因为消息的接收人数一般是个位数,拆表只会让查询变复杂。content_json 存的是 content 字段的嵌套 JSON 原样保留,业务查询时按 msgtype 解析。
msgtime 建索引时要放在 roomid 后面,这样「按会话查时间线」的审计查询能走索引。如果量级到千万行以上,建议按 msgtime 做 RANGE 分区,每月一个分区,历史分区直接 detach 归档,比 DELETE 省太多事。生产上我一般 raw 表保留 60 天,msg 表按业务要求保留,最常见的是 1 到 3 年。
4.3 媒体文件拉取:sdkfileid 与 md5 校验
图片、语音、视频这类消息,content 里给的是 sdkfileid 而不是文件内容,需要再调一次媒体下载接口。核心代码:
public async Task<byte[]> DownloadMediaAsync(string token, string fileId, byte[] aesKey, CancellationToken ct) { string url = "https://qyapi.weixin.qq.com/cgi-bin/msgaudit/get_media_data" + $"?access_token={token}&fileid={fileId}"; using var http = new HttpClient(); string json = await http.GetStringAsync(url, ct); using var doc = JsonDocument.Parse(json); var root = doc.RootElement; if (root.GetProperty("errcode").GetInt32() != 0) throw new WeComApiException(root.GetProperty("errcode").GetInt32(), root.GetProperty("errmsg").GetString()); // 返回的 data 是 base64,需要再用本条消息的 AES key 解密 byte[] encrypted = Convert.FromBase64String(root.GetProperty("data").GetString()); return AesDecrypt(encrypted, aesKey); }fileid 取的是 content 里对应类型的 sdkfileid 字段,不是其他接口的 media_id,这两个 id 体系完全不相干,混用必错。下载回来的字节流先用本条消息解出的 AES key 解密,再落盘。落盘前做两件事:第一对比 content 里声明的 md5,对不上说明解密过程或传输有问题,直接丢弃并告警;第二是确认文件扩展名和 msgtype 一致,防止拿到内容不对的包还当正常文件入库。
媒体下载没有 limit 参数,下载本身是串行的,大批量会话里面文件消息多的时候,要注意控制并发数。我一般用 SemaphoreSlim 限到 4 到 8 并发,避免把企微接口打限流。文件落盘路径按 roomid 和日期分目录,方便后续冷备。
5. 避坑排查:私钥、解密模板、游标并发三个翻车高发区
这章整理的是拆这个源码包和上线后遇到的高频问题,每条按现象、原因、解决展开。可以说,会话存档 80% 的联调时间都耗在这五个点上,后端代码本身反而没什么复杂的。
5.1 RSA 解密报 Bad Data:PEM 格式或填充模式错了
现象:调用 RSA.Decrypt 时抛 CryptographicException,提示 The data to be decrypted exceeds the maximum for this modulus,或者更笼统的 Bad Data。原因有两种:一是私钥字符串不是完整 PEM,配置文件里少了 BEGIN 行,或者中间被格式化工具折行;二是填充模式选成了 OAEP,而企微存档的随机密钥是用 PKCS1 加密的。解决:私钥从配置文件读出后先 Trim,再确认包含完整的 BEGIN/END 标记;解密统一走 RSAEncryptionPadding.Pkcs1。源码的 ArchiveCrypto 构造里加了密钥格式校验,格式不对会在启动时直接抛出来,而不是等到第一条消息才炸。
5.2 解密结果不是 JSON:别套用回调消息的解密模板
现象:解密成功,但解出来前面有一段乱码,或者结尾多出一截像企业 ID 的字符串,用 JsonSerializer 反序列化直接抛异常。原因:把企业微信回调事件那套解密模板搬过来了。回调消息的明文格式是「16 位随机字节 + 4 位消息长度 + 消息体 + corpid」,存档接口的明文没这层包装,解出来就是纯 JSON。两种密钥体系很像,但格式约定不同。解决:存档链路里 RSA 解出 AES key 后直接解消息体,不做任何前缀剥离。判断标准很朴素:明文以 { 开头就对了,以乱码开头就得检查是不是走错了模板。
5.3 一直拉到空数据:secret、授权范围、可信 IP 逐个查
现象:get_archive_data 返回 errcode 0,但 data 是空数组,next_seq 原地不动,后台明明能看到聊天记录。原因按出现频率排序:secret 用错,用的是自建应用 secret 而不是存档 secret;员工没点同意,或者管理员没把该成员划进可见范围;部署环境的出口 IP 不在可信名单里,gettoken 那步是过了,但拉取被静默过滤。解决:先用 getagreeinfo 查目标成员的授权状态,确认同意后看后台可见范围,最后检查可信 IP。这三个排查点按顺序来,五分钟能定位,不用扒日志。
5.4 消息重复或断档:游标取错和多实例并发
现象:库里同一 msgid 出现多次,或者某段时间的消息怎么都补不回来。原因:游标推进写成了 records 里最大的 seq,没使用服务端返回的 next_seq;或者部署了多个轮询实例又没加锁,两个进程交替拉同一段 seq,把游标拉乱了。解决:游标一律取 next_seq,且只在大于当前 seq 时才更新;多实例部署必须用 Redis SETNX 或数据库互斥锁保证同时只有一个轮询者在跑。源码的 PollService 里默认加了锁逻辑,单机部署直接忽略即可。
5.5 媒体文件乱码:fileid 或随机密钥复用
现象:get_media_data 下载成功,解密后文件打不开,或者能打开但内容和消息对不上。原因:fileid 取了其他接口的 media_id;更隐蔽的一种是 AES key 用错了,实现里图省事复用了上一条消息的 random key。每一条消息包都有自己独立的 encrypt_random_key,文件内容是用这条消息自己的 AES key 加密的,不能跨消息复用。解决:fileid 从本条消息 content 里的 sdkfileid 取;AES key 严格从本条消息的 encrypt_random_key 解出。落盘前用 content 声明的 md5 做校验,对不上直接进失败队列。
6. 验证与进阶:把存档链路做成可巡检的系统
联调阶段最怕的就是「明明代码通了,但没人能证明数据是对的」。我的做法是先写一个 30 行的冒烟用例,把拉取、解密、查询三段一次打通,然后再谈排期。冒烟逻辑很简单:测试账号在企业微信里手动发一条带唯一标记的文本,等一个轮询周期,程序去库里查这条消息。
public async Task<bool> Smoke_Archive_PullDecrypt() { string marker = $"archive-smoke-{Guid.NewGuid():N}"; // 手动步骤:测试账号发送包含 marker 的文本 var client = new ArchiveApiClient(...); var crypto = new ArchiveCrypto(pemPath); long seq = 0; bool found = false; for (int i = 0; i < 20 && !found; i++) { var (records, nextSeq) = await client.PullAsync(seq); foreach (var r in records) { string plain = crypto.DecryptRecord(r); if (plain.Contains(marker)) found = true; } seq = nextSeq; await Task.Delay(TimeSpan.FromSeconds(10)); } return found; }这个用例跑通,基本可以证明 corpid、secret、私钥、授权范围、可信 IP 全部正确。之后每轮轮询我都会额外统计三个指标:本轮拉取条数、解密失败条数、入库耗时。解密失败率超过 0.1% 或者连续五轮空数据但后台有新消息,就通过企业微信群机器人 webhook 推告警。告警脚本就是往 webhook 地址 POST 一个 JSON,几行代码的事,但能把「数据丢了几天没人发现」这种事故提前拦住。
审计查询是这套系统的最终出口,SQL 很简单,关键是索引要跟上:
SELECT m.sender, u.name AS sender_name, m.msgtime, m.msgtype, m.content_json FROM archive_msg m LEFT JOIN wecom_user u ON u.userid = m.sender WHERE m.roomid = :roomId AND m.msgtime BETWEEN :begin AND :end ORDER BY m.msgtime;保留策略上,raw 表 60 天一清,msg 表按业务年限做分区归档。从那以后,我每次接企微存档这类带密钥体系的项目,都强制先跑一遍冒烟用例,把「拉→解→查」打通再排期,否则联调阶段每个人都以为问题出在别人那边。这个习惯救过我至少三次,希望帮到你。
本文还有配套的精品资源,点击获取