
简介这是面向使用C#语言进行企业级应用开发的程序员的一套会话内容存档接口调用源码适用场景包括内部系统合规留存员工聊天记录、审计监管以及服务质量分析。资源包共81个文件以49个C#源码文件为主体同时包含动态链接库、配置文件、资源文件以及解决方案工程文件等压缩后整体大小约57.1MB。内容覆盖企业微信API调用、身份验证与权限管理、定时任务同步聊天记录、数据落库或落盘方案并给出异常处理与日志记录的可复用策略源码还针对文本、图片、语音、视频、文件等常见消息类型提供了对应的解析类方便理解和使用。配套的VC运行库一键安装包可帮助开发者在缺少运行环境下快速完成部署减少环境问题带来的调试成本。当前已有209人浏览学习适合具备C#基础并需要快速落地会话存档功能的开发人员。1. 企业微信会话内容存档用C#接为什么值得投入员工用企业微信和客户谈好的方案、报价、转账记录出纠纷时往往拿不出可信证据企业微信会话内容存档接口解决的就是这件事管理员在后台圈定存档范围范围内产生的内部单聊、内部群聊、外部单聊、外部群聊消息系统会自动加密推给企业自己的服务器企业解密后落盘、检索、审计都自控。C#调用接口源码要做的事情并不神秘就是把这个接口链路压缩成“取 token、拉消息流、RSAAES 解密、归档”四段代码。这篇主要聊 .NET 技术栈怎么把这套链路跑稳给可以直接改造的代码块并把最容易卡人的前置权限、私钥格式和 seq 游标问题提前标出来。2. 先吃透会话存档接口调用链路与双层加密2.1 上手前必须确认的三个前置条件会话存档接口和企微普通的审批、通讯录接口很不一样很多人在接入第一天就翻车往往不是因为代码而是前置条件漏了。我归纳下来有三个必须确认的。其一会话存档权限不是买了就能调。管理后台需要企业主体完成认证后才有会话存档的申请入口入口一般放在“安全与管理-会话存档”这类位置不同管理后台版本界面会有差异。开通时需要搭一个自建应用、拿到这个应用对应的 corpsecret还要在页面里手工圈选需要存档的成员或部门。这里有两个容易忽略的细节存档范围控制的是“消息覆盖范围”不在范围内的成员产生的消息不会进入接口消息流后期扩容了成员接口也不会把扩容之前的存量会话补推给你。其二加密密钥走的是“企业自己生成公钥、私钥自留”的模式。后台会让你提交一个 RSA 公钥企微服务端用这把公钥去加密数据中的随机密钥和消息体而私钥必须由企业自己保管。我一般建议私钥生成后直接放进密钥管理系统别散落在开发机或者 Git 仓库里。私钥一旦丢了历史消息永远解不开没有后悔药。其三网络层面要保证服务器出网稳定并且在管理后台配置可信 IP。企微接口的调用来源 IP 白名单并不是强制的但很多企业管理严格把白名单开着后如果公司出口 IP 会漂移接口会直接拒绝请求。开发阶段可以先不配白名单上线前再来收紧。判断前置条件是否就绪我习惯先跑一个只调 gettoken 的小程序能拿到 access_token 就说明密钥、IP、网络链路都通然后再去拉消息和解密。先验证 token、再验证数据这个顺序能把多个变量拆开排查。2.2 消息为什么是双层加密解密链路怎么串很多人会以为“会话内容存档”就是后台给明文但真实的接口设计里做的是双层加密这是有明确理由的。企微作为合规数据交换的中转方把消息交给企业前先用企业上传的 RSA 公钥加密了一个随机的 AES 密钥这个加密结果就是返回字段里的 encrypt_random_key然后再拿这个 AES 密钥对真正的聊天消息 JSON 做 AES 加密得到 encrypt_chat_msg。企业拿到后先用自己的 RSA 私钥解出 AES 密钥明文再用这个 AES 密钥去解 encrypt_chat_msg才得到原始消息结构。为什么不干脆全用 RSA因为聊天消息的体量太大逐条做非对称加密性能开销远高于对称加密。架构上的选择是把“密钥传输”和“数据加密”分开非对称只用来保护那一把对称密钥。这条链路落实到 C# 就是三块代码串联获取 access_token按 seq 拉取 chatdata对每条消息做“RSA 解密钥、AES 解消息”。顺序不要弄反先解密钥再解消息解出来按 UTF8 拿到 JSON 字符串。我见过有人把这两步搞反拿私钥去直接解 encrypt_chat_msg结果自然是乱码和一串异常。2.3 C#技术选型.NET版本、HttpClient与加密库如果你要写这套 C# 调用源码技术选型上的争议不多但几个边界值得提前说清楚。运行时我建议直接用 .NET 6做 Worker Service 部署成 Windows 服务或 Linux systemd 服务都很顺手。如果维护的是老系统.NET Framework 4.6.1 用 HttpClient 加 System.Security.Cryptography 也能跑通但在非 Windows 服务器上遇到的跨平台问题往往比省下来的一次升级成本还高。网络库层面企微 API 是标准 RESTful JSON用 HttpClient 足够。但要注意一个问题不能在静态类里随手 new HttpClient那样会造成句柄堆积和连接池混乱尤其在这种长时间循环拉消息的后台程序里风险会被放大。常见做法是注册 IHttpClientFactory按服务划分客户端并配置超时。JSON 解析上企微返回的字段名是下划线风格比如 access_token、expires_in、encrypt_random_key。用 System.Text.Json 写强类型模型时每个属性都得挂 JsonPropertyName否则映射出来全是 null。我实际操作时更倾向直接用 JsonDocument 或 JObject字段名原样取少一层模型转换的负担。网上很多案例用 Python 连企微.NET 的实现相对少所以自己对照接口文档写的时候要把字段名看得比模型封装重。加密库直接用 .NET 自带的 RSA 和 Aes 类就够。RSA 加载私钥时要注意不同工具导出的私钥有 PKCS#1 和 PKCS#8 两种格式RSA.ImportFromPem能处理带标签的 PKCS#1/PKCS#8但如果你的密钥是从老代码里拷出来的“裸 Base64”就得自己把-----BEGIN PRIVATE KEY-----的头尾补回去。填充模式用 Pkcs1企微 SDK 明确走的是 RSA_PKCS1_PADDING这一点下面排错还会再讲。3. 用C#实现会话存档调用源码Token、拉取、解密三段式3.1 获取access_token带过期缓存的最小实现任何企微接口程序第一步都是拿 access_token。这个东西的有效期是 7200 秒官方建议过期刷新实际使用上更大头的规则是“全局复用”。如果每个循环都重新去获取接口容易被限流还可能触发企微风控。下面这段是我最常用的 Token 获取类做成一个可以被后台服务注入的单例public class WeComTokenService { private readonly HttpClient _http; private readonly string _corpId; private readonly string _corpSecret; private string _accessToken string.Empty; private long _expireAt; // Unix 秒本地缓存到期时间 public WeComTokenService(HttpClient http, IConfiguration config) { _http http; _corpId config[WeCom:CorpId]!; _corpSecret config[WeCom:CorpSecret]!; } public async Taskstring GetTokenAsync(CancellationToken ct default) { // 还没到过期时间就直接复用缓存减少无效请求 if (_accessToken ! string.Empty DateTimeOffset.UtcNow.ToUnixTimeSeconds() _expireAt) return _accessToken; var url $https://qyapi.weixin.qq.com/cgi-bin/gettoken $?corpid{Uri.EscapeDataString(_corpId)} $corpsecret{Uri.EscapeDataString(_corpSecret)}; using var resp await _http.GetAsync(url, ct); var json await resp.Content.ReadAsStringAsync(ct); using var doc JsonDocument.Parse(json); var root doc.RootElement; if (root.TryGetProperty(errcode, out var errcode) errcode.GetInt32() ! 0) { throw new InvalidOperationException($gettoken失败: {root.GetProperty(errmsg).GetString()}); } _accessToken root.GetProperty(access_token).GetString()!; int expiresIn root.GetProperty(expires_in).GetInt32(); _expireAt DateTimeOffset.UtcNow.ToUnixTimeSeconds() expiresIn - 60; // 提前60秒过期 return _accessToken; } }这段代码的核心是把 token 的过期管理收敛在一个单例里。这里有两个参数值得说明expires_in 是接口返回的存活秒数我给它减了 60 秒作为提前量是因为后台服务循环间隔可能比较长宁可多刷新一次也不要拉消息拉到一半 token 失效corpId 和 corpSecret 则建议从环境变量或配置中心读不要写死在源码里省得代码泄露后整个应用权限被拖走。如果多线程同时触发刷新同一个进程里会有并发请求 gettoken 的情况。这个问题不大多个请求顶多让服务端多算一次只是日志会难看真要严格可以在类里加一个 SemaphoreSlim 把刷新动作串行化。3.2 用seq游标拉取聊天记录别按人按群查会话存档接口不支持“查某个人的聊天记录”它只提供一个全局消息流水getchatdata。调用时必须传入 seq 游标从上一轮的位置继续往下拉服务端按 limit 限制返回条数。有经验的工程师会把这里当成“拉 Kafka 消息”而不是“查 MySQL 表”整个拉取逻辑天然是流式的。public class ChatArchiveFetcher { private readonly HttpClient _http; private readonly WeComTokenService _tokenService; public ChatArchiveFetcher(HttpClient http, WeComTokenService tokenService) { _http http; _tokenService tokenService; } public async TaskListJsonElement FetchOnce(long seq, int limit, int timeType 1, CancellationToken ct default) { string token await _tokenService.GetTokenAsync(ct); var url $https://qyapi.weixin.qq.com/cgi-bin/msgaudit/getchatdata?access_token{token}; var body new { seq, limit, timeType }; using var resp await _http.PostAsJsonAsync(url, body, ct); var json await resp.Content.ReadAsStringAsync(ct); using var doc JsonDocument.Parse(json); var root doc.RootElement; if (root.TryGetProperty(errcode, out var code) code.GetInt32() ! 0) throw new InvalidOperationException($getchatdata失败: {root.GetProperty(errmsg).GetString()}); var list new ListJsonElement(); if (root.TryGetProperty(chatdata, out var chatData)) { foreach (var item in chatData.EnumerateArray()) list.Add(item.Clone()); // clone出来离开using doc后仍可安全使用 } return list; } }这段代码有三个关键点。第一seq 是服务端游标初次拉取从 0 开始拉完一轮必须取这轮最大的 seq 作为下一轮起点不能每次都从 0 拉。第二limit 我一般取 1000如果某次返回不足 1000说明消息已经追平了可以休眠几秒再轮询。第三timeType 表示拉取的会话类型维度常见取值有 1 和 2分别覆盖外部联系人和内部员工这边的会话不同企业开通的存档套餐不同取值能放开到什么程度也不完全一样。我习惯把它做成配置项两个 type 分开拉取再合并去重。需要提醒的是如果项目跑在 .NET Framework 而不是 .NET 6PostAsJsonAsync不一定有这个重载这时候用StringContent手拼 JSON 一样能达到效果只是代码会多几行。3.3 解密核心块RSA解出AES密钥AES解出消息明文解密块是整套源码的核心也是全项目里最可能折腾半天的地方。它做两件事先用 RSA 私钥解密 encrypt_random_key得到一把 AES 对称密钥再用这把密钥去解 encrypt_chat_msg得到消息明文 JSON。我把两步封装成一个工具类public static class WeComMsgDecryptor { /// summary用RSA私钥解出AES密钥再用AES解出消息明文/summary public static string Decrypt(string encryptRandomKey, string encryptChatMsg, string privateKeyPem) { if (string.IsNullOrEmpty(encryptRandomKey) || string.IsNullOrEmpty(encryptChatMsg)) return string.Empty; // 第一步RSA私钥解密 - 得到AES密钥文本 using var rsa RSA.Create(); rsa.ImportFromPem(privateKeyPem); // 支持 PKCS#1/PKCS#8 带标签 PEM var randomKeyBytes rsa.Decrypt( Convert.FromBase64String(encryptRandomKey), RSAEncryptionPadding.Pkcs1); // 第二步把AES密钥文本规整为16字节密钥IV也取这16字节 string aesKeyText Encoding.UTF8.GetString(randomKeyBytes); var key GetFixed16Bytes(aesKeyText); // 第三步AES-256-CBC PKCS7解密消息 using var aes Aes.Create(); aes.Key key; aes.IV key; aes.Mode CipherMode.CBC; aes.Padding PaddingMode.PKCS7; var cipherBytes Convert.FromBase64String(encryptChatMsg); using var decrypter aes.CreateDecryptor(); var plainBytes decrypter.TransformFinalBlock(cipherBytes, 0, cipherBytes.Length); return Encoding.UTF8.GetString(plainBytes); } private static byte[] GetFixed16Bytes(string input) { var bytes Encoding.UTF8.GetBytes(input); var result new byte[16]; Array.Copy(bytes, result, Math.Min(bytes.Length, result.Length)); return result; } }逻辑说明RSA 解密的对象是 Base64 字符串 encrypt_random_key解出来的是一个文本这个文本就是后面 AES 解密用的密钥。我实际遇到过解出来文本长度不足 16 字节的情况所以写了 GetFixed16Bytes 兜底如果文本超过 16 字节只取前 16 字节。AES 的 IV 直接取密钥本身这是企微存档解密链路里常见的一种 AES-CBC 用法如果你参考的是其他语言 SDK确认好 IV 策略后再保持一致。这里排错时要特别留神私钥格式不对代码会在 Decrypt 这一步抛“参数不正确”异常信息不会主动提示你是私钥问题。所以进入排错前先把 privateKeyPem 打到底部日志看完整性和换行再确认 Rsa.Decrypt 的填充是 Pkcs1 而不是 Oaep。填充模式不一致的话报错同样是“参数不正确”。3.4 落盘策略先JSON归档再做结构化查询解密之后拿到的是一段 JSON 字符串如果立刻想把所有字段拆成数据库表你会发现不同消息类型的字段结构差异很大文本、图片、语音、文件、撤回事件的 JSON 完全不是一个形态。我项目里用的是两级存储拉到的原始 JSON 先按日期分目录落盘保留期按企业合规要求配置同时把核心字段抽取到数据库事件表方便检索。落到文件的好处是将来上游字段调整、或者审计要重算某些统计指标时原始 JSON 还在可以重新解析重建相当于给自己留了后悔药。落到数据库的字段不贪多msgid 当唯一键seq 标记位置发件人、接收人列表、时间、消息类型、内容摘要再加一段原始文件路径查询和展示够用就行。真正复杂的内容萃取放到后面做异步任务去处理不要把拉取循环拖慢。这个阶段还容易出现一个误区为了“结构清晰”在拉取线程里直接做复杂的字段映射和入仓导致消费速度远跟不上生产速度。后面第 4 章会进一步讲结构化怎么做才稳。4. 把解密后的消息整理成能查询的存档记录4.1 消息体字段拆解msgtype、from/tolist与roomid解密后的消息 JSON 比想象中规整一条文本消息长这样{ msgid: abc123, action: send, from: { id: zhangsan, type: 1 }, msgtype: text, text: { content: 合同这版你看下 }, sendtime: 1672531200, tolist: [ { id: lisi, type: 1 } ], roomid: wr_xxxxx }msgid 全局唯一去重就靠它action 表示消息动态常见是 send 和 recall撤回消息一般只有 msgid 和撤回标记没有具体内容from 和 tolist 里的 type 字段区分“企业成员 id”和“外部联系人 id”type1 一般是内部员工type2 对应外部联系人这决定了落库时能不能区分我方员工和客户roomid 在群聊场景出现单聊为空。落表时我一般这样设计字段类型说明id自增长整型物理主键msgidvarchar(64)全局消息唯一 ID设唯一索引seqbigint消息在水流中的位置加分页msg_typevarchar(20)text / image / voice / file 等actionvarchar(20)send / recall 等from_uservarchar(64)发送者 idto_usersjson接收者 id 数组room_idvarchar(64)群聊 ID单聊为空send_timeint消息时间戳content_txttext文本内容或摘要raw_jsonlongtext原始 JSON便于重建写入时按 msgid 做唯一索引重复记录直接跳过这样即使拉取重复了也不会污染数据。4.2 媒体消息需要单独走下载接口不要只存JSONimage、voice、video、file 这类媒体消息解密后的 JSON 里没有二进制内容只有一个媒体文件的标识。你需要再调一个下载接口把真实文件取回来分片解密再拼成完整文件。这是很多初接会话存档最容易漏的一步JSON 入库了真到查图片的时候发现本地根本没有文件。我项目里的处理顺序是拿到解密后的明文 JSON判断 msgtype如果是媒体类型提取媒体标识放进下载队列由独立的消费者去调下载接口下载成功后把文件路径回填到数据库记录里。媒体下载接口的设计思路是分片拉取public async Task DownloadMediaAsync(string mediaId, string saveDir, string privateKey, CancellationToken ct) { string token await _tokenService.GetTokenAsync(ct); int nextIndex 0; string savePath Path.Combine(saveDir, ${mediaId}.file); using var fs new FileStream(savePath, FileMode.Create, FileAccess.Write); do { var body new { media_id mediaId, index nextIndex, count 100 }; var url $https://qyapi.weixin.qq.com/cgi-bin/msgaudit/getmediafile?access_token{token}; using var resp await _http.PostAsJsonAsync(url, body, ct); var json await resp.Content.ReadAsStringAsync(ct); using var doc JsonDocument.Parse(json); var root doc.RootElement; if (root.TryGetProperty(data, out var data)) { foreach (var part in data.EnumerateArray()) { string encKey part.GetProperty(encrypt_random_key).GetString()!; string encMsg part.GetProperty(encrypt_chat_msg).GetString()!; string plainText WeComMsgDecryptor.Decrypt(encKey, encMsg, privateKey); // 媒体接口解密出的结果文本一般是Base64编码再解码得到二进制 var rawBytes Convert.FromBase64String(plainText); fs.Write(rawBytes, 0, rawBytes.Length); } } nextIndex root.TryGetProperty(next_index, out var next) ? next.GetInt32() : 0; } while (nextIndex 0); }这段代码的逻辑说明每次循环传 index 和 count拿一批分片逐个解密后写入同一个文件流直到接口返回的 next_index 为 0。这里要注意两件事第一解密出来的结果文本先不要当字符串处理确认它是不是 Base64 编码后再转字节转错一步文件就打不开第二下载动作要尽快执行企微服务端对媒体文件保留有时效拖久了文件可能已经取不回来。4.3 多群并发与断点续传seq要落到持久化存储企业内部群一多单线程顺序拉取很容易追不上增量。这时候我会把架构拆成几段拉取、解密、媒体下载、入库各自独立中间用队列连接每段可以单独扩 worker 数量。最关键的一点是seq 游标必须落到持久化存储不能放在内存变量里。我吃过这个亏。第一次写的时候图省事把当前 seq 放在一个全局变量里凌晨服务器自动重启后第二天早上又从 0 开始拉数据库瞬间涌入几十万条重复记录。之后改成一张单行 meta 表每次拉完一批就把最大 seq 写进去重启之后先读 meta 再开始拉从根上堵住了重复消费。如果你用 Redis也是一样的套路把 seq 存成一个 key拉取成功后 SET 一下。注意不要用 INCR 之类的操作去推 seq直接用绝对值覆盖保证游标只增不减。并发层面多个 worker 各自拉取一批之后写入 meta 表时取所有批次的 max seq 合并。消息落库做不做批处理看量但 msgid 唯一索引一定要有这样即使两个 worker 拉了同一段消息入库阶段也会静默去重不会炸。5. 会话内容存档上线过程中的常见问题排查5.1 拉不到数据或提示无权限先查范围配置现象gettoken 成功但调 getchatdata 返回空数组或者 errode 明确提示没有权限。很多人第一时间怀疑代码其实先该检查后台到底圈选了哪些成员和部门。原因会话存档的生效范围是“消息产生那一刻该成员是否在存档范围内”范围之外的消息根本不会进入接口水流。另一个常被忽略的原因是虽然你有管理员身份但如果这个自建应用本身的“可见范围”没包含目标成员接口一样拒掉你。解决到管理后台把自建应用的可见范围调大或专门建一个仅供 API 使用的服务账号配完整权限。开发阶段先圈几个测试号跑通后再按部门逐步扩容。5.2 decrypt一直失败RSA私钥格式与填充模式现象RSA.Decrypt 抛出“参数不正确”或“填充无效”。原因基本有三个一是私钥 PEM 格式问题。.NET、Java、OpenSSL 互导私钥时常见 PKCS#1 和 PKCS#8 两种标签如果是从 Java 代码里硬拷出来的私钥字符串常带着换行截断或头尾标签丢失。二是填充模式用错企微用的是 RSAPKCS1PADDING对应 C# 的 RSAEncryptionPadding.Pkcs1换成 Oaep 就解不开。三是公钥和私钥不是同一对后台换了公钥但本地私钥没换。解决先把私钥完整打印出来看标签和换行再用 OpenSSL 转成统一的 PKCS#8 格式openssl pkcs8 -topk8 -in rsa_private.pem -out rsa_private_pkcs8.pem -nocrypt转完后再跑一次解密如果还报错重新生成一对密钥并在后台替换公钥确保本地私钥与线上公钥严格配对。5.3 解密后中文乱码或JSON解析失败编码与截断现象解密后能看到内容但不是中文而是乱码字符或者 JSON 解析时报“意外的字符”。原因通常是两步第一字节转字符串时用了错误编码或转换了两次。密文解密后是二进制字节流直接一次性按 UTF8 解码就行中间不要再转一次 Base64。第二加密文在传输或入库时被截断比如数据库字段长度不够、URL 编码把加号转成了空格导致 Base64 解码后长度不齐整。解决解密前先对 encryptChatMsg 做 Trim再去 Base64 解码确认解码后的字节长度是 16 的整数倍不是整数倍就说明传参已经破坏了密文。调试阶段把解密结果先写日志肉眼确认格式正常再交给 JSON 解析器。5.4 消息缺失或重复seq推进时机现象数据库里要么漏消息要么大量重复。原因seq 游标的更新时机不对。正确的顺序是先持久化这批消息里的最大 seq再逐条解密入库如果反过来逐条处理到一半服务挂了这批消息就永久丢失因为 seq 已经提前推进了。反过来如果忘了推进 seq重启后又会从头拉一遍。解决把 seq 的持久化写在拉取完成后、业务处理前中间任何一步失败下次重启都从上一个安全位点继续。重复问题靠 msgid 唯一索引兜底漏消息只能通过回放最近的 seq 备份点补救所以定期备份 meta 表也是上线后要养成的习惯。5.5 access_token频繁失效全局复用与刷新时机现象日志里每隔几分钟就报 40014 或 42001 的 token 无效错误。原因最常见的是多个服务实例各拿了一份 token某个实例刷新后其他实例手里的旧 token 就失效了。企微的 token 是应用维度的全局凭据同应用并发多个活跃 token 必然互相顶掉。解决在服务网关或 Redis 里做一层全局 token 缓存所有 worker 共享同一份或者确保进程内只有一个单例 TokenService 在管理刷新。刷新提前量设置在 60 到 300 秒之间不要卡在 7200 秒的最后几秒才刷新网络抖动一次就会让整个循环断掉。6. 存档系统上线前最值得做的三步验证代码跑通 demo 只是第一步上线前我一般会做三个验证每个都能把藏在边界里的坑翻出来。第一个叫“停机回放验证”。把服务停掉十分钟再启动观察日志里的 seq 是不是从持久化点继续而不是回到 0。能复现出“重启后从正确位置续跑”说明断点续传是可靠的如果发现回到 0说明 meta 表根本没写成功趁早修别等数据量大时再来查。第二个叫“撤回消息联动验证”。找一个测试号在存档范围内先发一条文本再撤回然后查库里是否出现了 recall 动作的消息。会话存档的核心价值就是纠纷追溯如果撤回记录没下来说明 timeType 参数或消息类型映射有缺口需要按 action 字段对一遍。第三个叫“文件完整性抽检”。分别拉图片、语音、文件三种媒体消息下载后打开确认不是 0 字节也不是头尾缺失。媒体下载的解密链路和纯文本 JSON 是两套逻辑没有验证过就上生产大概率会在某个周末被老板翻出一条打不开的聊天带头文件。上线后我还有一组固定指标要盯每小时的拉取量、当前最大 seq、解密失败率、媒体下载失败率。不用做很复杂能看到“某小时突然大量解密失败”就足以触发告警。我自己的习惯是每周翻一次解密失败日志看看是不是又冒出来一种没处理的消息类型比如自定义表情或新的 msgtype。读不出来的消息不要直接丢进死库保留原始密文备查等支持了新类型再补解一次。企微会话存档这套东西权限、密钥、游标三件事想清楚代码本身并不复杂。希望这篇企业微信会话内容存档C#调用接口源码的笔记能帮你避开我踩过的坑把聊天记录稳稳落到自己服务器上。本文还有配套的精品资源点击获取