本文仅对部分文件做一个简单分析,不对正确性负责, 分析用到的文件来源于网络, 可能为企微sqlite数据库,也可能不是,大家看个乐就好,如有雷同,纯属巧合。
重点是学习其中的巧妙设计。
目录
-
一、总览
-
二、跨库关联的核心标识符
-
三、各库表字段定义
-
3.1 user.db --- 通讯录 / 联系人
-
3.2 company.db --- 企业 / 应用
-
3.3 crm.db --- 客户 / 教育 / 侧边栏
-
3.4 govern.db --- 政务网格
-
3.5 session.db --- 会话
-
3.6 message.db --- 消息正文
-
3.7 message_lookup.db --- 消息索引
-
3.8 file.db --- 文件
-
3.9 forever_store.db --- 收藏 / 待办 / 公告 / 表情
-
3.10 tencent_doc_list.db --- 腾讯/微信文档
-
3.11 kv.db --- 通用 KV / 探针
-
-
四、跨库 ER 图(按类别)
-
4.1 组织与人员域
-
4.2 会话与消息域
-
4.3 客户与 CRM 域
-
4.4 文件 / 收藏 / 待办域
-
4.5 文档域
-
-
五、巧妙的设计
一、总览
这 11 个库是企业微信 PC 客户端的本地缓存/落盘数据,按业务域拆分到不同的 SQLite 文件中。整体可分为五大业务域:
| 数据库 | 业务域 | 职责(一句话) | 表数 |
|---|---|---|---|
user.db |
组织/人员 | 通讯录:内部成员、部门、外部联系人、微信好友、标签 | 39 |
company.db |
组织/人员 | 企业信息(自有/外部企业)、应用、关键词拦截规则 | 7 |
crm.db |
客户/CRM | 客户管理、快捷回复、教育行业(家长/学生/班级)、会话侧边栏 | 59 |
govern.db |
组织/人员 | 政务版「网格」组织节点 | 3 |
session.db |
会话/消息 | 会话列表、会话成员、草稿、未读、置顶、@提醒 | 41 |
message.db |
会话/消息 | 消息正文(主表 + 客服 + 小表 + 待删 + 撤回) | 27 |
message_lookup.db |
会话/消息 | 消息轻量索引(按会话/发送者快速定位) | 2 |
file.db |
文件 | 消息中的文件元数据、下载断点、本地加密缓存 | 5 |
forever_store.db |
文件/协同 | 收藏、待办、公告、自定义表情(不随漫游清理) | 18 |
tencent_doc_list.db |
文档 | 腾讯/微信文档「我创建的/最近浏览」列表 | 5 |
kv.db |
基础设施 | 通用键值、性能探针上报缓存 | 2 |
设计共性:
- 大量「主键 + protobuf blob」表(字段名常为
*_pb/pb_content/raw_pb/content/data),结构化字段只抽出用于检索/排序的少数几列,完整对象序列化进 blob。- 多个库各自带一张
general_kv(key,value)表,存本库范围内的杂项配置/游标。- 表名常带版本后缀(
_v2/_v4/_v9/_v11/V50),是客户端迭代时的 schema 版本演进痕迹。
二、跨库关联的核心标识符
这些 ID 在多个库间充当「外键」(SQLite 未声明物理外键,靠应用层 JOIN):
| 标识符 | 类型 | 含义 | 出现位置(库.表.列) |
|---|---|---|---|
| vid / user_id | int64(如 1688850851994195) |
用户唯一号(企微内部 ID) | user.user_table.id;session.conversation_user_table.user_id;message..sender_id;crm. 各 *_id |
| conversation_id | text,前缀区分类型 | 会话文本 ID。S: 单聊、R: 群聊、G:/C: 其他 |
session.conversation_table.id;message.message_table.conversation_id;file.file_table4.conversation_id |
| con_numeric_id | int | 会话数字 ID(自增),与 conversation_id 一一对应 |
session.conversation_table.con_numeric_id;message_lookup.*.con_numeric_id |
| server_id | int64 | 消息服务端 ID(跨设备稳定) | message..server_id;message_lookup..server_id;file.file_table4.server_id |
| message_id | int | 消息本地自增 ID | message.message_table.message_id;message_lookup.*.message_id;file.file_table4.message_id |
| corp_id / corpany_id | int | 企业 ID | user.user_table.corp_id;company.external_company_table_v2.corpany_id |
| department_id / party_id | int64 | 部门 ID(party 为教育版「班级/组织节点」) | user.department_tableV2.id;user.user_dept_tableV2.department_id;crm.party_table_v9.party_id |
| customer_id | int | 客户 ID(对应某个 external user 的 vid) | crm.customer_info_table_v4.customer_id |
| collection_id | int | 收藏条目 ID | forever_store.collectionV50.id;file.file_table4.collection_id |
| group_id / label_id | int | 外部联系人分组 / 标签 ID | user.external_usergroup*;crm.customer_labelV2.id |
关键映射链:
复制代码隐藏代码`用户: user_table.id (vid)
├── user_dept_tableV2.user_id ── department_id ─→ department_tableV2.id
├── external_user_relation_v3.user_id (我加的外部联系人)
└── corp_id ─→ company.external_company_table_v2.corpany_id
会话: conversation_table.id (text `"S:.."`/`"R:.."`) ⇄ con_numeric_id (int)
│ │
│ message_table.conversation_id(text) │ message_lookup_table.con_numeric_id(int)
└── conversation_user_table.conversation_id ── user_id (vid)
消息: message_table.message_id ⇄ message_lookup_table.message_id ⇄ file_table4.message_id
message_table.server_id ⇄ message_lookup_table.server_id`
三、各库表字段定义
说明:blob/二进制列统一标注为「pb/blob」;
general_kv等纯键值表只列一次说明。
3.1 user.db --- 通讯录 / 联系人
核心表是 user_table(人)、department_tableV2(部门)、user_dept_tableV2(人-部门关系),以及外部联系人/微信好友相关表。
user_table --- 所有已知用户(内部成员 + 外部联系人)的档案
| 列 | 类型 | 说明 |
|---|---|---|
| id | int PK | 用户 vid |
| name / english_name / real_name / verifying_name | text | 名称、英文名、实名、审核中名称 |
| email / create_mail / has_bind_email | text/int | 邮箱、企业邮箱、是否已绑定 |
| mobile / phone / office_phone / internation_code | text | 各类电话、国际区号 |
| number / account | text | 工号、账号 |
| birthday / gender | int | 生日、性别 |
| level / authority / info_level / display_order | int | 等级、权限、信息可见级别、展示顺序 |
| avator_url / workcardimage_url / has_updated_work_card_image | text/int | 头像、工牌图 |
| corp_id | int | 所属企业(→ company.external_company_table_v2.corpany_id) |
| position / address | text | 职务、地址 |
| external_corp_name / external_job | text | 外部联系人显示的企业名/职务 |
| unionid / user_vcode / circle_name / name_status | text/int | 微信 unionid、校验码等 |
| pb_content | blob | 完整用户对象 protobuf |
department_tableV2 --- 部门
| 列 | 类型 | 说明 |
|---|---|---|
| id | int PK | 部门 ID |
| name / englishname | text | 部门名 |
| parent_id | int | 上级部门(树形) |
| pre_id / display_order / sequence | int | 同级前驱、展示序、排序值 |
| partner_count / user_count | int | 合作伙伴数、人数 |
| dual_id / level / circleid / corpany_id / virtual_party | int | 层级、圈子、企业、是否虚拟部门 |
dept_tree_table --- 部门树的「孩子-兄弟」链(id / first_child_id / next_sibling_id)。
user_dept_tableV2 --- 用户与部门多对多关系
| 列 | 类型 | 说明 |
|---|---|---|
| department_id, user_id | int PK(复合) | 部门、用户 |
| job | text | 该部门下的职务 |
| is_main_job / is_border_man / is_virtual | int | 是否主部门/边界人/虚拟 |
| sort / staff_type / circleid / attr2 | int | 排序、成员类型、圈子 |
| user_department_hash | int | 关系哈希 |
| pb_extra | blob | 扩展 pb |
外部联系人 / 微信好友相关:
| 表 | 主键 | 用途 |
|---|---|---|
external_user_relation_v3 |
user_id | 我添加的外部联系人关系:备注 remarks/real_remarks、5 个备注电话、来源类型、企业备注、添加时间等 |
external_apply_v7 |
user_id | 外部联系人申请记录:status、reason、推荐类型、昵称、申请时间 |
external_user_ids / blacklist_external_userids / delete_external_userV1 |
value/user_id | 外部用户 ID 集合 / 黑名单 / 已删除 |
external_user_group_info_table |
group_id | 外部联系人分组(name/create_time) |
external_user_group_relationship_table_v2 |
group_id,user_id | 分组↔用户 |
externaluser_to_labelV1 |
userid,labelid | 外部用户↔标签(labelid → crm.customer_labelV2.id) |
wx_friend |
user_id | 微信好友(nick_name/wx_id/flag) |
wechat_contactV1 |
wxid | 微信联系人(name/avator/is_recommend) |
wechatuser_to_labelV1 |
openid,labelid | 微信用户↔标签 |
wx_open_user_info |
wx_open_id | 微信用户备注信息(备注名 + 5 个备注电话) |
out_contacts2 / out_contacts_kv |
id / key | 外部联系人原始数据缓存(data blob) |
mail_independent_contacts / _kv |
id / key | 邮件独立联系人 |
其他:
| 表 | 主键 | 用途 |
|---|---|---|
colleague |
user_id | 同事关系(create_seq/flag) |
company_chain / company_group4 |
id | 关联企业/企业互联链信息 |
corp_remark_member_new_v2 |
corp_id | 企业维度的成员备注 |
virtual_corp_remark_name_v2 |
corp_name | 虚拟企业备注名 |
user_dept_tableV2 见上 |
||
room_robot |
robotid | 群机器人(roomid/creator/name/head_url/webhook 等) |
inner_fwV2 / my_inner_fwV1 |
userid | 内部客服(kf)账号信息 |
user_source |
userid | 用户来源 |
user_work_time |
userid | 工作时间(content blob) |
user_info_token |
key | 用户信息 token |
star_user_table / important_user_table / vip_contact |
id/userid | 星标 / 重要 / VIP 联系人 |
can_build_index_user_ids / requirement_plaza_contact_userids |
value | 可建索引用户 / 需求广场联系人 |
half_self_attr_table_v1 |
id,fieldname | 自定义字段过滤配置 |
wechat_workmate / general_kv |
key | KV 杂项 |
3.2 company.db --- 企业 / 应用
| 表 | 主键 | 用途 |
|---|---|---|
external_company_table_v2 |
corpany_id | 外部企业详细信息:name/short_name/type/staff_count/logo/认证类型 auth_type/认证执照 auth_licence/认证时间/域名/是否互联网行业/是否海外企业 等约 30 列 |
external_company_info_table |
corpany_id | 外部企业补充信息(corpany_pb blob) |
self_corp_list_table |
corpany_id | 我所属的企业列表(self_corp_info blob) |
application_info |
app_id,business_id,circle_id | 工作台应用信息(pb_content blob,app_open_id 索引) |
application_kv |
key | 应用相关 KV |
hidden_corp_app_info_table |
key | 隐藏的企业应用信息 |
keyword_intercept_rule_table |
rule_id | 敏感词/关键词拦截规则(rule_ver + rule_data blob) |
3.3 crm.db --- 客户 / 教育 / 侧边栏
该库混合了三块功能:客户管理(CRM) 、教育行业组织(家长/学生/班级) 、会话右侧栏 & 快捷回复。
客户管理:
| 表 | 主键 | 用途 |
|---|---|---|
customer_info_table_v4 |
customer_id | 客户主表:follow_id(跟进人)、create/update/shift_time、predecessor_id(前任跟进人)、relatioin_flag、data_version |
customer_tag_info_table_v1 |
(customer_id,tag_type 索引) | 客户标记:备注 remark/desc/mobiles/remark_url/company_remark/follow_id |
customer_status |
id | 客户状态 |
customer_labelV2 |
id | 客户标签定义:name/data_type/label_groupid/label_type/business/service_groupid/sort_order |
customer_used_label |
label_id | 已用标签 |
shared_customer3 |
id | 共享客户(operator_staff_id/sequence) |
staff_table |
staff_id,staff_type | 员工(含状态 staff_state) |
transfer_tabel |
transfer_union_id | 在职/离职继承转移 |
crm_fw_info |
fw_id | 客服/服务窗信息(name/url) |
crm_service_group_v2 + idx_crm_service_group_index_table* |
服务组(含 FTS5 全文索引表组) | |
frq_crm_service_group_index_table |
data_id | 服务组使用频率 |
outer_payment_merch_info |
merch_id | 外部收款商户 |
product_albums / sended_product_albums / product_album_sync_keys |
pic_id,type | 产品图册 |
快捷回复 (个人版 personal / 企业版 corp,及客户专用 crmcustomer*):
| 表族 | 主键 | 用途 |
|---|---|---|
rapid_reply_groups_(corp|personal)_v2 |
group_id | 快捷回复分组 |
rapid_reply_items_(corp|personal)_v2 |
reply_id | 快捷回复条目(group_id 归属) |
rapid_reply_meta_info_v2 |
bussiness_group_id | 元信息 |
rapid_reply_(corp_)?table |
rapid_reply_id | 旧版快捷回复(content 文本直存) |
rapid_reply_sync_keys |
key | 同步游标 |
crm_customer_rapid_reply_* |
同上 | 客户场景专用的整套快捷回复(结构同上) |
教育行业 (家长/学生/班级,多用 _v9 后缀):
| 表 | 主键 | 用途 |
|---|---|---|
party_table_v9 |
party_id | 组织节点(班级/年级,树形 parent_id,pb_content blob) |
student_table_v9 |
student_id | 学生(party_id 班级、student_number、staff_id 关联老师) |
guardian_table_v9 |
guardian_id | 家长(name/mobile/openid/attribute/xid) |
guardian_relation_table_v9 |
student_id,guardian_id | 家长↔学生关系(guardian_type=父/母...) |
teacher_pos_table_v9 |
--- | 教师任职(teacher_id/party_id/subject/real_name) |
organization_node_table_v9 |
staff_id,party_id | 员工↔组织节点 |
subject_table |
--- | 学科 |
homework_draft_table_v2 |
homework_draft_id | 作业草稿 |
会话右侧栏 & banner:
| 表 | 主键 | 用途 |
|---|---|---|
conversation_right_sidebar_table / _status_table |
conversation_id | 会话右侧栏选中项 / 状态(→ session.conversation_table.id) |
*_banner_table*(b2b/personal/common_crop/open_kf/student/right_banner_v5 等十余张) |
right_banner_id | 各场景的运营 banner(item_order + item_pb) |
room_notify_draft / room_welcome_list_first_page_cache_table |
conv_id/key | 群通知草稿 / 群欢迎语缓存 |
labels_kv_table / spm_single_storage |
key/value | KV / 单值存储 |
3.4 govern.db --- 政务网格
| 表 | 主键 | 用途 |
|---|---|---|
grid_node_table_v4 |
grid_id | 政务版「网格」节点:parent_id(树)、grid_name、grid_type、member_contains_me / admin_contains_me(我是否为成员/管理员)、raw_pb |
kv_table / sync_key_table |
key | KV / 同步游标 |
3.5 session.db --- 会话
核心是 conversation_table(会话)与 conversation_user_table(会话成员),其余为会话维度的各种状态表。
conversation_table --- 会话主表(单聊/群聊统一)
| 列 | 类型 | 说明 |
|---|---|---|
| con_numeric_id | int PK 自增 | 会话数字 ID(被 message_lookup 引用) |
| id | text UNIQUE | 会话文本 ID(S: 单聊 / R: 群聊) |
| name | text | 会话名 |
| create_time / modify_time / create_user_id | int | 创建/修改时间、创建者 vid |
| last_message_time / last_message_id | int | 最后一条消息时间/ID |
| is_sticked / is_marked / is_blocked / is_collect | int | 置顶/标记/免打扰/收藏 |
| status / flag / fold_status | int | 状态、标志位、折叠状态 |
| notice_content / notice_user_id / notice_time | blob/int | 群公告内容/发布者/时间 |
| customer_room_type / personal_room_type | int | 客户群 / 个人群类型 |
| from_xid / to_vid / ex_owner | int | 单聊双方、群主 |
| session_id / roomname_remark / personal_room_avatar_url | text | 会话 session、群名备注、群头像 |
| room_info_pb / room_welcome_message / school_room_info / type_content / conv_extra_info_pb / local_member_info | blob | 各类 protobuf 扩展 |
conversation_user_table / increment_conversation_user_table --- 会话成员
| 列 | 类型 | 说明 |
|---|---|---|
| conversation_id, user_id | PK(复合) | 会话(text)、成员 vid |
| join_time / join_scene / invite_user_id | int | 加入时间/场景/邀请人 |
| gag_type / is_admin | int | 禁言类型 / 是否管理员 |
| as_vid / nick_name / is_classroom_nickname | int/text | 群昵称等 |
其他会话状态表(多为 KV/标记型):
| 表 | 主键 | 用途 |
|---|---|---|
conversation_extra_table |
id | 我在该会话的最后发送时间 |
conversation_member_nickname_table |
id 自增 | 群成员昵称(room_id/userid/nickname) |
draft_table_1 / conversation_long_article_draft_table |
conversation_id/key | 草稿 / 长文草稿 |
unread_conversation_table |
conversation_id | 未读游标(begin/current_cursor、unread_count) |
need_report_unread_count_conversation_table / unread_confirm_message_table |
未读上报 / 确认 | |
at_user_message_table |
(conversation_id,message_id 索引) | @我的消息 |
top_message / conversation_*notice* |
key | 置顶消息 / 公告尺寸 |
important_contacts_message_table / new_important_message_table |
重要联系人消息 | |
conversation_illegal_table |
id | 违规消息标记 |
conversation_receipt_mode_* / receipt_mode_count |
key/conv_id | 回执(已读)模式 |
conversation_mini_program_* / _recently_* |
key | 小程序列表 |
chain_room_*(4 张) |
key | 互联群(邀请/升级/时间戳/缓存) |
crm_customer_conversation_relation2 / inner_customer_conversation_relation / outer_customer_conversation_relationV2 / vip_conversation_relation |
conversation_numid/key | 会话↔客户/客服关系(连接 crm 域) |
room_version_2 / need_refresh_room_table / upload_conversation_avatar |
key/room_id | 群版本/刷新/头像上传 |
conversation_tag_*、conversation_confirm_table、meeting_conv_unread_cnt、financial_chat_info、shield_conversation_unread_flag_table、pull_message_finish_conversation、incomplete_conversation_table、delete_announce_table、conversation_new_notice_table、conversation_undone_payment_message_table、control_message |
各异 | 标签/确认/会议未读/理财会话/屏蔽/拉取状态/草稿等状态位 |
3.6 message.db --- 消息正文
多张结构几乎相同的消息表,区别在用途(主表 / 客服 / 小表 / 待删 / 撤回)。
message_table (及 message_small_table、need_deleted_message 结构相同)--- 消息主表
| 列 | 类型 | 说明 |
|---|---|---|
| message_id | int PK 自增 | 本地消息 ID |
| server_id | int64 | 服务端消息 ID |
| sequence | int | 会话内序号 |
| sender_id | int64 | 发送者 vid |
| conversation_id | text | 所属会话(→ session.conversation_table.id) |
| content_type | int | 消息类型(文本/图片/文件/语音/系统消息...) |
| send_time | int | 发送时间 |
| flag | int | 标志位(已读/失败等) |
| content | blob | 消息正文 protobuf |
| extra_content / local_extra_content | blob | 服务端/本地扩展 |
| devinfo / msg_from_devinfo / from_app_id / client_id | int/text | 设备来源、应用来源、客户端去重 ID |
| local_extra_content_translate_info / _time_nlp / _approval_nlp | blob | 本地 NLP:翻译 / 时间识别 / 审批识别 |
kf_message_tableV1 --- 客服(KF)消息,比主表多 receive_id、kf_id(客服账号)。
消息辅助表:
| 表 | 主键 | 用途 |
|---|---|---|
message_read_state_table / message_small_table_read_state_table / need_deleted_message_read_state_table |
message_id | 已读状态(read_state_pb) |
message_client_id |
message_id | 客户端 ID 去重 + 发送失败未通知标志 |
message_revoke_record_table_v2 |
con_nid,appinfo | 撤回记录 |
msg_voice2text |
message_id | 语音转文字结果(voice_id/text/collapse) |
message_appinfo / original_appinfo / message_hash_appinfo_table / message_appinfo_able |
msgid | 消息 appinfo(用于排序/分段定位)及其哈希 |
local_need_delete_messages_conversations_2 / need_deleted_messages_conversation / cancle_upload_message_file_table |
conversation_id/key | 待删除/取消上传 |
conv_read_msg_last_svrid_table |
key | 各会话最后已读 server_id |
history_message_section_table / need_pre_download_message / retry_send_item_kv_table / appinfo_updated_messageid_table |
key/value | 历史分段/预下载/重发/更新标记 |
c2b_payment_status_table / c2b_payment_transferid_table |
key | 支付状态/转账 ID |
e2e_pending_svrid_table / e2ee_contact_creds_info_table |
value/key | 端到端加密待处理/凭据 |
general_kv |
key | KV |
3.7 message_lookup.db --- 消息索引
轻量「索引库」,不存正文,用于按会话/发送者快速定位消息(配合 message.db 使用)。
| 表 | 列 | 说明 |
|---|---|---|
message_lookup_table |
message_id PK, server_id, con_numeric_id, send_time, sequence | 消息 → 会话(数字 ID) 的索引。索引:(con_numeric_id,send_time)、(con_numeric_id,server_id)、server_id |
message_sender_lookup_table |
message_id PK, server_id, con_numeric_id, sender_id, flag | 消息 → 发送者索引。索引:(con_numeric_id,sender_id)、server_id |
注意:这里用的是
con_numeric_id(int),需经session.conversation_table与 message.db 的conversation_id(text)互转。
3.8 file.db --- 文件
| 表 | 主键 | 用途 |
|---|---|---|
file_table4 |
origin,message_id,file_index | 消息附件元数据:message_type/extension_type、server_id/server_type、name/size/md5、sender_id、conversation_id、collection_id(→ 收藏)、url、receive_time、info_extension(blob) |
download_file_point |
file_id | 下载断点(check_point/file_path/last_time) |
local_encrypt_file |
cache_type,cache_key | 本地加密文件缓存(status/journal_data) |
general_kv |
key | KV |
file_table4 索引覆盖 conversation_id / sender_id / server_id / md5 / name / size / receive_time / message_type / extension_type,便于「文件管理」页多维筛选。
3.9 forever_store.db --- 收藏 / 待办 / 公告 / 表情
「永久存储」库------不随消息漫游清理的数据。
收藏:
| 表 | 主键 | 用途 |
|---|---|---|
collectionV50 |
id 自增 | 收藏条目:time/server_id/from_type/content_type/app_info/进度/是否本地删除 |
collection_messageV50 |
id 自增 | 收藏的「合并转发/聊天记录」里的子消息(collection_id 归属、消息内容、发送者中英文名、会话名) |
local_file_connection |
id 自增 | 收藏↔本地文件(collection_id/fileid/md5) |
p2p_collection_info_v1 |
(msg_id,collection_id) | 点对点收藏信息 |
待办(todo,有 4 套:当前/旧/new_todo_id/follower):
| 表 | 主键 | 用途 |
|---|---|---|
todo_v4 / msg_todo_v4 / old_todo_v4 |
id | 待办:content/status/creator_id/message_server_id/remind_timestamp/create_time/todo_rawpb 等 |
todo_follower_v4 / msg_todo_follower_v4 / old_todo_follower_v4 |
todo_id,follower_id | 待办关注人 |
new_todo_id |
value | 待办 ID 游标 |
公告:
| 表 | 主键 | 用途 |
|---|---|---|
announce_table |
id | 群公告:time/subject/summary/sender/is_read/attachment_count/image_url/url/status |
announce_to_msgsvrid |
announceid 自增 | 公告↔消息 server_id |
表情 & 其他:
| 表 | 主键 | 用途 |
|---|---|---|
emotion_group_info |
local_group_id 自增 | 自定义表情分组(group_id/icon) |
emotion_info |
--- | 表情明细(file_id/aes_key/url/md5/尺寸/封面) |
text_emotion_background_group |
groupid | 文字表情背景 |
conversation_tab_pb_table |
id | 会话标签页定义(name/order/tag_pb) |
conversation_tag_relationship_table_v9 |
conversation_id,id | 会话↔标签页关系 |
3.10 tencent_doc_list.db --- 腾讯/微信文档
| 表 | 主键 | 用途 |
|---|---|---|
tencent_doc_my_create_list_v11 |
id,doc_platform_type | 我创建的文档列表:title、type(0文档/1表格/2收集表)、creator_id、modify/open_time、share_code、doc_mail_url、desc、doc_item_pb |
tencent_doc_recent_view_list_v11 |
id,doc_platform_type | 最近浏览文档列表(结构同上) |
tencent_doc_prepare_fast_jump_info_v4 |
pre_fast_id | 快速跳转预备信息(doc_url/cookie/expire_time/is_sharecode) |
tencent_doc_js_write_data_cache_v4 |
js_data_key,doc_platform_type | JS 写入数据缓存 |
wedoc_open_id_v1 |
wwid,openid | 文档场景 wwid↔openid 映射 |
3.11 kv.db --- 通用 KV / 探针
| 表 | 主键 | 用途 |
|---|---|---|
general_kv |
key | 全局 KV(如各种升级标记 HasUpgraded*) |
wcprobe_report_table_1 |
buffer_id | 性能探针上报缓存:scene/begin_time/end_time/cost/error_type/launch_time/buffer 等 |
四、跨库 ER 图(按类别)
SQLite 中未声明物理外键,下列关系为应用层 JOIN 语义。
4.1 组织与人员域
涉及 user.db、company.db、crm.db(教育)、govern.db。以「用户 vid」「部门/组织节点」「企业」为中心。

4.2 会话与消息域
涉及 session.db、message.db、message_lookup.db、file.db、forever_store.db。核心枢纽是 conversation_table(同时持有 text id 与 int con_numeric_id)。

4.3 客户与 CRM 域
涉及 crm.db 与跨库的 user.db(外部用户)、session.db(会话关系)。

4.4 文件 / 收藏 / 待办域
涉及 file.db、forever_store.db,并回连 message.db、session.db。

4.5 文档域
涉及 tencent_doc_list.db,以「文档 id」为中心,creator_id 回连用户。

五、巧妙的设计
1. 按业务域物理分库
做法 :不是一个大 .db 装下所有表,而是拆成 11 个库:消息、会话、通讯录、企业、客户、文件、收藏、文档......
为什么巧妙 :SQLite 的并发粒度是「整个数据库文件 」级别的写锁。把高频写的消息库(message.db)和低频写的通讯录库(user.db)放在一起,收一条消息就会和「刷新通讯录」抢同一把锁。物理分库后:
- 各库有独立的写锁------收消息不阻塞通讯录同步,互不干扰;
- 各库有独立的 page cache------扫消息不会把通讯录的热页挤出缓存;
- 损坏隔离------某个库文件损坏不会带走全部数据;
- 可独立备份/迁移/清理 ------见
forever_store.db(收藏/待办/公告/表情)单独成库,正是因为它们「不随消息清理而删除」,物理隔离后清理消息时根本不需要碰这个库。
证据:11 个
.db文件按域划分;forever_store(永久存储)这个命名本身就点明了「与可清理数据隔离」的意图。
2. 胖表瘦索引:消息正文与索引分库
做法 :message.db 存消息正文(含 content、extra_content 等 blob),另起一个 message_lookup.db,只放两张纯标量的窄表:
复制代码隐藏代码`message_lookup_table(message_id, server_id, con_numeric_id, send_time, sequence)
message_sender_lookup_table(message_id, server_id, con_numeric_id, sender_id, flag)`
为什么巧妙 :IM 最高频的操作是「按会话翻页 」和「按发送者筛选 」。如果直接在胖消息表上建 (conversation_id, send_time) 索引,每次翻页虽然走索引,但回表读到的行带着巨大的 blob,page cache 被正文撑爆。
拆出的 lookup 表每行只有几十字节,索引为 (con_numeric_id, send_time) / (con_numeric_id, server_id) / (con_numeric_id, sender_id):
- 翻页/定位先在小表 里二分定位,拿到
message_id再去正文表精确点查; - 小表整张都能常驻内存,正文按需加载;
- 物理分库进一步保证「翻页扫索引」与「写入新消息正文」不互相锁。
这是经典的covering index / 冷热分离思路做到了物理分库级别。
证据:
message_lookup.db仅 2 表 5 列且全标量;索引全部以con_numeric_id打头。
3. 双 ID:文本主键 + 数字主键
做法:每个会话同时拥有两个 ID------
conversation_table.id:文本,如S:1688850851994195_1688852792312821(业务语义、跨设备稳定);conversation_table.con_numeric_id:本地自增整数。
正文库用文本 conversation_id,索引库用整数 con_numeric_id。
为什么巧妙:
- 整数索引又小又快 :
message_lookup里每条消息都要存一份会话引用,用 int 比存几十字节的文本主键,索引体积和比较成本都大幅下降------而消息表动辄百万行,这个差距被放大百万倍。 - 文本 ID 承载语义且跨端一致 :
S:/R:前缀、双方 vid 直接编码在串里,换设备登录也不变; - 两者在
conversation_table里一一映射,需要时一次 JOIN 互转。
用「对内整数、对外文本」的双 ID,兼得了索引效率 与语义稳定。
证据:
conversation_table同时声明con_numeric_id INTEGER PRIMARY KEY AUTOINCREMENT与id TEXT UNIQUE。
4. 标量列 + protobuf blob 混合存储
做法 :几乎每张业务主表都是「几个抽出来的标量列 + 一个 protobuf blob 」。例如 user_table 抽出 name/mobile/corp_id... 但保留 pb_content;party_table_v9 抽出 party_name/parent_id 但保留 pb_content;快捷回复表只留 group_id 加 raw_pb。
为什么巧妙 :这是「关系型的查询能力 」与「文档型的演进灵活性」的折中:
- 抽出的标量列正是用于索引、排序、JOIN 的那几个字段,保证查询性能;
- 其余字段全塞进 blob------加字段不需要
ALTER TABLE,改一下.proto定义即可,老客户端读到不认识的字段直接忽略。对一个要持续迭代、还要兼容多版本客户端的产品,这避免了无穷无尽的库迁移; - 协议复用:同一份 protobuf 既走网络传输又落本地盘,序列化/反序列化代码一套。
代价是 blob 内容对 SQL 不透明(不能 WHERE blob 里的字段),但凡是需要查询的字段都已经被抽成列了,所以这个代价被刻意规避掉了。
证据:列名模式
pb_content/*_pb/raw_pb/content/item_pb/doc_item_pb遍布所有库。
5. 用「表」做生命周期分区与软删除
做法 :message.db 里有多张结构完全相同 的消息表:message_table(主)、message_small_table、need_deleted_message(待删除);待办也有 todo_v4 / old_todo_v4 / msg_todo_v4 三套同构表。
为什么巧妙 :朴素做法是在主表加一个 status/is_deleted 列然后 WHERE status=... 过滤。但在百万行大表上:
- 软删除标记位会让主索引被已删除行污染,每次查询都要过滤掉它们;
- 删除/恢复要更新大表的行(连带 blob 重写)。
把不同生命周期的数据搬到不同的表:
- 主表
message_table永远只装"活跃"消息,索引干净、查询不带过滤条件; need_deleted_message就是回收站,删除 = 把行移动过去,主表立刻变小;old_todo_v4保留迁移前的旧待办,新逻辑只读todo_v4,需要时再回看旧表。
用「表」而非「列」做分区,让最热的主表始终保持精简。
证据:
need_deleted_message与message_table列定义逐字相同;todo_v4/old_todo_v4同构并存。
6. 已读状态单独成表:对抗写放大
做法 :消息的已读回执不放在消息行里,而是单独一张 message_read_state_table(message_id, read_state_pb),三类消息表各配一张(主表 / small / 待删)。
为什么巧妙 :群里一条消息,每多一个人读,已读状态就要更新一次。若已读状态是消息行的一个字段,更新它就要重写整行 ------而消息行带着 content 大 blob,重写一次几 KB 起步,几百人的群一条消息就是几百次大 blob 重写,写放大惊人。
拆成独立小表后,更新已读只重写 (message_id, read_state_pb) 这么一条窄记录,正文 blob 一个字节都不动。这是针对「高频更新的小字段 vs 低频更新的大字段」的标准拆分。
证据:
message_read_state_table/message_small_table_read_state_table/need_deleted_message_read_state_table三张专表,列只有 message_id + read_state_pb。
7. 主键里编码类型:会话 ID 前缀
做法 :会话文本 ID 用前缀编码类型------S: 单聊、R: 群聊,单聊串里还直接拼了双方 vid(S:vidA_vidB)。单聊和群聊统一存在同一张 conversation_table。
为什么巧妙:
- 省一个类型列、省一次判断:从 ID 本身就能 O(1) 看出会话类型,日志/调试时一眼可读;
- 单聊 ID 可由双方推导 :
S:vidA_vidB让客户端无需查库就能算出与某人单聊的会话 ID,秒开会话; - 多态统一表:单聊/群聊共用一张表和一套消息链路,业务代码不必分叉。
把"少量枚举型元信息"编码进主键字符串,是一种轻量的多态设计。
证据:样本
S:1688850851994195_1688852792312821、R:10707630871388967;message_table 中R:占多数、S:少量。
8. 无处不在的 general_kv:schema 的逃生舱
做法 :user.db、file.db、message.db、kv.db 等几乎每个库都自带一张 general_kv(key TEXT PK, value)。
为什么巧妙 :一个成熟客户端有成百上千个零散状态:「是否已执行过某次升级」(HasUpgradedCustomerConversationUnreadTuples)、「AI 机器人 ID 列表」(kAIBotUserIdListKey)、各种功能开关和一次性标记。
为每一个都建表/加列既臃肿又频繁触发迁移。general_kv 提供了一个schema-less 的扩展点:
- 新增一个标记 = 写一个新 key,零 DDL、零迁移;
- 每个库各自带一张,让 KV 落在与其语义相关的库里(升级标记跟着对应业务库走),而不是全塞进一个全局 KV 造成锁竞争。
这是在强 schema 的关系库里,给"不值得建表的小数据"留的一条逃生通道。
证据:多库均有
general_kv;样本 keyHasUpgradedCustomerConversationUnreadTuples、kAIBotUserIdListKey。
9. 增量同步:sync_key 游标与增量表
做法 :多处出现 sync_key_table / *_sync_keys(如 rapid_reply_sync_keys、crm_customer_rapid_reply_sync_keys、product_album_sync_keys),以及成对的全量表 + 增量表(conversation_user_table vs increment_conversation_user_table)。
为什么巧妙 :客户端不可能每次启动都全量拉取通讯录/会话/快捷回复。增量同步协议需要在本地持久化一个「我同步到哪了」的游标:
sync_key存服务端给的游标,下次只请求「比这个游标更新的变更」,省流量、省时间;- 增量表先承接本次拉到的变更,再合并进全量表------合并过程出错也不会污染全量数据,具备一定的事务/回滚友好性。
这是把「服务端增量推送」落地到本地的标准骨架。
证据:
govern.db的sync_key_table;crm 的多张*_sync_keys;session 的increment_conversation_user_table。
10. 版本后缀 + IF NOT EXISTS:可灰度的 schema 演进
做法 :表名普遍带版本号------customer_info_table_v4、party_table_v9、tencent_doc_my_create_list_v11、collectionV50;建表语句一律 CREATE TABLE IF NOT EXISTS。
为什么巧妙 :当一张表的结构需要重大改动(不是简单加列),与其原地 ALTER 冒险迁移,不如:
- 建一张新版本表 (
_v11),新代码写新表,必要时从旧表迁移数据; - 旧表可保留一段时间用于回滚 或懒迁移(用户用到时才搬);
IF NOT EXISTS让初始化幂等------无论全新安装还是升级覆盖,跑同一段建表脚本都安全,不会因表已存在而报错。
版本号 + 幂等建表,让 schema 演进可灰度、可回滚,而不是一次性的"惊险大迁移"。
证据:
_v2/_v4/_v9/_v11/V50后缀广泛存在;所有CREATE TABLE IF NOT EXISTS。
11. 左孩子右兄弟:部门树的双表示
做法:部门同时有两种表示------
department_tableV2.parent_id:每个部门指向父节点(自底向上);dept_tree_table(id, first_child_id, next_sibling_id):左孩子右兄弟链表(自顶向下)。
为什么巧妙:两种遍历方向各有最优结构:
- 要「展开某部门下的子部门 」(组织架构树 UI 最常见操作):用
parent_id得WHERE parent_id=?再排序;而first_child_id+next_sibling_id能顺着链表一路拿到全部直接子节点,天然有序、无需排序、无需扫描,O(子节点数); - 要「往上找祖先 」:用
parent_id逐级上跳。
用一份冗余的「孩子-兄弟」链表换取展开操作的高效,是 N 叉树存储的经典技巧。
证据:
dept_tree_table(first_child_id, next_sibling_id)与department_tableV2.parent_id并存。
12. client_id 去重:弱网下的幂等
做法 :消息表带 client_id,并专设 message_client_id(message_id, client_id, send_failed_unnotified) 表,client_id 建索引。
为什么巧妙:弱网下「发消息」极易出现:客户端发出 → 网络超时未收到 ACK → 重试 → 实际上服务端两条都收了。若无去重,用户会看到重复消息。
client_id 是客户端在发送前生成的唯一标识:
- 服务端/客户端以
client_id做幂等键,重试不会产生重复; send_failed_unnotified标记「发送失败但还没提示用户」,支撑失败重发与 UI 提示的解耦。
这是分布式系统里「生产者侧幂等」在 IM 客户端的落地。
证据:
message_client_id表 +client_id_index_索引;多张消息表均含client_id列。
13. 本地 vs 服务端扩展字段分离 + NLP 结果缓存
做法 :消息表把扩展内容分成 extra_content(服务端下发)与 local_extra_content(本地生成),并进一步细分出三列本地 NLP 结果:
复制代码隐藏代码`local_extra_content_translate_info `-- 翻译`
local_extra_content_time_nlp `-- 时间识别("明天下午3点"→日程)`
local_extra_content_approval_nlp `-- 审批识别
为什么巧妙:
- 来源分离:服务端扩展与本地计算结果各占一列,同步时服务端数据不会覆盖本地算出来的东西,反之亦然;
- 缓存昂贵计算 :翻译、时间抽取、审批意图识别都不便宜(可能要调模型/服务)。把结果落库缓存,同一条消息只算一次,重开会话直接读缓存,避免每次渲染都重算。
把「计算结果」也当作一等数据持久化,是用存储换计算的典型权衡。
证据:
message_table末尾三列local_extra_content_translate_info / _time_nlp / _approval_nlp;另有msg_voice2text缓存语音转文字。
14. 复用 SQLite 原生 FTS5 做全文搜索
做法 :crm.db 里给「服务组」建了全文索引,出现一组影子表:
复制代码隐藏代码idx_crm_service_group_index_table` (fts5 虚拟表, tokenize=fts5word)
idx_crm_service_group_index_table_config
idx_crm_service_group_index_table_content
idx_crm_service_group_index_table_data
idx_crm_service_group_index_table_docsize
idx_crm_service_group_index_table_idx`
为什么巧妙 :搜索需求(按关键词找客户/服务组)不必引入额外的搜索引擎或自己写分词倒排------直接用 SQLite 内置的 FTS5 扩展。那一组 _config/_content/_data/_docsize/_idx 是 FTS5 自动维护的内部表,tokenize=fts5word 还指定了适配中文场景的分词器。
零额外依赖地拿到全文检索能力,是"把数据库已有能力吃干榨净"的体现。
证据:
... using fts5(content, ..., tokenize=fts5word)及其 5 张配套影子表。
15. 收藏的二级结构与去重复用
做法 :收藏在 forever_store.db 里是两级结构------
collectionV50:一条收藏(可以是单条消息,也可以是一整段「合并转发的聊天记录」);collection_messageV50:该段聊天记录里的每条子消息 (collection_id归属,含发送者中英文名、会话名快照);local_file_connection(collection_id, fileid, md5):把收藏与本地已下载文件关联。
为什么巧妙:
- 支持复合收藏:一条收藏天然可以是「一串聊天记录」,靠主从两表表达"一对多",而不是把整段记录塞进一个字段;
- 快照冗余:子消息存了当时的发送者名、会话名------即便原会话/成员后来变了,收藏里看到的仍是收藏那一刻的样子(收藏本就该是"凝固的历史");
- 文件去重复用 :
local_file_connection用md5+fileid把收藏指向已下载的本地文件,避免「收藏一个文件就重新下一份」。
证据:
collectionV50+collection_messageV50(带 chiness_name/english_name/conversation_name 快照列)+local_file_connection(md5)。
总结:贯穿全局的几条原则
把上面 15 点抽象一下,能看到几条反复出现的设计哲学:
| 原则 | 体现 |
|---|---|
| 冷热/读写分离 | 分库(§1)、正文与索引分库(§2)、已读状态拆表(§6)、热小字段独立(§13) |
| 对内高效、对外稳定 | 双 ID(§3)、整数索引 + 文本语义(§3)、主键编码类型(§7) |
| 关系型 × 文档型折中 | 标量列 + protobuf blob(§4)------可查的抽成列,可变的塞进 blob |
| 用结构换性能 | 表分区做软删除(§5)、左孩子右兄弟树(§11)、计算结果落库(§13) |
| 为演进而设计 | 版本后缀 + 幂等建表(§10)、KV 逃生舱(§8)、增量同步游标(§9) |
| 分布式正确性 | client_id 幂等去重(§12)、增量表合并(§9) |
| 吃干榨净已有能力 | 复用 SQLite FTS5(§14)、一套 protobuf 走网络又落盘(§4) |
核心 takeaway:不要让一张大表既要查得快、又要改得勤、还要长得稳。这套 schema 反复在做的,就是把互相冲突的诉求拆到不同的表、不同的库、不同的字段里,让每一部分都能针对自己的访问模式做到最优。