Compare commits

..
412 Commits
Author SHA1 Message Date
richjohnrambo bc101b3278 fix: query tenant-sharded private messages
Deploy im-server production / deploy (push) Successful in 7m7s
2026-10-21 12:40:40 +08:00
richjohnrambo 39b0392545 feat: add trusted customer service friendships
Deploy im-server production / deploy (push) Successful in 11m38s
2026-10-20 22:24:34 +08:00
Your Name 93a6ff0d03 fix(ci): keep im deploy scoped to service
Deploy im-server production / deploy (push) Successful in 6m19s
2026-10-20 17:56:31 +08:00
richjohnrambo 9c10862a6d fix: recreate Caddy after IM CORS update
Deploy im-server production / deploy (push) Successful in 7m58s
2026-10-19 14:34:13 +08:00
richjohnrambo 5197960e58 fix: remove conflicting production IM CORS header
Deploy im-server production / deploy (push) Successful in 7m23s
2026-10-19 14:24:57 +08:00
richjohnrambo fe5f870b6b ci: print production IM Caddy route
Deploy im-server production / deploy (push) Successful in 7m25s
2026-10-19 14:16:11 +08:00
richjohnrambo 5b270860e7 ci: inspect production IM CORS routing
Deploy im-server production / deploy (push) Successful in 8m9s
2026-10-19 14:06:20 +08:00
Your Name 209f105b12 fix: honor admin internal api toggle
Deploy im-server production / deploy (push) Successful in 4m25s
2026-10-18 20:24:39 +08:00
Your Name 5d49008960 ci: deploy pro with docker compose
Deploy im-server production / deploy (push) Failing after 8m46s
2026-10-18 20:11:07 +08:00
richjohnrambo 73c3dc0541 feat(media): add MinIO storage switch and HTTPS OSS endpoint 2026-10-17 22:35:04 +08:00
richjohnrambo 4c2c35b55b fix: enforce group file upload governance 2026-10-17 17:08:52 +08:00
richjohnrambo 178ecff21f fix(ci): align test CPU request with ArgoCD 2026-10-17 13:15:04 +08:00
richjohnrambo 9132207e21 fix: group moderation and profile sync 2026-10-17 10:12:20 +08:00
Austin ea29fef40f docs: 更新IM接口文档,新增avatar_url字段及乐观锁说明 2026-10-15 14:38:17 +08:00
Austin dbc7c6a3e9 fix(avatar): 租户路径文件 ACL 补齐头像放行条
fileMapper.UserHasAccess 恒经 TenantFileUserHasAccess,共享库多租户
模式走 tenant_adapter 的 SQL;此前第 9 条(头像链接行对登录用户放行)
只加在了基础路径 FileUserHasAccess,导致共享库环境下他人下载头像仍
403。同步补齐,与 FileUserHasAccess 保持一致。
2026-10-15 13:38:39 +08:00
Your Name 3b186298a0 fix: configure Kafka brokers at runtime 2026-10-15 12:26:09 +08:00
Austin ab572a6c36 fix(avatar): 修复群成员拉取他人头像 403 及补充 avatar_url 回填规则 2026-10-14 22:30:13 +08:00
richjohnrambo 441009b81c fix privacy friend verification updates 2026-10-14 17:11:41 +08:00
Austin 297f1fa0b7 docs: 补充注册 username 规则说明
新增服务端 username 强校验规则,包括长度、字符限制、自动生成规则及错误返回详情,明确最小长度来自部署配置、上限为 10。
2026-10-14 16:49:19 +08:00
richjohnrambo 027dda786e fix media downloads through authenticated origin 2026-10-14 14:51:24 +08:00
richjohnrambo 87ec8f9833 fix kafka attachment message id binding 2026-10-14 12:13:45 +08:00
richjohnrambo 6930b23cce fix media download CORS headers 2026-10-14 10:09:59 +08:00
Austin e8804883fc feat: 新增群组成员数量统计功能并在接口中返回
实现了群组成员数量统计的接口,包含数据库层统计逻辑,在群组详情、设置等接口中返回实时成员数,同时修复了测试用例中参数空格问题。
2026-10-13 17:58:05 +08:00
Austin 43808c36d1 fix: 将im_filemsglinks.msgid字段从INT改为BIGINT适配雪花ID
修复msgid为INT类型无法容纳雪花ID的问题,避免插入时数值溢出导致Kafka消费分区停滞
同时新增数据库 schema 校验逻辑,提前发现不符合要求的字段类型
2026-10-13 17:07:41 +08:00
Austin b126b13b77 fix(server): Kafka消息持久化重试耗尽时死信处理避免无限重试
在Kafka消息持久化重试耗尽时,新增将消息死信的逻辑,而非保留未提交的偏移量,避免无法解决的故障导致分区消息无限重试阻塞后续消息。同时补充死信处理的日志,确保问题可追踪。
2026-10-13 15:40:19 +08:00
Austin f3b1a06da5 feat(server): 支持上传文件关联会话话题并做权限校验
新增上传时读取会话话题参数,校验上传者是否为该话题的成员,通过后将文件与话题关联,允许其他成员下载。权限校验仅针对点对点和群组话题,关联失败不会阻止文件上传。
2026-10-13 11:59:36 +08:00
Austin 6be7796049 feat: 为应用版本查询增加appCode参数支持
新增应用版本类型的AppCode字段,修改相关接口和实现以支持通过appCode查询指定应用的最新版本,增加appCode合法性校验,完善缓存键以区分不同应用的版本信息
2026-10-12 14:59:23 +08:00
Austin 0a33d21eb8 feat: 新增禾圃集的法律页面及路由 2026-10-12 09:33:34 +08:00
richjohnrambo 5d792f9f96 fix kafka message persistence completion 2026-10-12 09:19:28 +08:00
richjohnrambo c219ef6455 fix(server): suppress expected websocket close write errors 2026-10-11 22:50:54 +08:00
richjohnrambo 1e0b4c181d fix(server): raise topic publish queue capacity 2026-10-10 09:37:42 +08:00
Austin 54c594f39b docs: 更新清除聊天记录功能的服务端实现说明 2026-10-09 19:54:50 +08:00
Austin 2e1ceec717 docs: 更新系统通知相关接口文档及问题修复记录 2026-10-09 10:00:48 +08:00
Austin 1d8f9e3be4 feat: 为系统通知添加明确的操作结果摘要
为风险挑战确认和好友申请处理场景添加对应操作结果的中文摘要,替代原有的状态文本,让用户能更清晰地理解系统通知内容。
2026-10-08 20:25:28 +08:00
Austin 8228ceb89f fix: 处理撤回消息解密时的500错误
撤回消息会清空content但保留key_version,新增判断跳过此类消息解密,避免整个查询窗口被毒化
2026-10-08 19:43:27 +08:00
richjohnrambo f24133f5de fix(server): coalesce platform evict fanout 2026-10-08 00:23:12 +08:00
richjohnrambo e30b70a3a7 fix(server): limit implicit large group subscription meta 2026-10-07 23:13:47 +08:00
richjohnrambo ac6cd3c6e8 fix(server): shed overloaded group websocket fanout 2026-10-07 21:23:22 +08:00
richjohnrambo 58c15efad5 fix(loadtest): submit observed after quiet timeout 2026-10-07 00:41:54 +08:00
richjohnrambo 2732e05a83 fix(server): harden user cache update queue under load 2026-10-06 23:32:17 +08:00
richjohnrambo d3054f62b5 test(load): add idle and tunable group scenarios 2026-10-05 17:36:18 +08:00
richjohnrambo 965f94f154 fix(kafka): preserve partially fetched message batches 2026-10-04 23:37:10 +08:00
richjohnrambo b05e8f60dd perf(group): reduce websocket fanout serialization 2026-10-04 10:15:18 +08:00
richjohnrambo c46209f4e7 fix(deploy): diagnose stalled statefulset rollouts 2026-10-04 01:06:26 +08:00
richjohnrambo 7b25365ec3 fix(group): bound push pressure and synchronize load agents 2026-10-04 00:49:27 +08:00
richjohnrambo 746f75ab0f fix(kafka): retain failed message persistence 2026-10-03 22:32:08 +08:00
richjohnrambo db92c41241 fix(loadtest): apply direct dial IP assignment 2026-10-03 20:35:31 +08:00
richjohnrambo 80808349fb fix: absorb multiplex close bursts 2026-10-02 15:53:03 +08:00
richjohnrambo a406d63154 fix: stop writes to closed cluster sessions 2026-10-02 00:28:02 +08:00
richjohnrambo d43b1b63d3 fix: preserve cross-pod message order 2026-10-01 19:00:16 +08:00
richjohnrambo dccd0969d8 fix: register cluster kv payloads 2026-10-01 16:08:00 +08:00
richjohnrambo 73a594146f fix: trace delayed websocket acknowledgements 2026-10-01 15:19:57 +08:00
richjohnrambo 19584d0939 fix: harden high-fanout delivery under load 2026-09-30 23:04:54 +08:00
richjohnrambo 2b631ab176 fix: harden websocket heartbeat under load 2026-09-30 17:36:33 +08:00
richjohnrambo b83800c0f3 docs: update legal agreements 2026-09-30 17:04:49 +08:00
richjohnrambo 05b3da55ac fix: align group private chat permission semantics 2026-09-30 16:11:30 +08:00
richjohnrambo 99ae6328ff fix: handle backend issue list regressions 2026-09-30 15:35:03 +08:00
richjohnrambo 7685fb0c02 fix: persist upload original filename 2026-09-30 14:25:44 +08:00
richjohnrambo 097b028a19 docs: record exact 600 regression runs 2026-09-30 11:54:04 +08:00
richjohnrambo dcb131dcea fix: make ai task events realtime only 2026-09-30 11:37:49 +08:00
richjohnrambo d75c6fa0b4 docs: record async push load regression 2026-09-30 10:52:01 +08:00
richjohnrambo 06e08a65e4 fix: handle opaque attachment references 2026-09-30 10:50:42 +08:00
richjohnrambo 9f30995787 fix: move group push work off topic loop 2026-09-30 10:01:59 +08:00
richjohnrambo da8efb6e38 feat: expose websocket delivery pressure metrics 2026-09-29 23:15:29 +08:00
richjohnrambo b1545b6876 fix: keep load connections through verification 2026-09-29 17:52:13 +08:00
richjohnrambo 4b00b5ae37 fix: harden single-machine load handling 2026-09-29 17:52:13 +08:00
Austin ab73b69870 feat: 新增修改密码和注销账号功能
新增修改密码和注销账号的API接口,实现用户通过验证旧密码修改密码,以及通过短信验证流程注销账号的功能,同时补充相关常量和文档说明。
2026-09-29 16:13:32 +08:00
Austin cf9d559944 fix: 修复资源不存在/已解散返回错误码及网关502问题
修复好友申请不存在时返回正确的 ErrNotFound 避免解引用 panic,新增通用函数判断群不存在或已解散并返回对应错误码,更新文档统一资源不存在类错误语义,避免服务端返回5xx错误。
2026-09-29 13:34:24 +08:00
Austin 1a8043bee2 docs: 更新 IM 消息历史分页的接口约定和向上翻页规则
补充向上翻页加载更早历史的参数约定,明确 REST 消息接口为空占位禁止使用,历史分页统一走 WebSocket 的 get data 接口,同步更新接口文档说明。
2026-09-29 11:08:48 +08:00
Austin 7c550aaf9d feat: 新增关系类型及按话题查询通话记录接口
- 新增 RelationshipSelf 关系类型
- 新增 serveIMCallsByTopic 接口处理按话题查询通话记录
- 实现群成员资料接口返回本人时的 self 关系处理
- 更新接口文档说明本人查询时返回 relationship: self
2026-09-29 10:46:32 +08:00
Austin 0718da784f docs: 移除 IM API 文档中接口的“待实现”标记 2026-09-29 10:26:27 +08:00
Austin 2f8ded104d docs: 添加单聊已读/已收回执协议约定文档 2026-09-29 09:53:24 +08:00
Austin 1ff0894205 fix(db): 移除消息查询中已废弃的 deletedat 条件
docs: 更新文件上传和消息补偿相关接口文档
2026-09-28 17:45:04 +08:00
richjohnrambo a974572f72 test: stabilize distributed load verification 2026-09-28 09:50:33 +08:00
richjohnrambo d3c679878d test: throttle history verification 2026-09-28 08:17:14 +08:00
richjohnrambo cb2210aa3e test: harden group load verification 2026-09-28 00:58:24 +08:00
richjohnrambo 622fc50418 fix: keep load agents connected before start 2026-09-28 00:16:51 +08:00
richjohnrambo 6b8ed4a2cc perf: batch group unread fanout 2026-09-27 23:10:28 +08:00
richjohnrambo fc875cc84e test: report ambiguous group publish outcomes 2026-09-27 22:59:17 +08:00
richjohnrambo e90e92a59e docs: standardize load test runtime package names 2026-09-27 22:31:53 +08:00
richjohnrambo 72e6c7b2c7 docs: record p2p result and next load stage 2026-09-27 22:17:57 +08:00
richjohnrambo ea8732e2b1 fix: keep load test websocket sessions alive 2026-09-27 21:41:50 +08:00
richjohnrambo bde02e8eac perf: expose and reduce message publish latency 2026-09-27 21:08:35 +08:00
richjohnrambo 8e778726e0 fix: cache tenant routing on message hot path 2026-09-27 20:10:18 +08:00
richjohnrambo 36b52e120c docs: record p2p 500 pool retest 2026-09-27 20:01:32 +08:00
richjohnrambo 3bb6774786 fix: protect publish path from mysql exhaustion 2026-09-27 18:35:54 +08:00
richjohnrambo 36eb76a5e3 docs: record p2p 500 load test bottleneck 2026-09-27 17:45:39 +08:00
richjohnrambo 0cf88e6e97 docs: add staged dual-machine pressure test runbook 2026-09-27 17:10:01 +08:00
richjohnrambo 3fb95fd5d9 test: report exact ACK latency percentiles 2026-09-27 17:05:06 +08:00
richjohnrambo d96f661f30 fix: reduce persistence lag and retry audit deadlocks 2026-09-27 16:33:05 +08:00
richjohnrambo 704c65a7a4 docs: record post-GitOps p2p pressure results 2026-09-27 16:14:22 +08:00
richjohnrambo 188164ce82 test: capture ordering regressions and enforce deploy resources 2026-09-27 14:05:21 +08:00
richjohnrambo d0e1e98f5f repair stale loadtest friendship setup 2026-09-27 13:40:58 +08:00
richjohnrambo ecfe13ff0e use unique p2p preflight idempotency keys 2026-09-27 13:36:12 +08:00
richjohnrambo 8280ac5987 fix loadtest p2p preflight replay 2026-09-27 13:35:17 +08:00
richjohnrambo f9f7321b11 fix p2p permission refresh during load tests 2026-09-27 12:53:09 +08:00
richjohnrambo a57d1e8d4a test: support staged pressure connection counts 2026-09-27 09:48:46 +08:00
richjohnrambo 96eb30b46f ci: tolerate optional ingress RBAC 2026-09-27 09:37:19 +08:00
richjohnrambo ce3250be64 fix: harden multi-pod delivery and load verification 2026-09-27 03:37:32 +08:00
richjohnrambo a2de100f2b docs: record message persistence recovery 2026-09-27 03:09:34 +08:00
richjohnrambo ae2dc0d45e fix: align private message ids with snowflake 2026-09-27 02:53:25 +08:00
richjohnrambo c8d08e26e2 docs: analyze p2p load test cluster overload 2026-09-27 02:34:58 +08:00
richjohnrambo 9958b3c462 docs: add pressure runtime package transfer steps 2026-09-27 01:58:31 +08:00
richjohnrambo 07eb0b05ef docs: record allrich pressure test run 2026-09-27 01:50:24 +08:00
richjohnrambo 6d17154a72 fix: retry load-test websocket setup 2026-09-27 01:39:24 +08:00
richjohnrambo b3b5dd55a6 feat: support verified bulk load-test setup 2026-09-27 01:34:39 +08:00
richjohnrambo 021169555a fix: use group message shard directory 2026-09-27 00:40:24 +08:00
richjohnrambo 6e416fcfd0 docs: add machine B pressure test instructions 2026-09-27 00:24:28 +08:00
richjohnrambo c235139d83 docs: add dual-host pressure test runbook 2026-09-27 00:23:02 +08:00
richjohnrambo 2b90a2e759 feat: add dual-host IM pressure test tool 2026-09-26 21:50:43 +08:00
richjohnrambo f898a7171c feat: batch kafka message persistence 2026-09-24 22:45:07 +08:00
richjohnrambo 7f2df1dd4f fix: reject unrouted messages without seq 2026-09-24 18:33:09 +08:00
richjohnrambo 1f35d4f786 fix forwarded message read state on proxy topics 2026-09-24 17:24:46 +08:00
richjohnrambo 31a70c0bac feat: consume workbench kafka events 2026-09-24 16:56:33 +08:00
richjohnrambo 7405277f27 fix: omit admin-only actions from group notices 2026-09-24 16:47:19 +08:00
richjohnrambo 6453abdbd1 fix: return username format error on registration 2026-09-24 16:47:14 +08:00
richjohnrambo d41956019a fix unread cache invalidation for forwarded messages 2026-09-24 16:37:05 +08:00
richjohnrambo f952249dc3 feat: cache topic and subscription reads 2026-09-24 14:46:00 +08:00
richjohnrambo 0881256103 fix: retain cultivate feed media references 2026-09-24 14:27:17 +08:00
richjohnrambo 5cea4d1e2e fix: sync persist conversation system notices 2026-09-24 11:26:27 +08:00
richjohnrambo 5ce04bad05 docs: track pending image review worker 2026-09-24 11:13:30 +08:00
richjohnrambo 48ca3ae684 fix: align member meta default avatars 2026-09-24 11:00:19 +08:00
richjohnrambo 75216c311d fix cluster proxy cleanup routing 2026-09-24 10:18:07 +08:00
richjohnrambo 9893452817 fix: reserve seq for p2p system messages 2026-09-24 10:03:27 +08:00
richjohnrambo a3df9d269c fix: broadcast forwarded messages after kafka publish 2026-09-24 00:00:50 +08:00
richjohnrambo 68a0228dff feat: include avatar url in member public meta 2026-09-23 23:53:26 +08:00
richjohnrambo da02eae6a4 fix: broadcast async forwarded messages after persistence 2026-09-23 18:14:34 +08:00
richjohnrambo 3e92021254 feat: support quoted message headers 2026-09-23 17:46:51 +08:00
richjohnrambo aa9f562e1e refactor: retire legacy tinode database routing 2026-09-23 17:04:30 +08:00
richjohnrambo 204043382c fix: scope login audit records by tenant 2026-09-23 15:54:07 +08:00
richjohnrambo 7d4bc4d049 fix: notify recipients for forwarded messages 2026-09-23 15:18:27 +08:00
Austin 134ac8580e fix(security): 修复宵禁策略未生效于群聊话题的问题
添加注释说明宵禁仅作用于私聊和群聊,同时将话题类型判断从仅私聊扩展至群聊,确保两类消息话题均会应用宵禁策略。
2026-09-23 14:14:44 +08:00
richjohnrambo 40aba2a3f5 fix tenant-scoped user search 2026-09-23 13:45:39 +08:00
Austin 6bbe955e75 feat: 新增共库模式下登录失败日志表及相关功能
- 新增登录失败日志表(共库多租户版),添加 tenant_id 列实现租户隔离
- 调整登录失败日志逻辑,仅记录触发账号锁定的事件
- 新增管理端接口功能,为登录失败日志补充实时锁定状态
- 更新测试用例适配共库模式下的租户过滤逻辑
2026-09-23 13:18:28 +08:00
Austin b8b21d3548 feat(server): 为内部管理查询添加租户隔离支持
实现共库模式下的租户数据隔离,新增 TenantColumn 字段标识查询表的租户列,添加 internalAdminTenantCondition 函数生成租户过滤条件。修改所有内部管理查询的构建逻辑,在列表和详情查询中自动注入租户条件,防止跨租户数据泄漏。同时更新所有资源查询规格,配置对应表的租户列。
2026-09-23 12:11:26 +08:00
richjohnrambo a0d0bf574e unbind device push token on logout 2026-09-23 11:39:16 +08:00
richjohnrambo c4355ac236 fix: accept raw phone secret for personal registration 2026-09-23 11:33:16 +08:00
richjohnrambo ac20f1ed25 fix message mutation responses 2026-09-23 10:47:31 +08:00
richjohnrambo b413560455 fix tenant group topic and member cache 2026-09-23 10:35:06 +08:00
richjohnrambo 6303d07580 fix: avoid stale profile projections in shared schema 2026-09-23 10:28:15 +08:00
richjohnrambo 7f0903cd31 fix tenant scoping for group business tables 2026-09-23 10:10:08 +08:00
richjohnrambo 35a70ce9ff fix group tenant topic creation 2026-09-23 09:59:53 +08:00
richjohnrambo 6fa3009987 fix tenant sharding for group topics 2026-09-23 09:49:14 +08:00
richjohnrambo a968fd252e fix: use shared tables for tenant group members 2026-09-23 09:41:45 +08:00
richjohnrambo 7c1fab5065 租户库共库分表实现 2026-09-23 09:36:17 +08:00
richjohnrambo 137d6fb6da fix verification cache for redis-backed challenges 2026-09-23 09:02:56 +08:00
richjohnrambo e37b52c3c0 feat: complete shared database tenant sharding 2026-09-23 02:19:07 +08:00
richjohnrambo 1ec2feee47 fix: keep only tenant realname review policy 2026-09-23 02:08:13 +08:00
richjohnrambo 62856194e5 fix HTTP idempotency insert placeholders 2026-09-23 00:28:26 +08:00
richjohnrambo 1909074898 fix tenant friend request row scans 2026-09-22 20:26:43 +08:00
richjohnrambo 9ddf6ec5ee fix shared-db tenant runtime gaps 2026-09-22 19:28:33 +08:00
Austin 49e43aa3cf fix(mysql): read realname policy fallback from im_tenant
Tenant config was merged into im_tenant on the manager side; switch the
DB fallback query accordingly. Keep ErrNoRows/unknown-column fallbacks
so environments before the phase-1 migration degrade to defaults.
2026-09-22 19:08:42 +08:00
richjohnrambo 8e04f00e96 fix shared database account sequence initialization 2026-09-22 18:00:18 +08:00
richjohnrambo e4416da04a fix im server shared database config 2026-09-22 16:45:24 +08:00
richjohnrambo b8c93e8d6a fix(mysql): route tenant data to physical shard tables 2026-09-22 16:11:10 +08:00
richjohnrambo 76b462e334 docs(sql): add shared database sharding migrations 2026-09-22 15:19:56 +08:00
richjohnrambo 2e0612acbb fix(mysql): adapt tenant shared database fields 2026-09-22 14:40:15 +08:00
richjohnrambo 865b436264 fix: skip challenge requirement for fixed test code 2026-09-22 00:46:35 +08:00
richjohnrambo ac10b31aa1 fix: accept fixed legacy registration code 2026-09-22 00:39:14 +08:00
richjohnrambo 3276c6d1cd fix: allow legacy enterprise registration payloads 2026-09-22 00:23:53 +08:00
richjohnrambo dc36ce292e refactor: unify internal api authentication 2026-09-21 23:08:15 +08:00
richjohnrambo 186cfa21c8 feat: validate personal and enterprise auth tenants 2026-09-21 21:24:25 +08:00
richjohnrambo fc393cd685 docs: add aliyun realname integration notes 2026-09-21 21:24:25 +08:00
Your NameandClaude Sonnet 5 b30cce950a fix(docker): mirror tinode's own log output to stdout
Confirmed live: /var/log/tinode.log-only redirection meant Loki never
received a single real tinode runtime log line (election, rehash,
connection, error output) since the container started -- Promtail
collects standard container stdout/stderr, not files written inside
the container. Every Pod's collected log was just this script's own
"NO_DB_INIT=true: skipping..." init line, nothing from tinode itself.

Now more consequential with native Cluster enabled: failover/rehash
state transitions were completely invisible to the normal Grafana/Loki
monitoring path, visible only via manual kubectl exec.

tee mirrors to both stdout (collected) and the file (still readable via
kubectl exec). PIPESTATUS[0] is required since a bare $? after a pipe
would report tee's exit status, not tinode's -- which also let this
drop the now-redundant "tail last 200 lines to stderr on crash" block,
since the full live stream already went to stdout throughout the run.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 20:23:25 +08:00
Your NameandClaude Sonnet 5 4abd6604d7 fix(deploy): raise rollout timeout for StatefulSet's sequential update
StatefulSet's RollingUpdate replaces Pods one at a time in strict
sequence (unlike a Deployment, and unaffected by podManagementPolicy:
Parallel, which only governs initial scale-up/down) -- confirmed live:
run #152 restarted correctly (2/3 pods updated, then the 3rd) but hit
the old 120s ceiling before the last pod turned Ready, so CI reported
"failure" for a rollout that actually completed successfully seconds
later. 300s gives enough headroom for three sequential pod cycles
(image pull + start + native-Cluster rejoin) instead of tuning it to
the exact observed timing.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 19:49:44 +08:00
Your NameandClaude Sonnet 5 eca2ee56c0 fix(deploy): update rollout commands for StatefulSet migration
im-server moved from Deployment to StatefulSet in im-deploy-config
(native Cluster enablement) -- this workflow's kubectl commands still
targeted the old resource type, so every deploy since the migration
failed at the final step with "deployments.apps im-server not found"
(image built/pushed fine, just never got rolled out).

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-21 19:40:26 +08:00
Austin 9820e89e1c refactor(server/auth): 移除账号掩码函数并简化审计日志存储
1.  移除不再使用的maskLoginAccount工具函数
2.  登录审计日志直接存储原始账号而非掩码后的版本
3.  重命名测试函数以匹配新的业务逻辑
2026-09-21 17:50:55 +08:00
richjohnrambo 55c59878cf fix: resync user account sequence 2026-09-21 16:55:24 +08:00
Austin a147dc718a refactor(server): 优化登录锁定提示文案和时长计算逻辑
更新了loginLockedMessage函数,细化了锁定时长的提示分级,增加了过期回退文案,让提示更准确友好
2026-09-21 13:55:02 +08:00
richjohnrambo 407842db91 fix: prepare native cluster deployment 2026-09-21 13:52:34 +08:00
richjohnrambo 5f717c2162 refactor: read user profile fields from profile table 2026-09-21 13:44:04 +08:00
Austin d398467fb9 docs: add IM contact us interface docking documentation
Complete the 4251-line docking document for IM contact us interface, including function description, interface definition, client rendering rules, local debugging example, operation configuration steps and reference implementation
2026-09-21 13:28:12 +08:00
Austin 9748c7a478 Merge branch 'dev' of https://git.118im.com/im_team/im_server into dev 2026-09-21 13:14:30 +08:00
Austin 7829990180 feat: 添加联系我们配置接口和字典配置缓存
1. 新增DictionaryItem字典配置结构体
2. 添加字典配置缓存及失效逻辑
3. 实现联系我们接口的业务逻辑和测试用例
4. 注册联系我们API路由
5. 集成字典配置到admin内部客户端
6. 处理字典配置更新的kafka事件
2026-09-21 13:11:42 +08:00
richjohnrambo 05ad37ba80 fix: persist settings for websocket groups 2026-09-21 11:58:36 +08:00
richjohnrambo c891afba73 test: 更新登录锁定表初始化断言 2026-09-21 09:55:29 +08:00
richjohnrambo e3b93113f1 feat: 接入消息加密与实名认证能力 2026-09-21 09:53:39 +08:00
Austin 841c0c77cd feat: 实现登录失败锁定功能
新增了登录失败次数统计和临时锁定机制,包括:
1. 定义登录锁定数据结构和数据库表结构
2. 实现租户级别的锁定状态持久化接口
3. 添加登录速率限制安全策略配置
4. 在登录流程中加入锁定前置检查和失败计数逻辑
5. 提供管理员手动解锁的内部API接口
6. 新增中文本地化的锁定提示文案
2026-09-20 17:08:38 +08:00
Austin 21063d4af0 Merge branch 'dev' of https://git.118im.com/im_team/im_server into dev 2026-09-20 16:39:46 +08:00
Austin 532ff49737 fix(im): parse Drafty entities for message type classification (#31)
Favorites from Drafty messages (link/file/image/video/voice) always fell back to text because imMessageType only recognized flat file_id/url content. Derive the type from Drafty ent[] entities: LN->link, IM->image, VD->video, AU->voice, EX refined by mime. Attachments win over inline links.
2026-09-20 16:38:51 +08:00
richjohnrambo 5c276596b8 fix: support removing group members 2026-09-20 15:37:56 +08:00
Austin 48b3369799 Merge branch 'dev' of https://git.118im.com/im_team/im_server into dev 2026-09-20 14:52:08 +08:00
Austin b2e8b64c0b refactor(mysql): 适配PXC严格模式,替换全局锁方案
重构MySQL迁移逻辑,新增acquireMigrationLock适配Percona XtraDB Cluster的pxc_strict_mode限制,将原有的GET_LOCK迁移锁替换为更兼容的方案,同时为各迁移流程添加快速检查路径减少锁竞争
2026-09-20 14:52:01 +08:00
richjohnrambo db66a965fa feat: 支持养成后台调用IM内部接口 2026-09-19 17:07:39 +08:00
richjohnrambo de01a95c96 Merge remote-tracking branch 'origin/dev' into dev 2026-09-19 15:25:40 +08:00
richjohnrambo a08c0bd921 feat: 提供养成服务内部 IM 能力 2026-09-19 15:24:55 +08:00
richjohnrambo 0fcbbb9318 fix: publish member joined after group invitation 2026-09-18 20:58:00 +08:00
Austin 753a04ed12 fix: broadcast offline presence as soon as the last session drops
Logout left contacts seeing the user online until the me topic idle timeout
fired presUsersOfInterest(off), and even that frame was silently dropped
for p2p routes by handlePresence's xoriginal check. MarkUserOffline now
returns the remaining session count and cleanUp notifies users of interest
immediately once it hits zero; p2p presence is delivered straight to the
contact's me topic, mirroring presSingleUserOffline. Zentao #22.
2026-09-18 17:42:18 +08:00
Austin f4cedc7d75 fix: push realtime del presence to topic sessions on message recall
The REST recall response reached only the caller, so peers kept showing the
recalled message until a full resync. broadcastIMMessageRecalled now pushes
the native {pres what:del delseq} tombstone through the hub for both single
and batch recall (empty pres topic for p2p, required by handlePresence's
xoriginal check); clients flush the message and render the recalled
placeholder. Zentao #16, #2.
2026-09-18 17:42:09 +08:00
richjohnrambo 8b20118570 feat: enforce youth mode policy and improve search 2026-09-18 14:51:40 +08:00
richjohnrambo 9482bb19f6 fix: wake app for call pushes 2026-09-18 13:51:22 +08:00
richjohnrambo 153f541078 fix: add call push category 2026-09-18 13:38:57 +08:00
richjohnrambo 192bdede33 fix: route call accept across pods 2026-09-18 11:15:19 +08:00
Austin 4c809129eb Merge branch 'dev' of https://git.118im.com/im_team/im_server into dev 2026-09-18 10:56:00 +08:00
richjohnrambo 3a92d81fca feat: enforce friend add and curfew policies 2026-09-18 10:55:53 +08:00
Austin 7a52db3437 fix: auto-provision im_user_privacy_settings columns on startup
Zentao #24/#25: commit 50f8581 added friend_request_verification and group_invite_verification to userPrivacyByUID, but existing databases lack those columns, so GetPrivacy failed with Error 1054 and every cross-user profile/qrcodes/friend-request endpoint returned 500.

Add user_privacy_schema.go which, under GET_LOCK, checks information_schema and ALTERs in any missing privacy columns, wired into the root adapter Open() and per-tenant TenantAdapterFor so tenant-scoped databases are provisioned too.
2026-09-18 10:55:13 +08:00
richjohnrambo c6e7f6b923 feat: improve group member events and push delivery 2026-09-18 01:00:41 +08:00
richjohnrambo 0e2f4e73d1 refactor: remove workbench code from im server 2026-09-17 23:38:44 +08:00
richjohnrambo 69a777c81f feat: apply configured default user avatars 2026-09-17 21:36:19 +08:00
richjohnrambo b9aaccc830 fix: update uid encryption key defaults 2026-09-17 16:59:47 +08:00
richjohnrambo ac75caba86 fix: load uid key from manager config 2026-09-17 16:30:47 +08:00
richjohnrambo c8d9146604 fix: include username in me endpoint 2026-09-17 15:23:47 +08:00
richjohnrambo ed976c76ff docs: update im integration and schema notes 2026-09-17 14:59:28 +08:00
richjohnrambo 50f8581667 fix: resolve im backend issue list regressions 2026-09-17 14:59:16 +08:00
richjohnrambo 760de35e10 fix: allow username-only registration without invite 2026-09-17 13:45:57 +08:00
richjohnrambo 8279ae577f fix: decouple meeting note upload binding 2026-09-17 13:38:55 +08:00
richjohnrambo 734e042af6 feat: expose account fields in user profiles 2026-09-17 13:37:34 +08:00
richjohnrambo 816be46e03 fix: fan out unread presence to online sessions 2026-09-17 10:24:39 +08:00
richjohnrambo 7df3313c78 fix: shorten session heartbeat interval to 15s 2026-09-17 00:16:14 +08:00
richjohnrambo 0f8e3b82b7 feat: enforce tenant content moderation 2026-09-16 22:21:25 +08:00
richjohnrambo 0ae4af0c31 fix: fan out receipts and align tenant schema 2026-09-16 22:09:52 +08:00
richjohnrambo 6729f40665 fix: sync topic sequence after server data 2026-09-16 16:21:53 +08:00
richjohnrambo 7461216c03 feat: merge registration invite and verification input 2026-09-16 14:29:59 +08:00
richjohnrambo 370e9878ed fix: notify p2p message mutation on user channel 2026-09-16 13:49:33 +08:00
richjohnrambo fa95dd72af fix: 后台登录审计查询按字符串返回用户ID 2026-09-16 13:39:50 +08:00
richjohnrambo 1038d25abd fix: 后台消息查询按字符串返回用户ID 2026-09-16 13:24:02 +08:00
Austin a01cdae971 fix(realtime): evict fanout 增加时效校验,防止延迟重放误踢新登录
deliverRealtimeSessionControl 分发前校验 Ctrl.Timestamp,超过 10s 的 session-control(evict)消息直接丢弃并记 skip_stale 指标,避免 consumer 重启重放 PEL / stream 积压时旧消息的 skipSid 与新会话不符导致新登录被误踢。无时间戳消息放行以兼容旧发布方。

验证:TestRealtime 3/3、regression_kick_notification 14/14、regression_mobile_kick 21/21。
2026-09-16 13:22:47 +08:00
richjohnrambo c0ab94723c fix: 后台用户查询按字符串返回用户ID 2026-09-16 13:10:37 +08:00
Austin 846615db69 Merge branch 'dev' of https://git.118im.com/im_team/im_server into dev 2026-09-16 10:23:33 +08:00
Austin 4280b535c2 feat(session): 实现同平台抢登互踢功能,优化会话踢下线逻辑
新增同平台会话踢下线能力,将iOS/Android/mobile归为同一移动分组,web仅匹配web;扩展会话存储接口支持按平台、带原因踢除会话,同步更新集群广播逻辑与各调用点,统一传递踢下线原因参数
2026-09-16 10:23:27 +08:00
richjohnrambo 68b196215f fix: keep upload urls unchanged 2026-09-16 09:56:15 +08:00
richjohnrambo 1de69fcc5b fix: force file urls to https 2026-09-16 09:52:55 +08:00
richjohnrambo 9626e8ee7c fix: invalidate group settings cache on membership changes 2026-09-16 01:03:40 +08:00
richjohnrambo 139681fadb feat: shard group messages by month 2026-09-15 23:53:51 +08:00
richjohnrambo b7c2536527 fix: persist group join system notices 2026-09-15 19:20:07 +08:00
Austin dbb0a06db0 fix: 修复P2P主题别名订阅的兼容性问题
1. 订阅重复检查新增Original与RcptTo匹配判断,避免别名订阅误判已订阅
2. 重构订阅get查询处理逻辑,提取为独立函数
3. 针对P2P别名订阅场景返回200响应而非304,帮助客户端完成别名到标准主题名的重命名,避免发布失败
4. 优化群摘要接口的权限校验和异常处理逻辑
2026-09-15 18:53:49 +08:00
richjohnrambo 563ae9ee5e fix: fanout p2p system notices to users 2026-09-15 17:58:55 +08:00
richjohnrambo e29b5d3589 test: cover blocked profile visibility 2026-09-15 16:02:36 +08:00
richjohnrambo 152e2402fa fix: allow members to query own group mute 2026-09-15 15:55:13 +08:00
richjohnrambo bc27a5c574 feat: add message mutation sync 2026-09-15 15:54:52 +08:00
Austin 55afd976c9 fix(server): 修复群组权限缓存与解散群问题
1. 新增group事件处理逻辑,实时刷新内存中的群组权限与状态
2. 加载topic时过滤已解散的群组,防止被旧成员恢复
3. 补充群成员离开事件的用户ID字段,完善事件处理
2026-09-15 15:18:14 +08:00
Austin 5dd0a42002 fix: harden message seq allocation against stale redis replicas
- use sentinel failover client for group message runtime when
  sentinel_master_name/sentinel_addrs are configured, so seq counter
  reads never hit a stale replica after failover
- re-seed missing redis seq counter from DB topic seqid instead of
  trusting the caller in-memory baseline
- on unique-key conflict during async persist, re-allocate seq from DB
  latest, bump redis counter and fix the idempotency record so resends
  with the same client mid return the final seq
- add uk(topic, seqid) unique keys for im_messages in tenant DDL
2026-09-15 12:41:38 +08:00
richjohnrambo 4f6a996922 feat: support batch message recall 2026-09-15 10:46:07 +08:00
richjohnrambo 9ec5da5b02 fix: persist login sessions for workbench auth 2026-09-15 10:23:47 +08:00
richjohnrambo ef8091d805 fix: reject friend request to missing user 2026-09-15 00:48:36 +08:00
richjohnrambo 15abc1b6e8 fix: bake test middleware config 2026-09-14 23:59:35 +08:00
richjohnrambo 0cf2fb61a7 fix: restore docker middleware env config 2026-09-14 23:52:00 +08:00
richjohnrambo 5384a8fa7b fix: avoid profile default hot path writes 2026-09-14 23:34:22 +08:00
richjohnrambo fca0a5baf9 fix: keep friend request success when event publish fails 2026-09-14 23:29:33 +08:00
richjohnrambo 40510a616e fix: bake test middleware config into image 2026-09-14 23:28:07 +08:00
richjohnrambo 7196d0c839 fix: include jpush handler in docker config 2026-09-14 21:09:28 +08:00
richjohnrambo 79babdb6aa fix: scope realtime fanout dedup per node 2026-09-14 20:15:22 +08:00
kk123kk123 6291fa2b60 feat(群组): 添加群组解散状态支持与过滤逻辑
新增群组解散状态常量,在群组列表和详情接口中增加解散群组的过滤和报错处理,避免返回已解散的群组数据
2026-09-14 20:06:41 +08:00
richjohnrambo 275c9e5fcd fix: avoid auth version hot path writes 2026-09-14 19:04:36 +08:00
richjohnrambo 02eddb6010 fix: accept jpush token metadata fields 2026-09-14 17:34:12 +08:00
richjohnrambo 2581d7b8f0 fix: support account creation on pxc 2026-09-14 14:26:29 +08:00
richjohnrambo 8f849efc25 chore: point test config to new middleware 2026-09-14 13:58:08 +08:00
richjohnrambo 1c7417b8f3 fix: support legacy auto ids during registration 2026-09-14 12:35:00 +08:00
richjohnrambo 6f8933c35a fix: avoid main call timeout dependency 2026-09-14 12:18:05 +08:00
richjohnrambo 789e662bdb fix: expire stale call pushes 2026-09-14 12:11:43 +08:00
richjohnrambo 4854e090ef fix: harden group lifecycle and registration writes 2026-09-14 12:04:10 +08:00
Your NameandClaude Sonnet 5 990526647c ci: retarget deploy workflow at the k3s test cluster
Old VM's blue-green im-server is decommissioned. App config
(MySQL/Redis/API key/admin-internal secrets) now lives in the
cluster's own Deployment manifest, not injected through CI --
this workflow's job is just build (from source, via
docker/tinode/Dockerfile.build as before), push under :test,
and roll the deployment.

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-14 05:41:03 +08:00
richjohnrambo 67e8c2c6f6 feat: add workbench kafka migration support 2026-09-13 22:17:02 +08:00
richjohnrambo c46cb4b382 fix: join group directly when invitation needs no approval 2026-09-13 17:21:21 +08:00
richjohnrambo 7b304412b0 fix: repair group management edge cases 2026-09-13 16:04:31 +08:00
richjohnrambo a9afc6ff6a fix: align group management APIs 2026-09-13 16:03:55 +08:00
richjohnrambo f1279ac607 fix: preserve p2p topic for IM system notices 2026-09-13 16:00:58 +08:00
richjohnrambo ccbd385d2f chore: disable runtime mysql schema creation 2026-09-13 13:56:24 +08:00
richjohnrambo eefc3c2fbd fix: add group mute all system notice 2026-09-13 13:12:42 +08:00
richjohnrambo d107fba8bf fix: align IM client interaction edge cases 2026-09-13 10:06:52 +08:00
richjohnrambo e5fbca735d feat: centralize app release configuration 2026-09-12 17:07:49 +08:00
richjohnrambo 2728f35069 feat: expose public legal agreement pages 2026-09-12 14:18:24 +08:00
richjohnrambo ff432bc3ce fix: stabilize me subscription sync metadata 2026-09-12 14:09:43 +08:00
richjohnrambo ca3957e3b3 feat: support group invite privacy verification 2026-09-12 11:20:48 +08:00
richjohnrambo 80c802af20 add nickname to group member payloads 2026-09-12 10:06:28 +08:00
richjohnrambo a9a4970d9f fix p2p topic canonicalization and stale subscriptions 2026-09-12 00:08:30 +08:00
richjohnrambo f1dfffcfb5 include join request user profiles 2026-09-11 18:04:01 +08:00
richjohnrambo 1d433971d4 fix group invitation notifications 2026-09-11 17:34:16 +08:00
richjohnrambo cfab33f45a fix: link group invitations to join requests 2026-09-11 17:26:05 +08:00
richjohnrambo 367bed2940 fix im workbench and profile regressions 2026-09-11 17:25:24 +08:00
richjohnrambo 1c2132eebc fix: omit invalid jpush cid 2026-09-11 11:57:41 +08:00
richjohnrambo bafaf2d7b9 fix: normalize jpush aliases 2026-09-11 11:43:48 +08:00
richjohnrambo 05f4087240 fix: deliver friendship system notices live 2026-09-11 11:01:37 +08:00
richjohnrambo b9dc56eccf feat: auto-friend invite bound users 2026-09-11 10:46:20 +08:00
richjohnrambo 926b4bbd81 fix im forwarding and message list regressions 2026-09-11 09:00:38 +08:00
richjohnrambo 75d1aabcb0 fix group member display names 2026-09-10 21:57:25 +08:00
richjohnrambo 928a45ecfb feat: assign IM user account numbers 2026-09-10 21:51:23 +08:00
richjohnrambo 51fee50343 fix im profile display name remark priority 2026-09-10 16:01:00 +08:00
richjohnrambo 1f3a98605e fix im heartbeat and friend display name 2026-09-10 15:23:27 +08:00
richjohnrambo 14ee3d937f fix: order workbench modules 2026-09-10 15:19:29 +08:00
richjohnrambo 085a48c3d9 feat: publish text message for group announcements 2026-09-10 10:27:38 +08:00
richjohnrambo 6b2f1e06e0 feat: implement precise token logout 2026-09-10 09:43:02 +08:00
richjohnrambo 98234db263 fix: 兼容历史收藏源避免列表崩溃 2026-09-09 17:40:11 +08:00
richjohnrambo e0a6ee27bc fix: include api key in tenant websocket endpoint 2026-09-09 17:32:49 +08:00
richjohnrambo 23afc1b7f1 fix: 修复文件预览重定向与扩展名解析 2026-09-09 16:32:49 +08:00
richjohnrambo cae5039afe fix: verify OSS objects before serving 2026-09-09 16:28:56 +08:00
richjohnrambo c3e5cc0831 fix: reject dangling file attachment links 2026-09-09 15:55:18 +08:00
richjohnrambo bef158941a fix: validate group avatar attachments 2026-09-09 15:48:55 +08:00
richjohnrambo be61ec2b71 fix: backfill user public profile projections 2026-09-09 15:34:20 +08:00
richjohnrambo 45c6c7eacb fix: avoid admin dependency on login entrypoints 2026-09-09 15:19:13 +08:00
richjohnrambo 9159ec7fc7 feat: add IM service health and metrics endpoints 2026-09-09 14:24:40 +08:00
richjohnrambo 4c9bdd64e0 fix: 修复OSS头像预签名下载 2026-09-09 14:08:01 +08:00
richjohnrambo c43c099c5e fix: 兼容历史头像文件下载 2026-09-09 13:24:36 +08:00
richjohnrambo 92a72d1bea feat: enforce tenant login IP whitelists 2026-09-09 09:40:37 +08:00
richjohnrambo 9ac2b412cc fix: 优化群成员分页并补齐头像资料 2026-09-09 09:38:20 +08:00
richjohnrambo aae13c9265 feat: load oss media config from sys config 2026-09-08 22:19:00 +08:00
richjohnrambo 0f984b0ab4 Merge remote-tracking branch 'origin/dev' into dev 2026-09-08 16:29:05 +08:00
richjohnrambo 0b3c0b1c78 fix: resolve P1 backend consistency issues 2026-09-08 16:23:21 +08:00
richjohnrambo 034678fa53 fix: 兼容查询用户头像数据 2026-09-08 15:53:56 +08:00
richjohnrambo 06945a4050 fix: 统一用户资料投影并放开头像访问 2026-09-08 15:26:05 +08:00
richjohnrambo 62e989487b Backfill group file metadata on read 2026-09-08 14:17:10 +08:00
richjohnrambo 20c0b92d77 Fix group file metadata and access 2026-09-08 13:50:04 +08:00
richjohnrambo 8116fb6fe8 feat: 完善用户治理与登录白名单 2026-09-08 10:55:26 +08:00
richjohnrambo d239bd9ad0 fix: expose friend online status 2026-09-08 10:35:42 +08:00
richjohnrambo b295170eee Accept numeric favorite sequence 2026-09-08 10:26:30 +08:00
richjohnrambo 402dfb0d5a feat: 增加群聊消息查询条件 2026-09-07 21:52:55 +08:00
richjohnrambo 8ff9160113 Customize forward bundle titles 2026-09-07 19:57:36 +08:00
richjohnrambo 8a9e202b56 Preserve uploaded file extensions 2026-09-07 16:13:20 +08:00
richjohnrambo 35da3025a3 fix: 恢复IM内部接口默认启动配置 2026-09-07 16:02:56 +08:00
richjohnrambo 654cd93836 Fix meeting note detail payload 2026-09-07 15:13:44 +08:00
richjohnrambo 7e8fc8237b fix im p0 backend issues 2026-09-07 15:12:14 +08:00
richjohnrambo b92154bece ci: stop overriding Kafka deploy environment 2026-09-07 13:53:50 +08:00
richjohnrambo c68c5ac038 fix: 补齐IM治理功能启动配置 2026-09-07 12:06:46 +08:00
richjohnrambo 6b20a633ce feat: 接入后台消息治理与登录审计 2026-09-07 11:53:24 +08:00
richjohnrambo 90185b6c88 Support meeting note upload registration 2026-09-07 11:41:01 +08:00
richjohnrambo 160d33c759 Reduce Kafka publish latency for messages 2026-09-07 11:33:50 +08:00
richjohnrambo a56efd4454 Add sender summaries to forward bundles 2026-09-07 10:39:42 +08:00
richjohnrambo 6330c592cf Fix media URL extension for m4a uploads 2026-09-07 09:57:16 +08:00
richjohnrambo 7a1647bf17 Fix bootstrap meeting unread counts 2026-09-05 18:31:13 +08:00
richjohnrambo baf15848d3 feat: add meeting notes backend 2026-09-05 17:55:27 +08:00
richjohnrambo 9e290b76b3 feat: add jpush push handler 2026-09-05 17:06:59 +08:00
richjohnrambo efbfb7556a Fix P2P message duplicate seq retry 2026-09-04 20:15:25 +08:00
richjohnrambo b4d3fbe359 Clear stale unanswered calls before new invite 2026-09-04 18:16:26 +08:00
richjohnrambo 4ad6ca00b4 Prevent Redis message seq regression 2026-09-04 16:39:32 +08:00
richjohnrambo e6f2cbf36e Fix user lookup on unscoped tenant database 2026-09-04 16:34:11 +08:00
richjohnrambo 50ccdf5f89 Fix profile display backfill startup 2026-09-04 16:20:28 +08:00
richjohnrambo 7fb1ea70bc Sync profile source and message topic seq 2026-09-04 16:06:57 +08:00
richjohnrambo 47649d33e2 Fix friends display name fallback 2026-09-04 13:45:18 +08:00
richjohnrambo 0f1c1f9255 fix: enable local kafka docker integration 2026-09-04 11:57:38 +08:00
richjohnrambo a191ca0c93 Add group announcement edit permissions 2026-09-04 11:09:29 +08:00
richjohnrambo 62af772260 Read user profile display from profile table 2026-09-04 10:49:33 +08:00
richjohnrambo 3173313ab9 Refresh presence on websocket pong 2026-09-04 10:49:33 +08:00
Your NameandClaude Sonnet 5 9da072e6f9 ci: 部署时透传 admin_internal_api / hotgo secret
commit 7193ba2 把 config.template 里的 admin_internal_api.base_url/secret
和 internal_api hotgo-admin secret 从硬编码改成了环境变量占位符,但
deploy workflow 没有跟着传这几个 secret,导致 Fatal 退出、容器反复重启
(表现为 im-server-green 起不来)。这几个 secret 已经在 Gitea 里配好。

Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
2026-09-03 22:36:44 +08:00
richjohnrambo c1bfa503cf feat: add client call feature switches 2026-09-03 21:05:33 +08:00
richjohnrambo d6de2a0349 Notify P2P participants after HTTP forwarding 2026-09-03 20:04:18 +08:00
richjohnrambo e36decab28 Fix topic read actions for user topics 2026-09-03 19:55:44 +08:00
richjohnrambo 7193ba2316 fix: 完善后台组织内部查询 2026-09-03 17:11:36 +08:00
richjohnrambo db00e994d5 fix: keep group avatar update for legacy groups 2026-09-03 14:25:10 +08:00
richjohnrambo a0bd0b0649 Fix Kafka message seq idempotency 2026-09-03 13:21:26 +08:00
richjohnrambo 256f768d15 fix: allow group avatar uploads 2026-09-03 11:52:06 +08:00
richjohnrambo 871e0ba735 Fix conversation context topic fallback 2026-09-03 11:47:45 +08:00
richjohnrambo 2e4e1f0df3 Fix HTTP message forward delivery 2026-09-03 11:11:48 +08:00
richjohnrambo 54e58763ff Improve message forward compatibility 2026-09-03 10:37:42 +08:00
richjohnrambo 1eee0179f1 fix: return saved group nickname profile 2026-09-03 10:01:24 +08:00
richjohnrambo 8193491686 fix: persist group nickname for legacy groups 2026-09-03 09:58:18 +08:00
richjohnrambo a94f60c254 Add youth mode restriction labels 2026-09-02 13:47:12 +08:00
richjohnrambo 79947a2c6f Improve youth mode action token flow 2026-09-02 11:25:25 +08:00
richjohnrambo 7f6995d8fe Disable sensitive challenge rate limit for test 2026-09-02 10:51:15 +08:00
richjohnrambo ac45223e39 Fix IM report and group permission regressions 2026-09-02 10:01:38 +08:00
richjohnrambo 15a4174ed1 fix: support agora rtc join delivery flow 2026-09-02 01:42:19 +08:00
richjohnrambo dc5e09f2ae feat: add agora rtc token join endpoint 2026-09-02 01:05:15 +08:00
richjohnrambo 318ce04f7d Refine Kafka persistence topic boundaries 2026-09-01 23:23:08 +08:00
richjohnrambo f8a286cec2 fix: reuse active security challenge before rate limit 2026-09-01 23:00:14 +08:00
richjohnrambo 929f847003 Refactor Kafka publishing without DB outbox 2026-09-01 23:00:14 +08:00
richjohnrambo 18237cf3ec docs: add agora call integration flow 2026-09-01 19:01:37 +08:00
richjohnrambo edced2675b feat: add conversation system action messages 2026-09-01 13:12:24 +08:00
richjohnrambo 39e7e019c7 fix: scan attendance appeal idempotency key 2026-08-31 14:36:30 +08:00
richjohnrambo 6c6aa9f4c1 fix: scan attendance shifts from mysql 2026-08-31 14:10:34 +08:00
richjohnrambo 44ad626f63 chore: log attendance API failures 2026-08-31 13:54:32 +08:00
richjohnrambo 61ab313f35 fix: ensure attendance schema for tenant routing 2026-08-31 13:44:47 +08:00
richjohnrambo 79630c26f5 fix: compile attendance appeal insert 2026-08-31 13:30:44 +08:00
richjohnrambo cb7dc36cad feat: add attendance APIs 2026-08-31 13:27:12 +08:00
richjohnrambo 9f647de4d0 feat: switch client API branding to MeetingIM 2026-08-31 01:22:53 +08:00
richjohnrambo c6a243ee59 Add meeting display fields 2026-08-29 19:28:10 +08:00
richjohnrambo 866222cb39 Fix missing risk challenge response 2026-08-29 10:59:50 +08:00
richjohnrambo 42916b85aa Fix meeting list and media search issues 2026-08-29 09:46:50 +08:00
richjohnrambo 78e6308174 fix im business event delivery 2026-08-28 16:24:57 +08:00
richjohnrambo 9a46fdf43c feat: handle password reset admin command 2026-08-28 14:18:27 +08:00
richjohnrambo d756aeb4c2 fix: serialize internal user ids as strings 2026-08-28 13:07:17 +08:00
richjohnrambo 5987f2d87a fix: return not found for missing internal user 2026-08-28 13:05:26 +08:00
richjohnrambo 08a4f138e3 fix: skip nonce replay check for internal reads 2026-08-28 11:28:28 +08:00
richjohnrambo 4ea828fb09 fix: tolerate retried internal api nonce 2026-08-28 10:43:01 +08:00
richjohnrambo ba06ee3472 chore: use internal admin service url 2026-08-28 01:08:51 +08:00
richjohnrambo a54ddfa548 chore: use public admin internal api 2026-08-28 00:48:42 +08:00
richjohnrambo 94552a84b4 chore: configure internal api auth 2026-08-28 00:40:40 +08:00
richjohnrambo 84439d63cb chore: configure test kafka 2026-08-28 00:35:10 +08:00
richjohnrambo ca734a48ab feat: support kafka scram auth 2026-08-28 00:31:08 +08:00
richjohnrambo 5cc6eebc1a feat: add kafka admin integration 2026-08-28 00:05:54 +08:00
richjohnrambo b2e5097708 feat: add daily report list preview 2026-08-27 10:08:03 +08:00
richjohnrambo a0fc553c6f fix: improve conversation and report stability 2026-08-26 14:12:58 +08:00
richjohnrambo dc80aa5104 fix: stabilize mysql message reads 2026-08-26 00:03:16 +08:00
richjohnrambo 58cce46dee fix: restore tenant id on cached store objects 2026-08-25 23:20:19 +08:00
richjohnrambo eac2bc2a47 fix: load verification challenges from db 2026-08-25 22:35:09 +08:00
richjohnrambo c020171e52 fix: restore mysql docker build 2026-08-25 22:17:59 +08:00
richjohnrambo f900a08a01 feat: add redis cache and fanout support 2026-08-25 20:54:03 +08:00
richjohnrambo 07376c70e3 fix: stabilize im server startup and p2p topic scans 2026-08-24 16:20:02 +08:00
richjohnrambo e61058ec03 feat: add tenant entry registration APIs 2026-08-24 15:08:47 +08:00
richjohnrambo ed9413d923 feat: add file safety config integration 2026-08-23 23:50:21 +08:00
richjohnrambo 7b942da975 feat: add aliyun oss media handler 2026-08-23 23:20:33 +08:00
richjohnrambo a578acebf7 fix: handle nullable recalled_by in mysql messages 2026-08-23 23:08:56 +08:00
richjohnrambo 641697e755 docs: update IM integration contract examples 2026-08-23 23:05:45 +08:00
richjohnrambo d14942b1a1 fix: preserve group member user ids 2026-08-23 22:29:22 +08:00
richjohnrambo c89c466387 feat: implement first batch im enhancements 2026-08-21 11:42:45 +08:00
richjohnrambo 14abe58b88 test: add websocket protocol audit 2026-08-21 00:42:54 +08:00
richjohnrambo ff616b9c5d fix: align favorite batch endpoint 2026-08-20 23:29:10 +08:00
richjohnrambo 2b3480fa5f fix: stabilize test environment IM endpoints 2026-08-20 23:06:54 +08:00
richjohnrambo 6c6d2f6d3b fix: stabilize im audit endpoints 2026-08-20 22:34:36 +08:00
richjohnrambo 3e1ce5df26 feat: allow viewing user profile QR codes 2026-08-20 17:54:12 +08:00
richjohnrambo 6db2938672 feat: add bootstrap entry badges 2026-08-20 15:59:16 +08:00
richjohnrambo d467323ecb docs: clarify test registration code 2026-08-20 14:10:05 +08:00
richjohnrambo c09ea2ac53 fix: stabilize container validator startup 2026-08-20 13:28:28 +08:00
richjohnrambo 43e8af68d6 fix: adjust phone registration username flow 2026-08-20 12:08:55 +08:00
richjohnrambo 73ddb2d9fa fix: improve IM user account search 2026-08-19 17:23:36 +08:00
richjohnrambo b3d18f9d8d Merge remote-tracking branch 'origin/dev' into dev 2026-08-19 17:02:34 +08:00
richjohnrambo 405dad2eef fix: use fixed code for registration verification 2026-08-19 16:52:20 +08:00
richjohnrambo 4e75cfc65e fix: restore verification target user ids 2026-08-19 01:17:09 +08:00
richjohnrambo 219ecadf4c feat: complete registration invitation and auth redesign 2026-08-19 00:30:11 +08:00
richjohnrambo e063e7879e fix: canonicalize phone numbers as E.164 2026-08-18 01:23:18 +08:00
richjohnrambo bf562c7d56 feat: migrate IM identity and snowflake primary keys 2026-08-18 01:15:21 +08:00
richjohnrambo db322d80dd docs: sync IM integration guide with current behavior 2026-08-17 15:28:00 +08:00
richjohnrambo b116adc6a1 docs: clarify IM registration account payload 2026-08-17 15:23:13 +08:00
richjohnrambo c60ed2e9ed fix: page friend list by peer id 2026-08-17 15:20:02 +08:00
607 changed files with 90681 additions and 9521 deletions
+57 -23
View File
@@ -1,35 +1,69 @@
name: Deploy Backend (Blue-Green)
name: Deploy im-server production
on:
push:
branches:
- pro
workflow_dispatch:
inputs:
image_tag:
description: '镜像tag,留空则使用commit短哈希'
required: false
concurrency:
group: production-im-server
cancel-in-progress: false
permissions:
contents: read
env:
REGISTRY: git.olns.shop
IMAGE: git.olns.shop/pro/im_server
SERVICE: im-server
DEPLOY_HOST: 145.223.69.9
DEPLOY_USER: olns-deploy
jobs:
deploy:
runs-on: deploy-host
timeout-minutes: 60
steps:
- uses: actions/checkout@v4
- name: 确定镜像tag
id: vars
run: |
TAG="${{ github.event.inputs.image_tag }}"
[ -z "$TAG" ] && TAG=$(git rev-parse --short HEAD)
echo "tag=$TAG" >> "$GITHUB_OUTPUT"
- name: 构建镜像(从本仓库源码编译,非下载官方release)
run: docker build -t im-server:${{ steps.vars.outputs.tag }} -f docker/tinode/Dockerfile.build .
- name: 蓝绿部署
- name: Log in to Gitea Container Registry
env:
MYSQL_DSN: ${{ secrets.MYSQL_DSN }}
REDIS_ADDR: ${{ secrets.REDIS_ADDR }}
REDIS_PASSWORD: ${{ secrets.REDIS_PASSWORD }}
API_KEY_SALT: ${{ secrets.API_KEY_SALT }}
AUTH_TOKEN_KEY: ${{ secrets.AUTH_TOKEN_KEY }}
UID_ENCRYPTION_KEY: ${{ secrets.UID_ENCRYPTION_KEY }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
IMAGE=im-server:${{ steps.vars.outputs.tag }} /opt/apps/im-server/deploy.sh
set -euo pipefail
printf '%s' "$REGISTRY_TOKEN" | docker login "$REGISTRY" -u admin --password-stdin
- name: Build and push image
env:
GITHUB_SHA: ${{ github.sha }}
run: |
set -euo pipefail
SHORT_SHA="$(printf '%.12s' "$GITHUB_SHA")"
docker build -f docker/tinode/Dockerfile.build \
-t "${IMAGE}:pro" -t "${IMAGE}:${SHORT_SHA}" .
docker push "$IMAGE:pro"
docker push "$IMAGE:$SHORT_SHA"
- name: Deploy Compose service
env:
DEPLOY_SSH_KEY: ${{ secrets.DEPLOY_SSH_KEY }}
DEPLOY_KNOWN_HOSTS: ${{ secrets.DEPLOY_KNOWN_HOSTS }}
REGISTRY_TOKEN: ${{ secrets.REGISTRY_TOKEN }}
run: |
set -euo pipefail
install -d -m 700 "$HOME/.ssh"
KEY_FILE="$(mktemp "$HOME/.ssh/olns-deploy.XXXXXX")"
cleanup() {
shred -u "$KEY_FILE" 2>/dev/null || true
}
trap cleanup EXIT
printf '%s\n' "$DEPLOY_SSH_KEY" > "$KEY_FILE"
chmod 600 "$KEY_FILE"
printf '%s\n' "$DEPLOY_KNOWN_HOSTS" > "$HOME/.ssh/known_hosts"
chmod 600 "$HOME/.ssh/known_hosts"
SSH_ARGS=(-i "$KEY_FILE" -o BatchMode=yes -o StrictHostKeyChecking=yes)
printf '%s' "$REGISTRY_TOKEN" | ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \
"docker login git.olns.shop -u admin --password-stdin"
ssh "${SSH_ARGS[@]}" "$DEPLOY_USER@$DEPLOY_HOST" \
"cd /opt/olns && docker compose pull im-server && docker compose up -d --no-deps --wait im-server"
+7
View File
@@ -8,8 +8,15 @@
# IDE workspace files.
.idea/
# Trae workspace: AI test scripts, temp files, docs.
/.trae/
# Local Go build output.
/server/server
/.codex-gocache/
/.github/
/.github/
/docs/third/三方服务依赖信息.md
/docs/test/im_third_party_file_safety_test_plan.md
docs/*.zip
+2 -2
View File
@@ -80,8 +80,8 @@ See [instructions](./docker/README.md)
5. Open `tinode.conf` (located at `$GOPATH/pkg/mod/github.com/tinode/chat@vX.XX.X/server/`). Check that the database connection parameters are correct for your database. If you are using MySQL make sure [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name) in `"mysql"` section is appropriate for your MySQL installation. Option `parseTime=true` is required.
```js
"mysql": {
"dsn": "root@tcp(localhost)/tinode?parseTime=true",
"database": "tinode"
"dsn": "root@tcp(localhost)/meeting_im?parseTime=true",
"database": "meeting_im"
},
```
+1 -1
View File
@@ -45,7 +45,7 @@ rm -fR ./releases/tmp
mkdir -p ./releases/tmp/templ
# Copy templates and database initialization files
cp ./server/tinode.conf ./releases/tmp
cp ./server/meetingim.conf ./releases/tmp
cp ./server/templ/*.templ ./releases/tmp/templ
cp ./tinode-db/data.json ./releases/tmp
cp ./tinode-db/*.jpg ./releases/tmp
+183 -2
View File
@@ -18,11 +18,23 @@
多通过配置的方式处理 前端不要写死
一定要考虑好群聊的推送问题
离线和在线状态存到redis里面
离线的不推
离线和在线状态存到redis里面 这个是一个难点
离线的我们的服务不推 走极光的推送
在线的直接ws推
多用开关
校验营业执照
第三方
短信
kyc
营业执照
声网 Agora 音视频服务
极光推送 离线 Push 服务
oss对象存储
文件安全扫描 / 内容安全服务
admin后台群操作通过kafka投递到c端 c端通过ws传到客户端
记录先放到Kafka、 再异步落库。用考虑重复落库的可能
@@ -31,3 +43,172 @@ admin后台群操作通过kafka投递到c端 c端通过ws传到客户端
3. 熟悉群消息扩散策略,如读扩散、写扩散;
4. 理解多端同步、消息漫游、消息推送等 IM 核心机制;
5. 有 OpenIM 或类似 IM 项目经验优先。
1.注册增加邀请码 (非必填 )不填邀请码可以正常注册。只要手机号符合11位格式就行
2.验证码验证作为开关 关闭的情况下的话后台不向短信服务商发送请求,前端正常ui的倒计时,但是收不到验证码
3.正常不填邀请码,用真实手机号注册的,且后台开关打开的情况下,可以收到验证码的流程也能正常注册
这个是现在的情况
后台开关在系统设置 字典管理 增加一个系统枚举 系统开关的 ID = 171
用户的唯一标识用user_id来标记,然后新增user_name,也就是用户名字段
看看对接文档1.4的注册页面 增加逻辑 1.注册邀请码 (非必填 )不填邀请码可以正常注册。用真实手机号注册的,且后台发送注册验证码的开关打开的情况下,可以收到验证码的流程也能正常注册
2.发送注册验证码的开关关闭的情况下,后台不向短信服务商发送请求,前端正常ui的倒计时,但是收不到验证码,此时填了邀请码,且邀请码校验合格的话,没有验证码仍然可以注册
我看了很多users相关的表 你总结一下分别是什么作用
你看看手机号是如何存的
im_users的 tags 有啥用 存的啥
昵称用中文实现
对一样的,因为给好友改了备注,所以上面就会显示,但是昵称是好友,本身自己起的网名,如果对方没有起,系统会默认给一个
用户名和手机号注册的话 如果验证码开关是开的 以验证码为准 如果没开 就以邀请码流程 用户名不能忽略 要保存
前后端对接文档 补充 请求和响应的json
文档的覆盖测试
发的网站功能的复制
web地址:https://web.xcwykk.top/
管理后台地址:https://admin.adfahi.cn
账号:heiyue
密码:Heiyue123
测试名额:100
群操作
文件操作
redis的接入 性能提升
kafka的接入 异步操作 幂等性
弱网
QUIC传输技术
日志框架的接入
重中之重。加密 + 接口鉴权 防止被扫 日志要记录下来 每个小时告警
法相 (三方IM 资金电话确认), [Aug 27, 2026 at 22:13:24]:
....进群的时候拿群成员列表信息 这个怎么做成缓存 然后怎么更新缓存
2.多标签要独立的登录状态,可以用不同域名来解决这个问题,但是要token过期时间要限制一下 7天或者10天 每次登录都重置这个时间
3.转发消息 涉及到转发群和转发个人 大批量转发 消息会不会串
4.我在第一个标签转发了100个群,然后直接切换到第二个标签,你第一个标签还没转发完 你该怎么处理 会不会串到第二个标签页
群成员信息这个是很耗性能的 一个群几千个人 你要考虑是进一次就拿一次群成员信息 还是拿一次
网页上一个tab一个用户
我说的进不是给你拉进群 是你点击群聊这个会话框
敏感词 开关
组织机构树的处理逻辑 我现在处理逻辑
1,手机号注册,或者上架用的测试账号 可以看到好友的组织结构,
2,通过邀请进来的 只能看到自己
对了,强调一点,移动端所有加好友的入口都是需要后端控制的,管理后台需要开关控制能不能加好友。
然后还要增加宵禁功能,宵禁功能是单聊私聊都不能发消息。这个也是管理后台控制的。
tinode 移除
监控接口
制定联调计划
多租户的测试
不能全存
图片只存oss地址 给前端的是oss地址 文件也是oss地址
本地只存100条 历史分页50条拉
群聊要跨月建表
聊天的菜单元素是前端写死的么
群成员变动发消息 然后拉取 群成员 要考虑何时全部 何时只是一个
只有加入的群管理消息都可见 其他的比如移除啥的 只有管理可见
离线用户的推送 未读消息数的处理 每个用户的不一样 群成员每个消息的推送
前端返回一个现有序号 后端跟最新序号比对 然后一减 就是未读消息
全局禁止加好友 比如下班时间到了 不能加 从群成员去看 不能显示个人信息 个人名片也不能看 二维码也不能加
阿里云的oss放一个域名清单 要加密 apikey动态更换
设备类型 ip 全部采集入库
离线时间 和上线时间的采集
检查一下多处存同一个信息的。尤其是用户的
音视频要加上开关 控制前端的显示 已经处理
拆成工作台 + IM
文案部分全部拆出来 做配置
个人注册的企业码是默认system 不需要企业码的位置
先是企业合作,填写信息,审核之后得到企业码
企业注册的要带上企业码
kafka批量消费 批量落库
1.进群的时候拿群成员列表信息 这个怎么做成缓存 然后怎么更新缓存
2.多标签要独立的登录状态,可以用不同域名来解决这个问题,但是要token过期时间要限制一下 7天或者10天 每次登录都重置这个时间
3.转发消息 涉及到转发群和转发个人 大批量转发 消息会不会串
4.我在第一个标签转发了100个群,然后直接切换到第二个标签,你第一个标签还没转发完 你该怎么处理 会不会串到第二个标签页
新增图片的功能 ??
拉一下git提交记录 已经处理
多实例 断线重连 其他ws的坑
### 1. Fitness-Food(GitHub,MIT 协议|中文友好!重点推荐)
🔗 [https://github.com/saudademjj/fitness-food](https://github.com/saudademjj/fitness-food)
- 定位:**中文智能饮食管理系统**,完全匹配你的健身 APP 饮食模块
- 自带接口能力:
- 输入中文食物名称,返回全套营养素(热量 / 蛋白 / 碳水 / 脂肪、微量元素)
- 三餐记录、每日营养汇总、目标对比(减脂 / 增肌)
- PostgreSQL 内置中文食物库
- 技术栈:Next.js + Postgres,可以单独拆出后端 API 给你的 APP 调用
- 协议:MIT,可商用,二次开发无压力
我打算弄一个饮食健康的产品,主要是想记录三餐,目标对比,返回食物热量
默认system企业
内容审核要根据企业来分。做成开关 新建企业的时候加上
用户要和商户绑定 进去只能看某一个商户的数据 这个要改造 要从多商户的角度思考系统还有哪些问题
调研:
阿里云上传apk包 下载下来就装不上
各个厂商push的申请
新加一个状态,消息免打扰后,对方发消息,红点这么显示
Bundle ID:com.ysl.IM
KEY ID:J4ZUVHW59G
Team ID:2DN6KF37CH
单号登录 多设备顶号 已经实现
多标签登录的消息到达率
建一个千人大群
telegram报警功能
加上ai翻译的功能 ai笔记
后端要做一下 redis 的 sentinel适配, 不然重新选举之后可能会写错节点报错
https://api.118im.com 简单弄一个官网,https://api.118im.com/legal/privacy-policy需要把名字改对,TestFlight的beta版需要审核,这两个可能会是卡点  
+724
View File
@@ -0,0 +1,724 @@
package main
import (
"bytes"
"context"
"crypto/tls"
"encoding/base64"
"encoding/json"
"flag"
"fmt"
"io"
"net/http"
"net/url"
"os"
"sort"
"strings"
"time"
"github.com/gorilla/websocket"
)
type imResp struct {
Code int `json:"code"`
Reason string `json:"reason"`
Message string `json:"message"`
Data map[string]any `json:"data"`
RequestID string `json:"request_id"`
}
type result struct {
Chapter string
ID string
Name string
Method string
Path string
Status int
Code int
Reason string
Pass bool
Note string
Body string
}
type audit struct {
baseURL string
wsURL string
apiKey string
tenantCode string
client *http.Client
results []result
tokenA string
userA string
usernameA string
phoneA string
passA string
tokenB string
userB string
usernameB string
phoneB string
passB string
tokenC string
userC string
usernameC string
phoneC string
passC string
groupTopic string
groupVer int
groupQR string
friendReq string
p2pTopic string
meQR string
meQRPurpose string
meetingID string
meetingVer int
dailyID string
dailyVer int
monthlyID string
monthlyVer int
mailboxNoticeVersion string
actionTok string
abuseReason string
profileVer int
privacyVer int
feedbackCat string
}
func main() {
baseURL := flag.String("base", "https://api.118im.com", "HTTP base URL")
wsURL := flag.String("ws", "wss://api.118im.com/v0/channels", "WebSocket URL")
apiKey := flag.String("apikey", "AQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy", "MeetingIM API key")
tenant := flag.String("tenant", "default", "tenant code")
out := flag.String("out", "docs/test/im_frontend_backend_1_11_testenv_results_2026-08-20.md", "report path")
flag.Parse()
a := &audit{
baseURL: strings.TrimRight(*baseURL, "/"),
wsURL: *wsURL,
apiKey: *apiKey,
tenantCode: *tenant,
client: &http.Client{
Timeout: 20 * time.Second,
Transport: &http.Transport{
TLSClientConfig: &tls.Config{MinVersion: tls.VersionTLS12},
},
},
}
a.run()
if err := os.WriteFile(*out, []byte(a.report()), 0o644); err != nil {
panic(err)
}
fmt.Printf("wrote %s\n", *out)
}
func (a *audit) run() {
suffix := fmt.Sprintf("%08d", time.Now().UnixNano()%100000000)
a.usernameA, a.phoneA, a.passA = "a"+suffix, "155"+suffix, "AuditPass123"
a.usernameB, a.phoneB, a.passB = "b"+suffix, "156"+suffix, "AuditPass123"
a.usernameC, a.phoneC, a.passC = "c"+suffix, "159"+suffix, "AuditPass123"
a.testHTTP("1", "1.1", "企业码解析", "POST", "/v1/im/tenants/resolve", map[string]any{"tenant_code": a.tenantCode, "platform": "ios", "app_version": "1.0.0"}, "", true, []int{200}, func(r imResp) string {
return fmt.Sprintf("tenant=%v endpoints=%v", dig(r.Data, "tenant", "code"), dig(r.Data, "endpoints", "api"))
})
a.register("1", "1.2A", a.usernameA, a.phoneA, a.passA, true)
a.register("1", "1.2B", a.usernameB, a.phoneB, a.passB, false)
a.testHTTP("1", "1.6a", "忘记密码发码", "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "reset_password", "phone": a.phoneA}, "", true, []int{200}, func(r imResp) string {
challenge := asString(r.Data["challenge_id"])
if challenge == "" {
return "未返回 challenge_id"
}
a.testHTTP("1", "1.6b", "忘记密码验证码校验", "POST", "/v1/im/auth/password-reset/challenges/"+url.PathEscape(challenge)+"/verify", map[string]any{"code": "123456"}, "", true, []int{200}, func(rr imResp) string {
return "reset_token=" + mask(asString(rr.Data["reset_token"]))
})
return "challenge_id=" + mask(challenge)
})
a.testHTTP("1", "1.7a", "二次身份验证发起", "POST", "/v1/im/security/challenges", map[string]any{"scene": "close_youth_mode", "channel": "sms"}, a.tokenA, true, []int{200}, func(r imResp) string {
challenge := asString(r.Data["challenge_id"])
if challenge == "" {
challenge = asString(r.Data["id"])
}
if challenge != "" {
a.testHTTP("1", "1.7b", "二次身份验证码校验", "POST", "/v1/im/security/challenges/"+url.PathEscape(challenge)+"/verify", map[string]any{"code": "123456"}, a.tokenA, true, []int{200}, func(rr imResp) string {
a.actionTok = asString(rr.Data["action_token"])
return "action_token=" + mask(a.actionTok)
})
}
return "challenge_id=" + mask(challenge)
})
a.testHTTP("1", "1.8", "实名状态查询", "GET", "/v1/im/security/realname", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("1", "1.8-submit", "实名提交", "POST", "/v1/im/security/realname/submissions", map[string]any{"name": "张三", "id_type": "cn_id", "id_number": "110101199001011234"}, a.tokenA, true, []int{200, 409}, nil)
a.testHTTP("1", "1.11", "设置独立密码", "POST", "/v1/im/settings/independent-password", map[string]any{"password": "1234", "purpose": "youth_mode", "action_token": a.actionTok}, a.tokenA, true, []int{200, 400, 403}, nil)
a.testHTTP("1", "1.12", "应用版本查询", "GET", "/v1/im/app/releases/latest?platform=ios&current_version=1.0.0&channel=stable", nil, "", false, []int{200}, nil)
a.testHTTP("2", "2.1", "Bootstrap", "GET", "/v1/im/bootstrap", nil, a.tokenA, false, []int{200}, func(r imResp) string {
return fmt.Sprintf("entries=%v limits=%v", r.Data["entries"] != nil, r.Data["limits"] != nil)
})
a.testHTTP("2", "2.search", "会话搜索", "GET", "/v1/im/search/conversations?q=&cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("2", "2.sys", "系统通知列表", "GET", "/v1/im/system-notifications?limit=20", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("2", "2.sys-read", "系统通知已读不存在 ID", "POST", "/v1/im/system-notifications/nonexistent/read", map[string]any{"read_at": time.Now().UTC().Format(time.RFC3339)}, a.tokenA, true, []int{200, 404}, nil)
a.testHTTP("5", "5.search", "用户搜索 B", "GET", "/v1/im/users/search?q="+url.QueryEscape(a.usernameB)+"&type=account&cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("5", "5.profile", "查看 B 资料", "GET", "/v1/im/users/"+url.PathEscape(a.usernameB)+"/profile", nil, a.tokenA, false, []int{200, 403, 404}, nil)
a.testHTTP("5", "5.user-qr", "查看 B 加好友二维码", "POST", "/v1/im/users/"+url.PathEscape(a.usernameB)+"/qrcodes", map[string]any{"purpose": "add_friend", "expires_in_seconds": 86400}, a.tokenA, true, []int{200, 403, 404}, nil)
a.testHTTP("5", "5.friend-create", "创建好友申请 A->B", "POST", "/v1/im/friend-requests", map[string]any{"target_user_id": a.userB, "message": "联调好友申请", "source": "search"}, a.tokenA, true, []int{200}, func(r imResp) string {
a.friendReq = asString(r.Data["request_id"])
return "request_id=" + a.friendReq
})
if a.friendReq != "" {
a.testHTTP("5", "5.friend-accept", "B 同意好友申请", "POST", "/v1/im/friend-requests/"+url.PathEscape(a.friendReq)+"/accept", map[string]any{"version": 1}, a.tokenB, true, []int{200, 409}, func(r imResp) string {
a.p2pTopic = asString(r.Data["topic"])
return "topic=" + a.p2pTopic
})
}
a.testHTTP("3", "3.context", "P2P 会话上下文", "GET", "/v1/im/conversations/"+url.PathEscape(a.p2pTopic)+"/context", nil, a.tokenA, false, []int{200, 400, 404}, nil)
a.testHTTP("3", "3.p2p-create", "创建 P2P 会话目标接口", "POST", "/v1/im/conversations/p2p", map[string]any{"peer_user_id": a.userB}, a.tokenA, true, []int{200, 404, 405}, nil)
a.testHTTP("3", "3.conv-settings", "会话设置查询", "GET", "/v1/im/conversations/"+url.PathEscape(a.p2pTopic)+"/settings", nil, a.tokenA, false, []int{200, 404, 405}, nil)
a.testHTTP("3", "3.conv-messages", "会话消息列表", "GET", "/v1/im/conversations/"+url.PathEscape(a.p2pTopic)+"/messages?since=1&limit=20", nil, a.tokenA, false, []int{200, 404, 405}, nil)
a.testHTTP("4", "4.forward-targets", "转发目标列表", "GET", "/v1/im/forward-targets?query=&cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("4", "4.forward", "消息逐条转发", "POST", "/v1/im/messages/forward", map[string]any{"mode": "separate", "sources": []map[string]any{{"topic": a.p2pTopic, "seq": 1}}, "target_topics": []string{"slf"}}, a.tokenA, true, []int{200, 404, 405}, nil)
a.testHTTP("4", "4.bundle", "合并转发详情", "GET", "/v1/im/forward-bundles/fbundle_missing?cursor=", nil, a.tokenA, false, []int{200, 404, 405}, nil)
a.testHTTP("4", "4.recall", "消息撤回不存在 seq", "POST", "/v1/im/topics/"+url.PathEscape(a.p2pTopic)+"/messages/1/recall", map[string]any{"version": 1, "reason": "audit"}, a.tokenA, true, []int{200, 400, 404, 403}, nil)
a.testHTTP("4", "4.revision", "消息编辑不存在 seq", "POST", "/v1/im/topics/"+url.PathEscape(a.p2pTopic)+"/messages/1/revisions", map[string]any{"content": map[string]any{"text": "edit"}, "client_mid": "audit-mid", "version": 1}, a.tokenA, true, []int{200, 400, 403, 404, 409}, nil)
a.testHTTP("4", "4.receipts", "消息回执查询", "GET", "/v1/im/topics/"+url.PathEscape(a.p2pTopic)+"/messages/1/receipts?cursor=", nil, a.tokenA, false, []int{200, 400, 404, 403}, nil)
a.testHTTP("5", "5.friends", "好友列表", "GET", "/v1/im/friends?query=&initial=&cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("5", "5.requests", "好友申请列表", "GET", "/v1/im/friend-requests?direction=inbound&status=all&cursor=", nil, a.tokenB, false, []int{200}, nil)
a.testHTTP("5", "5.blocks", "黑名单列表", "GET", "/v1/im/blocks?cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("5", "5.block-put", "拉黑 B", "PUT", "/v1/im/blocks/"+url.PathEscape(a.userB), map[string]any{}, a.tokenA, true, []int{200}, nil)
a.testHTTP("5", "5.block-del", "解除拉黑 B", "DELETE", "/v1/im/blocks/"+url.PathEscape(a.userB), nil, a.tokenA, true, []int{200}, nil)
a.testHTTP("5", "5.me-qr", "我的二维码", "POST", "/v1/im/me/qrcodes", map[string]any{"purpose": "add_friend", "expires_in_seconds": 86400}, a.tokenA, true, []int{200}, func(r imResp) string {
a.meQR = asString(r.Data["qr_token"])
a.meQRPurpose = "add_friend"
return "qr_token=" + mask(a.meQR)
})
if a.meQR != "" {
a.testHTTP("5", "5.qr-resolve", "二维码解析", "POST", "/v1/im/qrcodes/resolve", map[string]any{"token": a.meQR, "purpose": a.meQRPurpose}, a.tokenB, true, []int{200}, nil)
}
a.testHTTP("6", "6.create", "创建群", "POST", "/v1/im/groups", map[string]any{"name": "auditgroup" + suffix, "description": "audit group", "member_user_ids": []string{a.userB}, "avatar_file_id": ""}, a.tokenA, true, []int{200}, func(r imResp) string {
a.groupTopic = asString(r.Data["topic"])
a.groupVer = asInt(r.Data["version"])
return fmt.Sprintf("topic=%s version=%d", a.groupTopic, a.groupVer)
})
groupPath := "/v1/im/groups/" + url.PathEscape(a.groupTopic)
a.testHTTP("6", "6.list", "群列表", "GET", "/v1/im/groups?category=owned&query=&cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("6", "6.detail", "群详情", "GET", groupPath, nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.patch", "更新群资料", "PATCH", groupPath, map[string]any{"description": "自动化测试群已更新", "version": a.groupVer}, a.tokenA, true, []int{200, 409, 404}, func(r imResp) string {
if asInt(r.Data["version"]) > 0 {
a.groupVer = asInt(r.Data["version"])
}
return fmt.Sprintf("version=%d", a.groupVer)
})
a.testHTTP("6", "6.settings", "群设置", "GET", groupPath+"/settings", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.member-settings", "群个人设置", "PATCH", groupPath+"/member-settings", map[string]any{"pinned": true, "mute_notifications": false}, a.tokenA, true, []int{200, 409, 404}, nil)
a.testHTTP("6", "6.my-profile", "群内昵称", "PUT", groupPath+"/members/me/profile", map[string]any{"nickname": "联调昵称", "version": 1}, a.tokenA, true, []int{200, 409, 404}, nil)
a.testHTTP("6", "6.members", "群成员", "GET", groupPath+"/members?query=&cursor=&mentionable=true", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.member-profile", "群成员资料", "GET", groupPath+"/members/"+url.PathEscape(a.userB)+"/profile", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.qrcode", "群二维码", "POST", groupPath+"/qrcodes", map[string]any{"expires_in_seconds": 86400}, a.tokenA, true, []int{200, 404}, func(r imResp) string {
a.groupQR = asString(r.Data["qr_token"])
return "qr_token=" + mask(a.groupQR)
})
a.testHTTP("6", "6.invite", "群邀请", "POST", groupPath+"/invitations", map[string]any{"user_ids": []string{a.userB}, "message": "邀请入群"}, a.tokenA, true, []int{200, 409, 404}, nil)
a.testHTTP("6", "6.join-requests", "入群申请列表", "GET", groupPath+"/join-requests?status=pending&cursor=", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.summary", "群概要", "GET", groupPath+"/summary", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.ann-latest", "群公告最新", "GET", groupPath+"/announcements/latest", nil, a.tokenA, false, []int{200, 404}, nil)
announcementID := ""
a.testHTTP("6", "6.ann-put", "发布群公告", "PUT", groupPath+"/announcements/current", map[string]any{"content": "联调公告", "version": 1}, a.tokenA, true, []int{200}, func(r imResp) string {
announcementID = asString(r.Data["announcement_id"])
return "announcement_id=" + announcementID
})
if announcementID != "" {
a.testHTTP("6", "6.ann-detail", "群公告详情", "GET", groupPath+"/announcements/"+url.PathEscape(announcementID), nil, a.tokenB, false, []int{200}, nil)
a.testHTTP("6", "6.ann-read", "群公告已读", "POST", groupPath+"/announcements/"+url.PathEscape(announcementID)+"/read", map[string]any{}, a.tokenB, true, []int{200}, nil)
}
a.testHTTP("6", "6.governance", "群治理配置", "GET", groupPath+"/governance", nil, a.tokenA, false, []int{200}, func(r imResp) string {
if version := asInt(r.Data["version"]); version > 0 {
a.groupVer = version
}
return fmt.Sprintf("version=%d", a.groupVer)
})
a.testHTTP("6", "6.governance-member-forbidden", "普通成员更新群治理被拒绝", "PATCH", groupPath+"/governance", map[string]any{"join_approval": true, "version": a.groupVer}, a.tokenB, true, []int{403}, nil)
a.testHTTP("6", "6.governance-patch", "更新群治理配置成功", "PATCH", groupPath+"/governance", map[string]any{"join_approval": true, "member_invite": true, "member_upload_files": true, "version": a.groupVer}, a.tokenA, true, []int{200}, func(r imResp) string {
if version := asInt(r.Data["version"]); version > 0 {
a.groupVer = version
}
return fmt.Sprintf("version=%d", a.groupVer)
})
a.testHTTP("6", "6.role-limits", "管理员数量限制", "GET", groupPath+"/role-limits", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.role-limits-member-forbidden", "普通成员读取管理员限制被拒绝", "GET", groupPath+"/role-limits", nil, a.tokenB, false, []int{403}, nil)
a.testHTTP("6", "6.mute-member-forbidden", "普通成员禁言他人被拒绝", "POST", groupPath+"/mutes/batch", map[string]any{"user_ids": []string{a.userA}, "reason": "越权测试"}, a.tokenB, true, []int{403}, nil)
a.testHTTP("6", "6.mute-member", "群主禁言成员", "POST", groupPath+"/mutes/batch", map[string]any{"user_ids": []string{a.userB}, "reason": "联调禁言"}, a.tokenA, true, []int{200}, nil)
a.testHTTP("6", "6.mute-self-view", "成员查看自身禁言状态", "GET", groupPath+"/mutes?cursor=", nil, a.tokenB, false, []int{200}, func(r imResp) string {
return fmt.Sprintf("items=%v", r.Data["items"])
})
a.testHTTP("6", "6.unmute-member", "群主解除成员禁言", "DELETE", groupPath+"/mutes/"+url.PathEscape(a.userB), nil, a.tokenA, true, []int{200}, nil)
a.testHTTP("6", "6.admin-add", "群主设置管理员", "POST", groupPath+"/admins/batch", map[string]any{"user_ids": []string{a.userB}, "version": a.groupVer}, a.tokenA, true, []int{200}, nil)
a.testHTTP("6", "6.admin-governance", "管理员更新群治理", "PATCH", groupPath+"/governance", map[string]any{"member_private_chat": true, "version": a.groupVer}, a.tokenB, true, []int{200, 409}, func(r imResp) string {
if version := asInt(r.Data["version"]); version > 0 {
a.groupVer = version
}
return fmt.Sprintf("version=%d", a.groupVer)
})
a.testHTTP("6", "6.admin-remove", "群主取消管理员", "DELETE", groupPath+"/admins/"+url.PathEscape(a.userB), nil, a.tokenA, true, []int{200}, nil)
a.registerThird("6", "6.join-user", a.usernameC, a.phoneC, a.passC)
joinRequestID, joinRequestVersion := "", 0
a.testHTTP("6", "6.join-create", "非成员提交入群申请", "POST", groupPath+"/join-requests", map[string]any{"message": "申请加入联调群", "source": "manual"}, a.tokenC, true, []int{200}, func(r imResp) string {
joinRequestID = asString(r.Data["request_id"])
joinRequestVersion = asInt(r.Data["version"])
return fmt.Sprintf("request_id=%s version=%d", joinRequestID, joinRequestVersion)
})
a.testHTTP("6", "6.join-list-member-forbidden", "普通成员读取入群申请被拒绝", "GET", groupPath+"/join-requests?status=pending&cursor=", nil, a.tokenB, false, []int{403}, nil)
a.testHTTP("6", "6.join-list-owner", "群主读取待审批申请", "GET", groupPath+"/join-requests?status=pending&cursor=", nil, a.tokenA, false, []int{200}, nil)
if joinRequestID != "" {
a.testHTTP("6", "6.join-accept", "群主通过入群申请", "POST", groupPath+"/join-requests/"+url.PathEscape(joinRequestID)+"/accept", map[string]any{"version": joinRequestVersion}, a.tokenA, true, []int{200}, nil)
a.testHTTP("6", "6.join-detail", "审批后新成员读取群详情", "GET", groupPath, nil, a.tokenC, false, []int{200}, nil)
a.testHTTP("6", "6.join-leave", "新成员主动退群", "POST", groupPath+"/leave", map[string]any{}, a.tokenC, true, []int{200}, nil)
}
a.testHTTP("6", "6.admins", "管理员列表", "GET", groupPath+"/members?role=admin&cursor=", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.mutes", "禁言列表", "GET", groupPath+"/mutes?cursor=", nil, a.tokenA, false, []int{200, 404}, nil)
a.testHTTP("6", "6.files", "群文件列表", "GET", groupPath+"/files?query=&type=&cursor=", nil, a.tokenA, false, []int{200, 404, 501}, nil)
a.testHTTP("7", "7.favorites", "收藏列表", "GET", "/v1/im/favorites?type=all&q=&cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("7", "7.favorite-batch", "批量收藏不存在消息", "POST", "/v1/im/favorites/batch", map[string]any{"messages": []map[string]any{{"topic": a.groupTopic, "seq": 1}}}, a.tokenA, true, []int{200, 400, 403, 404}, nil)
a.testHTTP("7", "7.message-search", "聊天记录搜索", "GET", "/v1/im/topics/"+url.PathEscape(a.groupTopic)+"/messages/search?q=%E8%81%94%E8%B0%83&type=text&cursor=", nil, a.tokenA, false, []int{200, 400, 403, 404}, nil)
a.testHTTP("7", "7.media", "会话媒体列表", "GET", "/v1/im/topics/"+url.PathEscape(a.groupTopic)+"/media?type=image&cursor=", nil, a.tokenA, false, []int{200, 400, 403, 404}, nil)
a.testHTTP("7", "7.msg-context", "消息上下文", "GET", "/v1/im/topics/"+url.PathEscape(a.groupTopic)+"/messages/1/context?before=20&after=20", nil, a.tokenA, false, []int{200, 400, 403, 404}, nil)
a.testHTTP("7", "7.file-download", "文件下载不存在文件", "GET", "/v0/file/s/nonexistent", nil, a.tokenA, false, []int{200, 400, 401, 403, 404}, nil)
a.testHTTP("8", "8.calls", "通话记录列表", "GET", "/v1/im/calls?limit=5", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("8", "8.topic-calls", "会话通话记录", "GET", "/v1/im/topics/"+url.PathEscape(a.p2pTopic)+"/calls?limit=5", nil, a.tokenA, false, []int{200, 400, 403, 404}, nil)
a.testHTTP("9", "9.workbench", "工作台", "GET", "/v1/im/workbench", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("9", "9.depts", "组织部门", "GET", "/v1/im/organization/departments?parent_id=&cursor=", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("9", "9.org-search-empty", "组织空关键词搜索", "GET", "/v1/im/organization/search?q=", nil, a.tokenA, false, []int{200, 400}, nil)
start := time.Now().UTC().Add(24 * time.Hour).Truncate(time.Minute)
end := start.Add(time.Hour)
a.testHTTP("9", "9.meeting-create", "创建会议", "POST", "/v1/im/meetings", map[string]any{"title": "联调会议", "start_at": start.Format(time.RFC3339), "end_at": end.Format(time.RFC3339), "location": "线上", "attendee_user_ids": []string{a.userB}, "reminder_minutes": []int{10}}, a.tokenA, true, []int{200}, func(r imResp) string {
a.meetingID = asString(r.Data["meeting_id"])
a.meetingVer = asInt(r.Data["version"])
return fmt.Sprintf("meeting_id=%s version=%d", a.meetingID, a.meetingVer)
})
a.testHTTP("9", "9.meeting-list", "会议列表", "GET", "/v1/im/meetings?limit=5", nil, a.tokenA, false, []int{200, 405}, nil)
if a.meetingID != "" {
a.testHTTP("9", "9.meeting-get", "会议详情", "GET", "/v1/im/meetings/"+url.PathEscape(a.meetingID), nil, a.tokenA, false, []int{200, 404}, nil)
}
today := time.Now().UTC().Format("2006-01-02")
a.testHTTP("9", "9.daily-list", "日报列表", "GET", "/v1/im/reports/daily?limit=5", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("9", "9.daily-create", "创建日报草稿", "POST", "/v1/im/reports/daily", map[string]any{"report_date": today}, a.tokenA, true, []int{200, 409}, func(r imResp) string {
a.dailyID = asString(r.Data["report_id"])
a.dailyVer = asInt(r.Data["version"])
return fmt.Sprintf("report_id=%s version=%d", a.dailyID, a.dailyVer)
})
a.testHTTP("9", "9.daily-stats", "日报统计", "GET", "/v1/im/reports/daily/statistics?date="+today, nil, a.tokenA, false, []int{200}, nil)
month := time.Now().UTC().Format("2006-01")
a.testHTTP("9", "9.monthly-list", "月报列表", "GET", "/v1/im/reports/monthly?limit=5", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("9", "9.monthly-create", "创建月报草稿", "POST", "/v1/im/reports/monthly", map[string]any{"month": month}, a.tokenA, true, []int{200, 409}, func(r imResp) string {
a.monthlyID = asString(r.Data["report_id"])
a.monthlyVer = asInt(r.Data["version"])
return fmt.Sprintf("report_id=%s version=%d", a.monthlyID, a.monthlyVer)
})
a.testHTTP("9", "9.monthly-stats", "月报统计", "GET", "/v1/im/reports/monthly/statistics?month="+month, nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("9", "9.mailbox-config", "总经理信箱配置", "GET", "/v1/im/executive-mailbox/config", nil, a.tokenA, false, []int{200}, func(r imResp) string {
a.mailboxNoticeVersion = asString(dig(r.Data, "notice", "version"))
return "notice_version=" + a.mailboxNoticeVersion
})
a.testHTTP("9", "9.mailbox-submit", "总经理信箱提交", "POST", "/v1/im/executive-mailbox/submissions", map[string]any{"subject": "联调建议", "content": "自动化测试提交", "anonymous": false, "attachment_file_ids": []string{}, "notice_version": a.mailboxNoticeVersion}, a.tokenA, true, []int{200, 400, 403}, nil)
a.testHTTP("10", "10.me", "我的账号", "GET", "/v1/im/me", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("10", "10.profile", "个人资料", "GET", "/v1/im/me/profile", nil, a.tokenA, false, []int{200}, func(r imResp) string {
a.profileVer = asInt(r.Data["version"])
return fmt.Sprintf("version=%d", a.profileVer)
})
a.testHTTP("10", "10.profile-patch", "更新个人资料", "PATCH", "/v1/im/me/profile", map[string]any{"nickname": "联调用户A", "signature": "audit", "version": a.profileVer}, a.tokenA, true, []int{200, 409}, nil)
a.testHTTP("10", "10.privacy", "隐私设置", "GET", "/v1/im/me/privacy", nil, a.tokenA, false, []int{200}, func(r imResp) string {
a.privacyVer = asInt(r.Data["version"])
return fmt.Sprintf("version=%d", a.privacyVer)
})
a.testHTTP("10", "10.privacy-patch", "更新隐私设置", "PATCH", "/v1/im/me/privacy", map[string]any{"add_me_methods": map[string]any{"account": true, "phone": true, "qr": true, "group": true}, "profile_visibility": "everyone", "last_seen_visibility": "friends", "read_receipts": true, "version": a.privacyVer}, a.tokenA, true, []int{200, 409}, nil)
a.testHTTP("10", "10.youth", "青少年模式", "GET", "/v1/im/settings/youth-mode", nil, a.tokenA, false, []int{200}, nil)
a.testHTTP("10", "10.independent-verify", "独立密码验证", "POST", "/v1/im/settings/independent-password/verify", map[string]any{"password": "1234", "purpose": "youth_mode"}, a.tokenA, true, []int{200, 400, 403}, nil)
a.testHTTP("10", "10.push", "设备 Push Token", "PUT", "/v1/im/devices/audit-device-"+suffix+"/push-token", map[string]any{"provider": "apns", "token": "audit-token-" + suffix, "platform": "ios", "app_version": "1.0.0"}, a.tokenA, true, []int{200}, nil)
a.testHTTP("11", "11.url", "URL 风险检查", "POST", "/v1/im/security/urls/check", map[string]any{"url": "https://example.com"}, a.tokenA, true, []int{200}, nil)
a.testHTTP("11", "11.abuse-reasons", "举报原因", "GET", "/v1/im/reports/abuse/reasons?target_type=user", nil, a.tokenA, false, []int{200}, func(r imResp) string {
if items, ok := r.Data["items"].([]any); ok && len(items) > 0 {
if first, ok := items[0].(map[string]any); ok {
a.abuseReason = asString(first["reason_code"])
if a.abuseReason == "" {
a.abuseReason = asString(first["code"])
}
}
}
return "reason=" + a.abuseReason
})
a.testHTTP("11", "11.abuse-submit", "提交举报", "POST", "/v1/im/reports/abuse", map[string]any{"target_type": "user", "target_id": a.userB, "reason_code": a.abuseReason, "description": "联调举报", "evidence": []map[string]any{}}, a.tokenA, true, []int{200, 400, 403}, nil)
a.testHTTP("11", "11.feedback-config", "反馈配置", "GET", "/v1/im/feedback/config", nil, a.tokenA, false, []int{200}, func(r imResp) string {
a.feedbackCat = "bug"
if items, ok := r.Data["categories"].([]any); ok && len(items) > 0 {
if s := asString(items[0]); s != "" {
a.feedbackCat = s
}
}
return "category=" + a.feedbackCat
})
a.testHTTP("11", "11.feedback", "提交反馈", "POST", "/v1/im/feedback", map[string]any{"category": a.feedbackCat, "content": "自动化联调反馈", "contact": "", "attachment_file_ids": []string{}, "client_context": map[string]any{"source": "imtestaudit"}, "include_diagnostics": true}, a.tokenA, true, []int{200}, nil)
a.testHTTP("6", "6.owner-version", "转让前读取群版本", "GET", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/governance", nil, a.tokenA, false, []int{200}, func(r imResp) string {
if version := asInt(r.Data["version"]); version > 0 {
a.groupVer = version
}
return fmt.Sprintf("version=%d", a.groupVer)
})
transferToken := a.securityActionToken("6", "6.owner-transfer", "group_owner_transfer", a.tokenA)
a.testHTTPHeaders("6", "6.owner-transfer-action", "群主转让", "POST", "/v1/im/groups/"+url.PathEscape(a.groupTopic)+"/owner-transfer", map[string]any{"target_user_id": a.userB, "version": a.groupVer}, a.tokenA, true, []int{200}, map[string]string{"X-Action-Token": transferToken}, nil)
dissolveToken := a.securityActionToken("6", "6.dissolve", "group_dissolve", a.tokenB)
a.testHTTPHeaders("6", "6.dissolve-action", "新群主解散群", "DELETE", "/v1/im/groups/"+url.PathEscape(a.groupTopic), map[string]any{"reason": "自动化验收完成"}, a.tokenB, true, []int{200}, map[string]string{"X-Action-Token": dissolveToken}, nil)
a.testHTTP("6", "6.dissolved-detail", "解散后群详情返回 Gone", "GET", "/v1/im/groups/"+url.PathEscape(a.groupTopic), nil, a.tokenA, false, []int{410}, nil)
a.testHTTP("1", "1.logout", "退出登录", "POST", "/v1/im/auth/logout", map[string]any{}, a.tokenA, true, []int{200, 404, 405}, nil)
}
func (a *audit) register(chapter, id, username, phone, password string, primary bool) {
var challenge, verification string
a.testHTTP(chapter, id+"-code", "注册发码 "+username, "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "registration_type": 1, "phone": phone}, "", true, []int{200}, func(r imResp) string {
challenge = asString(r.Data["challenge_id"])
return "challenge_id=" + mask(challenge)
})
if challenge != "" {
a.testHTTP(chapter, id+"-verify", "注册验证码校验 "+username, "POST", "/v1/im/auth/verification-codes/"+url.PathEscape(challenge)+"/verify", map[string]any{"code": "123456"}, "", true, []int{200}, func(r imResp) string {
verification = asString(r.Data["verification_token"])
return "verification_token=" + mask(verification)
})
}
token, userID, note, err := a.wsRegister(username, phone, password, challenge, verification)
res := result{Chapter: chapter, ID: id + "-ws-acc", Name: "WebSocket hi+acc 注册 " + username, Method: "WS", Path: "/v0/channels", Status: 0, Code: 0, Pass: err == nil, Note: note}
if err != nil {
res.Reason = err.Error()
}
a.results = append(a.results, res)
if primary {
a.tokenA, a.userA = token, userID
} else {
a.tokenB, a.userB = token, userID
}
}
func (a *audit) registerThird(chapter, id, username, phone, password string) {
var challenge, verification string
a.testHTTP(chapter, id+"-code", "入群用户注册发码 "+username, "POST", "/v1/im/auth/verification-codes", map[string]any{"scene": "register", "registration_type": 1, "phone": phone}, "", true, []int{200}, func(r imResp) string {
challenge = asString(r.Data["challenge_id"])
return "challenge_id=" + mask(challenge)
})
if challenge != "" {
a.testHTTP(chapter, id+"-verify", "入群用户验证码校验 "+username, "POST", "/v1/im/auth/verification-codes/"+url.PathEscape(challenge)+"/verify", map[string]any{"code": "123456"}, "", true, []int{200}, func(r imResp) string {
verification = asString(r.Data["verification_token"])
return "verification_token=" + mask(verification)
})
}
token, userID, note, err := a.wsRegister(username, phone, password, challenge, verification)
res := result{Chapter: chapter, ID: id + "-ws-acc", Name: "入群用户 WebSocket 注册 " + username, Method: "WS", Path: "/v0/channels", Pass: err == nil, Note: note}
if err != nil {
res.Reason = err.Error()
}
a.results = append(a.results, res)
a.tokenC, a.userC = token, userID
}
func (a *audit) wsRegister(username, phone, password, challenge, verification string) (string, string, string, error) {
u, _ := url.Parse(a.wsURL)
q := u.Query()
q.Set("apikey", a.apiKey)
u.RawQuery = q.Encode()
conn, _, err := websocket.DefaultDialer.Dial(u.String(), nil)
if err != nil {
return "", "", "", err
}
defer conn.Close()
deadline := time.Now().Add(15 * time.Second)
_ = conn.SetReadDeadline(deadline)
_ = conn.SetWriteDeadline(deadline)
hiID := "hi-" + username
if err = conn.WriteJSON(map[string]any{"hi": map[string]any{"id": hiID, "ver": "0.25", "ua": "imtestaudit/1.0", "platf": "go", "lang": "zh-CN", "tenant": a.tenantCode}}); err != nil {
return "", "", "", err
}
if _, err = waitCtrl(conn, hiID); err != nil {
return "", "", "", fmt.Errorf("hi: %w", err)
}
accID := "acc-" + username
secret := base64.StdEncoding.EncodeToString([]byte(username + ":" + password))
err = conn.WriteJSON(map[string]any{"acc": map[string]any{
"id": accID,
"user": "new",
"scheme": "basic",
"username": username,
"phone": phone,
"registration_type": 1,
"secret": secret,
"login": true,
"cred": []map[string]any{{
"meth": "tel",
"val": phone,
"resp": verification,
"params": map[string]any{"challenge_id": challenge},
}},
"desc": map[string]any{"public": map[string]any{"fn": username}},
}})
if err != nil {
return "", "", "", err
}
ctrl, err := waitCtrl(conn, accID)
if err != nil {
return "", "", "", fmt.Errorf("acc: %w", err)
}
params, _ := ctrl["params"].(map[string]any)
token := asString(params["token"])
user := asString(params["user"])
return token, user, "user=" + user + " token=" + mask(token), nil
}
func waitCtrl(conn *websocket.Conn, id string) (map[string]any, error) {
for i := 0; i < 20; i++ {
var msg map[string]any
if err := conn.ReadJSON(&msg); err != nil {
return nil, err
}
ctrl, ok := msg["ctrl"].(map[string]any)
if !ok || asString(ctrl["id"]) != id {
continue
}
code := asInt(ctrl["code"])
if code < 200 || code >= 300 {
return ctrl, fmt.Errorf("ctrl code=%d text=%s", code, asString(ctrl["text"]))
}
return ctrl, nil
}
return nil, fmt.Errorf("ctrl %s not received", id)
}
func (a *audit) testHTTP(chapter, id, name, method, path string, body any, token string, idem bool, want []int, note func(imResp) string) {
a.testHTTPHeaders(chapter, id, name, method, path, body, token, idem, want, nil, note)
}
func (a *audit) testHTTPHeaders(chapter, id, name, method, path string, body any, token string, idem bool, want []int, headers map[string]string, note func(imResp) string) {
status, parsed, raw, err := a.callHeaders(method, path, body, token, idem, headers)
r := result{Chapter: chapter, ID: id, Name: name, Method: method, Path: path, Status: status, Code: parsed.Code, Reason: parsed.Reason, Body: compact(raw)}
if err != nil {
r.Reason = err.Error()
} else {
for _, s := range want {
if status == s {
r.Pass = true
break
}
}
if note != nil {
r.Note = note(parsed)
}
}
a.results = append(a.results, r)
}
func (a *audit) call(method, path string, body any, token string, idem bool) (int, imResp, string, error) {
return a.callHeaders(method, path, body, token, idem, nil)
}
func (a *audit) callHeaders(method, path string, body any, token string, idem bool, headers map[string]string) (int, imResp, string, error) {
var reader io.Reader
if body != nil {
b, _ := json.Marshal(body)
reader = bytes.NewReader(b)
}
ctx, cancel := context.WithTimeout(context.Background(), 20*time.Second)
defer cancel()
target := path
if strings.HasPrefix(path, "/") {
target = a.baseURL + path
}
req, err := http.NewRequestWithContext(ctx, method, target, reader)
if err != nil {
return 0, imResp{}, "", err
}
req.Header.Set("X-MeetingIM-APIKey", a.apiKey)
req.Header.Set("X-IM-Schema", "1")
req.Header.Set("X-Request-ID", "audit-"+strings.ReplaceAll(time.Now().Format("150405.000000000"), ".", ""))
req.Header.Set("X-Tenant-Code", a.tenantCode)
if body != nil {
req.Header.Set("Content-Type", "application/json")
}
if token != "" {
req.Header.Set("Authorization", "Bearer "+token)
if strings.HasPrefix(path, "/v0/file/") {
req.Header.Set("X-MeetingIM-Auth", "token "+base64.StdEncoding.EncodeToString([]byte(token)))
}
}
if idem {
req.Header.Set("Idempotency-Key", "audit-"+fmt.Sprint(time.Now().UnixNano()))
}
for key, value := range headers {
req.Header.Set(key, value)
}
resp, err := a.client.Do(req)
if err != nil {
return 0, imResp{}, "", err
}
defer resp.Body.Close()
b, _ := io.ReadAll(io.LimitReader(resp.Body, 8192))
var parsed imResp
_ = json.Unmarshal(b, &parsed)
return resp.StatusCode, parsed, string(b), nil
}
func (a *audit) securityActionToken(chapter, id, scene, token string) string {
challengeID, actionToken := "", ""
a.testHTTP(chapter, id+"-challenge", scene+" 二次验证发起", "POST", "/v1/im/security/challenges", map[string]any{"scene": scene, "channel": "sms"}, token, true, []int{200}, func(r imResp) string {
challengeID = asString(r.Data["challenge_id"])
return "challenge_id=" + mask(challengeID)
})
if challengeID != "" {
a.testHTTP(chapter, id+"-verify", scene+" 二次验证码校验", "POST", "/v1/im/security/challenges/"+url.PathEscape(challengeID)+"/verify", map[string]any{"code": "123456"}, token, true, []int{200}, func(r imResp) string {
actionToken = asString(r.Data["action_token"])
return "action_token=" + mask(actionToken)
})
}
return actionToken
}
func (a *audit) report() string {
var b strings.Builder
now := time.Now().Format("2006-01-02 15:04:05 -0700")
fmt.Fprintf(&b, "# IM 前后端 1-11 章测试环境接口测试结果(2026-08-20)\n\n")
fmt.Fprintf(&b, "## 1. 测试范围\n\n")
fmt.Fprintf(&b, "- 对照文档:`docs/IM-前后端对接文档.md`\n")
fmt.Fprintf(&b, "- 测试时间:`%s`\n", now)
fmt.Fprintf(&b, "- 测试环境:`%s`\n", a.baseURL)
fmt.Fprintf(&b, "- WebSocket:`%s`\n", a.wsURL)
fmt.Fprintf(&b, "- 租户:`%s`\n", a.tenantCode)
fmt.Fprintf(&b, "- 测试账号 A:`%s` / `%s` / `%s`\n", a.usernameA, a.phoneA, a.userA)
fmt.Fprintf(&b, "- 测试账号 B:`%s` / `%s` / `%s`\n", a.usernameB, a.phoneB, a.userB)
fmt.Fprintf(&b, "- API Key 与 Token 已脱敏,不在文档中展开。\n\n")
pass, fail := 0, 0
chapters := map[string][2]int{}
for _, r := range a.results {
pair := chapters[r.Chapter]
if r.Pass {
pass++
pair[0]++
} else {
fail++
pair[1]++
}
chapters[r.Chapter] = pair
}
fmt.Fprintf(&b, "## 2. 总体结论\n\n")
fmt.Fprintf(&b, "本轮按前后端对接文档从第 1 章到第 11 章直接访问测试环境接口,共执行 `%d` 项,符合预期 `%d` 项,不符合预期 `%d` 项。符合预期不等于业务全链路完全验收;部分接口以 404/405/409/403 作为当前文档标注的未实现、无数据或权限分支记录。\n\n", len(a.results), pass, fail)
fmt.Fprintf(&b, "| 章 | 符合预期 | 不符合预期 |\n| --- | ---: | ---: |\n")
keys := make([]string, 0, len(chapters))
for k := range chapters {
keys = append(keys, k)
}
sort.Slice(keys, func(i, j int) bool { return chapterOrder(keys[i]) < chapterOrder(keys[j]) })
for _, k := range keys {
pair := chapters[k]
fmt.Fprintf(&b, "| %s | %d | %d |\n", k, pair[0], pair[1])
}
fmt.Fprintf(&b, "\n## 3. 需要关注的问题\n\n")
hasFail := false
for _, r := range a.results {
if !r.Pass {
hasFail = true
fmt.Fprintf(&b, "- `%s` %s `%s`:HTTP `%d`,code `%d`,reason `%s`。%s\n", r.ID, r.Method, r.Path, r.Status, r.Code, r.Reason, r.Note)
}
}
if !hasFail {
fmt.Fprintf(&b, "- 本轮自动化黑盒测试未发现非预期失败。\n")
}
fmt.Fprintf(&b, "\n## 4. 分章明细\n\n")
for _, k := range keys {
fmt.Fprintf(&b, "### 第 %s 章\n\n", k)
fmt.Fprintf(&b, "| ID | 场景 | 方法 | 路径 | HTTP | code/reason | 结果 | 备注 |\n| --- | --- | --- | --- | ---: | --- | --- | --- |\n")
for _, r := range a.results {
if r.Chapter != k {
continue
}
status := "通过"
if !r.Pass {
status = "未通过"
}
reason := strings.TrimSpace(fmt.Sprintf("%d/%s", r.Code, r.Reason))
fmt.Fprintf(&b, "| `%s` | %s | `%s` | `%s` | %d | `%s` | %s | %s |\n", r.ID, esc(r.Name), r.Method, esc(r.Path), r.Status, esc(reason), status, esc(r.Note))
}
fmt.Fprintf(&b, "\n")
}
fmt.Fprintf(&b, "## 5. 测试产生的数据\n\n")
fmt.Fprintf(&b, "- 用户 A:`%s`,username `%s`,phone `%s`\n", a.userA, a.usernameA, a.phoneA)
fmt.Fprintf(&b, "- 用户 B:`%s`,username `%s`,phone `%s`\n", a.userB, a.usernameB, a.phoneB)
fmt.Fprintf(&b, "- 好友申请:`%s`\n", a.friendReq)
fmt.Fprintf(&b, "- P2P Topic:`%s`\n", a.p2pTopic)
fmt.Fprintf(&b, "- 群 Topic:`%s`\n", a.groupTopic)
fmt.Fprintf(&b, "- 会议:`%s`\n", a.meetingID)
fmt.Fprintf(&b, "- 日报草稿:`%s`\n", a.dailyID)
fmt.Fprintf(&b, "- 月报草稿:`%s`\n", a.monthlyID)
fmt.Fprintf(&b, "\n这些数据位于测试环境租户 `%s`,可按需清理。\n", a.tenantCode)
return b.String()
}
func asString(v any) string {
switch t := v.(type) {
case string:
return t
default:
return ""
}
}
func asInt(v any) int {
switch t := v.(type) {
case int:
return t
case float64:
return int(t)
case json.Number:
n, _ := t.Int64()
return int(n)
default:
return 0
}
}
func dig(m map[string]any, path ...string) any {
var cur any = m
for _, p := range path {
obj, ok := cur.(map[string]any)
if !ok {
return nil
}
cur = obj[p]
}
return cur
}
func mask(s string) string {
if len(s) <= 8 {
return s
}
return s[:4] + "..." + s[len(s)-4:]
}
func compact(s string) string {
s = strings.TrimSpace(strings.ReplaceAll(s, "\n", " "))
if len(s) > 220 {
return s[:220] + "..."
}
return s
}
func esc(s string) string {
s = strings.ReplaceAll(s, "|", "\\|")
s = strings.ReplaceAll(s, "\n", " ")
return s
}
func chapterOrder(s string) int {
var n int
_, _ = fmt.Sscanf(s, "%d", &n)
return n
}
File diff suppressed because it is too large Load Diff
+61
View File
@@ -0,0 +1,61 @@
# im-server-test ArgoCD patch
The live StatefulSet is owned by the ArgoCD Application `im-server-test`.
Copy `statefulset-performance-patch.yaml` into that application's Git source
and reference it from the test overlay:
```yaml
patches:
- path: statefulset-performance-patch.yaml
```
Also include `backend-debug-metrics-rbac.yaml` as a resource in the test
overlay. It grants the existing `argocd/backend-debug` service account
namespaced read access to Pod metrics so the load-test collector can run
`kubectl top pods -n im-server-system`:
```yaml
resources:
- backend-debug-metrics-rbac.yaml
```
After ArgoCD sync, verify the effective configuration:
```bash
kubectl -n im-server-system get statefulset im-server -o jsonpath='{.spec.template.spec.containers[0].resources}'
kubectl -n im-server-system get statefulset im-server -o jsonpath='{.spec.template.spec.containers[0].livenessProbe.httpGet.path}'
kubectl -n im-server-system get statefulset im-server -o jsonpath='{.spec.template.spec.containers[0].env}'
kubectl -n im-server-system get statefulset im-server -o jsonpath='{.spec.template.spec.dnsConfig.options}'
kubectl auth can-i list pods.metrics.k8s.io \
--as=system:serviceaccount:argocd:backend-debug \
-n im-server-system
```
Expected values are requests `2 CPU / 512Mi`, limits `2 CPU / 1Gi`, and
liveness path `/livez`. The MySQL pool environment must be
`MYSQL_MAX_OPEN_CONNS=24`, `MYSQL_MAX_IDLE_CONNS=8`,
`MYSQL_REGISTRY_MAX_OPEN_CONNS=4`, and `MYSQL_REGISTRY_MAX_IDLE_CONNS=2`.
The MySQL connection lifetime must be `MYSQL_CONN_MAX_LIFETIME_SECONDS=1800`.
The WebSocket heartbeat environment must be `WS_PING_PERIOD_SECONDS=15` and
`WS_PONG_WAIT_SECONDS=90`.
The Pod DNS options must be `ndots:1`, `timeout:2`, and `attempts:2`. Keeping
`ClusterFirst` preserves short Kubernetes service-name resolution, while the
lower `ndots` value prevents fully qualified internal and external dependency
names from being expanded through every cluster search suffix first.
The application repository's deploy workflow waits for reconciliation and
fails if ArgoCD restores different values.
After the rollout, verify the generated resolver configuration on every Pod:
```bash
for pod in im-server-0 im-server-1 im-server-2; do
echo "--- ${pod}"
kubectl -n im-server-system exec "${pod}" -- cat /etc/resolv.conf
done
```
The output must retain the cluster DNS nameserver and search domains, with
`options ndots:1 timeout:2 attempts:2`. This mitigation reduces amplified DNS
traffic from the application Pods. If lookup timeouts remain under load,
CoreDNS capacity or NodeLocal DNSCache must be changed in the cluster
infrastructure repository by an administrator with `kube-system` access.
@@ -0,0 +1,27 @@
apiVersion: rbac.authorization.k8s.io/v1
kind: Role
metadata:
name: backend-debug-metrics-reader
namespace: im-server-system
rules:
- apiGroups:
- metrics.k8s.io
resources:
- pods
verbs:
- get
- list
---
apiVersion: rbac.authorization.k8s.io/v1
kind: RoleBinding
metadata:
name: backend-debug-metrics-reader
namespace: im-server-system
roleRef:
apiGroup: rbac.authorization.k8s.io
kind: Role
name: backend-debug-metrics-reader
subjects:
- kind: ServiceAccount
name: backend-debug
namespace: argocd
@@ -0,0 +1,71 @@
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: im-server
namespace: im-server-system
spec:
template:
spec:
dnsPolicy: ClusterFirst
dnsConfig:
options:
- name: ndots
value: "1"
- name: timeout
value: "2"
- name: attempts
value: "2"
containers:
- name: im-server
env:
- name: WS_PING_PERIOD_SECONDS
value: "15"
- name: WS_PONG_WAIT_SECONDS
value: "90"
- name: CLUSTER_PROXY_TO_MASTER_BUFFER
value: "4096"
- name: CLUSTER_PROXY_TO_MASTER_WORKERS
value: "16"
- name: CLUSTER_PROXY_TO_MASTER_ENQUEUE_TIMEOUT_MS
value: "300"
- name: CLUSTER_ROUTE_RETRIES
value: "3"
- name: CLUSTER_ROUTE_RETRY_BACKOFF_MS
value: "25"
- name: TOPIC_CLIENT_QUEUE_BUFFER
value: "4096"
- name: TOPIC_SERVER_QUEUE_BUFFER
value: "512"
- name: TOPIC_PUBLISH_ENQUEUE_WAIT_MS
value: "200"
- name: MYSQL_MAX_OPEN_CONNS
value: "24"
- name: MYSQL_MAX_IDLE_CONNS
value: "8"
- name: MYSQL_REGISTRY_MAX_OPEN_CONNS
value: "4"
- name: MYSQL_REGISTRY_MAX_IDLE_CONNS
value: "2"
- name: MYSQL_CONN_MAX_LIFETIME_SECONDS
value: "1800"
resources:
requests:
cpu: "2"
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
readinessProbe:
httpGet:
path: /healthz
port: 6060
timeoutSeconds: 3
periodSeconds: 10
failureThreshold: 3
livenessProbe:
httpGet:
path: /livez
port: 6060
timeoutSeconds: 3
periodSeconds: 20
failureThreshold: 3
+12 -4
View File
@@ -188,6 +188,14 @@ You can specify the following environment variables when issuing `docker run` co
| `AWS_S3_ENDPOINT`[^2] | string | | An endpoint URL (hostname only or fully qualified URI) to override the default endpoint; can be of any S3-compatible service, such as `minio-api.x.io` |
| `AWS_SECRET_ACCESS_KEY`[^2] | string | | AWS [Secret Access Key](https://aws.amazon.com/blogs/security/wheres-my-secret-access-key/) when using `s3` media handler. |
| `CLUSTER_SELF` | string | | Node name if the server is running in a Tinode cluster. |
| `CLUSTER_PROXY_TO_MASTER_BUFFER` | number | `4096` | Total bounded proxy-to-master queue capacity for each peer. |
| `CLUSTER_PROXY_TO_MASTER_WORKERS` | number | `16` | Ordered queue shards and synchronous RPC workers for each peer. |
| `CLUSTER_PROXY_TO_MASTER_ENQUEUE_TIMEOUT_MS` | number | `300` | Maximum wait for a full proxy-to-master shard before returning 503. |
| `CLUSTER_ROUTE_RETRIES` | number | `3` | Number of retries after a remote server-message route is rejected as overloaded. |
| `CLUSTER_ROUTE_RETRY_BACKOFF_MS` | number | `25` | Initial exponential retry delay for remote server-message routes. |
| `TOPIC_CLIENT_QUEUE_BUFFER` | number | `4096` | Per-topic client publish queue capacity. Increase for large groups with bursty publishers. |
| `TOPIC_SERVER_QUEUE_BUFFER` | number | `512` | Per-topic server-message queue capacity. |
| `TOPIC_PUBLISH_ENQUEUE_WAIT_MS` | number | `200` | Maximum wait for an attached session publish to enter a full topic queue before returning 503. |
| `DEBUG_EMAIL_VERIFICATION_CODE`[^2] | string | | Enable dummy email verification code, e.g. `123456`. Disabled by default (empty string). |
| `DEFAULT_COUNTRY_CODE`[^2] | string | `US` | 2-letter country code to assign to sessions by default when the country isn't specified by the client explicitly and it's impossible to infer it. |
| `EXT_CONFIG`[^1] | string | | Path to external config file to use instead of the built-in one. If this parameter is used, most other variables are ignored[^1]. |
@@ -202,10 +210,10 @@ You can specify the following environment variables when issuing `docker run` co
| `FCM_MEASUREMENT_ID` | string | | Google Analytics ID of the form `G-123ABCD789`. |
| `FS_CORS_ORIGINS`[^2] | string | `["*"]` | Cors origins when media is served from the file system. See `AWS_CORS_ORIGINS` for details. |
| `ICE_SERVERS_FILE`[^2] | string | | Path to JSON file with configuration of ICE servers to be used for video calls. |
| `MEDIA_HANDLER`[^2] | string | `fs` | Handler of large files, either `fs` or `s3`. |
| `MYSQL_DSN`[^2] | string | <code>'root@tcp(mysql)/tinode?&#8203;parseTime=true&#8203;&collation=utf8mb4_0900_ai_ci'</code> | MySQL [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name). |
| `MEDIA_HANDLER`[^2] | string | `sys_config` | Legacy override for the large file handler. The generated runtime config uses `sys_config` so production can resolve `oss` from backend configuration. |
| `MYSQL_DSN`[^2] | string | <code>'root@tcp(mysql)/meeting_im?&#8203;parseTime=true&#8203;&collation=utf8mb4_0900_ai_ci'</code> | MySQL [DSN](https://github.com/go-sql-driver/mysql#dsn-data-source-name). |
| `PLUGIN_PYTHON_CHAT_BOT_ENABLED`[^2] | bool | `false` | Enable calling into the plugin provided by Python chatbot. |
| `POSTGRES_DSN`[^2] | string | <code>'postgresql://postgres:postgres@&#8203;localhost:5432/tinode?&#8203;sslmode=disable&#8203;&connect_timeout=10'</code> | PostgreSQL [DSN](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). |
| `POSTGRES_DSN`[^2] | string | <code>'postgresql://postgres:postgres@&#8203;localhost:5432/meeting_im?&#8203;sslmode=disable&#8203;&connect_timeout=10'</code> | PostgreSQL [DSN](https://www.postgresql.org/docs/current/libpq-connect.html#LIBPQ-CONNSTRING). |
| `RESET_DB` | bool | `false` | Drop and recreate the database. |
| `SAMPLE_DATA` | string | _see comment →_ | File with sample data to load. Default `data.json` when resetting or generating new DB, none when upgrading. Use `` (empty string) to disable. |
| `SMTP_AUTH_MECHANISM`[^2] | string | `"plain"` | SMTP authentication mechanism to use; one of "login", "cram-md5", "plain". |
@@ -224,7 +232,7 @@ You can specify the following environment variables when issuing `docker run` co
| `TLS_DOMAIN_NAME`[^2] | string | | If non-empty, enables TLS (http**s**) and configures domain name of your container, e.g. `www.example.com`. In order for TLS to work you have to expose your HTTPS port to the Internet and correctly configure DNS. It WILL FAIL with `localhost` or unroutable IPs. |
| `TNPG_AUTH_TOKEN` | string | | Tinode Push Gateway authentication token. |
| `TNPG_ORG`[^2] | string | | Tinode Push Gateway organization name as registered at https://console.tinode.co |
| `UID_ENCRYPTION_KEY`[^2] | string | `la6YsO+bNX/+XIkOqc5Svw==` | base64-encoded 16 random bytes used as an encryption key for user IDs. |
| `UID_ENCRYPTION_KEY`[^2] | string | `dkXxWLwqUNRfYmdXRzOkZw==` | base64-encoded 16 random bytes used as an encryption key for user IDs. |
| `UPGRADE_DB` | bool | `false` | Upgrade database schema, if necessary. |
| `WAIT_FOR` | string | | If non-empty, waits for the specified database `host:port` to be available before starting the server. |
+5
View File
@@ -22,6 +22,11 @@ x-exporter:
x-tinode-env-vars: &tinode-env-vars
"STORE_USE_ADAPTER": "mysql"
"PPROF_URL": "/pprof"
"REDIS_ADDR": "host.docker.internal:56379"
"REDIS_PASSWORD": "HMTJzkm.h>Y+E9ep"
"REDIS_DB": 0
"REDIS_KEY_PREFIX": "im"
"REDIS_GROUP_FANOUT_ENABLED": "true"
# You can provide your own tinode config by setting EXT_CONFIG env var and binding your configuration file to
# "EXT_CONFIG": "/etc/tinode/tinode.conf"
"WAIT_FOR": "mysql:3306"
@@ -17,6 +17,11 @@ x-tinode:
x-tinode-env-vars: &tinode-env-vars
"STORE_USE_ADAPTER": "mysql"
"PPROF_URL": "/pprof"
"REDIS_ADDR": "host.docker.internal:56379"
"REDIS_PASSWORD": "HMTJzkm.h>Y+E9ep"
"REDIS_DB": 0
"REDIS_KEY_PREFIX": "im"
"REDIS_GROUP_FANOUT_ENABLED": "true"
# You can provide your own tinode config by setting EXT_CONFIG env var and binding your configuration file to
# "EXT_CONFIG": "/etc/tinode/tinode.conf"
"WAIT_FOR": "mysql:3306"
+84 -8
View File
@@ -41,7 +41,7 @@ ENV RESET_DB=false
ENV UPGRADE_DB=false
# Option to skip DB initialization when it's missing.
ENV NO_DB_INIT=false
ENV NO_DB_INIT=true
# Load sample data to database from data.json.
ARG SAMPLE_DATA=data.json
@@ -51,16 +51,77 @@ ENV SAMPLE_DATA=$SAMPLE_DATA
ENV DEFAULT_COUNTRY_CODE=US
# The MySQL DSN connection.
ENV MYSQL_DSN='root@tcp(mysql)/tinode?parseTime=true&collation=utf8mb4_0900_ai_ci'
ENV MYSQL_DSN='root@tcp(mysql)/meeting_im?parseTime=true&collation=utf8mb4_0900_ai_ci'
ENV REDIS_ADDR=
ENV REDIS_SENTINEL_MASTER_NAME=
ENV REDIS_SENTINEL_ADDRS_JSON='[]'
ENV REDIS_USERNAME=
ENV REDIS_PASSWORD=
ENV REDIS_DB=0
ENV REDIS_KEY_PREFIX=im
ENV REDIS_DIAL_TIMEOUT_MS=2000
ENV REDIS_READ_TIMEOUT_MS=2000
ENV REDIS_WRITE_TIMEOUT_MS=2000
ENV REDIS_PENDING_TTL_SECONDS=120
ENV REDIS_DONE_TTL_SECONDS=86400
ENV REDIS_FAILED_TTL_SECONDS=600
ENV REDIS_HOT_MESSAGE_TTL_SECONDS=86400
ENV REDIS_PRESENCE_TTL_SECONDS=120
ENV REDIS_CACHE_TTL_SECONDS=300
ENV REDIS_SHORT_STATE_TTL_SECONDS=600
ENV REDIS_GROUP_FANOUT_ENABLED=true
ENV KAFKA_ENABLED=false
ENV KAFKA_BROKERS_JSON='[]'
ENV KAFKA_CLIENT_ID=im-server
ENV KAFKA_CONSUMER_GROUP=im-server
ENV KAFKA_DIAL_TIMEOUT_MS=3000
ENV KAFKA_WRITE_TIMEOUT_MS=3000
ENV KAFKA_READ_TIMEOUT_MS=3000
ENV KAFKA_BATCH_TIMEOUT_MS=10
ENV KAFKA_REQUIRED_ACKS=1
ENV KAFKA_ALLOW_AUTO_TOPIC_CREATION=false
ENV KAFKA_TLS_ENABLED=false
ENV KAFKA_SASL_ENABLED=false
ENV KAFKA_SASL_MECHANISM=SCRAM-SHA-512
ENV KAFKA_SASL_USERNAME=
ENV KAFKA_SASL_PASSWORD=
ENV KAFKA_MAX_RETRY_COUNT=3
ENV KAFKA_RETRY_BACKOFF_MS='[10000,30000,60000,300000,900000]'
ENV KAFKA_MESSAGE_BATCH_SIZE=100
ENV KAFKA_MESSAGE_BATCH_WAIT_MS=10
ENV KAFKA_MESSAGE_SINGLE_RETRY_COUNT=3
ENV KAFKA_DEAD_LETTER_RETENTION_SECONDS=7776000
ENV KAFKA_TOPIC_ADMIN_COMMAND=im.admin.command
ENV KAFKA_TOPIC_ADMIN_COMMAND_RESULT=im.admin.command.result
ENV KAFKA_TOPIC_CONFIG_EVENT=im.config.event
ENV KAFKA_TOPIC_BUSINESS_EVENT=im.business.event
ENV KAFKA_TOPIC_MESSAGE_PERSIST=im.message.persist
ENV KAFKA_TOPIC_GROUP_MESSAGE_PERSIST=im.group.message.persist
ENV KAFKA_TOPIC_MESSAGE_PUSH=im.message.push
ENV KAFKA_TOPIC_FILE_SECURITY=im.file.security
ENV KAFKA_TOPIC_CACHE_INVALIDATE=im.cache.invalidate
ENV KAFKA_TOPIC_AUDIT_EVENT=im.audit.event
ENV KAFKA_TOPIC_DEAD_LETTER=im.dead.letter
ENV INTERNAL_API_ENABLED=true
ENV INTERNAL_API_TIME_WINDOW_SECONDS=300
ENV INTERNAL_API_HOTGO_SERVICE_NAME=hotgo-admin
ENV INTERNAL_API_HOTGO_SECRET=
ENV INTERNAL_API_CULTIVATE_SERVICE_NAME=cultivate-server
ENV INTERNAL_API_CULTIVATE_SECRET=
ENV ADMIN_INTERNAL_API_ENABLED=false
ENV ADMIN_INTERNAL_API_BASE_URL=
ENV ADMIN_INTERNAL_API_SERVICE_NAME=im-server
ENV ADMIN_INTERNAL_API_SECRET=
ENV ADMIN_INTERNAL_API_TIMEOUT_MS=3000
# The PostgreSQL DSN connection.
ENV POSTGRES_DSN='postgresql://postgres:postgres@localhost:5432/tinode?sslmode=disable&connect_timeout=10'
ENV POSTGRES_DSN='postgresql://postgres:postgres@localhost:5432/meeting_im?sslmode=disable&connect_timeout=10'
# Disable chatbot plugin by default.
ENV PLUGIN_PYTHON_CHAT_BOT_ENABLED=false
# Default handler for large files
ENV MEDIA_HANDLER=fs
ENV MEDIA_HANDLER=sys_config
# Whitelisted domains for file and S3 large media handler.
ENV FS_CORS_ORIGINS='["*"]'
@@ -99,7 +160,7 @@ ENV API_KEY_SALT=T713/rYYgW7g4m3vG6zGRh7+FM1t0T8j13koXScOAj4=
ENV AUTH_TOKEN_KEY=wfaY2RgF2S1OQI/ZlK+LSrp1KB2jwAdGAIHQ7JZn+Kc=
# Key to initialize UID generator
ENV UID_ENCRYPTION_KEY=la6YsO+bNX/+XIkOqc5Svw==
ENV UID_ENCRYPTION_KEY=dkXxWLwqUNRfYmdXRzOkZw==
# Disable TLS by default.
ENV TLS_ENABLED=false
@@ -128,9 +189,23 @@ ENV TNPG_AUTH_TOKEN=
# Tinode Push Gateway organization name as registered at console.tinode.co
ENV TNPG_ORG=
# Disable JPush by default. The handler can be enabled from HotGo sys_config
# through the im_jpush dictionary group when the config stanza is present.
ENV JPUSH_PUSH_ENABLED=false
ENV JPUSH_API_DOMAIN=https://api.jpush.cn
ENV JPUSH_PUSH_PATH=/v3/push
ENV JPUSH_WORKERS=8
ENV JPUSH_DEVICE_CACHE_TTL_SECONDS=30
ENV JPUSH_DEFAULT_TTL_SECONDS=86400
ENV JPUSH_CALL_TTL_SECONDS=60
ENV JPUSH_APNS_PRODUCTION=false
ENV JPUSH_USE_ALIAS=false
ENV JPUSH_CLASSIFICATION=1
ENV JPUSH_TITLE=MeetingIM
# Video calls configuration.
ENV WEBRTC_ENABLED=false
ENV ICE_SERVERS_FILE=
ENV ICE_SERVERS_FILE=/opt/tinode/ice-servers.json
# Use the target db by default.
# When TARGET_DB is "alldbs", it is the user's responsibility
@@ -153,6 +228,7 @@ WORKDIR /opt/tinode
# Copy config template to the container.
COPY config.template .
COPY entrypoint.sh .
COPY ice-servers.json .
# Get the desired Tinode build.
ADD https://github.com/tinode/chat/releases/download/v$BINVERS/tinode-$TARGET_DB.linux-amd64.tar.gz .
@@ -168,9 +244,9 @@ RUN mkdir /botdata
RUN chmod +x entrypoint.sh
RUN chmod +x credentials.sh
# Healthcheck: check if port 6060 is open
# Healthcheck: verify the process and database dependency are responsive.
HEALTHCHECK --interval=1m --timeout=3s --start-period=30s \
CMD nc -z localhost 6060 || exit 1
CMD wget -q -O /dev/null http://localhost:6060/healthz || exit 1
# Generate config from template and run the server.
ENTRYPOINT ["./entrypoint.sh"]
+79 -14
View File
@@ -14,7 +14,7 @@ COPY . .
RUN CGO_ENABLED=0 go build -tags mysql -o /out/tinode ./server
RUN CGO_ENABLED=0 go build -tags mysql -o /out/init-db ./tinode-db
# scripts/apply_hotgo_tenant_db.go is `//go:build ignore`-tagged (a standalone
# migration script, not part of any package) - build it by exact file path.
# startup database validator, not part of any package) - build it by exact path.
RUN CGO_ENABLED=0 go build -o /out/apply-hotgo-tenant-db ./scripts/apply_hotgo_tenant_db.go
# ---- Stage 2: runtime image ----
@@ -27,47 +27,112 @@ LABEL name="ImServer"
ENV WAIT_FOR=
ENV RESET_DB=false
ENV UPGRADE_DB=false
ENV NO_DB_INIT=false
ENV NO_DB_INIT=true
# Empty by default: do not load Tinode's canned demo users into a real deployment.
# Override explicitly if sample data is ever wanted.
ENV SAMPLE_DATA=
ENV DEFAULT_COUNTRY_CODE=US
# DB connection is provided at deploy time via secret-backed env var. No default value.
ENV MYSQL_DSN=
ENV STORE_USE_ADAPTER=mysql
ENV REDIS_ADDR=
ENV REDIS_SENTINEL_MASTER_NAME=
ENV REDIS_SENTINEL_ADDRS_JSON='[]'
ENV REDIS_USERNAME=
ENV REDIS_DB=0
ENV REDIS_DIAL_TIMEOUT_MS=2000
ENV REDIS_READ_TIMEOUT_MS=2000
ENV REDIS_WRITE_TIMEOUT_MS=2000
ENV REDIS_PENDING_TTL_SECONDS=120
ENV REDIS_DONE_TTL_SECONDS=86400
ENV REDIS_FAILED_TTL_SECONDS=600
ENV REDIS_HOT_MESSAGE_TTL_SECONDS=86400
ENV REDIS_PRESENCE_TTL_SECONDS=120
ENV REDIS_CACHE_TTL_SECONDS=300
ENV REDIS_SHORT_STATE_TTL_SECONDS=600
ENV REDIS_GROUP_FANOUT_ENABLED=true
ENV KAFKA_ENABLED=false
ENV KAFKA_BROKERS_JSON='[]'
ENV KAFKA_CLIENT_ID=im-server
ENV KAFKA_CONSUMER_GROUP=im-server
ENV KAFKA_DIAL_TIMEOUT_MS=3000
ENV KAFKA_WRITE_TIMEOUT_MS=3000
ENV KAFKA_READ_TIMEOUT_MS=3000
ENV KAFKA_BATCH_TIMEOUT_MS=10
ENV KAFKA_REQUIRED_ACKS=1
ENV KAFKA_ALLOW_AUTO_TOPIC_CREATION=false
ENV KAFKA_TLS_ENABLED=false
ENV KAFKA_SASL_ENABLED=false
ENV KAFKA_SASL_MECHANISM=SCRAM-SHA-512
ENV KAFKA_SASL_USERNAME=
ENV KAFKA_SASL_PASSWORD=
ENV KAFKA_MAX_RETRY_COUNT=3
ENV KAFKA_RETRY_BACKOFF_MS='[10000,30000,60000,300000,900000]'
ENV KAFKA_MESSAGE_BATCH_SIZE=100
ENV KAFKA_MESSAGE_BATCH_WAIT_MS=10
ENV KAFKA_MESSAGE_SINGLE_RETRY_COUNT=3
ENV KAFKA_DEAD_LETTER_RETENTION_SECONDS=7776000
ENV KAFKA_TOPIC_ADMIN_COMMAND=im.admin.command
ENV KAFKA_TOPIC_ADMIN_COMMAND_RESULT=im.admin.command.result
ENV KAFKA_TOPIC_CONFIG_EVENT=im.config.event
ENV KAFKA_TOPIC_BUSINESS_EVENT=im.business.event
ENV KAFKA_TOPIC_MESSAGE_PERSIST=im.message.persist
ENV KAFKA_TOPIC_GROUP_MESSAGE_PERSIST=im.group.message.persist
ENV KAFKA_TOPIC_MESSAGE_PUSH=im.message.push
ENV KAFKA_TOPIC_FILE_SECURITY=im.file.security
ENV KAFKA_TOPIC_CACHE_INVALIDATE=im.cache.invalidate
ENV KAFKA_TOPIC_AUDIT_EVENT=im.audit.event
ENV KAFKA_TOPIC_DEAD_LETTER=im.dead.letter
ENV INTERNAL_API_ENABLED=true
ENV INTERNAL_API_TIME_WINDOW_SECONDS=300
ENV INTERNAL_API_HOTGO_SERVICE_NAME=hotgo-admin
ENV INTERNAL_API_HOTGO_SECRET=
ENV INTERNAL_API_CULTIVATE_SERVICE_NAME=cultivate-server
ENV INTERNAL_API_CULTIVATE_SECRET=
ENV ADMIN_INTERNAL_API_ENABLED=false
ENV ADMIN_INTERNAL_API_BASE_URL=
ENV ADMIN_INTERNAL_API_SERVICE_NAME=im-server
ENV ADMIN_INTERNAL_API_SECRET=
ENV ADMIN_INTERNAL_API_TIMEOUT_MS=3000
# Tenant registry lives in the hotgo database (shared with im_manager); tenant
# DB routing resolves to the same tinode DB for the seeded "default" tenant.
# business data lives in the shared meeting_im DB and is scoped by tenant_id.
ENV TENANT_REGISTRY_DB=hotgo
ENV MEDIA_HANDLER=fs
ENV MEDIA_HANDLER=sys_config
ENV FS_CORS_ORIGINS='["*"]'
ENV AWS_CORS_ORIGINS='["*"]'
ENV SMTP_HOST_URL='http://localhost:6060'
ENV TEL_HOST_URL='http://localhost:6060'
ENV TLS_ENABLED=false
ENV FCM_PUSH_ENABLED=false
ENV FCM_INCLUDE_ANDROID_NOTIFICATION=true
ENV TNPG_PUSH_ENABLED=false
ENV JPUSH_PUSH_ENABLED=false
ENV JPUSH_API_DOMAIN=https://api.jpush.cn
ENV JPUSH_PUSH_PATH=/v3/push
ENV JPUSH_WORKERS=8
ENV JPUSH_DEVICE_CACHE_TTL_SECONDS=30
ENV JPUSH_DEFAULT_TTL_SECONDS=86400
ENV JPUSH_CALL_TTL_SECONDS=60
ENV JPUSH_APNS_PRODUCTION=false
ENV JPUSH_USE_ALIAS=false
ENV JPUSH_CLASSIFICATION=1
ENV JPUSH_TITLE=MeetingIM
ENV WEBRTC_ENABLED=false
ENV ICE_SERVERS_FILE=/opt/tinode/ice-servers.json
ENV SERVER_STATUS_PATH=''
ENV ACC_GC_ENABLED=false
ENV PLUGIN_PYTHON_CHAT_BOT_ENABLED=false
# Secrets: intentionally NO default values. Must be injected at deploy time.
# An empty/missing value fails loudly at startup rather than silently falling
# back to Tinode's publicly-known demo keys.
ENV API_KEY_SALT=
ENV AUTH_TOKEN_KEY=
ENV UID_ENCRYPTION_KEY=
RUN apk add --no-cache ca-certificates bash grep
WORKDIR /opt/tinode
COPY docker/tinode/config.template .
COPY docker/tinode/entrypoint.sh .
COPY docker/tinode/ice-servers.json .
COPY tinode-db/credentials.sh .
COPY server/templ ./templ
COPY --from=build /out/tinode .
COPY --from=build /out/init-db .
COPY --from=build /out/apply-hotgo-tenant-db .
@@ -76,7 +141,7 @@ RUN mkdir -p /botdata static \
&& chmod +x entrypoint.sh credentials.sh tinode init-db apply-hotgo-tenant-db
HEALTHCHECK --interval=1m --timeout=3s --start-period=30s \
CMD nc -z localhost 6060 || exit 1
CMD wget -q -O /dev/null http://localhost:6060/healthz || exit 1
ENTRYPOINT ["./entrypoint.sh"]
+115 -13
View File
@@ -5,6 +5,8 @@
"static_mount": "/",
"grpc_listen": ":16060",
"grpc_keepalive_enabled": true,
"ws_ping_period_seconds": $WS_PING_PERIOD_SECONDS,
"ws_pong_wait_seconds": $WS_PONG_WAIT_SECONDS,
"api_key_salt": "$API_KEY_SALT",
"max_message_size": 4194304,
"max_subscriber_count": 128,
@@ -15,7 +17,7 @@
"default_country_code": "$DEFAULT_COUNTRY_CODE",
"media": {
"use_handler": "$MEDIA_HANDLER",
"use_handler": "sys_config",
"max_size": 33554432,
"gc_period": 60,
"gc_block_size": 100,
@@ -34,6 +36,9 @@
"presign_ttl": 3600,
"cache_control": "max-age=86400",
"cors_origins": $AWS_CORS_ORIGINS
},
"oss": {
"use_sys_config": true
}
}
},
@@ -74,31 +79,111 @@
"use_adapter": "$STORE_USE_ADAPTER",
"adapters": {
"mysql": {
"database": "tinode",
"database": "meeting_im",
"dsn": "$MYSQL_DSN",
"max_open_conns": 64,
"max_idle_conns": 64,
"conn_max_lifetime": 60,
"max_open_conns": $MYSQL_MAX_OPEN_CONNS,
"max_idle_conns": $MYSQL_MAX_IDLE_CONNS,
"registry_max_open_conns": $MYSQL_REGISTRY_MAX_OPEN_CONNS,
"registry_max_idle_conns": $MYSQL_REGISTRY_MAX_IDLE_CONNS,
"conn_max_lifetime": $MYSQL_CONN_MAX_LIFETIME_SECONDS,
"sql_timeout": 10,
"tenant_registry_db": "$TENANT_REGISTRY_DB",
"tenant_db_routing": true
},
"postgres": {
"database": "tinode",
"database": "meeting_im",
"dsn": "$POSTGRES_DSN"
},
"rethinkdb": {
"database": "tinode",
"database": "meeting_im",
"addresses": "rethinkdb"
},
"mongodb": {
"database": "tinode",
"database": "meeting_im",
"addresses": "mongodb",
"replica_set": "rs0"
}
},
"redis": {
"addr": "$REDIS_ADDR",
"sentinel_master_name": "$REDIS_SENTINEL_MASTER_NAME",
"sentinel_addrs": $REDIS_SENTINEL_ADDRS_JSON,
"username": "$REDIS_USERNAME",
"password": "$REDIS_PASSWORD",
"db": $REDIS_DB,
"key_prefix": "$REDIS_KEY_PREFIX",
"dial_timeout_ms": $REDIS_DIAL_TIMEOUT_MS,
"read_timeout_ms": $REDIS_READ_TIMEOUT_MS,
"write_timeout_ms": $REDIS_WRITE_TIMEOUT_MS,
"pending_ttl_seconds": $REDIS_PENDING_TTL_SECONDS,
"done_ttl_seconds": $REDIS_DONE_TTL_SECONDS,
"failed_ttl_seconds": $REDIS_FAILED_TTL_SECONDS,
"hot_message_ttl_seconds": $REDIS_HOT_MESSAGE_TTL_SECONDS,
"presence_ttl_seconds": $REDIS_PRESENCE_TTL_SECONDS,
"cache_ttl_seconds": $REDIS_CACHE_TTL_SECONDS,
"short_state_ttl_seconds": $REDIS_SHORT_STATE_TTL_SECONDS,
"group_fanout_enabled": $REDIS_GROUP_FANOUT_ENABLED
}
},
"kafka": {
"enabled": true,
"brokers": $KAFKA_BROKERS_JSON,
"client_id": "im-server",
"consumer_group": "im-server",
"dial_timeout_ms": 3000,
"write_timeout_ms": 3000,
"read_timeout_ms": 3000,
"batch_timeout_ms": 10,
"required_acks": -1,
"allow_auto_topic_creation": false,
"tls_enabled": false,
"sasl_enabled": false,
"sasl_mechanism": "SCRAM-SHA-512",
"sasl_username": "",
"sasl_password": "",
"max_retry_count": 3,
"retry_backoff_ms": [10000, 30000, 60000, 300000, 900000],
"message_batch_size": $KAFKA_MESSAGE_BATCH_SIZE,
"message_batch_wait_ms": $KAFKA_MESSAGE_BATCH_WAIT_MS,
"message_single_retry_count": $KAFKA_MESSAGE_SINGLE_RETRY_COUNT,
"dead_letter_retention_seconds": 7776000,
"topics": {
"admin_command": "im.admin.command",
"admin_command_result": "im.admin.command.result",
"config_event": "im.config.event",
"business_event": "im.business.event",
"message_persist": "im.message.persist",
"group_message_persist": "im.group.message.persist",
"message_push": "im.message.push",
"file_security": "im.file.security",
"cache_invalidate": "im.cache.invalidate",
"audit_event": "im.audit.event",
"dead_letter": "im.dead.letter"
}
},
"internal_api": {
"enabled": $INTERNAL_API_ENABLED,
"time_window_seconds": $INTERNAL_API_TIME_WINDOW_SECONDS,
"services": {
"$INTERNAL_API_HOTGO_SERVICE_NAME": {
"secret": "$INTERNAL_API_HOTGO_SECRET",
"allow": ["admin_query"]
},
"$INTERNAL_API_CULTIVATE_SERVICE_NAME": {
"secret": "$INTERNAL_API_CULTIVATE_SECRET",
"allow": ["cultivate"]
}
}
},
"admin_internal_api": {
"enabled": $ADMIN_INTERNAL_API_ENABLED,
"base_url": "$ADMIN_INTERNAL_API_BASE_URL",
"service_name": "$ADMIN_INTERNAL_API_SERVICE_NAME",
"secret": "$ADMIN_INTERNAL_API_SECRET",
"timeout_ms": $ADMIN_INTERNAL_API_TIMEOUT_MS
},
"acc_validation": {
"email": {
"add_to_tags": true,
@@ -174,6 +259,22 @@
}
}
}
},
{
"name": "jpush",
"config": {
"enabled": $JPUSH_PUSH_ENABLED,
"api_domain": "$JPUSH_API_DOMAIN",
"push_path": "$JPUSH_PUSH_PATH",
"workers": $JPUSH_WORKERS,
"device_cache_ttl_seconds": $JPUSH_DEVICE_CACHE_TTL_SECONDS,
"default_ttl_seconds": $JPUSH_DEFAULT_TTL_SECONDS,
"call_ttl_seconds": $JPUSH_CALL_TTL_SECONDS,
"apns_production": $JPUSH_APNS_PRODUCTION,
"use_alias": $JPUSH_USE_ALIAS,
"classification": $JPUSH_CLASSIFICATION,
"title": "$JPUSH_TITLE"
}
}
],
@@ -185,11 +286,12 @@
"cluster_config": {
"self": "",
"nodes": [
{"name": "tinode-0", "addr": "tinode-0:12000"},
{"name": "tinode-1", "addr": "tinode-1:12001"},
{"name": "tinode-2", "addr": "tinode-2:12002"}
],
"nodes": $CLUSTER_NODES_JSON,
"proxy_to_master_buffer": $CLUSTER_PROXY_TO_MASTER_BUFFER,
"proxy_to_master_workers": $CLUSTER_PROXY_TO_MASTER_WORKERS,
"proxy_to_master_enqueue_timeout_ms": $CLUSTER_PROXY_TO_MASTER_ENQUEUE_TIMEOUT_MS,
"route_retries": $CLUSTER_ROUTE_RETRIES,
"route_retry_backoff_ms": $CLUSTER_ROUTE_RETRY_BACKOFF_MS,
"failover": {
"enabled": true,
"heartbeat": 100,
+64 -8
View File
@@ -12,6 +12,23 @@ if [ ! -z "$EXT_CONFIG" ] ; then
else
CONFIG=working.config
: "${REDIS_KEY_PREFIX:=im}"
: "${WS_PING_PERIOD_SECONDS:=15}"
: "${WS_PONG_WAIT_SECONDS:=90}"
: "${CLUSTER_PROXY_TO_MASTER_BUFFER:=4096}"
: "${CLUSTER_PROXY_TO_MASTER_WORKERS:=16}"
: "${CLUSTER_PROXY_TO_MASTER_ENQUEUE_TIMEOUT_MS:=300}"
: "${CLUSTER_ROUTE_RETRIES:=3}"
: "${CLUSTER_ROUTE_RETRY_BACKOFF_MS:=25}"
: "${MYSQL_MAX_OPEN_CONNS:=24}"
: "${MYSQL_MAX_IDLE_CONNS:=8}"
: "${MYSQL_REGISTRY_MAX_OPEN_CONNS:=4}"
: "${MYSQL_REGISTRY_MAX_IDLE_CONNS:=2}"
: "${MYSQL_CONN_MAX_LIFETIME_SECONDS:=1800}"
if [ -z "$CLUSTER_NODES_JSON" ] ; then
CLUSTER_NODES_JSON='[{"name":"tinode-0","addr":"tinode-0:12000"},{"name":"tinode-1","addr":"tinode-1:12001"},{"name":"tinode-2","addr":"tinode-2:12002"}]'
fi
# Remove the old config.
rm -f working.config
@@ -113,8 +130,28 @@ if [ ! -z "$WAIT_FOR" ] ; then
until nc -z -v -w5 ${DB[0]} ${DB[1]}; do echo "waiting for ${WAIT_FOR}..."; sleep 3; done
fi
# Initialize the database if it has not been initialized yet or if data reset/upgrade has been requested.
init_stdout=./init-db-stdout.txt
run_with_retries() {
local name="$1"
shift
local attempts="${STARTUP_RETRY_ATTEMPTS:-10}"
local delay="${STARTUP_RETRY_DELAY_SECONDS:-3}"
local i=1
while [ "$i" -le "$attempts" ]; do
"$@"
local status=$?
if [ "$status" -eq 0 ]; then
return 0
fi
echo "$name failed with status $status (attempt $i/$attempts)."
if [ "$i" -eq "$attempts" ]; then
return "$status"
fi
sleep "$delay"
i=$((i + 1))
done
}
init_db_once() {
./init-db \
--reset=${RESET_DB} \
--upgrade=${UPGRADE_DB} \
@@ -122,19 +159,31 @@ init_stdout=./init-db-stdout.txt
--data=${SAMPLE_DATA} \
--no_init=${NO_DB_INIT} \
1>${init_stdout}
}
apply_hotgo_tenant_db_once() {
IM_MYSQL_DSN="${MYSQL_DSN}" ./apply-hotgo-tenant-db
}
init_stdout=./init-db-stdout.txt
if [ "$NO_DB_INIT" = "true" ] ; then
echo "NO_DB_INIT=true: skipping init-db and apply-hotgo-tenant-db."
else
# Initialize the database if it has not been initialized yet or if data reset/upgrade has been requested.
run_with_retries "init-db" init_db_once
if [ $? -ne 0 ]; then
echo "./init-db failed. Quitting."
exit 1
fi
# Create/upgrade the hotgo tenant registry and the workbench/org/meetings/
# reports/mailbox/etc. tables that init-db above does not manage. Idempotent
# (IF NOT EXISTS / ON DUPLICATE KEY UPDATE throughout), safe to run every start.
IM_MYSQL_DSN="${MYSQL_DSN}" ./apply-hotgo-tenant-db
# Validate that startup is pointed at the shared meeting_im database. Schema
# changes are applied separately and are never performed during pod startup.
run_with_retries "apply-hotgo-tenant-db" apply_hotgo_tenant_db_once
if [ $? -ne 0 ]; then
echo "./apply-hotgo-tenant-db failed. Quitting."
exit 1
fi
fi
# If sample data was provided, try to find Tino password.
if [ ! -z "$SAMPLE_DATA" ] ; then
@@ -151,5 +200,12 @@ fi
args=("--config=${CONFIG}" "--static_data=$STATIC_DIR" "--cluster_self=$CLUSTER_SELF" "--pprof_url=$PPROF_URL")
# Run the tinode server.
./tinode "${args[@]}" 2>> /var/log/tinode.log
# Run the tinode server, mirroring output to both stdout (so kubectl logs/Promtail's
# standard container-log collection actually sees it -- confirmed live that the old
# file-only redirect meant Loki never received a single real tinode log line, only
# this script's own earlier init-db output) and the log file (kept for exec-based
# inspection, e.g. `kubectl exec ... -- cat /var/log/tinode.log`). PIPESTATUS[0] is
# required here: with output piped through tee, a bare $? would report tee's exit
# status (almost always 0) instead of tinode's.
./tinode "${args[@]}" 2>&1 | tee -a /var/log/tinode.log
exit "${PIPESTATUS[0]}"
+5
View File
@@ -0,0 +1,5 @@
[
{
"urls": ["stun:stun.l.google.com:19302"]
}
]
@@ -0,0 +1,821 @@
# IM 006 商户双机消息压力测试方案
日期:2026-09-25
## 1. 测试目标
> 双机实际操作以 [`loadtest/im-pressure/README.md`](../loadtest/im-pressure/README.md) 为准。当前开发机固定为机器 A,负责数据准备、协调器、agent A、集中采集和最终报告;另一台机器固定为机器 B,只运行 agent B 并回传结果。
> 当前测试环境实际企业码为 `allrich`,沿用本方案最初为 006 商户定义的账号规模、场景和统计口径。已完成的 200 对复测、故障根因和修复证据见 [`docs/test/IM-单聊压测Pod日志根因分析与改进方案-20260927.md`](./test/IM-单聊压测Pod日志根因分析与改进方案-20260927.md)。
本次测试使用 006 商户的 2000 个账号,在两台压测机上各运行 1000 个在线 WebSocket 用户,验证多 Pod 场景下:
1. 1000 对 P2P 会话同时收发时,消息是否被接受、实时投递并最终持久化。
2. 2000 人大群中 1000 个成员轮流发言时,跨 Pod 群广播的丢失率、重复率、乱序率和延迟。
3. 300 对 P2P 与 500 个群发送者同时工作时,混合负载是否相互影响。
4. Tinode 原生 Cluster、Kafka 消息持久化、数据库分表和 WebSocket 下行是否出现容量瓶颈。
本轮不在正式发压窗口内注册账号、加好友或建群。所有数据准备完成并核验后,再开始消息压力测试。
## 2. 本方案对需求的精确定义
### 2.1 账号与机器分片
账号使用只包含小写字母和数字、且不超过 10 个字符的用户名,以满足当前注册校验:
| 压测机 | 账号 | 数量 | 用户名示例 |
| --- | --- | ---: | --- |
| Machine A | A0001-A1000 | 1000 | `lta0001`、`lta1000` |
| Machine B | B0001-B1000 | 1000 | `ltb0001`、`ltb1000` |
固定配对关系为 `A0001 <-> B0001`,依此类推到 `A1000 <-> B1000`。“号码相同”指账号序号相同,不使用相同手机号。
每个账号最终记录:`username`、`user_id`、`token`、`token_expires`、`machine`、`pair_no`、`p2p_topic`、`group_topic`。密码只从环境变量读取,不写入结果文件或日志。
### 2.2 群聊需求解释
“1000 个人间隔 0.1 秒发消息,2 轮合计 2000 条”按全局调度解释:
- 每 100ms 只安排一个发送者发送一条消息。
- 每轮 1000 个发送者各发送 1 条,共 1000 条,持续约 100 秒。
- 连续执行 2 轮,共 2000 条,正式发压约 200 秒。
- 不是每个发送者每 100ms 发一条;后一种解释会产生 10000 条/秒和 100 万条/100 秒,与“合计 2000 条”矛盾。
### 2.3 P2P 场景默认强度
原需求未指定每对消息数。本方案默认每对双方各发 10 条:
- 1000 对同时开始。
- 每个用户每秒发送 1 条,共 10 秒;每次加 0-200ms 确定性抖动,避免所有请求落在同一毫秒。
- 总发布量:`1000 × 2 × 10 = 20000` 条。
- 主要期望投递量:20000 次对端投递;发送方自身回显单独统计,不混入对端丢失率。
消息数、间隔和抖动必须可通过命令行参数修改,但每轮结果必须记录实际参数。
## 3. 协议与接口契约
### 3.1 WebSocket 建连与账号注册
WebSocket 地址为 `/v0/channels`,Upgrade 携带普通 API Key。每个连接严格执行:
```text
Upgrade
-> hi(tenant="006")
-> acc 或 login
-> sub(me)
-> sub(目标 P2P/群 Topic)
-> READY
```
006 是企业商户,注册使用 `registration_type: 0` 和 `tenant_code: "006"`:
```json
{
"acc": {
"id": "prep-acc-lta0001",
"user": "new",
"scheme": "basic",
"username": "lta0001",
"registration_type": 0,
"tenant_code": "006",
"secret": "base64(lta0001:password)",
"login": true,
"desc": {"public": {"fn": "LT A0001"}}
}
}
```
当前 006 环境必须确认未强制手机号或邀请码注册。准备程序应先尝试 basic 登录:已存在且密码正确则复用;仅在账号不存在时注册。账号存在但密码不匹配必须停止,禁止覆盖真实账号。
### 3.2 HTTP 公共请求头
好友、隐私和群接口使用登录返回的同一 Token:
```text
X-MeetingIM-APIKey: <ordinary api key>
X-IM-Schema: 1
X-Request-ID: <unique request id>
Authorization: Bearer <token>
Idempotency-Key: <unique key for write requests>
```
所有密钥和 Token 通过环境变量或权限受控文件传入,日志只记录账号索引和脱敏 ID。
### 3.3 好友准备
对每个序号执行:
1. A 调用 `POST /v1/im/friend-requests`,Body 为 `{"target_user_id":"usr_b","message":"loadtest","source":"search","source_topic":""}`。
2. 若返回 `pending`,B 调用 `POST /v1/im/friend-requests/{request_id}/accept`。
3. 若已经是好友,读取好友列表或会话 context 并复用关系。
4. 保存响应中的 P2P `topic`;必要时调用 `POST /v1/im/conversations/p2p`,Body 为 `{"peer_user_id":"usr_b"}`。
5. 最终核验 1000 对关系均为 `friend`,且双方能订阅同一个 P2P Topic。
好友创建会产生 `friend_added` 系统消息,因此正式统计只识别本轮 `run_id` 对应的 `x-im-client-mid`,不能把 Topic 的初始 `seq` 当成 0。
### 3.4 2000 人群准备
1. 使用 A0001 调用 `POST /v1/im/groups` 创建群。创建接口最多附带 99 个成员,建议只创建群主。
2. 先确认所有账号的 `group_invite_verification=false`,避免进入当前尚未完整开放的邀请确认链路。
3. 调用 `POST /v1/im/groups/{topic}/invitations` 分批邀请,每批不超过 100 人,每批使用独立 `Idempotency-Key`。
4. 邀请完成后分页调用 `GET /v1/im/groups/{topic}/members`,核验去重后的有效成员恰好为 2000。
5. 2000 个 WebSocket 全部订阅该群,并完成屏障消息验证后才允许发压。
直接邀请会产生群业务事件和系统消息。准备结束后至少静默 30 秒,并记录正式测试前的群 `baseline_seq`;统计时仍以 `run_id/client_mid` 为准。
## 4. 环境硬门槛
任一门槛不满足,本轮标记为 `INVALID`,不能把结果当作服务端性能结论:
1. `max_subscriber_count >= 2500`。仓库配置默认是 128,必须在所有 Pod 上统一调高并滚动重启。
2. 006 商户 `max_group_members` 为 0(不限)或不小于 2000;目标群 `max_member_count` 同样满足要求。
3. IM Server 所有 Pod 版本和配置一致,原生 Cluster 成员数与 Running Pod 数一致。
4. Kafka 的 P2P、群消息持久化 Topic 可写,消费组无持续积压,死信在基线时为 0;测试环境的 `im.message.persist` 和 `im.group.message.persist` 均为 3 分区、3 副本。
5. 两台压测机到目标入口网络稳定,文件描述符上限、临时端口和 NAT 容量足够。
6. 两台压测机使用 chrony/NTP,同一时刻的时钟偏差绝对值小于 5ms,否则跨机器单向延迟无效。
7. 每台压测机至少预留 2 CPU、4GB 内存和稳定的 100Mbps 网络;测试进程自身 CPU 不应长期超过 70%。
8. 2000 个连接全部 READY;正式阶段不允许把连接失败用户从分母中静默删除。
## 5. 多 Pod 拓扑
仅通过同一个负载均衡入口连接,不能证明消息确实跨 Pod。推荐提供仅供压测使用的三个直连入口,并保持业务数据路径不变:
```text
Machine A: A0001-A0500 -> Pod 0
A0501-A1000 -> Pod 1
Machine B: B0001-B1000 -> Pod 2
```
这样 1000 对 P2P 的双方一定不在同一个 Pod;群成员也覆盖全部 3 个 Pod。若只能提供两个直连入口,则 Machine A 固定 Pod 0、Machine B 固定 Pod 1,并在另一轮把 Machine B 切到 Pod 2 重跑。
入口必须能明确确认实际 Pod,例如握手诊断日志或反向代理访问日志能关联 `session -> pod`。不要依赖负载均衡概率分布。
Topic 消息在启用 Tinode 原生 Cluster 时由 Cluster 路由;此时 Redis realtime 指标中的 `publish/skip_native_cluster` 是预期状态,不代表丢失。Redis 仍需观察用户级业务事件和 Session 控制类 fanout。
## 6. 统一消息格式
所有正式测试消息都使用 JSON 文本消息,并设置 `noecho:false`:
```json
{
"pub": {
"id": "s1-a0001-00001",
"topic": "p2p_or_grp",
"noecho": false,
"head": {
"mime": "application/json",
"x-im-schema": 1,
"x-im-type": "text",
"x-im-client-mid": "lt-20260925-s1-a0001-00001"
},
"content": {
"text": "loadtest",
"run_id": "lt-20260925-s1",
"scenario": "p2p",
"sender_index": 1,
"message_index": 1,
"sent_at_unix_nano": 0
}
}
}
```
`x-im-client-mid` 在 `tenant + topic + sender` 范围内必须唯一,重试时必须复用原值。发送程序用 `{ctrl.id}` 关联请求,用 `ctrl.params.seq` 记录服务端分配的 Topic 序号。
## 7. 执行阶段
### 7.1 阶段 P0:预检与热身
1. 两台机器各以 50 连接/秒建立 1000 条连接,总建连时间约 20 秒。
2. 每个用户订阅 `me`、自己的 P2P Topic 和群 Topic。
3. 群主发送 1 条不计入正式结果的群屏障消息;必须在 10 秒内被 2000 个连接全部收到。
4. 每个 P2P Topic 发送 1 条不计入正式结果的屏障消息;双方必须收到,且对端投递为 1000/1000。
5. 等待 30 秒,确认错误计数、Kafka lag、CPU、内存和连接数回到稳定状态。
### 7.2 阶段 S1:1000 对 P2P 同时聊天
参与者:全部 2000 人,1000 个 P2P Topic。
默认调度:
- A、B 双方各发送 10 条,每用户 1 条/秒,持续 10 秒。
- 每个用户使用由账号索引计算的 0-200ms 固定抖动,确保可复现。
- 总发布量 20000,平均发布速率约 2000 条/秒。
- 发布完成后保留连接并等待 30 秒排空,再进行历史核对。
主要期望:每条被服务端接受的消息恰好被对应的另一方实时收到一次。发送方回显也应收到一次,但作为独立指标。
### 7.3 阶段 S2:2000 人群、1000 人发言
参与者:2000 个在线订阅者。发送者选择 A0001-A0500 与 B0001-B0500,共 1000 人,避免所有发布压力只落在一台压测机上。
默认调度:
- Machine A 每 200ms 发 1 条,Machine B 在其后错开 100ms 发 1 条,合并后全局间隔为 100ms。
- 每轮每个发送者恰好发送 1 条,共 1000 条、约 100 秒。
- 连续 2 轮,共 2000 条、约 200 秒。
- `noecho:false`,因此每条被接受的消息期望投递到 2000 个在线连接。
- 理论实时投递总量:`2000 × 2000 = 4000000` 次,平均约 20000 次/秒。
- 发布结束后等待 60 秒排空,然后由多个校验账号分页拉取历史。
### 7.4 阶段 S3:P2P 与群聊混合
正式窗口为 100 秒:
- P2P:A0001-A0300 与 B0001-B0300,共 300 对;双方各发 5 条,总发布量 `300 × 2 × 5 = 3000`。在第 0、20、40、60、80 秒附近发送,并加入固定抖动。
- 群聊:A0301-A0550 与 B0301-B0550,共 500 个发送者;每人发 2 条。两台机器错峰形成全局 100ms 间隔,两轮共 1000 条,持续约 100 秒。
- 其余用户保持在线并接收群消息。
- 群实时理论投递总量:`1000 × 2000 = 2000000` 次。
- P2P 主要理论对端投递总量:3000 次;发送方回显另计 3000 次。
- 发布结束后等待 60 秒排空,再做 P2P 和群历史核对。
混合场景的群发送者与 P2P 发送者故意分开,便于判断瓶颈来自连接写队列、群广播还是同一连接并发写。通过后可增加一轮“发送者重叠”增强测试。
## 8. 丢失率与正确性口径
不能只统计压测端成功调用 `WriteJSON` 的数量。每轮必须同时输出以下四层结果。
### 8.1 发送接受率
```text
publish_accept_rate = ctrl_202_with_seq / publish_attempted
publish_reject_rate = (ctrl_4xx + ctrl_5xx + ctrl_timeout + write_error) / publish_attempted
```
只有收到 `ctrl.code=202` 且包含有效 `params.seq` 的消息,才进入后续“期望投递”分母。
### 8.2 实时投递丢失率
实时到达率必须由 2000 个 WebSocket 连接实际监听得到,不能使用发送 ACK、Kafka offset 或数据库行数代替。每个连接建立后启动一个常驻 read loop,全量读取服务端下行,并把 `{ctrl}`、`{data}`、`{meta}`、`{pres}`、`{info}` 分流处理。
监听器要求:
1. 每个 WebSocket 只能有一个 goroutine 调用 `ReadMessage/ReadJSON`,避免多个读取者争抢数据;`ctrl` 通过 `id` 分发给等待中的发布请求。
2. 只统计 `head.x-im-client-mid` 前缀和 `content.run_id` 都匹配本轮的 `{data}`,忽略准备阶段系统消息、其他业务消息和历史噪声。
3. 记录 `receiver_index + client_mid + seq + first_received_at`。相同接收者再次收到同一 `client_mid` 计为重复,不增加到达数。
4. 服务端允许 `{data}` 早于对应 `{ctrl 202}` 到达,因此监听器先保存观测事实,最终再与 ACK ledger 对账,不能因为 ACK 尚未到达而丢弃该 `{data}`。
5. P2P 以配对的另一方为主要期望接收者;发送者自身回显单独统计。
6. 群消息以发压开始时的 2000 个 READY 成员为固定分母。因为 `noecho:false`,发送者本人也属于期望接收者。
7. 正式窗口内连接断开时不得从分母中删除。记录断开时间、所属机器和 Pod;服务端导致的断开计入结果,压测机自身饱和导致的断开则将整轮标记为无效。
8. P2P 发布结束后继续监听 30 秒,群聊和混合场景继续监听 60 秒。窗口内未收到计为实时丢失;窗口后到达另记 `late_delivery`,不能把它改写为正常实时到达。
期望投递账本由成功 ACK 生成:
```text
P2P expected key = run_id + client_mid + peer_receiver_index
Group expected key = run_id + client_mid + each_ready_receiver_index
```
群场景 2000 条消息对应 400 万个期望键。实现时为每个接收者维护按消息序号索引的 bitset,不保存 400 万份完整 JSON;延迟使用 HDR Histogram 或固定桶,缺失和重复才输出逐条明细。
P2P 主要口径:
```text
p2p_realtime_loss =
(accepted_messages - unique_peer_data_received_within_drain_window)
/ accepted_messages
```
群聊口径:
```text
group_expected_deliveries = accepted_group_messages × ready_group_subscribers
group_realtime_loss =
(group_expected_deliveries - unique_group_deliveries_within_drain_window)
/ group_expected_deliveries
```
每个接收者按 `run_id + x-im-client-mid` 去重。重复到达不用于抵消其他消息的缺失。
同时输出:
```text
arrival_rate = unique_deliveries_within_drain_window / expected_deliveries
loss_rate = 1 - arrival_rate
late_rate = unique_late_deliveries / expected_deliveries
duplicate_rate = duplicate_deliveries / expected_deliveries
```
实时监听结束后继续执行历史回查,将缺失分为:
- `realtime_lost_but_persisted`:WS 窗口内没收到,但历史中存在,说明实时投递链路丢失,客户端可通过重连补偿。
- `persisted_but_late`:超过实时排空窗口才由 WS 收到,同时历史中存在。
- `durable_lost`:WS 未收到且最长等待后历史也不存在,属于真正的持久化消息丢失。
- `receiver_disconnected`:期望接收者在消息窗口内断线,单独统计并保留在实时丢失分母中。
### 8.3 持久化丢失率
Kafka 持久化是异步链路,`ctrl 202` 不等于数据库已经可查。排空后通过 `{get what:"data"}` 分页回查:
```text
persistence_loss =
(accepted_unique_messages - unique_messages_found_in_history)
/ accepted_unique_messages
```
- P2P:1000 个 Topic 各由一侧账号回查。
- 群聊:至少使用分别连接不同 Pod 的 3 个成员回查,三份结果应完全一致。
- 最长等待 120 秒,每 5 秒重试一次;超时仍不存在才记为持久化丢失。
### 8.4 重复与乱序
```text
duplicate_rate = duplicate_deliveries / expected_deliveries
out_of_order_rate = receivers_observing_seq_regression / ready_receivers
```
同时记录:同一 `client_mid` 对应多个 `seq`、同一 `seq` 对应多个测试消息、接收端 Topic `seq` 回退、ACK `seq` 与下行 `seq` 不一致。这些都必须单独报错,不能归入普通丢失。
## 9. 延迟口径
记录以下时间点:
- `t_write`:压测端开始写 `{pub}`。
- `t_ack`:收到对应 `{ctrl 202}`。
- `t_data`:接收者收到并完成 JSON 解析。
输出:
```text
ack_latency = t_ack - t_write
e2e_realtime_latency = t_data - sent_at
```
每个场景输出 p50、p95、p99、p99.9 和 max。跨机器延迟仅在 NTP 偏差达标时有效;否则仍可统计 ACK 延迟,但端到端单向延迟标记为无效。
群聊 400 万次投递不应把所有延迟样本常驻内存。使用固定桶直方图或 HDR Histogram,并按接收端机器和 Pod 分组。
## 10. 建议通过标准
第一轮基线建议使用:
| 指标 | 标准 |
| --- | --- |
| READY 连接 | 2000/2000 |
| 发布接受率 | 100% |
| 持久化丢失率 | 0 |
| 同一 `client_mid` 多 `seq` | 0 |
| P2P 对端实时丢失率 | <= 0.001%,且任何丢失都要输出明细 |
| 群实时投递丢失率 | <= 0.001%,发布版本验收目标为 0 |
| 重复投递率 | 0 |
| Topic `seq` 回退 | 0 |
| ACK p95 / p99 | <= 500ms / <= 1s;必须用原始 ACK 时间戳计算,不能用直方图桶上界代替 |
| 实时投递 p95 / p99 | <= 1s / <= 2s;跨机器时要求时钟偏差达标 |
| `deliver/hub_full`、WS 写队列满 | 0 |
| Kafka producer/commit 错误 | 0 |
| Kafka 消费重试、单条降级、死信新增 | 0 |
| Kafka 排空 | 发布结束后 60 秒内 lag 回到基线/0 |
| Kafka 到 MySQL 落库 p95 / p99 | < 200ms / < 500ms |
| MySQL 实际落库数 | 与 `ctrl 202` 唯一消息数完全一致 |
| MySQL deadlock、lock timeout、连接池耗尽 | 0 |
| Redis 操作错误、淘汰、拒绝连接 | 0 |
| Redis command p95 / p99 | < 5ms / < 10ms |
| Pod CPU throttling ratio | < 10% |
| Pod CPU、内存峰值 | < limit 的 80% |
| 容器/节点网络 drop、error | 0 新增或不高于基线噪声 |
| 压测机 CPU、内存、网络 | < 70%,无 read-loop backlog |
| Pod 重启、OOM、持续 GC/内存增长 | 0 |
如果实时有缺失但历史回查完整,结论应写“实时链路丢失、可由历史恢复”,不能写成“消息未丢失”。如果历史也缺失,则属于持久化消息丢失,直接判失败。
## 11. 全链路观测与资源采集
### 11.1 观测目标与时间窗口
监控必须能把同一轮测试拆成以下链路:
```text
压测机 WriteJSON
-> IM Pod 收到 pub
-> Redis 分配消息 seq / 幂等 claim
-> Kafka producer 写入消息持久化 Topic
-> IM Pod 返回 ctrl 202 并实时广播
-> Kafka consumer 拉取与组批
-> MySQL 批量提交
-> Kafka offset commit
-> WebSocket 历史查询可见
```
每轮采集窗口为:正式 `start_at` 前 10 分钟、正式测试全程、排空与历史核对结束后 10 分钟。Prometheus 在压测期间建议使用 5 秒抓取周期,重要 counter 以 `increase` 计算,不使用瞬时值推算总量。
每轮在 Grafana 或结果文件中记录四个时间点:
- `baseline_start`:基线开始。
- `load_start`:正式发送开始。
- `load_end`:最后一次发布结束。
- `reconcile_end`:历史核对完成。
所有图表使用相同 UTC 时间轴,并标注 `run_id`、scenario、Git commit、Pod 镜像版本和压测参数。
### 11.2 采集组件
推荐最小采集栈:
| 对象 | 采集方式 | 主要用途 |
| --- | --- | --- |
| IM Server | `/metrics`、`/debug/vars` | WS、HTTP、DB、fanout、Go runtime、连接池 |
| Kubernetes Pod/Container | kube-state-metrics + kubelet/cAdvisor | CPU、内存、磁盘 IO、网络、重启、限流 |
| Kubernetes Node | node-exporter | 节点 CPU、内存、磁盘、网络、conntrack |
| Kafka | kafka-exporter + Broker JMX Exporter | consumer lag、吞吐、ISR、请求队列、磁盘 |
| Redis | redis-exporter | OPS、延迟、内存、连接、命中率、淘汰、网络 |
| MySQL | mysqld-exporter + performance_schema | QPS、事务、锁、Buffer Pool、慢 SQL、IO |
| 两台压测机 | node-exporter + 压测 agent `/metrics` | 生成端 CPU/网络/连接和客户端统计 |
| 链路探测 | blackbox-exporter 或压测 agent 主动探测 | DNS、TCP、HTTP、依赖 RTT |
如果环境没有完整 Prometheus 栈,压测前至少以 5 秒周期抓取上述 HTTP 指标,并同步保存 `kubectl top`、Pod 日志、Kafka consumer group、Redis INFO 和 MySQL status。手工快照只能作为兜底,不能替代连续时间序列。
### 11.3 当前已有与必须补充的应用指标
当前 IM Server 已经提供:
| 指标 | 状态 | 用途 |
| --- | --- | --- |
| `ws_connections_active` | 已有 | 当前 Pod/实例的活跃 WS 数 |
| `http_requests_total` | 已有 | HTTP 请求量和错误码 |
| `http_request_duration_seconds` | 已有 | HTTP 延迟 |
| `db_query_duration_seconds` | 已有 | MySQL query/exec/commit 等通用耗时 |
| `realtime_fanout_events_total` | 已有 | Redis realtime 发布、接收和本地投递结果 |
| `realtime_fanout_delivery_duration_seconds` | 已有 | Redis realtime 消息年龄 |
| `/debug/vars` 中 `DbStats` | 已有 | Go SQL 连接池状态 |
| `/debug/vars` 中 Cluster/Session/Topic 计数 | 已有 | Cluster 节点、会话和活跃 Topic |
为准确测量 Kafka 落库速度,压测版本必须再补以下低基数指标。严禁把 `topic`、`user_id`、`client_mid`、`run_id` 或 tenant ID 放入 Prometheus label:
```text
message_persist_events_total{stage,kind,status}
message_persist_duration_seconds{stage,kind,status}
message_persist_end_to_end_seconds{kind,status}
message_persist_batch_size{kind}
message_persist_inflight{kind}
kafka_consumer_commit_total{topic_kind,status}
redis_operation_duration_seconds{operation,status}
redis_operation_errors_total{operation}
mysql_pool_connections{state}
mysql_pool_wait_total
mysql_pool_wait_duration_seconds_total
cluster_route_events_total{operation,status}
cluster_route_duration_seconds{operation,status}
ws_outbound_events_total{status}
ws_outbound_queue_depth
```
标签约定:
- `kind`:仅允许 `p2p`、`group`。
- `stage`:`producer_write`、`consumer_fetch`、`db_batch`、`consume_finish`。
- `status`:`ok`、`error`、`duplicate`、`retry`、`dead`。
- `operation`:Redis 使用 `seq_reserve`、`unique_claim`、`hot_cache`、`fanout_publish`、`fanout_receive`;Cluster 使用 `route`、`proxy_join`、`proxy_leave`。
`message_persist_end_to_end_seconds` 从 Kafka envelope 的 `occurred_at` 计算到 MySQL 提交并标记消费成功的时刻。它是回答“消息从服务端接受到真正落库用了多久”的核心指标。
另外应在自定义 Prometheus Registry 中注册 Go collector 和 process collector,提供标准 `go_*`、`process_*` 指标;在完成前从 `/debug/vars` 采集对应 runtime 数据。
### 11.4 Kafka 处理速度与积压
Kafka 需要同时观察应用层和 Broker 层,至少包含:
1. P2P Topic `im.message.persist` 的生产/消费条数与字节数。
2. 群 Topic `im.group.message.persist` 的生产/消费条数与字节数。
3. 对应 consumer group 每个 partition 的 lag、总 lag 和 lag 恢复到 0 的时间。
4. producer 写入延迟、错误、重试和 timeout。
5. consumer fetch 速率、批量大小、DB 批次耗时、offset commit 延迟和失败。
6. 批量落库失败后降级为单条写入的次数。
7. 消费重试、Redis 幂等 claim 失败和死信新增数。
8. Broker request queue、network processor idle、request handler idle、ISR shrink、under-replicated/offline partition 和磁盘使用率。
不同 exporter 的 lag 指标名可能是 `kafka_consumergroup_lag` 或 `kafka_consumergroup_group_lag`,部署时必须以实际 `/metrics` 为准建立 dashboard,不允许直接照抄不存在的指标名。
典型查询:
```promql
sum by (kind, stage, status) (
rate(message_persist_events_total[1m])
)
```
```promql
histogram_quantile(
0.99,
sum by (le, kind) (
rate(message_persist_end_to_end_seconds_bucket[5m])
)
)
```
```promql
sum by (consumergroup, topic) (
kafka_consumergroup_lag{
topic=~"im.message.persist|im.group.message.persist"
}
)
```
每轮报告必须给出:峰值生产速度、峰值消费速度、峰值 lag、lag 清零耗时、平均/最大 batch size、DB batch p95/p99、端到端落库 p95/p99/max、重试数和死信数。
### 11.5 数据库落库正确性与速度
006 商户使用默认库 `meeting_im`,但消息表名按数据库中的数值 `tenant_id` 生成,不能直接把企业码字符串当表后缀。准备阶段先从租户注册信息解析并记录 006 的真实 `tenant_id`,再生成只读核对 SQL。
当前消息存储位置:
- P2P:`im_<tenant_id三位补零>_messages`,例如真实 tenant ID 为 6 时是 `im_006_messages`。
- 群聊:`im_<tenant_id三位补零>_group_messages_YYYYMM`。
- 群消息分片注册:`im_group_message_shards`。
- Kafka 消费幂等状态:Redis `kafka_consume` namespace,TTL 7 天。
- Kafka 死信:MySQL `im_kafka_dead_letters`。
正式核对优先使用 WebSocket 历史接口,SQL 作为服务端审计证据。SQL 由核对程序使用白名单表名生成,不允许把外部输入直接拼入表名。
P2P 落库数量示例:
```sql
SELECT
COUNT(*) AS rows_written,
COUNT(DISTINCT client_msg_id) AS unique_client_mid,
MIN(createdat) AS first_persisted_at,
MAX(createdat) AS last_persisted_at
FROM im_006_messages
WHERE client_msg_id LIKE 'lt-20260925-s1-%';
```
群消息落库数量示例:
```sql
SELECT
COUNT(*) AS rows_written,
COUNT(DISTINCT client_msg_id) AS unique_client_mid,
MIN(created_at) AS first_persisted_at,
MAX(created_at) AS last_persisted_at
FROM im_006_group_messages_202609
WHERE client_msg_id LIKE 'lt-20260925-s2-%';
```
重复检查:
```sql
SELECT client_msg_id, COUNT(*) AS cnt, MIN(seqid), MAX(seqid)
FROM im_006_messages
WHERE client_msg_id LIKE 'lt-20260925-s1-%'
GROUP BY client_msg_id
HAVING COUNT(*) <> 1;
```
群表执行同样检查。P2P 有 1000 个 Topic,不能用整张表的 `MAX(seq)-MIN(seq)+1` 判断丢失;必须以压测端保存的 `client_mid -> topic -> ack_seq` manifest 与数据库逐条比对。
MySQL 性能至少采集:
- `Threads_connected`、`Threads_running`、连接使用率和连接池 wait count/duration。
- QPS、Com_insert、事务提交/回滚、行写入速度。
- InnoDB Buffer Pool 命中率、dirty pages、checkpoint age、log waits。
- row lock waits/time、deadlock、慢 SQL、临时磁盘表。
- data/log fsync、读写 IOPS、吞吐、磁盘 await、util 和剩余空间。
- P2P 批量 INSERT、群月表批量 INSERT 的 p95/p99。
- 主从架构下的 replication lag;压测历史回查必须明确读主还是读从。
数据库落库速度报告包含两个值:
```text
db_write_throughput = 测试消息成功落库数 / 首次到最后一次 DB commit 时间
drain_time = 最后一条 ctrl 202 到 Kafka lag=0 且全部历史可见的时间
```
### 11.6 Redis 观测
Redis 在当前消息链路中承担 seq 分配、`client_mid` 幂等 claim、群热消息缓存、Kafka 消费幂等状态和用户级 realtime fanout。需要采集:
- `instantaneous_ops_per_sec`、命令总量与各命令耗时。
- connected/blocked clients、连接拒绝和连接创建速度。
- used memory、RSS、碎片率、maxmemory 使用率。
- evicted/expired keys、keyspace hit/miss。
- network input/output bytes、Pub/Sub channel/pattern/subscriber 数。
- Redis 主线程 CPU、事件循环延迟、slowlog 数量。
- Sentinel/主从环境的 role、replication offset、replication lag、failover。
- `seq_reserve`、`unique_claim`、`hot_cache`、`kafka_consume` 操作错误和 p95/p99。
压测期间禁止使用无界 `KEYS` 或全库 `SCAN` 做统计,这会污染结果。Kafka 消费状态只通过应用指标抽样统计,不逐条扫描 Redis key。
常见异常判定:
- evicted keys 或 rejected connections 增加:直接判失败。
- used memory 持续增长且排空后不回落:检查 hot message、幂等 key TTL 和消费记录泄漏。
- p99 command latency 超过 10ms:标记风险;超过 50ms 且与消息延迟尖刺相关时判失败。
- replication lag 持续扩大或发生 failover:本轮单独标记,不与无故障基线混合。
### 11.7 Pod、容器与节点资源
每个 IM Pod 单独展示,不只看 namespace 汇总:
```promql
sum by (pod) (
rate(container_cpu_usage_seconds_total{namespace="im-server-system",container="im-server"}[1m])
)
```
```promql
sum by (pod) (
container_memory_working_set_bytes{namespace="im-server-system",container="im-server"}
)
```
```promql
sum by (pod) (
rate(container_network_receive_bytes_total{namespace="im-server-system",pod=~"im-server-.*"}[1m])
)
```
```promql
sum by (pod) (
rate(container_network_transmit_bytes_total{namespace="im-server-system",pod=~"im-server-.*"}[1m])
)
```
必须采集:
- CPU 使用核数、CPU limit 使用率、CFS throttled seconds/periods。
- memory working set、RSS、cache、limit 使用率、OOM 和 page fault。
- 容器文件系统 read/write bytes、IOPS、IO wait;若根文件系统不承载业务数据,也要观察日志写入压力。
- 网络接收/发送 bytes、packets、errors、drops、TCP retransmit。
- Pod restart、readiness、liveness、Pending/NotReady 时间。
- Go goroutine、heap、stack、GC 次数、GC pause、进程文件描述符。
- `LiveSessions`、`LiveTopics`、`LiveClusterNodes`、Cluster leader 和代理 Topic 数。
- Node CPU steal/iowait、内存压力、磁盘压力、网络错误、conntrack 使用率。
资源报告既要给峰值,也要给排空后是否回落。单个 Pod 明显高于其他 Pod 时,结合连接分布、Topic master 位置和网络流量判断是否发生热点。
建议风险线:
- Pod CPU 持续超过 limit 的 80%,或 throttling ratio 超过 10%。
- memory working set 超过 limit 的 80%,或排空 10 分钟后仍持续增长。
- 节点磁盘 util 持续超过 80%,await 持续超过 20ms。
- 网络 drop/error 或 TCP retransmit 明显增长。
- 任一 Pod restart/OOM/NotReady,直接判本轮失败。
### 11.8 网络时延与压测机资源
两台压测机分别持续测量:
- 到各 Pod 直连入口和负载均衡入口的 DNS、TCP connect、TLS handshake、HTTP `/healthz` RTT。
- WebSocket Ping/Pong RTT p50/p95/p99/max,每 5 秒一次,不能用业务 `note kp` 代替协议 Ping。
- 到 Kafka、Redis、MySQL 服务地址的 TCP connect RTT;只做低频只读探测,不绕过业务链路写数据。
- 两台压测机之间的 RTT、丢包和 NTP offset。
- 压测进程 CPU、RSS、GC、goroutine、FD、socket、send/receive bytes、read-loop queue depth。
压力生成端出现 CPU > 70%、网卡 > 70%、FD 接近上限、read-loop backlog 或 GC 尖刺时,本轮标记 `LOAD_GENERATOR_SATURATED`,不能把客户端处理不过来算成服务端丢消息。
### 11.9 现有 fanout 与 Cluster 查询
每轮开始前记录 counter 快照,结束后记录增量:
```promql
sum by (stage, kind, target_type, status) (
increase(realtime_fanout_events_total[10m])
)
```
```promql
histogram_quantile(
0.99,
sum by (le, kind, target_type) (
rate(realtime_fanout_delivery_duration_seconds_bucket[5m])
)
)
```
Topic 消息使用原生 Cluster 时,`publish/skip_native_cluster` 增加属于正常现象。重点检查 Cluster 连接数、route/proxy 错误、`node for topic not found`、gob 编解码错误、Hub 队列满和 WS 写失败。
### 11.10 日志、快照与关联规则
压测日志统一带 `run_id`、scenario、machine、account_index 和脱敏 Topic hash。服务端日志不新增用户 ID 或 `client_mid` 的高基数 Prometheus label;需要定位时通过压测端保存的 manifest 关联。
每轮保存:
- Prometheus 查询结果或 Grafana dashboard snapshot。
- IM Pod、Kafka、Redis、MySQL 在测试窗口内的错误日志。
- Kafka consumer group 压测前、峰值、结束、排空后的 offset/lag 快照。
- Redis INFO、SLOWLOG 摘要和 exporter 指标快照。
- MySQL GLOBAL STATUS、InnoDB status 摘要、慢 SQL 摘要和表行数核对。
- `kubectl top pod/node`、Pod restart/condition/event 和容器日志摘要。
- 两台压测机 node/process 指标及 agent summary。
日志采集必须过滤 Token、密码、API Key、Kafka/Redis/MySQL 凭证和完整消息正文。
### 11.11 建议仪表盘布局
一张压测 dashboard 分为六行:
1. 业务结果:attempt、ACK、实时接收、历史可见、丢失、重复、乱序、p95/p99。
2. IM Pod:连接数、发布/下行速率、Cluster route、CPU、内存、GC、goroutine。
3. Kafka:producer/consumer rate、lag、batch、commit、retry、dead letter、broker health。
4. Redis:OPS、command latency、memory、clients、network、eviction、replication。
5. MySQL:insert/transaction rate、DB batch latency、pool wait、locks、Buffer Pool、IO。
6. 基础设施:Pod/Node CPU、memory、disk IO、network、压测机资源和网络 RTT。
所有面板至少按 Pod、Kafka Topic/consumer group、Redis instance、MySQL instance 和压测机分组;不得只展示总和。
## 12. 压测程序结构
现有 `loadtest/realtime_fanout_probe.go` 只支持单群和单进程,不能直接用于本方案。新工具使用 Go 实现并提供六个子命令:
```text
im-pressure prepare 注册、好友、群和 manifest
im-pressure verify 只读核验 2000 账号、1000 对关系和 2000 群成员
im-pressure coordinator 协调双机 READY、统一起跑时间、ACK 台账与结果上报
im-pressure agent 在每台压测机运行连接与场景
im-pressure collect 抓取 Prometheus、K8s 和依赖组件快照
im-pressure merge 合并两台机器结果并生成报告
```
推荐文件:
```text
loadtest/im-pressure/config.example.yaml
loadtest/im-pressure/cmd/im-pressure/
loadtest/im-pressure/internal/protocol/
loadtest/im-pressure/internal/prepare/
loadtest/im-pressure/internal/runner/
loadtest/im-pressure/internal/collector/
loadtest/im-pressure/internal/report/
```
协调方式:Machine A 启动 coordinator,等待 A/B 两个 agent 都上报 1000 个 READY,然后发布一个至少晚于当前时间 30 秒的 UTC `start_at`。到达 `start_at` 后,A/B 必须在前后 3 秒内向 coordinator 完成最终起跑确认;只有双方都确认后才开始发送,任一方迟到则两边共同终止并将本轮标记为 `INVALID`。两台机器按相同 `run_id` 和确定性调度执行,任何 agent 失联同样判定为 `INVALID`。
`agent` 自身暴露 `/metrics`,提供连接状态、发布、接收、重复、乱序、断连及 Go/process 指标;ACK 和投递延迟分布写入最终 summary。`collect` 只执行只读查询,并把各数据源的原始快照与查询时间一起保存;连接凭证通过环境变量传入,不写入结果目录。
## 13. 结果文件
每轮保留以下文件:
```text
results/<run_id>/config.redacted.json
results/<run_id>/manifest.sha256
results/<run_id>/agent-a-summary.json
results/<run_id>/agent-b-summary.json
results/<run_id>/publish.jsonl
results/<run_id>/missing.jsonl
results/<run_id>/duplicate.jsonl
results/<run_id>/history-reconcile.json
results/<run_id>/db-reconcile-p2p.json
results/<run_id>/db-reconcile-group.json
results/<run_id>/timeline.json
results/<run_id>/metrics/im-server.prom
results/<run_id>/metrics/load-agent.prom
results/<run_id>/metrics/kafka.json
results/<run_id>/metrics/redis.json
results/<run_id>/metrics/mysql.json
results/<run_id>/metrics/kubernetes.json
results/<run_id>/metrics/network.json
results/<run_id>/logs/im-server-errors.log
results/<run_id>/logs/kafka-errors.log
results/<run_id>/logs/mysql-slow-summary.log
results/<run_id>/grafana-snapshot.json
results/<run_id>/report.md
```
缺失明细至少包含 `run_id`、场景、Topic 脱敏哈希、发送者索引、接收者索引、`client_mid`、ACK `seq`、发送时间、最后等待时间、发送 Pod 和接收 Pod。不得写入 Token、密码、API Key 或完整手机号。
最终 `report.md` 必须分别给出:
- 压测业务量和消息正确性结论。
- 实时投递与历史恢复结论。
- Kafka 生产、消费、lag、排空和死信结论。
- MySQL 落库数量、吞吐、耗时、锁与 IO 结论。
- Redis OPS、耗时、内存、幂等/seq 错误和 fanout 结论。
- 每个 Pod/Node 以及两台压测机的资源峰值和瓶颈判断。
- 测试是否有效、是否通过,以及所有未通过项的原始证据路径。
## 14. 推荐执行顺序
1. 在测试窗口前完成环境门槛检查和 2000 账号准备。
2. 完成 1000 对好友关系与 2000 人群,运行 `verify` 直到全部通过。
3. 先用 20 对 P2P、100 人群做冒烟测试,验证统计口径。
4. 执行 P0 热身。
5. P2P 按 200 对、500 对、1000 对逐级执行;每一级通过后才能进入下一级。
6. 执行 S2 群聊,等待恢复基线至少 10 分钟。
7. 执行 S3 混合场景。
8. 合并结果,按实时链路与持久化链路分别给出结论。
9. 全部稳定后再单独进行 Pod 重启、断网或 Kafka/Redis 故障注入,不与本轮无故障基线混跑。
### 14.1 P2P 容量阶梯
三个档位复用同一批账号和好友关系,只裁剪 runtime manifest,不重新注册或重复加好友。每档至少执行两轮;发布验收档建议执行三轮。任何一轮出现消息丢失、重复、乱序、非预期拒绝、数据库不一致或 Pod 重启,都必须停止升级并先定位。
| 档位 | A/B 用户 | WS 总数 | 每用户消息 | Publish 总数 | 目标 |
|---|---:|---:|---:|---:|---|
| P2P-200 | 各 200 | 400 | 10 | 4000 | 验证正确性、指标和基础延迟 |
| P2P-500 | 各 500 | 1000 | 10 | 10000 | 验证 Kafka 三分区、MySQL 和 Cluster 中档容量 |
| P2P-1000 | 各 1000 | 2000 | 10 | 20000 | 单聊发布验收 |
每档的执行流程固定为:生成唯一 `run_id`;开始前采集至少 2 分钟基线;启动 coordinator;A/B agent 全部 READY;统一时间发压;P2P 发送结束后继续监听至少 30 秒;等待 Kafka lag 归零;执行历史回查与 MySQL `client_mid` 精确核验;发送结束后继续采集至少 5 分钟;最后合并报告。
每档升级门槛:Ready、Publish ACK、实时到达、历史查询和 MySQL 精确匹配全部达到期望数;丢失、重复、乱序、断连、403/502/503、Cluster queue overflow、Kafka retry/dead letter、MySQL deadlock、Pod 重启均为 0;Kafka lag 在发送结束后 60 秒内归零;Pod 无持续 CPU throttling。ACK P95 超标时必须按压测机分别报告,并先排除 DNS、TCP、TLS、WebSocket RTT 和时钟偏差。
### 14.2 P2P 通过后的业务场景
1. **GROUP-2000**:2000 条 WS 全部加入同一群,A/B 各 500 个发送者;每人发送 2 条,两轮合计 2000 条。实时投递期望量按每条消息的实际 Ready 接收者计算,并保留 60 秒 drain。
2. **MIXED**:300 对私聊用户双方各发 5 条,共 3000 条私聊 Publish;同时 A/B 各 250 个群发送者每人发 2 条,共 1000 条群 Publish。
3. **稳定性复跑**:GROUP 和 MIXED 至少各两轮,确认无冷启动偶然性。
4. **故障注入**:分别执行单 Pod 滚动重启、Kafka 短暂不可用与恢复、Redis 短暂不可用与恢复;故障轮只验证恢复能力,不与无故障性能基线混合统计。
### 14.3 当前下一步
当前已通过 P2P-200 的消息正确性、Kafka 60 秒排空、MySQL 精确落库、零乱序、零审计死锁和零 CPU throttling。下一步执行 **P2P-500**:A/B 各取账号索引 `1..500`,共 1000 条 WS、10000 条 Publish。若连续两轮满足上述升级门槛,再进入 P2P-1000。
@@ -0,0 +1,599 @@
# IM Admin 内部查询接口缺口清单
> 目标:梳理 HotGo/Admin 迁移到“Admin 只访问 hotgo,IM Server 只访问租户库”后,IM Server 需要补齐的 `/internal/admin-query/*` 只读接口。
> 当前日期:2026-08-27
> 适用项目:`im_manager` Admin 后台、`im_server` IM 服务端。
## 1. 背景
Kafka 写链路已经在 Admin 侧补齐:Admin 写操作落 `im_admin_commands`、`im_admin_outbox`,通过 Kafka 发给 IM Server,IM Server 执行业务后回传 `im.admin.command.result`。
查询链路还没有完全隔离。Admin 当前仍有不少页面通过 `imtenant.DB()` 或 `resolveImTenantDB()` 直连租户库。若要严格满足目标架构,Admin 查询租户业务数据时应调用 IM Server 的内部只读接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/...
```
IM Server 当前已经具备部分内部查询接口,但还不能覆盖 Admin 后台页面。
## 2. 已有接口
以下接口在 IM Server 当前实现中已经存在,可供 Admin 先迁移:
| 接口 | 用途 | 备注 |
|---|---|---|
| `GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}` | 用户详情 | 需要增强字段,见 P0。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}` | 群详情和治理配置 | 需要增强 topic 基础字段,见 P0。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/members` | 群成员列表 | 当前偏 cursor 分页,Admin 表格还需要 total。 |
| `GET /internal/admin-query/tenants/{tenant_code}/messages/{topic}/{seq_id}` | 消息详情 | 当前 Admin 暂未完整迁移消息页面。 |
| `GET /internal/admin-query/tenants/{tenant_code}/files/{file_id}` | 文件详情 | 还缺文件列表和引用列表。 |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters` | Kafka 死信列表 | Admin 已接入。 |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters/{dead_id}` | Kafka 死信详情 | Admin 已接入。 |
## 3. 统一约定
### 3.1 鉴权
沿用 IM Server 内部 API HMAC 鉴权:
```text
X-Service-Name
X-Timestamp
X-Nonce
X-Signature
```
服务端需要校验:
1. service 是否存在。
2. service 是否包含 `admin_query` capability。
3. timestamp 是否在允许窗口内。
4. nonce 是否防重放。
5. 签名是否匹配。
### 3.2 分页返回
Admin 的 `BasicTable` 更适合 page/per_page/total 格式。建议所有列表接口统一返回:
```json
{
"code": 0,
"data": {
"items": [],
"total": 0,
"page": 1,
"per_page": 20
}
}
```
如果 IM Server 内部更适合 cursor,也建议同时返回:
```json
{
"code": 0,
"data": {
"items": [],
"total": 0,
"page": 1,
"per_page": 20,
"next_cursor": "",
"has_more": false
}
}
```
### 3.3 详情返回
详情接口统一返回:
```json
{
"code": 0,
"data": {}
}
```
### 3.4 时间与 ID
1. 时间统一返回 RFC3339/RFC3339Nano 字符串。
2. 雪花 ID、Tinode 用户 ID、19 位整数 ID 建议以字符串返回,避免前端丢精度。
3. 入参同时兼容字符串 ID 和纯数字 ID 时,IM Server 内部自行解析。
## 4. P0:优先补齐
P0 是 Admin 当前核心页面迁移所需接口,补完后可以先迁移用户、群组、文件等高频页面。
### 4.1 用户列表
```text
GET /internal/admin-query/tenants/{tenant_code}/users
```
用途:IM 用户管理列表。
建议查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `user_id` | string | 用户 ID,支持精确查询。 |
| `nickname` | string | 昵称模糊查询。 |
| `auth_name` | string | 登录标识,匹配 username/phone/email/凭证。 |
| `state` | int | 用户状态。 |
| `created_at_start` | string | 创建时间开始。 |
| `created_at_end` | string | 创建时间结束。 |
| `last_seen_start` | string | 最后在线开始。 |
| `last_seen_end` | string | 最后在线结束。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量,最大建议 200。 |
建议返回字段:
| 字段 | 说明 |
|---|---|
| `user_id` | 字符串用户 ID。 |
| `username` | 用户名。 |
| `phone` | 手机号。 |
| `nickname` | 昵称。 |
| `public_account` | 公开账号。 |
| `state` | 用户状态。 |
| `gender` | 性别。 |
| `region` | 地区。 |
| `auth_names` | 登录标识摘要。 |
| `credentials` | 凭证摘要。 |
| `auth_version` | 鉴权版本。 |
| `password_updated_at` | 密码更新时间。 |
| `last_seen` | 最后在线时间。 |
| `user_agent` | 最后设备 UA。 |
| `created_at` | 创建时间。 |
| `updated_at` | 更新时间。 |
当前 Admin 直连涉及表:`im_users`、`im_user_profiles`、`im_user_security`、`im_credentials`。
### 4.2 用户详情增强
现有接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}
```
建议增强返回字段:
| 字段 | 说明 |
|---|---|
| `profile` | 结构化用户资料。 |
| `public` | Tinode public JSON。 |
| `trusted` | Tinode trusted JSON。 |
| `access` | 访问权限 JSON。 |
| `tags` | 标签。 |
| `security` | `auth_version/password_updated_at` 等安全信息。 |
| `credentials` | 凭证列表,包含 method/value/done。 |
### 4.3 用户设备列表
```text
GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}/devices
```
用途:用户详情中的登录设备列表。
建议查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `device_id` | string | 设备 ID。 |
| `platform` | string | 平台。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量。 |
建议返回字段:
| 字段 | 说明 |
|---|---|
| `device_id` | 设备 ID。 |
| `platform` | 平台。 |
| `user_agent` | UA。 |
| `last_seen` | 最后在线。 |
| `created_at` | 创建时间。 |
| `updated_at` | 更新时间。 |
当前 Admin 直连涉及表:`im_devices`。
### 4.4 群列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups
```
用途:IM 群组管理列表。
建议查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `topic` | string | 群 Topic。 |
| `owner_id` | string | 群主 ID。 |
| `state` | int | Tinode topic 状态。 |
| `group_status` | string | 业务群状态。 |
| `created_at_start` | string | 创建时间开始。 |
| `created_at_end` | string | 创建时间结束。 |
| `touched_at_start` | string | 活跃时间开始。 |
| `touched_at_end` | string | 活跃时间结束。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量。 |
建议返回字段:
| 字段 | 说明 |
|---|---|
| `topic` | 群 Topic。 |
| `state` | Tinode 状态。 |
| `owner_id` | 群主 ID。 |
| `seq_id` | 最新消息序号。 |
| `del_id` | 删除序号。 |
| `sub_count` | Tinode 订阅成员数。 |
| `touched_at` | 活跃时间。 |
| `created_at` | 创建时间。 |
| `updated_at` | 更新时间。 |
| `public` | Tinode public JSON。 |
| `trusted` | Tinode trusted JSON。 |
| `tags` | 标签。 |
| `aux` | 辅助 JSON。 |
| `group_name` | 群名称。 |
| `avatar_file_id` | 群头像文件 ID。 |
| `description` | 群简介。 |
| `group_status` | 业务群状态。 |
| `join_approval` | 入群审批开关。 |
| `mute_all` | 全员禁言。 |
| `member_upload_files` | 成员上传文件。 |
| `member_private_chat` | 成员私聊。 |
| `member_invite` | 成员邀请。 |
| `max_member_count` | 最大成员数。 |
| `max_admin_count` | 最大管理员数。 |
| `business_member_count` | 业务成员数。 |
| `settings_version` | 治理配置版本。 |
| `settings_updated_at` | 治理配置更新时间。 |
当前 Admin 直连涉及表:`im_topics`、`im_group_settings`。
### 4.5 群详情增强
现有接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}
```
当前接口主要返回 `im_group_settings`。建议增强 Tinode topic 基础字段:
| 字段 | 说明 |
|---|---|
| `state` | Topic 状态。 |
| `seq_id` | 最新消息序号。 |
| `del_id` | 删除序号。 |
| `sub_count` | 订阅数量。 |
| `public` | public JSON。 |
| `trusted` | trusted JSON。 |
| `access` | access JSON。 |
| `tags` | tags JSON。 |
| `aux` | aux JSON。 |
| `touched_at` | 活跃时间。 |
### 4.6 群成员列表增强
现有接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/members
```
建议增强查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `user_id` | string | 用户 ID 精确过滤。 |
| `role` | string | `owner/admin/member`。 |
| `member_status` | string | 成员状态。 |
| `q` | string | 昵称、公开账号等关键词。 |
| `eligible_role` | string | 候选角色筛选。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量。 |
| `cursor` | string | 兼容 cursor 分页。 |
| `limit` | int | 兼容 cursor 分页。 |
建议增强返回:
| 字段 | 说明 |
|---|---|
| `total` | 总数,Admin 表格需要。 |
| `items[].user_id` | 用户 ID。 |
| `items[].nickname` | 昵称。 |
| `items[].state` | 用户状态。 |
| `items[].mode_want` | 期望权限。 |
| `items[].mode_given` | 授予权限。 |
| `items[].recv_seq_id` | 接收序号。 |
| `items[].read_seq_id` | 已读序号。 |
| `items[].del_id` | 删除序号。 |
| `items[].role` | 业务角色。 |
| `items[].member_status` | 成员状态。 |
| `items[].group_nickname` | 群昵称。 |
| `items[].pinned` | 置顶。 |
| `items[].mute_notifications` | 免打扰。 |
| `items[].join_source` | 加入来源。 |
| `items[].joined_by` | 邀请或处理人。 |
| `items[].joined_at` | 加入时间。 |
| `items[].left_at` | 离开时间。 |
| `items[].removed_by` | 移除人。 |
| `items[].version` | 版本。 |
当前 Admin 直连涉及表:`im_group_members`、`im_subscriptions`、`im_users`、`im_user_profiles`。
### 4.7 入群申请列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/join-requests
```
建议查询参数:`status`、`applicant_user_id`、`page`、`per_page`。
建议返回字段:`request_id`、`topic`、`applicant_user_id`、`inviter_user_id`、`source`、`message`、`status`、`reject_reason`、`handled_by`、`handled_at`、`expires_at`、`version`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_join_requests`。
### 4.8 群邀请列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/invitations
```
建议查询参数:`status`、`target_user_id`、`inviter_user_id`、`page`、`per_page`。
建议返回字段:`invitation_id`、`topic`、`inviter_user_id`、`target_user_id`、`message`、`status`、`result`、`join_request_id`、`expires_at`、`handled_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_invitations`。
### 4.9 群公告列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/announcements
```
建议查询参数:`status`、`page`、`per_page`。
建议返回字段:`announcement_id`、`topic`、`author_user_id`、`content`、`status`、`version`、`published_at`、`deleted_by`、`deleted_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_announcements`。
### 4.10 群禁言列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/mutes
```
建议查询参数:`status`、`user_id`、`page`、`per_page`。
建议返回字段:`topic`、`user_id`、`muted_by`、`muted_at`、`expires_at`、`reason`、`status`、`revoked_by`、`revoked_at`、`version`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_mutes`。
### 4.11 群管理日志
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/admin-logs
```
建议查询参数:`action`、`operator_id`、`page`、`per_page`。
建议返回字段:`log_id`、`topic`、`operator_id`、`target_user_id`、`action`、`object_type`、`object_id`、`before_data`、`after_data`、`request_id`、`idempotency_key`、`created_at`。
当前 Admin 直连涉及表:`im_group_admin_logs`。
### 4.12 群文件列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/files
```
用途:群治理页面中的群文件列表。不要和已有单文件详情接口混淆。
建议查询参数:`status`、`file_type`、`file_name`、`uploader_id`、`page`、`per_page`。
建议返回字段:`group_file_id`、`topic`、`file_id`、`source_topic`、`source_seq`、`uploader_id`、`file_name`、`mime_type`、`file_type`、`file_size`、`status`、`deleted_by`、`deleted_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_files`。
### 4.13 文件列表
```text
GET /internal/admin-query/tenants/{tenant_code}/files
```
用途:IM 文件管理列表。
建议查询参数:`file_id`、`user_id`、`status`、`mime_type`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`file_id`、`user_id`、`mime_type`、`file_size`、`file_name`、`location`、`etag`、`status`、`quarantine_reason`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_fileuploads`。
### 4.14 文件引用列表
```text
GET /internal/admin-query/tenants/{tenant_code}/files/{file_id}/links
```
用途:文件详情中展示消息引用。
建议返回字段:`topic`、`seq_id`、`user_id`、`created_at`。
当前 Admin 直连涉及表:`im_filemsglinks`。
## 5. P1:后台审核和运营页面
### 5.1 审计日志
```text
GET /internal/admin-query/tenants/{tenant_code}/audit-logs
GET /internal/admin-query/tenants/{tenant_code}/audit-logs/{id}
```
建议查询参数:`actor_user_id`、`action`、`target_type`、`target_id`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`request_id`、`actor_user_id`、`action`、`target_type`、`target_id`、`ip`、`user_agent`、`detail`、`created_at`。
当前 Admin 直连涉及表:`im_audit_logs`。
### 5.2 实名审核
```text
GET /internal/admin-query/tenants/{tenant_code}/realname-verifications
GET /internal/admin-query/tenants/{tenant_code}/realname-verifications/{id}
```
建议查询参数:`user_id`、`status`、`real_name`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`user_id`、`real_name`、`id_card_no_masked`、`front_file_id`、`back_file_id`、`status`、`reject_reason`、`reviewed_by`、`reviewed_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_realname_verifications`。
### 5.3 举报工单
```text
GET /internal/admin-query/tenants/{tenant_code}/abuse-reports
GET /internal/admin-query/tenants/{tenant_code}/abuse-reports/{id}
GET /internal/admin-query/tenants/{tenant_code}/reports/abuse/{report_id}/target
```
建议查询参数:`reporter_user_id`、`target_type`、`target_id`、`status`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`report_id`、`reporter_user_id`、`target_type`、`target_id`、`reason_code`、`description`、`evidence_file_ids`、`status`、`handler_id`、`handled_at`、`handle_result`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_abuse_reports`。
### 5.4 意见反馈
```text
GET /internal/admin-query/tenants/{tenant_code}/feedback-submissions
GET /internal/admin-query/tenants/{tenant_code}/feedback-submissions/{id}
```
建议查询参数:`user_id`、`category`、`status`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`feedback_id`、`user_id`、`category`、`content`、`contact`、`file_ids`、`status`、`handler_id`、`handled_at`、`reply`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_feedback_submissions`。
### 5.5 业务 Outbox
```text
GET /internal/admin-query/tenants/{tenant_code}/outbox/business-events
GET /internal/admin-query/tenants/{tenant_code}/outbox/group-events
GET /internal/admin-query/tenants/{tenant_code}/outbox/business-events/{id}
GET /internal/admin-query/tenants/{tenant_code}/outbox/group-events/{id}
```
建议查询参数:`event_type`、`status`、`target_type`、`target`、`created_at_start`、`created_at_end`、`page`、`per_page`。
当前 Admin 直连涉及表:`im_business_events_outbox`、`im_group_events_outbox`。
## 6. P2:如果要完全消除租户库直连
这些不是 Kafka 联调第一优先级,但如果最终要求 Admin 完全不碰租户库,也需要 IM Server 提供内部查询或改为 hotgo 摘要表。
### 6.1 组织架构
```text
GET /internal/admin-query/tenants/{tenant_code}/org/departments
GET /internal/admin-query/tenants/{tenant_code}/org/members
```
当前 Admin 直连涉及表:`im_org_departments`、`im_org_members`、`im_users`。
### 6.2 工作台应用
```text
GET /internal/admin-query/tenants/{tenant_code}/workbench/apps
```
当前 Admin 直连涉及工作台相关租户表。
### 6.3 租户任务看板摘要
```text
GET /internal/admin-query/tenants/{tenant_code}/dashboard/summary
```
当前 Admin 会统计多个租户库中的待处理实名、举报、反馈、Outbox 等数量。
### 6.4 租户配置类查询
如果这些配置仍放在租户库,也需要补接口;如果后续迁到 hotgo 控制库,则不需要通过 IM Server 查询。
```text
GET /internal/admin-query/tenants/{tenant_code}/feedback-config
GET /internal/admin-query/tenants/{tenant_code}/mailbox-config
GET /internal/admin-query/tenants/{tenant_code}/abuse-reasons
GET /internal/admin-query/tenants/{tenant_code}/app-releases
```
涉及模块:反馈配置、总经理信箱配置、举报原因、App 发布策略。
## 6.5 IM Server 补齐状态
截至 2026-08-27,IM Server 已补齐本清单中的内部查询入口,统一走 `/internal/admin-query/tenants/{tenant_code}/...`,并沿用 HMAC 鉴权。
已接入范围:
1. P0:用户列表、用户详情增强、用户设备、群列表、群详情增强、群成员 total、入群申请、群邀请、群公告、群禁言、群管理日志、群文件、文件列表、文件详情、文件引用。
2. P1:审计日志、实名审核、举报工单、举报目标快照、意见反馈、业务 Outbox、群 Outbox。
3. P2:组织部门、组织成员、工作台应用、任务看板摘要、反馈配置、总经理信箱配置、举报原因、App 发布策略。
实现说明:
1. IM Server 内部使用白名单只读查询规格,不提供任意 SQL 入口。
2. 列表接口统一返回 `items/total/page/per_page`。
3. 详情接口统一返回单对象。
4. 复杂字段先返回租户库原始 JSON 或轻量关联摘要,Admin 联调时如需展示专用字段再做二次增强。
5. 若租户库未创建某些 P1/P2 表,对应接口会返回内部错误;发版前需要确保租户库 schema 已完整迁移。
## 7. Admin 侧迁移建议
### 7.1 第一批迁移
在 IM Server 补完 P0 后,Admin 优先迁移:
1. 用户列表、用户详情、用户设备。
2. 群列表、群详情、群成员。
3. 群入群申请、邀请、公告、禁言、管理日志、群文件。
4. 文件列表、文件详情、文件引用。
### 7.2 第二批迁移
在 IM Server 补完 P1 后,Admin 再迁移:
1. 审计日志。
2. 实名审核。
3. 举报工单。
4. 意见反馈。
5. 业务 Outbox。
### 7.3 保留 hotgo 查询的范围
以下数据天然属于 hotgo 控制库,Admin 可继续查 hotgo,不需要走 IM Server:
1. 租户表和租户绑定表。
2. Admin 命令表:`im_admin_commands`。
3. Admin Outbox 表:`im_admin_outbox`。
4. Admin Kafka 审计事件表:`im_audit_events`。
5. 菜单、角色、管理员、系统配置、字典等 HotGo 系统表。
## 8. 验收标准
1. Admin 查询租户业务数据时,不再出现新的 `imtenant.DB()` 调用。
2. Admin 查询 IM Server 内部接口时,统一使用 HMAC 签名。
3. 列表接口支持 `page/per_page/total`,兼容后台表格分页。
4. 详情接口返回结构化 JSON,不返回数据库内部敏感字段。
5. 用户 ID、雪花 ID 等大整数以字符串返回。
6. IM Server 为内部查询写审计日志,至少包含 `tenant_code`、`service_name`、`path`、`query`、`trace_id`、`status`、`duration_ms`。
7. 关闭 IM Server 内部查询能力时,Admin 应得到明确错误,不泄露租户库信息。
@@ -0,0 +1,207 @@
# MeetingIM App 版本更新前后端对接文档
## 1. 适用范围
本文档用于 iOS、Android、Web 客户端检查新版本,以及执行推荐更新和强制更新。
- 版本策略为全局配置,存储在 `hotgo.im_app_releases`,不区分租户。
- iOS 安装包由 App Store 托管,客户端通过 `store_url` 跳转。
- Android APK 由发布人员上传到专用 OSS 发布目录,客户端通过 `package_url` 下载。
- Web 端没有安装包时,`store_url` 和 `package_url` 可以为空,使用 `update_type`、`min_supported_version` 和 `release_notes` 控制强刷、兼容提醒或维护提示。
- 客户端不直接访问数据库,也不自行拼接 OSS 地址。
## 2. 检查版本接口
```http
GET https://api.118im.com/v1/im/app/releases/latest
```
该接口允许未登录调用。
### 2.1 请求 Header
| Header | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `X-IM-Schema` | 是 | `1` | MeetingIM HTTP 协议版本 |
| `X-Client-Platform` | 建议 | `ios` / `android` / `web` | 客户端平台 |
### 2.2 Query 参数
| 参数 | 必填 | 示例 | 说明 |
| --- | --- | --- | --- |
| `apikey` | 是 | `AQAAAA...` | 应用 API Key |
| `platform` | 是 | `android` | 仅支持 `ios`、`android`、`web` |
| `current_version` | 是 | `1.2.0` | 当前版本,使用数字点分格式 |
| `channel` | 否 | `stable` | 发布渠道,默认 `stable` |
Android 请求示例:
```bash
curl -H 'X-IM-Schema: 1' \
-H 'X-Client-Platform: android' \
'https://api.118im.com/v1/im/app/releases/latest?apikey=AQAAAA...&platform=android&current_version=1.2.0&channel=stable'
```
## 3. 成功响应
```json
{
"code": 0,
"data": {
"platform": "android",
"channel": "stable",
"current_version": "1.2.0",
"latest_version": "1.3.0",
"min_supported_version": "1.1.0",
"has_update": true,
"force_update": false,
"update_type": "recommended",
"release_notes": "优化消息稳定性并修复已知问题。",
"store_url": "",
"package_url": "https://download.example.com/app-releases/android/stable/1.3.0/meetingim-1.3.0.apk",
"checksum": "0123456789abcdef0123456789abcdef0123456789abcdef0123456789abcdef",
"remind_after_seconds": 86400,
"published_at": "2026-09-12T08:00:00Z"
},
"request_id": "req_xxx",
"server_time": "2026-09-12T08:10:00Z"
}
```
### 3.1 响应字段
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `current_version` | string | 客户端传入的当前版本 |
| `latest_version` | string | 后台发布的最新版本 |
| `min_supported_version` | string | 最低可继续使用的版本 |
| `has_update` | boolean | 是否存在更高版本 |
| `force_update` | boolean | 是否必须完成更新后才能继续使用 |
| `update_type` | string | `none`、`recommended`、`force` |
| `release_notes` | string | 更新说明 |
| `store_url` | string | iOS App Store 地址 |
| `package_url` | string | Android APK 的 OSS/CDN HTTPS 下载地址 |
| `checksum` | string | Android APK 的 SHA-256,小写十六进制 |
| `remind_after_seconds` | number | 推荐更新关闭后再次提醒的间隔 |
| `published_at` | string | 发布时间,UTC RFC 3339 |
## 4. 更新判断规则
客户端以服务端返回结果为准,不自行重新计算强制更新规则。
| 条件 | 客户端行为 |
| --- | --- |
| `has_update=false` | 不弹更新窗口;手动检查时提示已是最新版本 |
| `has_update=true, force_update=false, update_type=recommended` | 弹推荐更新窗口,允许取消或稍后更新 |
| `has_update=true, force_update=true` | 弹强制更新窗口,不允许关闭,不允许进入主业务 |
| `update_type=none` | 不主动弹窗,可展示版本信息 |
当 `current_version < min_supported_version` 时,服务端无条件返回强制更新:
```json
{
"has_update": true,
"force_update": true,
"update_type": "force"
}
```
## 5. Android APK 与 OSS
### 5.1 OSS 对象路径
建议使用独立于聊天附件的发布目录:
```text
app-releases/android/{channel}/{version}/meetingim-{version}.apk
```
示例:
```text
app-releases/android/stable/1.3.0/meetingim-1.3.0.apk
```
### 5.2 下载地址要求
1. `package_url` 必须使用 HTTPS。
2. 地址必须长期稳定,至少覆盖该版本的完整支持周期。
3. 可以使用 OSS 公共读地址,或绑定 OSS/CDN 的固定下载域名。
4. 不得把有效期只有几分钟的 OSS 签名 URL 保存到版本表。
5. 如果 Bucket 必须私有,应提供固定公开下载接口,由接口实时生成签名并返回 302;版本表保存该固定接口地址。
6. APK 响应建议设置 `Content-Type: application/vnd.android.package-archive`。
7. 文件名、对象路径和 URL 必须包含版本号,禁止覆盖旧版本对象。
聊天附件接口 `/v0/file/s/{file_id}` 需要用户鉴权,不作为 Android 安装包的默认下载地址。
### 5.3 发布步骤
1. Android 构建正式签名 APK。
2. 计算 APK 的 SHA-256。
3. 上传到 OSS 的 `app-releases/android/{channel}/{version}/` 目录。
4. 使用浏览器或 `curl -I` 验证 HTTPS 地址可以下载。
5. 在 Admin 的“IM 全局版本更新”中编辑 Android 策略。
6. 填写最新版本、最低支持版本、更新类型、更新说明、`package_url` 和 SHA-256。
7. 保存后由 Kafka `app.release.updated` 事件通知 IM Server 清理缓存。
8. 使用旧版本号请求检查接口,确认下载地址、校验值和强更状态。
计算 SHA-256 示例:
```bash
shasum -a 256 meetingim-1.3.0.apk
```
## 6. Android 客户端处理
1. 调用版本检查接口。
2. `has_update=false` 时结束检查。
3. 根据 `force_update` 展示推荐更新或不可关闭的强更弹窗。
4. 使用系统下载组件请求 `package_url`,支持进度展示和失败重试。
5. 下载完成后计算本地文件 SHA-256,并与 `checksum` 比较。
6. 校验 APK 包名和签名证书必须与已安装应用一致。
7. 通过 Android `FileProvider` 发起安装,不使用 `file://` URI。
8. 未授予“安装未知应用”权限时,引导用户进入系统授权页。
9. 强更下载或安装失败时保留重试、网络设置和退出应用入口,不能绕过进入主业务。
客户端不得在 SHA-256 或应用签名校验失败后继续安装。
## 7. iOS 客户端处理
1. iOS 只读取 `store_url`,忽略 `package_url` 和 `checksum`。
2. `store_url` 必须是 `https://apps.apple.com/...` 地址。
3. 推荐更新允许取消;强制更新不得进入主业务。
4. 点击更新后使用系统能力打开 App Store。
## 8. 错误响应
缺少或错误的 `current_version`:
```json
{
"code": 30000,
"reason": "MALFORMED_REQUEST",
"message": "malformed request",
"details": {
"field": "current_version"
}
}
```
| HTTP 状态 | 原因 |
| --- | --- |
| 400 | 缺少 `X-IM-Schema: 1` |
| 400 | 缺少或错误的 `current_version` |
| 400 | `platform` 不是 `ios`、`android` 或 `web` |
| 500 | Admin 版本配置缺失、格式错误或内部配置接口不可用 |
## 9. 当前测试环境
```text
Base URL: https://api.118im.com
版本接口: /v1/im/app/releases/latest
API Key: AQAAAAABAAAoeOI7tA3HsYvdzDhYhZJy
协议版本: X-IM-Schema: 1
默认渠道: stable
```
当前测试库已配置 iOS、Android、Web `1.0.0` 默认策略。正式验证 Android 更新前,需要先上传测试 APK,并在 Admin 填写对应 `package_url` 与 SHA-256。
@@ -0,0 +1,747 @@
# IM HotGo 注册短信开关与邀请码管理改造交接文档
> 文档状态:已按数据库直连方案实现,作为现状与后续 Kafka 演进说明
> 对应实施顺序:注册改造计划第 4 项(HotGo 后台)
> HotGo 项目:`/Users/youshunlai/go_workspace/im_manager`
> IM 服务项目:`/Users/youshunlai/go_workspace/im_server`
> 编写日期:2026-08-18
## 1. 文档用途
本文供新的开发会话直接接手 HotGo 项目使用。阅读本文后,应能明确:
1. 为什么要在 HotGo 中增加注册短信开关和邀请码管理。
2. 哪些数据属于 HotGo 控制库,哪些数据属于 IM 租户业务库。
3. HotGo 后端、前端、菜单权限、统一租户身份、审计和配置需要修改什么。
4. HotGo 与 IM 如何通过租户业务库协作,以及后续如何迁移到 Kafka。
5. 如何验证实现没有破坏租户隔离,也没有泄露邀请码明文。
本文指导 HotGo 项目改造。HotGo 负责邀请码生成和管理;IM 仍独占客户端注册、短信验证码和邀请码使用记录事务。
## 2. 开发前必须阅读
在修改 `/Users/youshunlai/go_workspace/im_manager` 前,先阅读以下文档:
1. `/Users/youshunlai/go_workspace/im_server/docs/IM-注册、邀请码与认证数据模型改造需求文档.md`
2. `/Users/youshunlai/go_workspace/im_server/docs/IM-前后端对接文档.md` 中 1.4、1.5 注册章节
3. `/Users/youshunlai/go_workspace/im_server/docs/IM-后台需求文档.md` 中租户权限、用户管理和审计章节
4. `/Users/youshunlai/go_workspace/im_server/docs/IM-独立库多租户改造方案.md`
规则冲突时按以下优先级处理:
1. 本文针对 HotGo 的落地边界和接口约定。
2. 《IM 注册、邀请码与认证数据模型改造需求文档》。
3. 《IM 独立库多租户改造方案》。
4. 其他较早的需求和设计文档。
`IM-服务端多租户改造方案.md` 已废弃,不得作为实现依据。当前多租户方案是“一套 IM 服务、一个 HotGo 控制库、每租户一个独立 IM 业务库”。
## 3. 需求前因后果
### 3.1 注册页面最终规则
客户端注册页面同时展示用户名、手机号、短信验证码、密码和邀请码,但用户名与手机号是二选一关系:
1. 手机号非空时,以手机号方式为准,忽略用户名输入。
2. 手机号方式注册后,`im_users.username` 和 `im_users.phone` 都保存规范化后的 `+86` 手机号。
3. 手机号为空时使用普通用户名,用户名必须匹配 `^[a-z0-9]{1,10}$`,此时 `phone=NULL`。
4. 普通用户名方式不能自助找回密码,只能由企业管理员重置。
5. 邀请码固定为 5 位 ASCII 大小写英文字母,区分大小写、租户内唯一,有效期内可供多人使用。
### 3.2 为什么需要短信开关
短信服务在不同部署环境中可能暂未启用,因此需要平台级开关:
| 场景 | 短信开关开启 | 短信开关关闭 |
| --- | --- | --- |
| 点击获取验证码 | IM 服务真实调用短信服务商 | IM 服务返回正常挑战和倒计时,但不调用服务商 |
| 手机号注册 | 必须验证短信;邀请码不能绕过 | 必须提供有效邀请码,可无验证码注册 |
| 用户名注册 | 不发送短信,必须提供有效邀请码 | 不发送短信,必须提供有效邀请码 |
| 客户端是否知道开关 | 不直接暴露 | 不直接暴露 |
HotGo 只负责管理开关值。是否调用短信服务商、是否允许注册,全部由 IM 服务端判定。
### 3.3 为什么当前改为 HotGo 直接访问租户库
邀请码属于 IM 租户业务数据:
1. 每个租户有独立业务库。
2. 邀请码批次和主表位于 `hotgo` 控制库,使用明细位于对应租户业务库。
3. HotGo 邀请码管理直接访问控制库;密码重置等租户业务功能仍通过 `im_tenant.id` 和服务端保存的 DSN 选择租户库。
4. 当前部署中 HotGo 到 IM 的内部 HTTP 地址不可达,继续维护 HMAC 管理 API 会增加一套没有必要的调用链。
因此当前采用“控制库主数据 + 租户库使用事实”的混合边界:HotGo 负责邀请码生成、查询、导出和撤销;IM 从控制库校验邀请码,并在租户库记录使用关系和 outbox 事件。两端各自只写自己负责的数据。
密码重置等需要 IM 立即感知的操作写入 `im_business_events_outbox`。当前 IM 轮询消费,后续改为 Kafka 时保留事件契约,只替换传输层。
## 4. 系统边界
```mermaid
flowchart LR
A["HotGo 管理后台浏览器"] -->|HotGo 登录 Token| B["HotGo 后端"]
B -->|读写全局开关| C[("hotgo.hg_sys_config")]
B -->|读取管理员 tenant_id| D[("hotgo.im_tenant")]
B -->|管理邀请码| G[("hotgo.im_registration_invite_batches")]
B -->|管理邀请码| H[("hotgo.im_registration_invite_codes")]
B -->|其他租户业务按服务端 DSN| F[("租户 IM 业务库")]
F --> I["im_registration_invite_uses"]
J["IM 注册与发码接口"] -->|短时缓存读取| C
J -->|按 tenant_id 校验| H
J -->|记录使用关系与事件| F
B -->|写 security.password_reset| K["im_business_events_outbox"]
F --> K
K -->|当前轮询,后续 Kafka| J
```
### 4.1 HotGo 负责
1. 在 `hg_sys_config` 保存全局注册短信开关。
2. 只允许平台超级管理员修改开关。
3. 为邀请码提供管理页面、表单、权限和审计入口。
4. 从当前登录管理员身份解析可信的 IM `tenant_id`。
5. 通过 `imtenant.DB()` 直连所选租户业务库,不接受浏览器传入 DSN。
6. 对邀请码明文查询、查看和导出执行独立权限校验。
### 4.2 IM 服务负责
1. 读取并缓存短信开关。
2. 实际发送或抑制短信。
3. 注册时大小写敏感地校验邀请码,并记录邀请码与新用户的使用关系。
4. 邀请码使用后不改变 active 状态。
5. 消费 `security.password_reset` 事件并清退在线会话。
6. 登录时以 `auth_version` 拒绝密码重置前签发的旧 Token。
7. 消费 `registration.invite_used`,将租户库使用统计幂等投影到控制库。
### 4.3 明确禁止
1. 浏览器直接连接租户数据库。
2. 浏览器通过 Header、Query 或 Body 自报可信 `tenant_id`。
3. HotGo 后端根据浏览器传入的 `db_name` 或 `db_dsn` 连接数据库。
4. HotGo 修改 `im_registration_invite_uses`;该表只允许 IM 注册事务写入。
5. HotGo 绕过 outbox 直接修改在线会话状态。
6. 在普通访问日志、错误日志、埋点或 URL 中记录完整邀请码。
## 5. 当前 HotGo 项目现状
目标仓库 `/Users/youshunlai/go_workspace/im_manager` 基于 GoFrame 2、Vue 3 和 Naive UI。
已确认的现状:
1. 通用配置后端位于 `server/api/admin/config`、`server/internal/controller/admin/sys/config.go`、`server/internal/logic/sys/config.go`。
2. 通用配置前端位于 `web/src/views/system/config` 和 `web/src/api/sys/config.ts`。
3. `hg_sys_config` 支持 `group/key/type/value/default_value`,前端不能通过通用接口新增不存在的配置项。
4. `sys_switch` 字典实际定义为“开启=`1`、关闭=`2`”。
5. HotGo 管理接口权限按“路由路径 + HTTP Method”使用 Casbin 校验,前端按钮权限也使用路由路径,例如 `hasPermission(['/notice/edit'])`。
6. `hotgo.im_tenant` 是全系统唯一租户注册表,由 HotGo 维护,不存在另一套需要映射的“IM 租户”。
7. 后台管理员通过 `hg_admin_member.tenant_id` 直接引用 `im_tenant.id`,登录身份必须携带该字段;不再使用独立租户绑定表。
8. `hg_sys_log` 会保存 GET Query 和 POST Body;现有逻辑只自动掩码名称包含 `password` 的 POST 字段,邀请码查询参数会泄漏,必须补充脱敏。
## 6. 建议开发顺序
### 阶段 A:先完成 HotGo 基础边界
1. 增加全局短信开关配置种子和专用读写接口。
2. 增加 `hg_admin_member.tenant_id` 外键与登录身份解析,统一使用 `im_tenant.id`。
3. 增加租户库直连、Snowflake ID 和数据库事件写入能力。
4. 增加邀请码相关菜单、路由权限和后台代理接口。
5. 增加邀请码管理页面。
6. 增加敏感字段日志脱敏和操作审计。
### 阶段 B:与 IM 服务联调
1. 核对 IM 租户库迁移已执行且表结构一致。
2. 核对服务凭证、租户上下文和操作人上下文可被 IM 验证。
3. 联调单个生成、批量生成、分页查询、撤销、详情和导出。
4. 联调短信开关在 IM 服务缓存时间内生效。
IM 管理接口未完成时,可以通过接口 Client 抽象和测试 Stub 开发页面,但不得用 HotGo 直写租户数据库作为临时方案。
## 7. 全局注册短信开关
### 7.1 数据定义
在 HotGo 控制库 `hg_sys_config` 中增加且只保留一条配置:
```text
group = im
name = 注册短信发送
type = bool
key = register_sms_enabled
value = 1
default_value = 1
status = 1
is_default = 1
```
建议的 MySQL 幂等种子:
```sql
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT
'im', '注册短信发送', 'bool', 'register_sms_enabled', '1', '1', 10,
'控制 IM 注册验证码是否真实发送', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM hg_sys_config
WHERE `group` = 'im' AND `key` = 'register_sms_enabled'
);
```
同时为 `(group, key)` 增加唯一约束。加索引前先检查并清理重复配置,不能让 IM 服务随机读到多条值。
```sql
ALTER TABLE hg_sys_config
ADD UNIQUE KEY uk_hg_sys_config_group_key (`group`, `key`);
```
新装数据库种子、实际部署迁移及项目保留的 MySQL/PostgreSQL/SQLite 初始化脚本应按项目发布策略同步,不能只改一份示例 SQL。
### 7.2 `sys_switch` 映射陷阱
`sys_switch` 字典值与真实配置值不相同:
| UI 字典值 | UI 含义 | `hg_sys_config.value` |
| --- | --- | --- |
| `1` | 开启 | `1` |
| `2` | 关闭 | `0` |
实现要求:
1. 页面加载时将后端 bool 值映射为字典值:`true -> 1`,`false -> 2`。
2. 页面提交时反向映射:`1 -> true`,`2 -> false`。
3. 不得把字典值 `2` 原样写入 bool 配置;GoFrame 的 bool 转换可能把非零值当成 true。
4. 数据库只允许保存可解释为 `1/0` 的值。
### 7.3 后端接口
不要直接让页面复用通用 `/config/update`。新增专用接口,以便强制执行平台管理员校验:
| 方法 | HotGo 路径 | 权限 |
| --- | --- | --- |
| GET | `/imRegistration/config/get` | 平台超级管理员 |
| POST | `/imRegistration/config/update` | 平台超级管理员 |
读取响应:
```json
{
"registerSmsEnabled": true,
"updatedAt": "2026-08-18T16:00:00+08:00"
}
```
更新请求:
```json
{
"registerSmsEnabled": false
}
```
后端必须在业务逻辑层再次调用 `service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx))`,不能只依赖菜单是否可见。更新时只允许修改固定的 `group=im/key=register_sms_enabled`,不接受前端传 group 或 key。
### 7.4 页面位置
建议在 `web/src/views/system/config/system.vue` 增加“IM 注册”设置项,并新增:
```text
web/src/views/system/config/ImRegistrationSetting.vue
```
页面只展示一个“注册短信发送”开关和保存按钮。选项通过 `dict.loadOptions(['sys_switch'])` 加载,并按 7.2 做显式映射。
### 7.5 IM 服务读取约定
HotGo 不需要额外暴露公开配置接口。IM 服务直接从控制库读取:
```sql
SELECT value
FROM hg_sys_config
WHERE `group` = 'im'
AND `key` = 'register_sms_enabled'
AND status = 1
LIMIT 1;
```
约定:
1. 种子默认值为开启。
2. IM 服务缓存建议为 30 秒,缓存时长可配置。
3. 临时读取失败时优先使用最近一次成功值。
4. 进程启动后无缓存且配置缺失、停用或非法时,按“开启”处理,防止邀请码绕过短信验证,并记录告警。
5. HotGo 更新后不承诺立即生效,验收标准为 30 秒内生效。
## 8. 统一租户身份
### 8.1 唯一租户主键
`hotgo.im_tenant.id` 是 HotGo 和 IM 服务共同使用的唯一 `tenant_id`。HotGo 负责维护租户注册信息和业务库路由,IM 服务根据该 ID 选择租户业务库。不得再创建 `im_admin_tenant_binding` 或其他租户映射表。
### 8.2 管理员租户归属
后台管理员记录直接引用统一租户:
```sql
ALTER TABLE hg_admin_member
ADD COLUMN tenant_id BIGINT NULL COMMENT '统一租户ID,引用 im_tenant.id',
ADD INDEX idx_hg_admin_member_tenant (tenant_id),
ADD CONSTRAINT fk_hg_admin_member_im_tenant
FOREIGN KEY (tenant_id) REFERENCES im_tenant(id);
```
平台账号的 `tenant_id` 可以为空。租户管理员及其创建的下级后台账号必须继承同一个 `im_tenant.id`。只有平台超级管理员可以为后台账号选择或修改租户,租户域管理员提交的 `tenantId` 必须被后端忽略并以当前登录身份为准。
### 8.3 解析规则
租户管理员请求邀请码接口时:
1. 从 HotGo 登录上下文取得 `member_id`、`tenant_id` 和部门类型。
2. 仅允许合法的租户级管理员角色进入;普通用户和商户角色不得管理邀请码。
3. `tenant_id` 必须大于 0,并且对应的 `im_tenant` 必须存在且为 active。
4. 将该 `tenant_id` 写入 IM 服务调用上下文。
5. 忽略并拒绝浏览器提交的 `tenantId`。
平台超级管理员可以跨租户查看邀请码;列表和导出未选择租户时查询全部。创建、批次查看、撤销及其他写操作必须先选择一个 `im_tenant`。此选择只对 `VerifySuperId` 通过的管理员开放,HotGo 后端仍要检查目标租户存在且 active。
后台用户管理应提供仅平台超级管理员可见的租户选择控件。租户管理员未关联 `im_tenant.id` 时返回明确错误“当前管理员未关联租户”,不能回退到默认租户。
## 9. 租户库直连与事件通道
### 9.1 数据库选择
HotGo 从可信登录上下文取得 `im_tenant.id`。邀请码批次和邀请码主表直接通过 `imtenant.RegistryDB()` 访问 HotGo 控制库;只有密码重置等租户业务操作才通过 `imtenant.DB()` 打开租户业务库。浏览器传入的 `tenantId` 只允许平台超级管理员选择,租户管理员始终使用登录关系中的租户 ID;任何请求都不能提交或覆盖 DSN。
若配置了 `database.imRegistry`,租户注册表从该连接读取;未配置时回退到 HotGo 默认库。业务操作始终使用按租户动态建立的连接组。
### 9.2 Snowflake 配置
HotGo 直接新增 IM 表记录时使用与 IM 相同位布局的 Snowflake ID:
```yaml
imDirectDb:
snowflakeWorkerId: 1023
```
也可由 `IM_DIRECT_DB_SNOWFLAKE_WORKER_ID` 覆盖。每个 HotGo 写库实例必须使用唯一 worker ID,并且不能与 IM 节点重叠。当前单实例默认预留 `1023`,多实例部署前必须分配 worker ID。
### 9.3 写入职责
HotGo 可以写:
1. HotGo 控制库中的 `im_registration_invite_batches`、`im_registration_invite_codes`。
2. 管理员重置密码时的 `im_auth.secret` 和 `im_user_security`。
3. 管理操作产生的 `im_business_events_outbox`。
IM 独占写入:
1. 客户端注册事务及用户创建。
2. 租户业务库中的 `im_registration_invite_uses` 以及同事务的 `registration.invite_used` outbox 事件。
3. 客户端登录、改密、短信找回等认证事务。
### 9.4 Kafka 演进
管理员重置密码在数据库事务内写入:
```json
{
"event_type": "security.password_reset",
"payload": {
"user_id": 198000000000000001,
"auth_version": 2,
"operator_id": 10001,
"request_id": "..."
}
}
```
当前 IM 在注册写入 outbox 后立即触发一次消费,并每秒按在线会话涉及的租户轮询 pending 事件。`security.password_reset` 消费后清退该用户会话;`registration.invite_used` 消费时从租户使用明细重新聚合 `COUNT/MAX(used_at)`,覆盖 HotGo 控制库的 `usage_count/last_used_at`,成功后标记 delivered。离线用户不依赖密码重置轮询,后续登录时由 `auth_version` 直接拒绝旧 Token。
邀请码使用事件契约:
```json
{
"event_type": "registration.invite_used",
"payload": {
"tenant_id": 1,
"invite_code_id": 198000000000000001,
"user_id": 198000000000000002,
"request_id": "acc-request-id",
"used_at": "2026-08-19T10:00:00.000Z"
}
}
```
迁移 Kafka 时,生产者继续在业务事务内写 outbox,由独立投递器发布同一事件;消费者保持事件类型与 payload 不变。Kafka 投递确认后再标记 outbox delivered,避免数据库提交成功但消息丢失。
## 10. 邀请码接口契约
### 10.1 数据表契约
HotGo 在控制库直接维护带 `tenant_id` 的 `im_registration_invite_batches` 和 `im_registration_invite_codes`。邀请码状态仅为 `active/expired/revoked`,使用后仍为 active;`im_registration_invite_uses` 只由 IM 注册事务写入租户库。控制库中的 `usage_count/last_used_at` 由 outbox 消费器幂等投影,HotGo 列表不跨租户库做联表聚合。
平台超级管理员在列表和导出接口中可不传 `tenantId` 以查询全部租户;创建、批次查看、撤销等写操作必须明确选择一个 active 租户。租户管理员只能查询和操作登录关系绑定的 `im_tenant.id`。
邀请码明文不得放入 URL。查询完整邀请码时必须使用 HotGo POST Body,普通请求日志需对 `inviteCode` 脱敏。
### 10.2 HotGo 对浏览器接口
沿用 HotGo 现有命名风格,建议增加:
| 方法 | HotGo 路径 | 说明 |
| --- | --- | --- |
| POST | `/imInvite/list` | 分页查询,过滤条件放在 JSON Body |
| POST | `/imInvite/create` | 单个生成 |
| POST | `/imInvite/batchCreate` | 批量生成 |
| GET | `/imInvite/batchView` | 批次详情 |
| POST | `/imInvite/revoke` | 单个撤销 |
| POST | `/imInvite/batchRevoke` | 批量撤销 |
| POST | `/imInvite/export` | 明文导出,过滤条件放在 JSON Body |
HotGo controller 只做 DTO 转换、调用 service 和包装响应。租户解析、平台管理员判断、权限复核、审计和租户库访问放在 logic/service 层。
### 10.3 单个生成
HotGo 请求:
```json
{
"expiresAt": "2026-09-01T00:00:00+08:00",
"remark": "新员工入职",
"idempotencyKey": "7be3de87-994f-46c6-86bd-844ffb54f83f"
}
```
规则:
1. `expiresAt` 必填,必须晚于服务端当前时间。
2. `remark` 最长 255 字符。
3. 推荐单个生成也传幂等键,避免提交成功但响应超时时重复创建。
4. 邀请码由 HotGo 使用 `crypto/rand` 生成并依靠租户库唯一索引兜底,HotGo 不接受用户自定义 code。
响应数据:
```json
{
"id": 198000000000000001,
"code": "AbCdE",
"status": "active",
"expiresAt": "2026-09-01T00:00:00+08:00",
"remark": "新员工入职",
"createdBy": 10001,
"createdAt": "2026-08-18T16:10:00+08:00"
}
```
### 10.4 批量生成
HotGo 请求:
```json
{
"quantity": 100,
"expiresAt": "2026-09-01T00:00:00+08:00",
"remark": "2026年8月新员工入职",
"idempotencyKey": "4dc7f13e-448c-4b49-b056-57a2fde18f95"
}
```
规则:
1. `quantity` 为 1 到 1000,由 HotGo 校验。
2. 同一个提交动作必须复用同一个幂等键,直到得到明确成功响应。
3. 前端可在打开弹窗时生成 UUID,失败重试时复用;成功或关闭弹窗后废弃。
4. HotGo 在批次表保存幂等键;相同参数重试返回原批次,不重复生成。
响应数据:
```json
{
"batchId": 198000000000000100,
"quantity": 100,
"activeCount": 100,
"usageCount": 0,
"revokedCount": 0,
"expiredCount": 0,
"expiresAt": "2026-09-01T00:00:00+08:00",
"createdAt": "2026-08-18T16:15:00+08:00"
}
```
### 10.5 分页查询
列表请求使用 POST JSON Body,支持字段:
```text
page, perPage, inviteCode, status, batchId, createdBy,
createdAtStart, createdAtEnd, expiresAtStart, expiresAtEnd
```
要求:
1. `inviteCode` 仅支持完整 5 位精确查询,不做模糊查询,不转换大小写。
2. `status` 为 `active/expired/revoked`,邀请码使用后仍保持 `active`。
3. HotGo 查询前将 `expires_at <= now` 的 active 记录更新为 expired,不依赖定时任务。
4. 默认按 `created_at DESC, id DESC` 排序。
5. `perPage` 设置合理上限,建议不超过 100。
列表字段至少包括:
```json
{
"id": 198000000000000001,
"batchId": 198000000000000100,
"code": "AbCdE",
"status": "active",
"expiresAt": "2026-09-01T00:00:00+08:00",
"usageCount": 12,
"lastUsedAt": "2026-08-20T09:30:00+08:00",
"createdBy": 10001,
"remark": "新员工入职",
"createdAt": "2026-08-18T16:10:00+08:00"
}
```
### 10.6 撤销
1. 只有 `active` 且未过期的邀请码可撤销;已有使用记录不影响撤销。
2. 已过期、已撤销的邀请码不能恢复为 active。
3. 批量撤销只撤销批次内仍可撤销的码,并返回各状态统计。
4. 页面操作前显示确认框,确认内容不必重复显示完整邀请码。
5. 最终状态由租户库条件更新结果决定,不以前端状态为准。
### 10.7 导出
1. 导出必须具备独立 `/imInvite/export` 权限。
2. 导出过滤条件与列表一致。
3. HotGo 后端分页读取租户库并流式写出,不把整份文件加载到内存。
4. 建议输出带 UTF-8 BOM 的 CSV,字段至少包含邀请码、状态、批次、到期时间、累计使用次数、最后使用时间、创建人、创建时间和备注。
5. 文件名建议为 `registration-invite-codes-YYYYMMDD-HHmmss.csv`。
6. 每次导出必须写审计,但审计详情不得包含邀请码明文列表。
## 11. 页面和交互
建议页面路径:
```text
web/src/views/im/registrationInvite/index.vue
web/src/views/im/registrationInvite/model.ts
web/src/views/im/registrationInvite/CreateModal.vue
web/src/views/im/registrationInvite/BatchCreateModal.vue
web/src/views/im/registrationInvite/BatchViewModal.vue
web/src/api/im/registrationInvite.ts
```
页面放在“IM 管理 / 注册邀请码”菜单下。使用现有 `BasicForm`、`BasicTable`、`TableAction`、`usePermission` 和 Naive UI 组件。
页面必须具备:
1. 完整邀请码、状态、批次 ID、创建人、创建时间和到期时间筛选。
2. 单个生成按钮。
3. 批量生成按钮。
4. 导出按钮。
5. 列表展示完整邀请码、状态、到期时间、批次、使用情况、创建信息和备注。
6. active 行提供撤销操作,其他状态不显示撤销操作。
7. 批次详情展示总数、active、used、expired、revoked 统计。
8. 所有日期时间明确按本地时区展示,提交时发送带时区的 RFC 3339 时间。
9. 加载、空数据、接口错误、重复提交和无权限状态处理完整。
邀请码区分大小写,任何输入组件、复制操作和查询参数都不得自动转为大写或小写。
## 12. 菜单与权限
HotGo 的权限标识使用接口路径。建议创建以下按钮权限:
```text
/imRegistration/config/get
/imRegistration/config/update
/imInvite/list
/imInvite/create
/imInvite/batchCreate
/imInvite/batchView
/imInvite/revoke
/imInvite/batchRevoke
/imInvite/export
```
权限分配:
| 能力 | 平台超级管理员 | 租户管理员 | 其他角色 |
| --- | --- | --- | --- |
| 查看/修改短信开关 | 允许 | 禁止 | 禁止 |
| 选择任意 IM 租户管理邀请码 | 允许 | 禁止 | 禁止 |
| 管理当前绑定租户邀请码 | 允许 | 按权限允许 | 禁止 |
| 查看邀请码明文 | 允许 | 需独立权限 | 禁止 |
| 导出邀请码明文 | 允许 | 需独立权限 | 禁止 |
服务端 Casbin、业务逻辑层租户范围校验、前端按钮隐藏三层都要实现。前端隐藏按钮不是安全边界。
## 13. 日志脱敏与审计
### 13.1 必须修复现有访问日志泄露
`server/internal/logic/sys/log.go` 当前会保存 Query 和 Body,且只掩码 password。至少增加以下字段名的大小写不敏感脱敏:
```text
inviteCode
invite_code
serviceSecret
service_secret
authorization
```
建议抽成统一的递归敏感字段过滤器,同时处理 Query、JSON Body、Form 和 Header。Header 中已有简化逻辑也必须确认不会保存内部服务凭证。
对 `/imInvite/list` 的明文精确查询,邀请码只能放在 POST Body,`hg_sys_log.post_data` 中只能出现 `******`。`hg_sys_log.get_data`、Nginx URL 和其他访问日志中都不能出现真实邀请码。HTTP Client 的 debug 日志同样不得打印完整 URL、Query、请求体或响应体。
### 13.2 业务审计
仅靠普通访问日志不够。建议在 HotGo 控制库增加独立审计表,或者接入项目已有的等价不可变审计设施。最低字段为:
```sql
CREATE TABLE im_admin_operation_audit (
id BIGINT NOT NULL AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL,
tenant_id BIGINT NULL,
operator_id BIGINT NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(32) NOT NULL,
target_id BIGINT NULL,
result VARCHAR(16) NOT NULL,
ip VARCHAR(64) NOT NULL DEFAULT '',
detail_json JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
KEY idx_im_admin_audit_tenant_time (tenant_id, created_at),
KEY idx_im_admin_audit_operator_time (operator_id, created_at),
UNIQUE KEY uk_im_admin_audit_request_action (request_id, action)
);
```
必须审计:
1. 短信开关修改,记录旧值和新值。
2. 单个生成和批量生成,记录 code ID 或 batch ID、数量和到期时间,但不记录 code 明文。
3. 邀请码明文查询和详情查看,记录查询行为,但不记录搜索明文。
4. 单个撤销和批量撤销。
5. 明文导出,记录过滤条件摘要和导出数量,不记录明文内容。
6. 后台管理员租户归属变更。
IM 服务也应写自己的业务审计。HotGo 审计记录后台操作者,IM 审计记录实际租户库操作,两者通过 `request_id` 关联。
## 14. 错误处理
HotGo 应将数据库错误稳定映射为后台可理解的错误,不向浏览器返回 DSN、SQL、堆栈或内部配置。
至少处理:
| 场景 | HotGo 行为 |
| --- | --- |
| 当前管理员未关联租户 | 返回明确业务错误,不使用默认租户 |
| 目标租户停用 | 拒绝操作 |
| 无查看/导出权限 | 403 |
| 邀请码已过期或已撤销 | 显示不可撤销,不本地修改状态 |
| 幂等键重复且请求相同 | 返回原结果 |
| 幂等键重复但参数不同 | 返回幂等冲突 |
| 租户库连接失败 | 返回“租户业务库暂不可用”,保留 Request ID |
| 数据库写入失败 | 事务回滚,返回统一后台错误并记录脱敏日志 |
| 导出数据过大 | 使用服务端限制或异步导出,不耗尽内存 |
## 15. 建议代码落点
遵循现有 HotGo 分层,建议新增或修改:
```text
server/api/admin/imregistration/
server/api/admin/iminvite/
server/internal/controller/admin/admin/im_registration.go
server/internal/controller/admin/admin/im_invite.go
server/internal/model/input/adminin/im_registration.go
server/internal/model/input/adminin/im_invite.go
server/internal/service/im.go
server/internal/logic/admin/im_registration.go
server/internal/logic/admin/im_invite.go
server/internal/library/imtenant/db.go
server/internal/library/imtenant/snowflake.go
server/internal/router/admin.go
server/internal/logic/sys/log.go
web/src/api/im/registrationInvite.ts
web/src/views/im/registrationInvite/
web/src/views/system/config/ImRegistrationSetting.vue
web/src/views/system/config/system.vue
```
如果新增 `im_admin_operation_audit`,按 HotGo 现有 GoFrame DAO 生成流程生成 `dao/do/entity`,不要手工修改标有 “Code generated” 的文件。禁止新增 `im_admin_tenant_binding`。
同时提交:
1. 生产数据库幂等迁移。
2. 新装数据库初始化数据。
3. 菜单和按钮权限种子。
4. 默认角色授权或明确的部署授权步骤。
5. Snowflake worker 配置示例,但不提交真实 DSN。
## 16. 测试清单
### 16.1 短信开关
1. 无配置时 IM 按开启处理并告警。
2. 平台超级管理员可查看和修改。
3. 租户管理员即使手工调用接口也不能修改。
4. UI 选择“关闭”后数据库保存 `0`,不是 `2`。
5. UI 重新加载时 `0` 正确显示为“关闭”。
6. IM 服务在 30 秒内读取到新值。
7. 开关审计包含操作者、旧值、新值和 Request ID。
### 16.2 租户隔离
1. 租户 A 管理员只能看到租户 A 邀请码。
2. 修改 Query、Body 或 Header 中的 tenant ID 不能访问租户 B。
3. 未绑定管理员不能回退到 default 租户。
4. 停用租户不能生成、查询、撤销或导出。
5. 平台超级管理员选择租户后只能操作所选租户,切换时列表立即清空并重新请求。
### 16.3 邀请码
1. 单个生成返回恰好 5 位 `[A-Za-z]`。
2. 批量生成数量正确,最大值限制生效。
3. `AbCdE` 和 `abcde` 可作为不同邀请码精确查询。
4. 重复提交相同幂等键不产生第二个批次。
5. active 可撤销,expired/revoked 不可撤销。
6. 同一 active 邀请码可供多个不同用户注册,累计使用次数正确增长且状态保持 active。
7. 列表、详情和导出权限相互独立。
8. 导出文件数量、过滤条件和明文字段正确。
### 16.4 安全和日志
1. `hg_sys_log.get_data/post_data/header_data` 中找不到真实邀请码和服务 secret。
2. 应用日志中找不到邀请码明文、内部 Token 和 Authorization。
3. 每个敏感操作都有 HotGo 审计和 IM 审计,并可通过 Request ID 对应。
4. 无权限接口返回 403,不能只依赖前端按钮隐藏。
5. 数据库连接和 SQL 错误不泄露内部实现信息。
## 17. 完成标准
满足以下条件才能认为 HotGo 第 4 项完成:
1. 全局注册短信开关可由平台超级管理员配置,关闭值正确保存为 `0`。
2. IM 服务能在约定缓存时间内读取开关,缺失和异常时采用安全默认值。
3. HotGo 管理员身份能可靠映射到 `im_tenant.id`,不存在默认租户回退。
4. 邀请码单个、批量、查询、详情、撤销和导出全部通过所选租户库完成。
5. 平台与租户权限、跨租户隔离和服务端复核全部通过测试。
6. 邀请码明文只出现在授权页面和授权导出中,不进入普通日志。
7. 配置修改和邀请码敏感操作均有可关联的审计记录。
8. 后端 Go 测试、前端类型检查/构建和核心页面联调通过。
## 18. 新会话可直接使用的任务说明
在新的开发会话中可直接发送:
```text
请修改 HotGo 项目 /Users/youshunlai/go_workspace/im_manager。
先完整阅读 /Users/youshunlai/go_workspace/im_server/docs/IM-HotGo注册短信开关与邀请码管理改造交接文档.md,
再阅读其中列出的上游 IM 文档。按照交接文档的开发顺序完成 HotGo 的注册短信全局开关、
统一租户身份、租户库直连、邀请码管理后台、菜单权限、日志脱敏和审计。
只允许通过服务端 `im_tenant` 注册信息选择租户库,不要信任浏览器自报 tenant_id 或 DSN,
不要修改 `im_registration_invite_uses`,不要把邀请码明文写入普通日志。
开始前先检查 im_manager 当前工作区已有改动,并保留与本任务无关的用户修改。
```
+955
View File
@@ -0,0 +1,955 @@
# IM Kafka 接入方案
> 日期:2026-08-26
> 关联文档:[IM Kafka 接入范围识别](./IM-Kafka接入范围识别.md)、[IM 前后端逐图对接协议](./IM-前后端对接文档.md)
> 目标:明确 Admin 后台、IM 服务、HotGo 控制库、租户业务库之间的 Kafka 接入边界、查询方式、鉴权方式和首批落地范围。
## 0. 与范围识别文档的关系
[IM Kafka 接入范围识别](./IM-Kafka接入范围识别.md) 是本方案的前置输入,负责按前后端对接文档逐章识别哪些业务适合 Kafka、哪些仍应走 WebSocket/Redis/MySQL/OSS。本方案在该识别结果基础上,进一步确定服务边界、库访问边界、前后台交互模型、内部查询接口、鉴权方式和首批落地步骤。
两份文档分工如下:
| 文档 | 作用 |
| --- | --- |
| [IM Kafka 接入范围识别](./IM-Kafka接入范围识别.md) | 回答“哪些业务该用 Kafka,哪些不该用”。 |
| 本方案 | 回答“Kafka 怎样接入,Admin 与 IM 怎样交互,服务和库怎样隔离”。 |
实施时以范围识别文档的逐章清单确定业务覆盖面,以本方案确定技术实现方式。若后续新增需求,先补充范围识别,再同步调整本方案的 Topic、Command、内部接口和鉴权规则。
## 1. 核心原则
系统按“服务和库一一对应”设计,不允许不同服务直连对方数据库。
| 服务 | 只能直接访问 | 说明 |
| --- | --- | --- |
| Admin/HotGo 服务 | `hotgo` 控制库 | 管理后台、租户主数据、后台配置、审核、操作单。 |
| IM 服务 | 当前租户业务库 | 用户、好友、群、消息、文件引用、工作台业务数据。 |
| Redis | IM 实时状态与缓存 | 在线状态、短状态、热点消息、缓存分层。 |
| Kafka | Admin 与 IM 的命令、事件、结果桥 | 后台写操作、异步任务、业务事件、审计、缓存失效。 |
| OSS | 文件对象存储 | 上传下载字节流不进入 Kafka。 |
最终边界:
1. Admin 不直连租户库。
2. IM 不直连 `hotgo` 库。
3. Admin 写租户业务操作统一通过 Kafka Command。
4. IM 写后台业务状态不直写 `hotgo`,通过 Kafka Result/Event 返回 Admin。
5. Admin 查询租户业务数据通过 IM 内部只读 API。
6. IM 查询租户配置、字典、路由通过 Admin 内部只读 API,或消费 Kafka 配置事件维护本地副本。
7. 在线实时投递继续走 Redis + WebSocket,Kafka 不替代 WS 主链路。
## 2. 总体架构
```text
Admin/HotGo 服务
-> 写 hotgo 操作单
-> 生产 Kafka Command
-> IM 服务消费 Command
-> 写租户业务库
-> 生产 Kafka Result/Event
-> Admin 消费 Result/Event
-> 更新 hotgo 操作状态
Admin 查询租户业务数据
-> 调 IM 内部只读 API
-> IM 查租户业务库
-> 返回 Admin
IM 查询租户配置/字典/路由
-> 优先读本地缓存/本地副本
-> 缓存缺失时调 Admin 内部只读配置 API
-> Admin 查 hotgo
-> 返回 IM
```
## 3. Kafka 使用范围
| 场景 | 是否使用 Kafka | 说明 |
| --- | --- | --- |
| Admin 后台群操作通知 C 端 | 是 | 后台写 hotgo 操作单,投 Kafka,IM 执行租户库变更并 WS 下发。 |
| Admin 后台用户封禁、解封、重置密码 | 是 | IM 消费后更新租户库、踢在线、通知客户端。 |
| Admin 后台租户配置、字典、功能开关变更 | 是 | Admin 写 hotgo 后投配置事件,IM 刷新缓存或本地副本。 |
| 企业注册、实名、举报、反馈审核结果 | 是 | Admin 审核后投 Kafka,IM 生成通知或更新租户侧状态。 |
| 工作台会议、日报、月报、信箱通知 | 是 | 状态变更、提醒、红点更新走 Kafka。 |
| 私聊/群聊消息异步落库 | 是 | WS 收消息后完成鉴权、幂等和 seq,再投 Kafka 异步持久化。 |
| 文件安全检测、恶意文件检测 | 是 | OSS 上传完成后投检测任务,消费者调用第三方并更新状态。 |
| 离线 Push | 是 | 消息或业务事件落地后投 Push 任务。 |
| 在线消息实时 fanout | 不作为主链路 | 继续 Redis Pub/Sub/Stream + WS。 |
| 文件上传下载字节流 | 否 | 走 OSS/文件服务;Kafka 只处理上传完成后的事件。 |
| 验证码、Action Token、限流、在线状态 | 否 | 继续 Redis。 |
| 普通查询接口 | 否 | 走内部只读 API 或本服务数据库。 |
## 4. 前后台交互模型
### 4.1 后台写租户业务
Admin 后台所有会影响租户业务库或 C 端展示的写操作,统一走 Kafka Command。
```text
Admin
1. 校验后台管理员权限
2. 写 hotgo.admin_command,状态 pending
3. 发送 Kafka command
IM
4. 消费 command
5. 校验租户、对象、版本、权限和幂等
6. 写租户业务库
7. 必要时通过 WS 通知在线 C 端
8. 发送 Kafka result
Admin
9. 消费 result
10. 更新 hotgo.admin_command 状态
```
典型操作:
| 操作 | Command | IM 执行动作 | C 端通知 |
| --- | --- | --- | --- |
| 群资料修改 | `group.profile.update` | 更新群表和 Topic 资料 | `group.profile.updated` |
| 设置/删除管理员 | `group.admin.set` / `group.admin.remove` | 更新成员角色 | `group.role.updated` |
| 禁言/解禁 | `group.member.mute` / `group.member.unmute` | 更新禁言表 | `group.mute.updated` |
| 踢人 | `group.member.remove` | 更新成员状态和订阅 | `group.member.removed` |
| 解散群 | `group.delete` | 解散 Topic、撤销权限 | `group.deleted` |
| 用户封禁 | `user.disable` | 更新用户状态、踢在线 | `user.disabled` |
| 重置密码 | `user.password.reset` | 更新密码和 `auth_version` | 可选安全通知 |
### 4.2 后台查询租户业务
Kafka 不适合做普通查询。Admin 需要实时查看租户业务数据时,调用 IM 提供的内部只读 API。
只读 API 只允许 `GET`,不提供通用 SQL 或万能查询接口。
| 接口 | 用途 |
| --- | --- |
| `GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}` | 查询租户用户详情。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}` | 查询群详情。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/members` | 查询群成员。 |
| `GET /internal/admin-query/tenants/{tenant_code}/messages/{topic}/{seq}` | 查询消息受控快照。 |
| `GET /internal/admin-query/tenants/{tenant_code}/files/{file_id}` | 查询文件引用和审核状态。 |
| `GET /internal/admin-query/tenants/{tenant_code}/reports/abuse/{report_id}/target` | 查询举报目标快照。 |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters` | 查询 IM 租户库 Kafka 死信列表。 |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters/{dead_id}` | 查询单条死信详情和原始 envelope。 |
后台列表、统计、高频页面优先使用 `hotgo` 摘要表。摘要由 IM 通过 Kafka 事件同步给 Admin,Admin 查询自己的库。
推荐摘要表:
| 摘要表 | 来源 |
| --- | --- |
| `admin_im_user_snapshot` | IM 用户资料变更事件。 |
| `admin_im_group_snapshot` | IM 群资料、成员数、状态变更事件。 |
| `admin_im_group_member_stat` | 群成员统计事件。 |
| `admin_im_file_audit_snapshot` | 文件审核和引用状态事件。 |
| `admin_im_workbench_snapshot` | 工作台红点、会议、日报/月报状态事件。 |
### 4.3 IM 查询后台配置
IM 不直连 `hotgo`。IM 需要租户路由、租户状态、字典、第三方配置时,使用两种方式:
1. 常态:Admin 修改配置后发送 Kafka 配置事件,IM 消费后更新进程内缓存或本地副本。
2. 兜底:IM 缓存缺失或启动预热时,调用 Admin 内部只读配置 API。
| 接口 | 用途 |
| --- | --- |
| `GET /internal/im-config/tenants/{tenant_code}` | 查询租户状态、名称、路由标识。 |
| `GET /internal/im-config/tenants/{tenant_code}/limits` | 查询人数、群数、上传大小等限制。 |
| `GET /internal/im-config/tenants/{tenant_code}/features` | 查询功能开关。 |
| `GET /internal/im-config/dictionaries?types=oss,file_security,sms,jpush,agora` | 查询字典配置。 |
| `GET /internal/im-config/app/releases/latest` | 查询 App 版本配置。 |
Admin 内部配置 API 只返回 IM 运行需要的字段,不返回后台管理员账号、菜单权限、审核私有字段。
## 5. Topic 规划
| Topic | 用途 |
| --- | --- |
| `im.admin.command` | Admin 发给 IM 的租户业务写命令。 |
| `im.admin.command.result` | IM 回传命令执行结果。 |
| `im.config.event` | Admin 发给 IM 的配置、字典、租户状态变更。 |
| `im.business.event` | IM 内部或 Admin 触发的 C 端业务事件。 |
| `im.message.persist` | 私聊消息异步落库。 |
| `im.group.message.persist` | 群聊消息异步落库。 |
| `im.message.push` | 离线 Push 任务。 |
| `im.file.security` | 文件内容安全、恶意文件检测任务。 |
| `im.cache.invalidate` | 跨节点缓存失效。 |
| `im.audit.event` | IM 与 Admin/HotGo 的跨系统审计事件。 |
| `im.dead.letter` | 消费失败死信。 |
首版可合并部分 Topic,但事件体必须保留 `biz_type` 和 `event_type`,方便后续拆分。
## 6. 消息结构
统一 Kafka Envelope:
```json
{
"event_id": "evt_01K...",
"trace_id": "req_01K...",
"tenant_code": "default",
"source_service": "hotgo-admin",
"target_service": "im",
"event_type": "group.member.mute",
"biz_type": "group",
"biz_id": "grp...",
"idempotency_key": "idem_01K...",
"version": 3,
"occurred_at": "2026-08-26T10:00:00.000Z",
"actor": {
"type": "admin",
"id": "admin_1",
"display_name": "管理员"
},
"payload": {}
}
```
Command Result:
```json
{
"event_id": "evt_result_01K...",
"trace_id": "req_01K...",
"tenant_code": "default",
"command_id": "cmd_01K...",
"command_event_id": "evt_01K...",
"status": "succeeded",
"reason": "",
"message": "",
"result": {},
"occurred_at": "2026-08-26T10:00:01.000Z"
}
```
失败时 `status=failed`,`reason` 使用稳定错误码,例如 `GROUP_VERSION_CONFLICT`、`FORBIDDEN`、`RESOURCE_NOT_FOUND`、`COMMAND_DUPLICATED`。
## 7. 幂等、事务与重试
### 7.1 生产端
Admin 写操作必须先落 `hotgo.admin_command`,再发送 Kafka。推荐使用 outbox 模式:
1. 同一事务写 `admin_command` 和 `admin_outbox`。
2. outbox poller 发送 Kafka。
3. Kafka 发送成功后标记 outbox 已发送。
4. 发送失败按退避重试。
IM 内部业务事件也按同样方式写租户库 outbox,避免“数据库成功、事件丢失”。
### 7.2 消费端
每个消费者维护消费幂等记录:
| 字段 | 说明 |
| --- | --- |
| `event_id` | Kafka 事件唯一 ID。 |
| `consumer_group` | 消费组。 |
| `status` | `processing/succeeded/failed/dead`。 |
| `retry_count` | 重试次数。 |
| `last_error` | 最近失败原因。 |
| `created_at/updated_at` | 时间。 |
消费前先插入或锁定 `event_id + consumer_group`。已成功的事件直接跳过;处理中超时可重新抢占。业务表必须再用唯一键兜底,例如 `tenant + command_id`、`tenant + client_mid`、`tenant + topic + seq`。
### 7.3 重试与死信
| 类型 | 策略 |
| --- | --- |
| 临时失败 | 指数退避重试。 |
| 版本冲突 | 不自动重试,回传 failed,由 Admin 刷新后重新发命令。 |
| 权限失败 | 不重试。 |
| 对象不存在 | 不重试,除非明确是延迟同步。 |
| 第三方失败 | 可重试,超过次数写死信和失败表。 |
### 7.4 消费失败处理流程
消费者收到消息后按固定流程处理:
```text
收到 Kafka 消息
-> 校验 Envelope 格式、签名字段、租户和事件类型
-> 抢占 event_id + consumer_group 幂等记录
-> 已 succeeded:提交 offset,跳过
-> processing 超时:允许重新抢占
-> 执行业务逻辑
-> 成功:标记 succeeded,提交 offset
-> 可重试失败:retry_count + 1,按退避策略延迟重试
-> 超过最大重试:写入死信,标记 dead,提交 offset
-> 不可重试失败:标记 failed 或 dead,必要时回传 command result,提交 offset
```
重试建议参数:
| 参数 | 建议值 | 说明 |
| --- | --- | --- |
| `max_retry_count` | 5~10 | 第三方接口可取高值,权限/参数错误不重试。 |
| `retry_backoff` | 10s、30s、1m、5m、15m | 指数退避,可加随机抖动。 |
| `processing_timeout` | 5m~30m | 超过后允许其他消费者重新抢占。 |
| `dead_letter_retention` | 30~90 天 | 便于排障和人工补偿。 |
### 7.5 死信队列处理
进入死信表示该事件已超过自动恢复能力,系统不能再无限重试。死信必须形成“可查看、可诊断、可重放、可关闭”的闭环。
死信写入两处:
1. Kafka `im.dead.letter` Topic:保留原始事件,便于流式订阅、告警和后续自动补偿。
2. 数据库失败表:便于 Admin 后台查询、人工处理和审计。
推荐失败表字段:
| 字段 | 说明 |
| --- | --- |
| `dead_id` | 死信记录 ID。 |
| `event_id` | 原 Kafka 事件 ID。 |
| `source_topic` | 原 Topic。 |
| `consumer_group` | 失败消费组。 |
| `tenant_code` | 租户编码。 |
| `event_type` | 事件类型。 |
| `biz_type` / `biz_id` | 业务对象。 |
| `payload` | 原始 Envelope。 |
| `retry_count` | 已重试次数。 |
| `last_reason` | 稳定失败原因。 |
| `last_error` | 最近错误摘要,敏感字段脱敏。 |
| `status` | `dead/pending_replay/replayed/ignored/resolved`。 |
| `replay_count` | 人工重放次数。 |
| `created_at/updated_at` | 时间。 |
| `resolved_by/resolved_at` | 人工处理人和处理时间。 |
| `resolve_note` | 处理备注。 |
死信来源分类:
| 分类 | 示例 | 默认处理 |
| --- | --- | --- |
| 临时资源失败超过上限 | DB 短暂不可用、第三方超时 | 修复环境后允许重放。 |
| 业务状态冲突 | 群版本冲突、对象已被删除 | 不自动重放,Admin 刷新状态后重新发命令。 |
| 权限失败 | 管理员无权限、租户停用 | 不重放,标记 ignored 或 failed result。 |
| 参数错误 | Envelope 缺字段、payload 非法 | 不重放,修复生产方后关闭。 |
| 第三方永久失败 | 文件违规、Push token 无效 | 按业务结果落状态,不重放或仅重放特定步骤。 |
Admin 后台需要提供死信处理页:
| 操作 | 说明 |
| --- | --- |
| 查看详情 | 展示事件、租户、业务对象、错误原因、重试次数和 trace_id。 |
| 重放 | 将原事件或修正后的事件重新投递到原 Topic,生成新的 `event_id`,保留 `parent_event_id`。 |
| 忽略 | 确认该事件无需继续处理,状态改为 `ignored`。 |
| 标记已处理 | 线下补偿完成后改为 `resolved`。 |
| 重新生成命令 | 对版本冲突类命令,不直接重放旧事件,而是基于最新状态创建新 Command。 |
重放规则:
1. 重放必须生成新的 `event_id`,原 `event_id` 保留在 `parent_event_id`。
2. 重放前重新校验租户状态、对象是否存在、操作者权限和业务版本。
3. 参数错误、权限失败、版本冲突默认禁止直接重放。
4. 文件安全检测、Push、审计写入等可重试任务允许重放。
5. 每次重放都写审计,记录处理人、原因、原事件和新事件。
死信告警:
| 指标 | 建议 |
| --- | --- |
| 单 Topic 死信数 | 5 分钟内超过阈值告警。 |
| 同一 `event_type` 连续失败 | 可能是代码或配置问题,立即告警。 |
| 同一租户大量失败 | 检查租户库、配置或权限状态。 |
| 死信积压时间 | 超过 24 小时未处理提醒。 |
对 Admin Command 类事件,进入死信后必须回传 `im.admin.command.result`,状态为 `failed` 或 `dead`,让后台操作单结束等待状态。否则后台会长期显示处理中。
## 8. 内部接口鉴权
内部 API 鉴权非常关键,必须同时做网络层、服务层和业务层控制。
### 8.1 网络层
1. `/internal/*` 只允许内网网关或内网地址访问。
2. 禁止公网直接暴露内部接口。
3. 有条件时启用 mTLS。
### 8.2 服务签名
内部请求统一携带:
| Header | 说明 |
| --- | --- |
| `X-Service-Name` | 调用方服务名,例如 `hotgo-admin` 或 `im-server`。 |
| `X-Timestamp` | Unix 秒或毫秒时间戳。 |
| `X-Nonce` | 随机串,短期内不可重复。 |
| `X-Request-ID` | 请求追踪 ID。 |
| `X-Signature` | HMAC 签名。 |
签名内容:
```text
METHOD + "\n" +
PATH_WITH_QUERY + "\n" +
BODY_SHA256 + "\n" +
X-Timestamp + "\n" +
X-Nonce
```
校验规则:
1. 时间窗建议 5 分钟。
2. `nonce` 写 Redis,TTL 5 分钟,防重放。
3. 每个服务独立 `service_id/service_secret`。
4. 每个服务配置允许访问的内部接口白名单。
### 8.3 业务授权
Admin 调 IM 查询时,签名 payload 或 Header 中必须包含后台操作者信息:
| 字段 | 说明 |
| --- | --- |
| `admin_user_id` | 后台用户 ID。 |
| `admin_role` | 后台角色。 |
| `admin_permissions` | 本次已校验过的权限列表。 |
| `tenant_code` | 被查询租户。 |
| `reason` | 查询原因或工单号。 |
IM 仍需根据 `tenant_code + permission + resource` 做二次校验,并写内部查询审计。
IM 调 Admin 配置接口时,Admin 只校验服务身份和接口白名单,不返回超出 IM 运行需要的字段。
## 9. 文件与工作台
### 9.1 文件
文件上传下载不走 Kafka,OSS 是文件字节流主链路。当前只有消息落库使用 Kafka;文件生命周期改为同步处理:
1. `im.file.security`:仍可用于内容安全、恶意文件检测。
2. 消息附件引用绑定在消息落库 consumer 内同步执行。
3. 群文件登记/删除由对应 HTTP 业务接口同步落库。
### 9.2 工作台
工作台查询不走 Kafka,状态变化和提醒当前也改为同步处理:
1. 会议创建、更新、取消、提醒、响应:业务接口同步生成通知/红点事件。
2. 日报/月报提交、撤回、重开、催办:业务接口同步生成通知/红点事件。
3. 总经理信箱提交、处理结果:业务接口同步处理。
4. 工作台模块配置变更:`im.config.event` 或 `im.cache.invalidate`。
## 10. 首批落地建议
| 阶段 | 内容 |
| --- | --- |
| 第一步 | 增加 Kafka 配置、生产者、消费者、健康检查、统一 Envelope。 |
| 第二步 | 增加消费幂等表、outbox 表、死信表或死信 Topic。 |
| 第三步 | 接入 `im.admin.command` 和 `im.admin.command.result`,先覆盖后台群治理操作。 |
| 第四步 | 增加 IM 内部只读查询 API,供 Admin 查用户、群、消息、文件快照。 |
| 第五步 | 增加 Admin 内部配置 API,供 IM 查询租户配置、字典、第三方参数。 |
| 第六步 | 接入文件安全检测、离线 Push、工作台事件。 |
| 第七步 | 消息异步落库接入 `im.message.persist`,完善幂等和重试。 |
## 11. 分端任务拆分
### 11.1 Admin/HotGo 侧任务
Admin 侧只访问 `hotgo` 库,负责后台操作入口、配置主数据、命令生产、结果消费和死信处理。
| 模块 | 任务 | 产出 |
| --- | --- | --- |
| Kafka 基础 | 增加 Kafka producer/consumer 配置、健康检查、统一 Envelope 组装。 | 可发送 `im.admin.command`、消费 `im.admin.command.result`。 |
| 操作单 | 新增 `admin_command`、`admin_outbox` 表和状态机。 | `pending/running/succeeded/failed/dead` 操作状态。 |
| Outbox | 后台写操作单与 outbox 同事务落库,poller 发送 Kafka。 | 防止 hotgo 写成功但 Kafka 丢事件。 |
| Command 生产 | 群治理、用户封禁/解封、重置密码、审核结果、工作台配置等后台操作生成 Command。 | `im.admin.command` 事件。 |
| Result 消费 | 消费 IM 回传结果并更新操作单。 | 后台页面可展示最终成功/失败原因。 |
| 配置事件 | 租户配置、字典、功能开关、第三方配置变更后发送 `im.config.event`。 | IM 刷新缓存或本地副本。 |
| 配置查询 API | 提供 `/internal/im-config/*` 只读接口。 | IM 启动预热和缓存缺失兜底查询。 |
| 摘要表 | 建立用户、群、文件、工作台等后台摘要表。 | 后台列表统计优先查 hotgo。 |
| 死信后台 | 提供死信查看、重放、忽略、标记已处理、重新生成命令。 | 运维闭环。 |
| 内部鉴权 | 实现服务签名校验、nonce 防重放、接口白名单、审计。 | 保护 Admin 内部配置 API。 |
| 审计 | 记录后台操作者、租户、资源、Command、Result、死信处理。 | 可追踪后台操作链路。 |
Admin 首批优先级:
1. Kafka 基础配置、Envelope、producer。
2. `admin_command` + `admin_outbox`。
3. 群治理 Command 生产。
4. `im.admin.command.result` 消费。
5. `/internal/im-config/*` 配置查询 API。
6. 死信管理页。
### 11.2 IM Server 侧任务
IM Server 侧只访问租户业务库,负责执行 Admin Command、维护租户业务状态、通过 WS 通知 C 端,并向 Admin 回传结果。
| 模块 | 任务 | 产出 |
| --- | --- | --- |
| Kafka 基础 | 增加 Kafka producer/consumer 配置、健康检查、统一 Envelope 解析。 | 可消费 Command、生产 Result/Event。 |
| 消费幂等 | 新增租户库消费幂等表或统一消费日志。 | `event_id + consumer_group` 幂等。 |
| Command 消费 | 消费 `im.admin.command`。 | 后台命令进入 IM 执行链路。 |
| 群治理执行器 | 执行群资料、管理员、禁言、踢人、解散、群主转让等命令。 | 更新租户库和 Topic 状态。 |
| 用户治理执行器 | 执行封禁、解封、重置密码、踢在线等命令。 | 更新用户状态和 `auth_version`。 |
| Result 生产 | 执行成功/失败后发送 `im.admin.command.result`。 | Admin 操作单闭环。 |
| WS 通知 | Command 成功后生产 C 端业务事件并投递在线客户端。 | `info.what="im|group"` 下发。 |
| 配置消费 | 消费 `im.config.event`,刷新进程内缓存或本地副本。 | 租户配置、字典、第三方配置生效。 |
| 配置兜底查询 | 调 Admin `/internal/im-config/*`。 | IM 启动预热和缓存缺失恢复。 |
| 内部只读 API | 提供 `/internal/admin-query/*`。 | Admin 实时查询租户业务数据。 |
| 文件任务 | 上传完成、附件绑定、群文件登记/删除同步处理。 | 文件审核、引用绑定、回收。 |
| 消息异步落库 | 接入 `im.message.persist`、`im.group.message.persist`。 | 私聊/群聊消息削峰落库。 |
| Push 任务 | 生产 `im.message.push`。 | 离线 Push 异步投递。 |
| 死信处理 | 写失败表、投 `im.dead.letter`、Command 死信回传 Result。 | 失败可诊断、可补偿。 |
| 内部鉴权 | 校验 Admin 调用 `/internal/admin-query/*` 的签名、权限和审计。 | 保护租户业务查询。 |
IM Server 首批优先级:
1. Kafka 基础配置、Envelope、consumer/producer。
2. 消费幂等表和死信写入。
3. `im.admin.command` 群治理消费执行。
4. `im.admin.command.result` 回传。
5. WS 业务事件下发。
6. `/internal/admin-query/*` 只读查询 API。
### 11.3 双方联调契约
| 契约 | Admin 负责 | IM Server 负责 |
| --- | --- | --- |
| Envelope 字段 | 生产完整字段,保证 `event_id/trace_id/tenant_code/event_type/idempotency_key`。 | 校验字段完整性和事件类型白名单。 |
| Command 状态 | 创建操作单,展示状态。 | 回传成功、失败、死信结果。 |
| 幂等 | 同一后台操作复用 `command_id/idempotency_key`。 | 同一 Command 重复消费只执行一次。 |
| 错误码 | 展示 IM 返回的稳定 `reason`。 | 返回稳定 `reason`,不让 Admin 解析错误文本。 |
| 查询接口 | 带管理员身份、权限、原因和签名。 | 校验签名、权限和租户资源,写审计。 |
| 配置接口 | 提供最小必要配置字段。 | 不直连 hotgo,使用缓存、事件或内部 API。 |
| 死信 | 提供处理入口和人工操作审计。 | 写死信、回传 Command dead/failed 状态。 |
| Trace | 生成并传递 `trace_id`。 | 日志、Kafka、WS 下发沿用同一 `trace_id`。 |
### 11.4 首批联调用例
| 用例 | 预期 |
| --- | --- |
| Admin 禁言群成员 | Admin 操作单 pending,IM 消费后更新租户库,C 端收到 `group.mute.updated`,Admin 状态 succeeded。 |
| Admin 重复提交同一禁言操作 | IM 只执行一次,Result 返回同一最终状态。 |
| Admin 删除管理员但群版本冲突 | IM 返回 `GROUP_VERSION_CONFLICT`,Admin 操作单 failed,不自动重试。 |
| IM 消费时临时 DB 失败 | 自动退避重试,未超限不进入死信。 |
| IM 消费超过重试上限 | 写 `im.dead.letter`,Admin 操作单进入 dead/failed。 |
| Admin 查询群成员 | Admin 调 `/internal/admin-query/*`,IM 校验签名和权限后返回只读数据。 |
| IM 启动加载租户配置 | IM 优先读缓存,缺失时调 `/internal/im-config/*`,Admin 只返回必要配置。 |
### 11.5 IM Server 已接入的首批 Admin Command
IM Server 首批消费 `im.admin.command`,执行后回传 `im.admin.command.result`。所有命令必须带 `tenant_code`,`payload.actor_user_id` 必须是租户内 IM 用户 ID,用于落群事件和 C 端通知。
| Command `event_type` | 说明 | 必填 payload |
| --- | --- | --- |
| `group.profile.update` | 修改群名称、头像、简介。 | `command_id,actor_user_id,topic,profile,version` |
| `group.governance.update` | 修改进群审批、全员禁言、成员邀请等治理开关。 | `command_id,actor_user_id,topic,governance,version` |
| `group.admin.set` | 设置一个或多个管理员。 | `command_id,actor_user_id,topic,user_ids` |
| `group.admin.remove` | 删除管理员,降为普通成员。 | `command_id,actor_user_id,topic,user_id` |
| `group.member.mute` | 禁言成员。 | `command_id,actor_user_id,topic,user_id,reason,expires_at` |
| `group.member.unmute` | 解除禁言。 | `command_id,actor_user_id,topic,user_id` |
| `group.member.remove` | 移出群成员。 | `command_id,actor_user_id,topic,user_id` |
| `group.owner.transfer` | 转让群主。 | `command_id,actor_user_id,topic,target_user_id,version` |
当前 IM Server 消费 `im.admin.command` 时已经接入 `im_kafka_consume_log`:
1. 按 `event_id + consumer_group` 抢占消费。
2. 已成功事件直接跳过,不重复执行业务。
3. 处理中且未超时的事件跳过,避免并发重复执行。
4. 失败或处理超时的事件允许重新抢占。
5. 不可重试错误或超过重试次数后写入 `im_kafka_dead_letters`,并投递 `im.dead.letter`。
6. Admin Command 进入死信时回传 `im.admin.command.result`,`status=dead`。
### 11.6 IM Server 已接入的配置事件
IM Server 首批消费 `im.config.event`,用于 Admin 修改 HotGo 配置后通知 IM 刷新进程内缓存。当前实现以“缓存失效”为主,收到事件后清除对应缓存,下次业务读取时重新加载。
| Config `event_type` | 说明 | IM 行为 |
| --- | --- | --- |
| `tenant.updated` | 租户基础信息变更。 | 清理租户 code/id 缓存。 |
| `tenant.status.changed` | 租户启停、状态变化。 | 清理租户 code/id 缓存。 |
| `tenant.limits.updated` | 租户人数、群数、上传限制等变化。 | 清理租户 code/id 缓存。 |
| `tenant.features.updated` | 租户功能开关变化。 | 清理租户 code/id 缓存。 |
| `dictionary.updated` | 字典配置变化。 | 清理第三方/字典配置缓存。 |
| `third_party.updated` | OSS、文件安全、短信、极光、声网等第三方配置变化。 | 按 group 清理第三方配置缓存。 |
| `registration.config.updated` | 注册、短信开关变化。 | 清理注册配置缓存。 |
事件示例:
```json
{
"event_id": "evt_01K_config",
"trace_id": "req_01K",
"tenant_code": "default",
"source_service": "hotgo-admin",
"target_service": "im-server",
"event_type": "third_party.updated",
"biz_type": "config",
"biz_id": "im_third_party",
"occurred_at": "2026-08-26T10:05:00.000Z",
"payload": {
"tenant_code": "default",
"group": "im_third_party",
"keys": ["oss.endpoint", "file_security.enabled"]
}
}
```
禁言命令示例:
```json
{
"event_id": "evt_01K_mute",
"trace_id": "req_01K",
"tenant_code": "default",
"source_service": "hotgo-admin",
"target_service": "im-server",
"event_type": "group.member.mute",
"biz_type": "group",
"biz_id": "grp...",
"idempotency_key": "cmd_01K_mute",
"occurred_at": "2026-08-26T10:00:00.000Z",
"payload": {
"command_id": "cmd_01K_mute",
"actor_user_id": "usr_admin",
"topic": "grp...",
"user_id": "usr_target",
"reason": "刷屏",
"expires_at": null
}
}
```
### 11.7 IM Server 已接入的内部只读查询 API
为满足“Admin 不直连租户库”的边界,IM Server 提供内部只读查询接口给 HotGo/Admin 调用。接口默认关闭,通过 `internal_api.enabled=true` 开启。
请求统一鉴权头:
| Header | 说明 |
| --- | --- |
| `X-Service-Name` | 调用方服务名,如 `hotgo-admin`。 |
| `X-Timestamp` | Unix 秒级时间戳,默认 300 秒时间窗。 |
| `X-Nonce` | 一次性随机串,用于防重放。 |
| `X-Request-ID` | 调用链路 ID。 |
| `X-Signature` | HMAC-SHA256 签名,支持 hex 或 base64,可带 `sha256=` 前缀。 |
nonce 防重放优先使用 Redis 短状态 `SETNX + TTL`,Redis 未配置时退化为单节点进程内防重放;生产多节点必须配置 Redis。
签名明文:
```text
METHOD + "\n" +
PATH_WITH_QUERY + "\n" +
SHA256_HEX(BODY) + "\n" +
X-Timestamp + "\n" +
X-Nonce
```
当前首批接口:
| 接口 | 说明 |
| --- | --- |
| `GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}` | 查询租户内用户基础资料。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}` | 查询群资料和治理配置。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/members` | 查询群成员分页快照,支持 `limit/cursor/role/eligible_role/q`。 |
| `GET /internal/admin-query/tenants/{tenant_code}/messages/{topic}/{seq_id}` | 查询单条私聊或群聊消息,`seq_id` 为 `BIGINT`。 |
| `GET /internal/admin-query/tenants/{tenant_code}/files/{file_id}` | 查询文件元信息和存储位置。 |
配置示例:
```json
{
"internal_api": {
"enabled": true,
"time_window_seconds": 300,
"services": {
"hotgo-admin": {
"secret": "replace-with-random-secret",
"allow": ["admin_query"]
}
}
}
}
```
Docker 环境变量:
| 环境变量 | 说明 |
| --- | --- |
| `INTERNAL_API_ENABLED` | 是否开启内部接口,默认 `false`。 |
| `INTERNAL_API_TIME_WINDOW_SECONDS` | 签名时间窗。 |
| `INTERNAL_API_HOTGO_SERVICE_NAME` | Admin 服务名,默认 `hotgo-admin`。 |
| `INTERNAL_API_HOTGO_SECRET` | Admin 与 IM 共享签名密钥。 |
### 11.8 IM 访问 Admin 配置查询接口
为满足“IM 服务不直连 hotgo 库”的目标,IM Server 增加 Admin 内部配置查询客户端。该客户端默认关闭,开启后 `store.Tenants`、注册短信开关、第三方配置读取会优先请求 Admin 内部接口,失败时仍可回落旧数据库适配器,便于灰度迁移。
配置示例:
```json
{
"admin_internal_api": {
"enabled": true,
"base_url": "http://hotgo-admin:8080",
"service_name": "im-server",
"secret": "replace-with-random-secret",
"timeout_ms": 3000
}
}
```
接口约定:
| 接口 | 用途 |
| --- | --- |
| `GET /internal/im-config/tenants/by-code/{tenant_code}` | 按企业码获取租户路由信息,需返回 `id/code/name/state/db_name/db_dsn`。 |
| `GET /internal/im-config/tenants/by-id/{tenant_id}` | 按租户 ID 获取租户路由信息。 |
| `GET /internal/im-config/tenants?state=active&limit=1000` | 获取活跃租户列表,用于 Kafka Outbox 扫描。 |
| `GET /internal/im-config/registration/sms` | 获取注册短信开关,返回 `{"enabled": true}`。 |
| `GET /internal/im-config/third-party/{group}?keys=a,b` | 获取第三方配置,返回 `{"values": {"a": "..."}}`。 |
鉴权沿用内部接口 HMAC 方案,请求头为 `X-Service-Name`、`X-Timestamp`、`X-Nonce`、`X-Signature`;签名文本为 `METHOD + "\n" + RequestURI + "\n" + BodySHA256 + "\n" + Timestamp + "\n" + Nonce`。
Docker 环境变量:
| 环境变量 | 说明 |
| --- | --- |
| `ADMIN_INTERNAL_API_ENABLED` | 是否启用 IM 到 Admin 的配置查询客户端,默认 `false`。 |
| `ADMIN_INTERNAL_API_BASE_URL` | Admin 内部接口根地址。 |
| `ADMIN_INTERNAL_API_SERVICE_NAME` | IM 服务名,默认 `im-server`。 |
| `ADMIN_INTERNAL_API_SECRET` | Admin 与 IM 共享签名密钥。 |
| `ADMIN_INTERNAL_API_TIMEOUT_MS` | 请求超时时间。 |
### 11.9 IM Server 已接入的消息异步落库
IM Server 已接入 `im.message.persist`,用于把在线发送链路和最终消息落库解耦。
当前处理方式:
1. WS/HTTP 发送入口先完成权限校验,并由 topic 内存态分配 `seq_id`。
2. Kafka 开启时,私聊发送入口直接生产 `im.message.persist`,群聊发送入口直接生产 `im.group.message.persist`,不查库、不落库、不写 DB outbox;客户端在 Kafka 写入成功后拿到已分配的 `seq_id`。
3. Kafka 关闭时,继续走原同步落库路径,兼容本地和未接 Kafka 环境。
4. 私聊和群聊使用独立 Topic、独立消费组和独立物理表:私聊批量写入租户分表 `im_%03d_messages`,群聊按租户和月份批量写入 `im_%03d_group_messages_YYYYMM`。
5. 消费侧复用 `im_kafka_consume_log` 做 `event_id + consumer_group` 幂等。
6. 消费者最多聚合 `message_batch_size` 条消息,在 `message_batch_wait_ms` 时间窗内形成批次并执行一条多值 `INSERT`。
7. 批量写入失败后逐条降级;每条执行首次写入和 `message_single_retry_count` 次重试,默认共尝试 4 次。
8. 单条仍失败时,完成 `im.dead.letter` 投递和 `im_kafka_dead_letters` 终态写入后才提交该批 offset;提交失败会持续重试,避免跳过失败消息。
消息批量落库参数:
| 配置项 | 默认值 | 说明 |
| --- | ---: | --- |
| `message_batch_size` | `100` | 单批最大消息数,上限 500。 |
| `message_batch_wait_ms` | `10` | 从取得第一条消息开始的批次聚合等待时间。 |
| `message_single_retry_count` | `3` | 批量失败后单条额外重试次数,不含首次执行。 |
### 11.10 IM Server 已接入的缓存失效消费
IM Server 已订阅 `im.cache.invalidate`,用于接收 Admin 或其他节点发出的通用缓存失效事件。消费侧复用 `im_kafka_consume_log` 做幂等,失败按统一退避重试,超过上限进入死信。
事件体示例:
```json
{
"event_id": "cache:tenant:default:1720000000",
"event_type": "cache.tenant.invalidate",
"tenant_code": "default",
"payload": {
"tenant_code": "default",
"tenant_id": 1,
"namespace": "tenant"
}
}
```
支持的失效范围:
| `event_type` / `namespace` | 处理方式 |
| --- | --- |
| `cache.tenant.invalidate` / `tenant` | 清理租户注册表与租户配置进程内缓存。 |
| `cache.registration.invalidate` / `registration` | 清理注册短信开关缓存。 |
| `cache.third_party.invalidate` / `third_party` | 按 `group` 清理第三方配置缓存。 |
| `cache.dictionary.invalidate` / `dictionary` | 按 `group` 清理字典/配置类缓存。 |
| `cache.redis_object.invalidate` / 任意 Redis `namespace` | 按 `keys` 删除用户、群、关系、权限、未读等 Redis 对象缓存。 |
| `cache.group_hot_messages.invalidate` / `group_hot_messages` | 按 `topic` 和可选 `ranges` 清理群热消息缓存。 |
### 11.11 IM Server 已接入的死信处理闭环
IM Server 已将死信从“只写表”补齐为“可查询、可忽略、可解决、可重放”的处理闭环。Admin 不直连租户库,列表和详情通过 IM 内部只读 API 查询;写操作仍通过 `im.admin.command` 投递给 IM。
Admin 查询接口:
| 接口 | 说明 |
| --- | --- |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters?status=dead&limit=50&cursor=` | 死信分页列表,支持 `status/biz_type/biz_id/event_type` 过滤。 |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters/{dead_id}` | 单条死信详情,包含原始 Kafka envelope。 |
Admin 处理命令:
| `event_type` | 说明 | Payload 关键字段 |
| --- | --- | --- |
| `kafka.dead_letter.replay` | 删除原消费日志并按原 `source_topic` 重投 envelope,成功后标记 `replayed`。 | `dead_id`、`actor_user_id`、`note` |
| `kafka.dead_letter.ignore` | 人工确认无需处理,标记 `ignored`。 | `dead_id`、`actor_user_id`、`note` |
| `kafka.dead_letter.resolve` | 线下修复后标记 `resolved`。 | `dead_id`、`actor_user_id`、`note` |
重放规则:
1. IM 先把 `im_kafka_dead_letters.status` 改为 `pending_replay`,并删除对应 `im_kafka_consume_log`。
2. IM 按死信记录中的 `source_topic` 重新投递原 envelope。
3. 投递成功后把死信标记为 `replayed`,并累计 `replay_count`。
4. 如果重放后仍失败,普通消费者会再次写入新的 dead 记录,保留完整处理痕迹。
### 11.12 IM Server 已接入的用户治理命令
IM Server 已在 `im.admin.command` 中接入用户治理类后台动作。Admin 侧写 hotgo 操作单并发送 Kafka Command,IM 消费后修改租户库用户状态、踢在线连接、清理缓存并回传 `im.admin.command.result`。
支持命令:
| `event_type` | 行为 | Payload 关键字段 |
| --- | --- | --- |
| `user.ban` | 封禁用户,设置 `StateSuspended` 并踢所有在线会话。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.unban` | 解封用户,设置 `StateOK`。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.freeze` | 冻结账号,设置 `StateSuspended` 并踢所有在线会话。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.unfreeze` | 恢复账号,设置 `StateOK`。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.device.kick` | 踢指定设备在线连接,并删除设备推送记录。 | `actor_user_id`、`user_id` 或 `target_user_id`、`device_id`、`reason` |
| `user.logout_all` | 踢用户所有在线连接,不修改账号状态。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.account.cancel` | 注销账号,默认软删除;`hard=true` 时硬删除。 | `actor_user_id`、`user_id` 或 `target_user_id`、`hard`、`reason` |
| `user.account.delete` | 删除账号,按硬删除处理。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
命令示例:
```json
{
"event_id": "admin:user:ban:default:usrabc:001",
"tenant_code": "default",
"source_service": "hotgo-admin",
"target_service": "im-server",
"event_type": "user.ban",
"biz_type": "user_governance",
"biz_id": "usrabc",
"idempotency_key": "admin-operation-id",
"occurred_at": "2026-08-26T10:10:00.000Z",
"payload": {
"command_id": "admin-operation-id",
"actor_user_id": "usrAdmin",
"user_id": "usrabc",
"reason": "违规处理"
}
}
```
处理说明:
1. 封禁/冻结共用 Tinode 用户状态 `StateSuspended`;解封/恢复共用 `StateOK`。
2. 状态变更后 IM 会清理用户缓存,并向本节点 Hub 投递用户状态变更。
3. 踢设备只影响匹配 `device_id` 的在线连接,同时删除该设备 Push token 记录。
4. 全端登出只踢在线连接,不修改用户状态。
5. 注销/删除会停止用户相关 Topic、清理缓存并调用 `store.Users.Delete`。
6. 命令执行成功后统一回传 `im.admin.command.result`;跨系统审计通过 `im.audit.event` 投递给 Admin/HotGo 同步落表。
7. 群消息消费成功后继续写入 Redis 热消息缓存,便于 24 小时内热点拉取。
事件示例:
```json
{
"event_id": "msg.persist.1.grpabc.10086",
"tenant_code": "default",
"source_service": "im-server",
"target_service": "im-server",
"event_type": "message.persist.group",
"biz_type": "message",
"biz_id": "grpabc:10086",
"idempotency_key": "client-mid-001",
"version": 10086,
"occurred_at": "2026-08-26T10:10:00.000Z",
"actor": {
"type": "user",
"id": "usr..."
},
"payload": {
"message_kind": "group",
"read_by_sender": true,
"attachment_urls": [],
"message": {
"SeqId": 10086,
"Topic": "grpabc",
"From": "abcdef",
"Head": {
"x-im-client-mid": "client-mid-001"
},
"Content": {
"txt": "hello"
}
}
}
}
```
### 11.9 IM Server 已移除 Kafka DB Outbox
IM Server 不再维护租户库 `im_kafka_outbox`,Kafka 事件统一由发送入口直接生产。生产侧使用 Kafka writer 的 `max_retry_count` 控制发送重试;消费侧使用 `im_kafka_consume_log` 做处理幂等和失败重试,超过上限后进入 `im.dead.letter`。
本轮暂不实现 `im.message.push` 离线 Push Kafka 化,已记录到 [IM 第一至十一章后续增强项](./IM-未实现后续增强项.md)。
### 11.10 IM Server 文件生命周期同步处理
IM Server 不再生产或消费 `im.file.lifecycle`。文件相关的小流量动作改为同步执行。
当前事件:
| `event_type` | 生产时机 | 消费行为 |
| --- | --- | --- |
| `file.upload.completed` | HTTP/gRPC 上传成功并完成文件记录后。 | 当前无需 Kafka 消费。 |
| `file.attachments.bind` | 消息落库 consumer 成功后,如果消息携带附件。 | 同步写 `im_filemsglinks` 或 `im_group_files`。 |
| `group.file.created` | 群文件 API 登记成功后。 | HTTP 业务接口同步完成。 |
| `group.file.deleted` | 群文件 API 删除成功后。 | HTTP 业务接口同步完成。 |
说明:
1. 消息附件绑定在消息落库 consumer 内同步完成。
2. Kafka 关闭时,仍沿用原同步消息落库与附件绑定路径,兼容本地环境。
3. 文件对象字节流仍走 OSS/文件服务,Kafka 不承载上传/下载内容。
4. 合规删除、临时文件 TTL 回收、OSS 删除动作仍保留为后续深化项。
### 11.11 IM Server 工作台事件消费处理
工作台拆分为独立 `im_workbench` 服务后,工作台业务变更由 `im_workbench` 生产 `im.workbench.event`,IM Server 新增独立 Kafka consumer 消费该 Topic,并转为系统通知和 C 端业务实时事件。
说明:IM Server 当前保留旧工作台 HTTP 代码和同步处理函数,仅作为迁移过渡兼容。待 `im_workbench` 全部接口稳定后,再统一删除 IM Server 内的旧工作台业务代码。
当前生产范围:
| 模块 | 事件 |
| --- | --- |
| 会议 | `meeting.created`、`meeting.updated`、`meeting.canceled`、`meeting.reminded`、`meeting.response.updated` |
| 日报 | `daily_report.draft.created`、`daily_report.draft.saved`、`daily_report.submitted`、`daily_report.reminded` |
| 月报 | `monthly_report.draft.created`、`monthly_report.draft.saved`、`monthly_report.submitted`、`monthly_report.reopened` |
| 总经理信箱 | `mailbox.submitted` |
当前消费行为:
1. `im_workbench` 直接发送 JSON 事件到 `im.workbench.event`,Topic 配置键为 `workbench_external`。
2. IM Server 按 `event_id + consumer_group` 做消费幂等。
3. 按 `target_user_ids` 为非发布人生成系统通知。
4. 写入 `im_business_events_outbox`,由现有 WS 业务事件投递器下发在线客户端。
5. 消费失败按统一 Kafka 重试逻辑处理,超过最大重试次数后进入 `im.dead.letter`。
### 11.12 IM Server 已接入的审计事件
IM Server 对跨系统审计仍生产 `im.audit.event`。该 topic 由 Admin/HotGo 消费并同步写审计表;IM Server 本地不消费该 topic,也不通过它做内部异步落库。
当前审计范围:
| 类型 | 事件 |
| --- | --- |
| 工作台 | 会议、日报、月报、总经理信箱的状态变更。 |
| Admin Command | 后台群治理命令执行成功。 |
| Kafka 死信 | `kafka.dead_letter.created`。 |
| 文件生命周期 | 上传完成、附件绑定、群文件新增/删除。 |
审计事件当前不在 IM 内部通过 Kafka 落审计表,只作为 IM -> Admin/HotGo 的跨系统写事件。
## 12. 风险与约束
1. Kafka 是异步链路,Admin 后台命令不能默认立即成功,只能显示 `pending/running/succeeded/failed`。
2. 查询不能通过 Kafka 常规化实现,高频实时查询必须走内部只读 API 或 hotgo 摘要表。
3. 内部 API 不能暴露公网,签名、nonce、防重放和审计必须首批完成。
4. 配置事件可能丢失或延迟,IM 必须保留启动预热和缓存缺失时的 Admin 配置查询兜底。
5. Kafka 消费可能重复,所有消费者和业务写入都必须幂等。
6. Admin 和 IM 不跨库直连后,排障要依赖 `trace_id` 串联 hotgo 操作单、Kafka 事件、IM 执行日志和 WS 下发日志。
File diff suppressed because it is too large Load Diff
+91
View File
@@ -0,0 +1,91 @@
# IM Kafka 接入范围识别
> 日期:2026-08-26
> 依据:[IM 前后端逐图对接协议](./IM-前后端对接文档.md)
> 目标:从前后端对接文档逐章识别适合接入 Kafka 的链路,明确 Kafka 与 WebSocket、Redis、MySQL、OSS 的边界。
## 1. 总体边界
Kafka 主要用于跨系统异步事件、削峰落库、可靠重试、失败补偿、后台操作通知 C 端和审计流水;不替代 WebSocket 的在线实时连接,不替代 Redis 的短状态和热读缓存,也不替代 MySQL 的最终状态。
| 类型 | 建议承载 | 说明 |
| --- | --- | --- |
| 在线实时消息投递 | Redis Pub/Sub/Stream + WebSocket | 低延迟 fanout,节点收到后直接写 WS。 |
| 异步落库、重试、补偿 | Kafka | 适合消息持久化、通知、Push、审核、审计等可重放任务。 |
| 短状态、限流、验证码、在线状态 | Redis | TTL 明确,不需要 Kafka。 |
| 最终业务状态 | MySQL | Kafka 消费成功后写入或更新最终状态。 |
| 文件对象存储 | OSS | Kafka 只处理文件元数据、安全检测和生命周期事件。 |
## 2. 推荐 Topic
| Topic | 用途 | 典型生产方 | 典型消费方 |
| --- | --- | --- | --- |
| `im.message.persist` | 私聊消息异步落库 | IM WS 节点 | 消息持久化消费者 |
| `im.group.message.persist` | 群聊消息异步落库 | IM WS 节点 | 群消息持久化消费者 |
| `im.message.push` | 离线 Push 提醒 | 消息落库消费者、业务事件生产方 | 极光/厂商 Push 消费者 |
| `im.business.event` | 好友、群、会议、工作台等 C 端业务事件 | IM HTTP、Admin 后台 | IM 节点事件投递消费者 |
| `im.group.admin.event` | Admin 后台群操作投递到 C 端 | Admin 后台 | IM 节点 WS 投递消费者 |
| `im.cache.invalidate` | 跨节点缓存失效 | 业务写接口、后台配置 | IM 节点缓存消费者 |
| `im.file.security` | 文件内容安全和恶意文件检测 | 上传完成链路 | 阿里云安全检测消费者 |
| `im.file.lifecycle` | 已不采用 | 文件生命周期小流量动作同步处理 | 无 |
| `im.audit.event` | 跨系统审计事件 | IM Server、Admin 后台 | Admin/HotGo 审计消费者 |
| `im.workbench.event` | 工作台拆分后的业务事件、红点、系统通知、WS 下发 | im_workbench | IM Server 工作台事件消费者 |
| `im.dead.letter` | 失败事件归档 | 各消费者 | 运维补偿任务 |
## 3. 幂等与重试
Kafka 消息统一携带 `event_id`、`trace_id`、`tenant_code`、`biz_type`、`biz_id`、`action`、`version`、`occurred_at` 和 `payload`。消费端按 `event_id + consumer_group` 做幂等记录,业务表也必须保留唯一键兜底。
消息发送链路使用 `x-im-client-mid` 或 HTTP `Idempotency-Key` 作为业务幂等来源;Kafka 只负责可靠投递,不能作为唯一幂等判断。消费者失败按指数退避重试,超过上限写 `im.dead.letter` 或失败表,由后台补偿。
## 4. 逐章识别
| 章节 | 适合 Kafka 的范围 | 不建议 Kafka 的范围 |
| --- | --- | --- |
| 0. 公共对接约定 | 产品业务事件 outbox 可改为 Kafka 投递;消息异步落库、系统通知、离线 Push、跨节点缓存失效使用 Kafka;`x-im-client-mid` 幂等结果可作为生产 Kafka 前置校验。 | WebSocket 建连、`hi/login/sub/get/note` 本身不走 Kafka;在线状态继续 Redis。 |
| 1. 账号注册、登录与身份安全 | 企业注册申请、企业入口反馈、实名提交、密码重置成功、账号封禁/解封、管理员重置密码、审核结果通知、审计日志可走 Kafka。营业执照和反馈截图上传完成后投 `im.file.security`。 | 登录鉴权、验证码校验、Action Token、限流不走 Kafka;这些必须同步返回或走 Redis。 |
| 2. 消息首页、全局导航与系统通知 | 系统通知创建、已读异步统计、入口红点变更、离线 Push 提醒、消息摘要更新可走 Kafka。 | 首页查询、会话列表拉取、客户端断线重连流程不走 Kafka。 |
| 3. 单聊会话、会话建立与消息状态 | 私聊消息异步落库、好友申请/通过/拒绝/删除事件、P2P 订阅建立后的通知、媒体消息文件绑定事件、离线 Push 可走 Kafka。 | 在线 P2P 实时投递主链路不建议 Kafka;关系判断和发送权限校验必须同步完成。 |
| 4. 消息输入、媒体内容与消息操作 | @提醒 Push、消息撤回/编辑事件、收藏批量创建、转发任务、forward bundle 生成、消息搜索索引更新、附件 ACL 绑定可走 Kafka。 | 输入框、草稿、录音状态、上传进度和客户端 pending 队列不走 Kafka。 |
| 5. 通讯录、好友关系与黑名单 | 好友申请事件、好友关系变更、黑名单变更、二维码使用审计、资料快照更新、搜索索引更新、系统通知生成可走 Kafka。 | 好友列表查询、用户搜索同步结果、二维码解析鉴权不走 Kafka。 |
| 6. 群聊创建、资料维护与成员治理 | 群创建、成员加入/退出、邀请/审批、公告更新、资料更新、角色变更、禁言、群主转让、解散、群文件登记/删除、Admin 后台群操作投递到 C 端都适合 Kafka。Admin 后台写库后生产 `im.group.admin.event`,IM 节点消费后用 WS 下发给在线客户端。 | 群内在线消息 fanout 主链路继续 Redis + WS;群成员权限校验、成员数上限校验必须同步事务完成。 |
| 7. 文件下载、收藏管理与聊天记录检索 | 文件安全检测、恶意文件扫描、文件生命周期、收藏快照异步补全、收藏转发、搜索索引更新、合规删除事件可走 Kafka。文件下载本身不走 Kafka。 | OSS 上传/下载字节流、Range 续传、下载进度不走 Kafka;这些走文件服务和客户端任务管理。 |
| 8. 实时语音与视频通话 | 通话记录落库、未接来电 Push、通话结束审计、后台计费或统计、异常通话风控可走 Kafka。 | WebRTC offer/answer/ICE、接听、挂断等实时信令不建议 Kafka 主链路;仍走 WS。 |
| 9. 企业工作台与办公协同 | 工作台红点更新、会议创建/更新/取消/提醒/响应、日报/月报提交和催办、总经理信箱提交、工作台系统通知、后台配置变更、审计都适合 Kafka。 | 工作台模块列表、组织架构查询、报表详情查询不走 Kafka;普通查询走 MySQL/Redis 缓存。 |
| 10. 个人资料、通用设置、隐私与青少年模式 | 资料变更事件、隐私配置变更、青少年模式开启/关闭、多设备退出、Push Token 变更审计、App 发布配置缓存失效可走 Kafka。头像上传后走文件安全检测。 | 本地清缓存、系统权限引导、密码验证、Action Token 不走 Kafka。 |
| 11. 反诈安全、举报与意见反馈 | 风控确认审计、举报受理、证据文件安全检测、意见反馈、诊断包处理、后台审核通知、合规处置事件可走 Kafka。 | 风险弹窗同步拦截结果、举报原因查询、反馈配置查询不走 Kafka。 |
| 12. 联调实施清单 | Kafka 需要覆盖消息幂等、系统事件可信生产、文件安全检测、后台群操作到 C 端、离线 Push、失败重试和死信补偿验收。 | 前端基础联调请求不直接依赖 Kafka,除非该接口声明为异步任务。 |
## 5. 文件链路结论
文件上传、下载、Range、ETag、OSS 对象读写不走 Kafka。Kafka 只接入文件完成后的业务事件:
1. 上传完成后投递 `im.file.security`,调用阿里云内容安全和恶意文件检测。
2. 安全检测完成后更新文件审核状态,并通过 `im.business.event` 或系统通知告知相关用户。
3. 消息附件引用绑定、群文件登记等小流量动作同步落库,不再投递 `im.file.lifecycle`。
4. 业务撤回、删除、合规处置或临时文件 TTL 回收如需异步化,另行评估专门任务队列。
## 6. 工作台链路结论
工作台“查询类接口”不需要 Kafka,状态变化和提醒类事件当前也同步处理:
1. 会议创建、更新、取消、提醒、参会响应同步生成系统通知、红点并通过 WS 通知在线客户端。
2. 日报/月报提交、撤回、重开、管理员催办同步生成红点、催办 Push 和审计。
3. 总经理信箱提交同步处理,后台处理结果再回推 C 端。
4. 工作台模块配置变更生产 `im.cache.invalidate`,IM 节点刷新进程内配置缓存。
## 7. 已补充:缓存失效与 Admin 配置查询
1. `im.cache.invalidate` 已接入 IM 消费端,支持租户、注册短信、第三方配置、字典配置、Redis 对象缓存、群热消息缓存失效。
2. IM 已增加 Admin 内部配置查询客户端,开启 `admin_internal_api.enabled=true` 后优先通过 Admin 查询 hotgo 侧配置,旧数据库读取作为灰度兜底。
3. Admin 侧需要补齐 `/internal/im-config/*` 只读接口,并与 IM 使用同一套 HMAC 签名密钥。
4. Kafka 死信已补齐查询、忽略、解决、重放闭环:查询走 IM 内部 API,处理走 `im.admin.command`。
5. 用户治理类后台动作已接入 `im.admin.command`:封禁/解封、冻结/恢复、踢指定设备、全端登出、注销/删除账号。
## 8. 首批建议实现
1. 先实现 Kafka 基础生产者、消费者、配置、健康检查和消费幂等表。
2. 接入 `im.group.admin.event`,解决 Admin 后台群操作通过 Kafka 投递到 C 端。
3. 接入 `im.message.persist` 和 `im.group.message.persist`,将私聊、群聊消息在 IM 内部异步落库和幂等重试闭环。
4. 接入 `im.file.security`,把 OSS 上传完成后的文件安全检测异步化。
5. 工作台事件保持 IM 内部同步处理;跨系统审计仍通过 `im.audit.event` 投递给 Admin/HotGo。
@@ -0,0 +1,597 @@
# IM Redis 三层数据分层改造方案
日期:2026-08-25
## 1. 背景与目标
当前 IM 服务需要接入 Redis,但不希望出现“进程内缓存一份、Redis 缓存一份、MySQL 再存一份”的重复状态,避免后续一致性维护困难。
本方案按业务数据特征拆成三层:
- 进程内:只放低频变更配置和本节点运行态。
- Redis:放小体量、高频读取、需要跨节点一致的热数据。
- MySQL:放最终持久化数据、事务数据、审计数据和大体量列表。
第一阶段不把消息正文作为 Redis 的长期存储。万人大群可以把 24 小时内热消息放入 Redis 作为读加速,私聊消息最终来源是租户库 `im_messages`,群聊消息最终来源是租户库 `im_group_messages`。
## 2. 核心原则
1. 同一类业务状态只确定一个主读取层,不在进程内和 Redis 同时维护两套可变状态。
2. 进程内缓存只承载低频配置或本机连接态,不承载跨节点业务状态。
3. 在线、离线、最近活跃等跨节点状态统一从 Redis 查询。
4. MySQL 仍是需要持久化、事务和审计的数据最终来源。
5. Redis 缓存 MySQL 数据时采用 cache-aside 模式,写入以 MySQL 事务成功为准。
6. Redis key 必须带租户标识,虽然业务库是一租户一库,但 Redis 是共享基础设施,需要避免不同租户数据串扰。
## 3. 三层数据分类
| 层级 | 数据类型 | 典型数据 | 读写特点 | 一致性要求 |
| --- | --- | --- | --- | --- |
| 进程内 | 静态或低频配置、本机运行态 | 系统配置、字典、租户配置、OSS/短信/内容安全/声网配置、本机 WebSocket 连接、topic channel | 启动加载,定时刷新;连接态只在本机维护 | 配置允许秒级延迟;连接态只代表本节点 |
| Redis | 小体量热数据、跨节点状态、短期状态 | 用户在线状态、用户基础信息、群基础信息、群成员集合、好友/黑名单集合、24 小时热消息、验证码、临时 token、限流计数 | 高频读,TTL 或主动失效 | 跨节点查询一致,允许短暂最终一致 |
| MySQL | 持久化和事务数据 | 消息、文件记录、群申请审批、举报、审核任务、操作日志、账务类数据、大分页列表 | 事务写入,分页查询,审计追溯 | 最终权威来源 |
## 4. 进程内层设计
### 4.1 适合放进程内的数据
- 系统配置:后台 hotgo 中的系统参数。
- 字典配置:`hg_sys_dict_type`、`hg_sys_dict_data` 等字典项。
- 租户配置:租户启停、基础配置、默认策略。
- 第三方配置:OSS、阿里云内容安全、短信、声网等接入参数。
- 本节点运行态:当前进程内的 WebSocket session、local topic channel、本机订阅关系、本机连接数。
### 4.2 刷新策略
- 服务启动时加载一次配置。
- 定时刷新,建议 60 秒。
- 后台配置变更后,后续可补充主动失效或广播刷新。
- 配置读取失败时保留旧值,并打印告警日志。
### 4.3 边界说明
进程内层不保存用户在线状态的全局结果。进程内只知道“本节点有哪些连接”,跨节点判断用户是否在线必须查 Redis。
## 5. Redis 层设计
### 5.1 第一阶段必须接入
#### 用户在线状态
在线状态是 Redis 第一优先级改造内容。原因是 WebSocket 长连接固定落在某个节点上,本机内存可以管理本机连接,但其他节点无法直接知道该用户是否在线。
建议 key:
```text
im:{tenant_id}:presence:user:{uid}
im:{tenant_id}:presence:user:{uid}:sessions
im:{tenant_id}:presence:node:{node_id}:users
```
`presence:user:{uid}` 建议使用 hash:
```text
online = 1
node_id = 当前节点 ID
session_count = 当前会话数
last_active_at = 最近活跃时间
last_seen_at = 最近可见时间
device = 设备类型
ua = 客户端 UA
```
生命周期:
1. WebSocket 登录成功后写入 Redis 在线状态。
2. 心跳或业务包刷新 TTL。
3. 断开连接时扣减 session 数,最后一个 session 断开后标记离线。
4. 节点异常退出时依赖 TTL 自动过期,避免永久在线。
5. 其他节点查询用户在线状态时只查 Redis。
TTL 建议:
- 在线状态 hash:60 到 120 秒。
- session 集合:60 到 120 秒,随心跳刷新。
- node 用户集合:可不强依赖 TTL,用于辅助排查和节点清理。
当前代码已经接入基础 PresenceStore:
- 登录成功后写入 `presence:user:{uid}`、`presence:user:{uid}:sessions`、`presence:node:{node_id}:users`。
- 客户端每次上行业务包刷新 `last_active_at` 和 TTL。
- session 清理时移除当前 session,最后一个 session 离线后将 `online` 置为 `0`。
- 对外提供 `store.GetUserPresence` 作为后续用户在线状态查询入口。
- 群 Topic 在线成员已同步维护 `topic:{topic}:online_members` 和 `topic:{topic}:node:{node_id}:online_members`,订阅/离开时更新,客户端心跳或任意上行业务包会刷新已订阅群的在线成员 TTL。
- 订阅列表中的用户在线状态已改为 Redis 优先读取;Redis 未启用或读取失败时,回退到原有进程内在线判断。
#### 短期状态
以下数据适合放 Redis,减少 MySQL 压力并天然支持过期:
- 注册验证码、登录验证码。
- 邀请码短期校验结果。
- 企业码登录临时 token。
- 登录失败次数、风控计数、接口限流计数。
- 幂等 key、短期操作锁。
### 5.2 第二阶段建议接入
#### 用户基础信息
适合缓存:
- 用户昵称、头像、状态签名。
- 用户公开资料。
- 用户账号到 UID 的映射。
不建议缓存:
- 密码 hash。
- 高敏认证字段。
- KYC 原始材料。
读写规则:
1. 查询先读 Redis。
2. 未命中读 MySQL。
3. MySQL 命中后写入 Redis。
4. 用户资料变更时先提交 MySQL,再删除或更新 Redis。
#### 群相关数据
群数据体量相对可控,且聊天过程中读取频繁,适合逐步放入 Redis:
- 群基础信息:群名、头像、简介、人数、群类型。
- 群配置:入群审批、禁言策略、邀请权限、公告权限。
- 群成员集合:成员 UID 集合。
- 群成员角色:群主、管理员、普通成员。
- 群成员状态:禁言、移出、黑名单。
建议 key:
```text
im:{tenant_id}:group:{group_id}:profile
im:{tenant_id}:group:{group_id}:settings
im:{tenant_id}:group:{group_id}:members
im:{tenant_id}:group:{group_id}:admins
im:{tenant_id}:group:{group_id}:muted
```
#### 关系数据
好友、黑名单、关注类关系适合用 Redis set 承载热读:
```text
im:{tenant_id}:relation:friends:{uid}
im:{tenant_id}:relation:blocklist:{uid}
```
用于快速判断:
- 是否好友。
- 是否被拉黑。
- 是否允许发起会话。
- 是否允许查看在线状态或最近在线时间。
### 5.3 第三阶段可选接入
- 首页 bootstrap 聚合结果:短 TTL 缓存,减少多表聚合查询。
- 群成员分页快照深度优化:基础 HTTP 结果快照已接入,后续大群可继续补 sorted set、稳定游标和主动失效。
- 未读数深度优化:基础总未读 Redis 热缓存已接入,后续可继续补 Topic 维度未读计数、bootstrap 聚合缓存和主动失效事件。
### 5.4 万人大群实时推送专项
万人大群的实时推送需要和 Redis 改造一起考虑。核心目标是避免普通小群模式下的全量 fanout 压垮服务端,同时保证在线用户实时可达、离线用户通过极光 Push 提醒、客户端重连后可按 seq 补齐消息。
该部分作为 Redis 改造的近期实现项处理,不再仅作为未实现增强项记录。
#### 数据分层
| 层级 | 承载内容 | 说明 |
| --- | --- | --- |
| 进程内 | 本节点在线连接、本节点 topic 订阅、本节点待发送队列 | 只负责当前节点连接和发送,不保存全局群成员在线状态。 |
| Redis | 群在线成员索引、用户所在节点、群分片、原子 seq 发号器、24 小时热消息、短期去重 key | 用于判断哪些在线用户需要实时推送、原子获取群消息 seq,以及近期高频消息读取加速。 |
| MySQL 租户库 | `im_group_messages` 群消息正文、群消息 seq 结果、消息索引、离线同步游标、审计数据 | 作为群消息最终存储,保存 Redis 分配后的 seq,不负责实时发号。 |
| 极光 Push | 离线提醒 | 只做提醒,不承载消息正文最终一致性。 |
#### 建议 Redis key
```text
im:{tenant_id}:presence:user:{uid}
im:{tenant_id}:topic:{topic}:online_members
im:{tenant_id}:topic:{topic}:node:{node_id}:online_members
im:{tenant_id}:topic:{topic}:shard:{shard_id}:members
im:{tenant_id}:topic:{topic}:last_seq
im:{tenant_id}:topic:{topic}:hot_msg_seqs
im:{tenant_id}:topic:{topic}:hot_msg:{seq}
im:{tenant_id}:topic:{topic}:uniq:msg:{from_uid}:{client_msg_id}
im:{tenant_id}:push:dedup:{topic}:{seq}:{uid}
```
说明:
- `topic:online_members` 保存当前群在线成员集合,可由用户上线、订阅群、退订群、断开连接维护。
- `topic:node:{node_id}:online_members` 用于跨节点推送时按节点拆分,减少广播范围。
- `topic:shard:{shard_id}:members` 可用于万人群成员分片,后续支持分批扫描和补偿。
- `topic:last_seq` 是群消息 seq 发号器,使用 Redis `INCR` 或 Lua 脚本原子获取下一个 seq。
- `topic:hot_msg_seqs` 使用 sorted set 保存 24 小时内热消息 seq,score 为 seq 或发送时间。
- `topic:hot_msg:{seq}` 保存单条热消息正文快照,TTL 建议 24 小时。
- `topic:uniq:msg:{from_uid}:{client_msg_id}` 保存群消息发送唯一 key,用于客户端重试幂等和服务端去重。
- `push:dedup` 用于极光 Push 或实时推送去重,TTL 建议按分钟级设置。
#### 唯一 key 与幂等规则
Redis 唯一 key 用于挡住短时间内的重复提交,MySQL 唯一索引作为最终兜底。不能只依赖 Redis 做永久唯一性,因为 Redis key 会过期,也可能被运维清理。
群消息发送唯一 key:
```text
im:{tenant_id}:topic:{topic}:uniq:msg:{from_uid}:{client_msg_id}
```
建议 value:
```json
{
"status": "pending|done|failed",
"seq": 12345,
"message_id": 987654321,
"created_at": "2026-08-25T12:00:00.000+08:00"
}
```
处理规则:
1. 客户端必须传 `client_msg_id`,同一发送人、同一 Topic 内唯一。
2. 服务端收到群消息后先用 `SET key value NX EX ttl` 抢占唯一 key,初始状态为 `pending`。
3. 抢占成功才继续 Redis `INCR` 获取群 `seq`。
4. 抢占失败时读取 key:
- `done`:直接返回已生成的 `seq/message_id`。
- `pending`:返回处理中或短暂等待后重试,避免重复发号。
- `failed`:允许客户端用同一个 `client_msg_id` 重试,或要求重新生成 `client_msg_id`。
5. 消息写入 `im_group_messages` 成功后,将唯一 key 更新为 `done`,记录 `seq/message_id`。
6. 消息写入失败时,将唯一 key 更新为 `failed` 并设置较短 TTL;已经获取的 `seq` 不回退,允许空洞。
7. `im_group_messages` 仍保留 `UNIQUE(topic, from_user, client_msg_id)`,防止 Redis key 过期后重复落库。
TTL 建议:
- `pending`:30 到 120 秒。
- `done`:24 小时到 7 天,按客户端重试窗口决定。
- `failed`:5 到 30 分钟。
- Push 去重 key:5 到 30 分钟。
#### Seq 发号规则
- 群消息 seq 由 Redis 维护,不从 MySQL 查询最大 seq。
- 每个 Topic 独立一个 `topic:last_seq`,使用 `INCR im:{tenant_id}:topic:{topic}:last_seq` 原子获取。
- 只要求单 Topic 内单调递增,不要求全局连续。
- 如果取到 seq 后消息持久化失败,该 seq 可以形成空洞;客户端按 seq 补拉时允许跳过服务端确认不存在的 seq。
- Topic 创建或 Redis key 丢失时,当前代码使用 topic 内存态 `lastID` 作为 Redis 初始化基线;该值来自 topic 加载结果或当前进程已确认的最新 seq,然后再执行原子 `INCR`。
- Redis 不可用时不能用数据库临时发号继续写大群消息,应返回可重试错误,避免多源发号造成 seq 冲突。
#### 发送流程
1. 服务端收到群消息后,先完成权限校验和禁言校验。
2. 使用 Redis 唯一 key 抢占 `topic + from_uid + client_msg_id`,防止客户端重试重复发送。
3. 抢占成功后,使用 Redis `INCR` 或 Lua 脚本从 `topic:last_seq` 原子获取下一个群 `seq`。
4. 消息正文带着该 `seq` 写入租户库 `im_group_messages`。
5. 落库成功后将唯一 key 更新为 `done`,记录 `seq/message_id`。
6. 对万人大群或高热群,将消息快照写入 `topic:hot_msg:{seq}`,并把 seq 加入 `topic:hot_msg_seqs`,TTL 保持 24 小时。
7. 查询 Redis 中该群在线成员,按 `node_id` 分组。
8. 本节点在线成员直接从进程内连接表推送。
9. 其他节点在线成员通过 Redis Pub/Sub 通知目标节点推送;当前代码使用 `im:0:group_fanout` 广播精简后的 `ServerComMessage`,订阅节点收到后投递到本机 `hub.routeSrv`,只发送给本节点已加载 Topic 的在线连接。
10. 不在线成员不做服务端全量离线消息 fanout,只更新离线游标或依赖消息 seq 补拉,并触发极光 Push。
11. 客户端重连后按 Topic seq 拉取缺失消息,优先读 Redis 热消息,未命中再回源租户库 `im_group_messages`。
#### 读取流程
客户端消息读取仍以 seq 为准,Redis 只优化 24 小时内的热读:
1. 在线实时收到消息后,客户端本地落库并更新本地最大 seq。
2. 重连、切换设备或发现 seq 不连续时,调用历史消息接口按 `topic + after_seq` 补拉。
3. 服务端先按 seq 范围读取 Redis 热消息。
4. Redis 命中的消息直接返回;缺失、过期或不连续的 seq 回源租户库 `im_group_messages`。
5. 回源命中后可按策略重新写入 Redis,保持 24 小时 TTL。
6. 极光 Push 唤醒后,客户端仍走正常登录和 seq 补拉链路。
#### 热消息缓存边界
- 只缓存 24 小时内高频读取的群消息,普通群可不开启或按群规模阈值开启。
- Redis 中的消息快照不作为审计和历史同步最终来源。
- 删除、撤回、编辑消息后,必须同步删除或更新对应 `topic:hot_msg:{seq}`,避免客户端读到旧正文。
- 大消息、文件二进制、音视频内容不直接放 Redis,只缓存消息元数据和 OSS object key。
- Redis 容量按租户和群设置上限,超过阈值时优先淘汰低热群或旧 seq。
- 当前代码只在 Redis 能完整命中一页群消息时直接返回;如果 Redis 缺失、过期、不连续或读取带有用户软删除视角,则回源 MySQL,避免缓存缺口导致消息缺失或读到用户已删除内容。
#### 离线 Push 口子
极光 Push 只用于离线提醒,建议 payload 只放最小必要信息:
```json
{
"type": "group_message",
"topic": "grp_xxx",
"seq": 12345,
"tenant_id": "tenant_xxx"
}
```
不建议在 Push payload 中放完整消息正文,避免隐私泄露、厂商通道截断和多端一致性问题。
#### 一致性边界
- Redis 的在线成员集合允许短暂不准,最终由心跳 TTL 和订阅变更修正。
- Redis 唯一 key 负责短期幂等和去重,MySQL 唯一索引负责最终唯一性兜底。
- seq 是否已分配以 Redis `topic:last_seq` 为准;群消息是否真实存在,以租户库 `im_group_messages` 为准。
- Redis 发号成功但消息持久化失败时,允许 seq 空洞,不回退 `topic:last_seq`。
- 实时推送失败不影响消息持久化,通过客户端 seq 补拉恢复;Redis 热消息缺失时回源租户库 `im_group_messages`。
- 极光 Push 失败不影响消息存储,只影响离线提醒,需要记录失败并可观测。
- Redis Pub/Sub 只负责在线实时投递,不作为可靠消息队列;节点短暂断线错过实时消息时,客户端仍以 Topic seq 补拉恢复。
## 6. MySQL 层设计
### 6.1 必须保留在 MySQL 的数据
- 私聊消息正文、私聊消息索引、离线同步游标。
- 群消息正文、群消息索引、群消息审计数据。
- 文件记录、OSS object key、文件安全审核结果。
- 群邀请、进群申请、审批记录。
- 群公告、公告确认、公告历史。
- 用户认证、KYC 记录、企业注册资料。
- 举报、投诉、内容安全审核任务。
- 操作日志、审计日志、管理后台变更记录。
- 需要事务一致性的业务状态。
### 6.2 消息表拆分
当前 `im_messages` 后续只作为私聊、文件助手等非群聊消息的主存储。群聊消息新增租户库业务表 `im_group_messages`,避免所有消息挤在一张表里,也方便后续针对大群按 `topic + seq`、时间、热度做分区、归档和缓存。
建议表结构:
```sql
CREATE TABLE IF NOT EXISTS im_group_messages (
id BIGINT NOT NULL AUTO_INCREMENT,
topic VARCHAR(64) NOT NULL COMMENT '群 Topic,如 grp_xxx',
group_id BIGINT NOT NULL COMMENT '业务群 ID,便于分区、治理和统计',
seqid BIGINT NOT NULL COMMENT '群内消息序号,由 Redis 原子发号',
from_user BIGINT NOT NULL COMMENT '发送人用户 ID',
client_msg_id VARCHAR(64) NULL COMMENT '客户端幂等消息 ID',
msg_type VARCHAR(32) NOT NULL DEFAULT 'text' COMMENT 'text/image/file/audio/video/system 等',
status VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT 'normal/recalled/deleted/blocked',
content JSON NULL COMMENT '消息正文或 Drafty 内容',
head JSON NULL COMMENT '消息头,包含 @、引用、客户端能力等',
mentioned_user_ids JSON NULL COMMENT '@ 用户列表,便于提醒和检索',
mention_all TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否 @ 全体',
reply_seqid BIGINT NULL COMMENT '回复的群消息 seq',
root_seqid BIGINT NULL COMMENT '线程或合并上下文根消息 seq',
file_count INT NOT NULL DEFAULT 0 COMMENT '附件数量',
content_size INT NOT NULL DEFAULT 0 COMMENT '正文 JSON 序列化后的字节数',
has_risk TINYINT(1) NOT NULL DEFAULT 0 COMMENT '是否命中风控或内容安全',
audit_status VARCHAR(32) NOT NULL DEFAULT 'pass' COMMENT 'pass/pending/reject',
version INT NOT NULL DEFAULT 1 COMMENT '编辑版本',
recalled_at DATETIME(3) NULL,
recalled_by BIGINT NULL,
deleted_at DATETIME(3) NULL,
expires_at DATETIME(3) NULL COMMENT '可选保留期,便于过期归档或清理',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE KEY uk_im_group_messages_group_seq (group_id, seqid),
UNIQUE KEY uk_im_group_messages_topic_seq (topic, seqid),
UNIQUE KEY uk_im_group_messages_topic_sender_client_mid (topic, from_user, client_msg_id),
KEY idx_im_group_messages_group_created (group_id, created_at),
KEY idx_im_group_messages_group_status_seq (group_id, status, seqid),
KEY idx_im_group_messages_topic_created (topic, created_at),
KEY idx_im_group_messages_topic_from_seq (topic, from_user, seqid),
KEY idx_im_group_messages_audit (audit_status, created_at),
KEY idx_im_group_messages_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
设计说明:
- `seqid` 使用 `BIGINT`,避免大群长期高频消息导致 `INT` 不够用。
- `group_id + seqid` 和 `topic + seqid` 都保持唯一,业务治理优先用 `group_id`,协议补拉仍可用 `topic`。
- `client_msg_id` 用于发送幂等,索引带上 `topic`,避免不同群复用客户端消息 ID 时互相冲突。
- `status` 标记普通、撤回、删除、拦截状态,拉历史时可以返回占位或过滤。
- `mentioned_user_ids`、`mention_all` 单独存,方便 @ 提醒、离线 Push 和后台治理,不必每次解析正文。
- `audit_status`、`has_risk` 为内容安全和风控留字段,高风险消息可先 pending 后再放行。
- `reply_seqid`、`root_seqid` 为引用回复、线程、合并转发上下文预留。
- 文件二进制不进表,表内只保存消息内容和 OSS object key 等元数据;群消息附件会同步登记到 `im_group_files`,并记录 `source_topic/source_seq` 追溯来源消息。
- 超大规模租户后续可按 `group_id` hash 分表,或按 `created_at/expires_at` 做归档分区。
读写规则:
1. 私聊消息继续写 `im_messages`。
2. 群聊消息写 `im_group_messages`。
3. 群消息发号由 Redis `topic:last_seq` 原子递增完成,`im_group_messages.seqid` 只保存结果。
4. Redis 24 小时热消息命中时直接返回;未命中按 `topic + seqid` 回源 `im_group_messages`。
5. 撤回、编辑、删除群消息时,以 `im_group_messages` 为准,并同步更新或删除 Redis 热消息。
### 6.3 消息正文说明
第一阶段不把消息正文作为 Redis 长期存储。万人大群和高热群允许缓存 24 小时内热消息,作为客户端重连补拉、最近历史翻页和多端同步的读加速。
消息正文主要读取场景:
- 新设备登录后的历史消息同步。
- 离线用户重新上线后的消息补拉。
- 客户端本地消息缺失后的重拉。
- 举报、风控、内容安全复查。
- 管理后台审计或客服处理。
这些场景的最终来源仍是 MySQL 的消息表。私聊回源 `im_messages`,群聊回源 `im_group_messages`。Redis 热消息只覆盖短期高频读取,命中则减少数据库压力,未命中必须回源。
## 7. 读写一致性规则
### 7.1 进程内配置
- 进程启动加载配置。
- 定时刷新配置。
- 配置读取失败时使用旧值。
- 配置变更允许短时间延迟生效。
### 7.2 Redis 运行态
- 在线状态以 Redis 为跨节点查询来源。
- 本机内存只用于管理本机连接。
- Redis 写入失败时,在线状态降级为未知或离线,不阻断基础连接能力。
- 心跳持续刷新 TTL,节点异常时依赖 TTL 自动回收。
### 7.3 Redis 缓存 MySQL 热数据
- 读:先 Redis,未命中再 MySQL,最后回填 Redis。
- 写:先 MySQL 事务提交,再删除或更新 Redis。
- 消息 seq:群聊先通过 Redis `INCR` 或 Lua 脚本原子获取 seq,再带 seq 写租户库 `im_group_messages`;私聊仍按现有 `im_messages` 链路处理。
- 消息热缓存:消息持久化成功后,再写 Redis 24 小时热消息;热消息写失败不影响消息发送成功。
- Redis 故障:直接回源 MySQL,记录降级日志和指标。
- 不允许因为 Redis 写失败回滚已经成功的 MySQL 业务事务,除非该业务本身强依赖 Redis 状态。
### 7.4 MySQL 持久化
- 需要审计、事务、历史追溯的数据直接写 MySQL。
- Redis 和进程内缓存只做读取加速或运行态存储。
- 私聊消息使用 `im_messages`,群聊消息使用 `im_group_messages`,避免单表无限膨胀。
## 8. 租户隔离
当前方案是一租户一库,业务表中不再保留 `tenant_id` 字段。但 Redis、进程内配置、日志指标仍属于共享运行环境,必须保留租户维度。
Redis key 统一包含租户标识:
```text
im:{tenant_id}:...
```
如果请求早期只有企业码或租户编码,应先解析到租户 ID 或租户库配置,再进入业务缓存读取。
## 9. 代码改造建议
### 9.1 基础设施
- 增加 Redis 配置:地址、密码、DB、连接池、超时、key 前缀。
- 增加 Redis client 初始化和健康检查。
- 增加统一 key builder,避免业务代码手写 key。
- 增加缓存指标和日志。
当前代码将 Redis 配置挂在 `store_config.redis` 下;不配置则不启用 Redis 群消息发号和唯一 key,群消息仍兼容旧的本机 `lastID + 1` 发号。
群消息物理表按自然月拆分时,落库路由、跨月查询、幂等和迁移方案见:[IM-群消息按月分表改造方案](./IM-群消息按月分表改造方案.md)。
配置示例:
```json
{
"store_config": {
"redis": {
"addr": "127.0.0.1:6379",
"username": "",
"password": "",
"db": 0,
"key_prefix": "im",
"dial_timeout_ms": 2000,
"read_timeout_ms": 2000,
"write_timeout_ms": 2000,
"pending_ttl_seconds": 120,
"done_ttl_seconds": 86400,
"failed_ttl_seconds": 600,
"hot_message_ttl_seconds": 86400,
"presence_ttl_seconds": 120,
"cache_ttl_seconds": 300,
"short_state_ttl_seconds": 600,
"group_fanout_enabled": true
}
}
}
```
`group_fanout_enabled` 默认关闭。启用后,群消息本节点投递完成后会发布 Redis Pub/Sub,其他节点订阅后只向本节点已在线的同群连接推送;如果节点没有加载该 Topic,则读取 Redis 本节点群在线成员集合做兜底直投,本地会话订阅表作为兼容兜底,实时投递失败再由客户端 seq 补拉恢复。
当前第二阶段已接入:
- 用户资料单对象缓存:`store.Users.Get` 优先读 Redis,用户更新/删除后失效。
- Topic 单对象缓存:`store.Topics.Get` 优先读 Redis,Topic 更新、转让、删除后失效。
- 单条订阅关系缓存:`store.Subs.Get` 优先读 Redis,订阅更新/删除后 active/any 两种缓存同时失效。
- 群治理资料缓存:群设置 `GroupBusiness.GetSettings`、群成员 `GroupBusiness.GetMember` 优先读 Redis,更新、离群、角色变更后刷新或失效。
- 群成员分页快照:`GET /v1/im/groups/{topic}/members` 按租户、Topic、操作者权限、筛选条件、cursor 和 limit 缓存响应快照,未命中回源订阅列表。
- 用户总未读热缓存:`store.Users.GetUnreadCount` 先读 Redis,未命中回源 MySQL 并回填;在线用户 unread updater 更新内存计数时同步写 Redis。
- 好友/黑名单 Redis Set 热缓存:关系上下文判断先查 `friends:{uid}`、`blocklist:{uid}` 正向 set,命中后减少 MySQL 查询;未命中仍回源 MySQL,新增/删除好友、拉黑/解除拉黑会同步更新 set。
- 验证码短状态缓存:验证码创建、查询、验证、失败、消费会同步 Redis,`GetActive` 使用 `scene + phone_hash` 的 active key 做重发窗口热读。
- Redis 频控:验证码创建按 `tenant + scene + phone_hash` 使用 Redis 原子 `INCR + EXPIRE` 做 1 小时 5 次限制,手机号只使用 hash,不写明文。
- 动作 token 短状态缓存:创建时写 Redis,消费成功后删除缓存;最终消费仍以 MySQL 状态机为准,保留审计一致性。
- SeqId 收口:消息、Topic 游标、订阅读/收游标、消息范围、收藏/举报/群文件引用、通话消息引用、JSON/gRPC 协议结构均改为 `int64`;MySQL 新建表 schema 中对应字段改为 `BIGINT`。
- 进程内配置缓存:租户解析、注册短信开关、第三方配置读取已加本地 TTL 缓存,减少 HotGo 控制库频繁查询;缓存过期后按下一次读取刷新。
### 9.2 接口抽象
建议新增接口:
```go
type PresenceStore interface {
MarkOnline(ctx context.Context, tenantID string, uid string, sessionID string, meta PresenceMeta) error
Refresh(ctx context.Context, tenantID string, uid string, sessionID string) error
MarkOffline(ctx context.Context, tenantID string, uid string, sessionID string) error
Get(ctx context.Context, tenantID string, uid string) (Presence, error)
}
type ConfigCache interface {
GetDict(ctx context.Context, key string) (DictValue, bool)
GetTenantConfig(ctx context.Context, tenantID string) (TenantConfig, bool)
Refresh(ctx context.Context) error
}
type ReadModelCache interface {
GetUserProfile(ctx context.Context, tenantID string, uid string) (UserProfile, bool)
SetUserProfile(ctx context.Context, tenantID string, profile UserProfile) error
DeleteUserProfile(ctx context.Context, tenantID string, uid string) error
}
```
### 9.3 业务接入顺序
1. 先接入 Redis client 和配置。
2. 接入在线状态 PresenceStore。
3. 群消息接入 Redis `SET NX` 唯一 key 与 `INCR` 原子 seq 发号,群消息正文写入 `im_group_messages`。
4. WebSocket 登录、心跳、断开链路写入 PresenceStore。
5. 用户在线状态查询接口改为读 Redis。
6. 配置和字典改为进程内 TTL 缓存刷新。
7. 再逐步接入用户、群、好友关系热数据缓存。
## 10. 故障降级
| 场景 | 降级策略 |
| --- | --- |
| Redis 不可用,查询在线状态 | 返回未知或离线,记录告警 |
| Redis 不可用,读取用户/群缓存 | 回源 MySQL |
| Redis 不可用,验证码或短期 token | 涉及安全的接口应失败关闭 |
| Redis 不可用,万人大群发消息 | 无法原子获取群 seq,应返回可重试错误;不使用 MySQL 临时发号,避免 seq 冲突 |
| 配置刷新失败 | 使用进程内旧配置 |
| MySQL 不可用 | 持久化业务失败,不依赖 Redis 兜底 |
## 11. 监控指标
- Redis 连接状态。
- Redis 命令耗时。
- Redis 缓存命中率。
- 在线用户 key 数量。
- 心跳刷新失败次数。
- Redis 回源 MySQL 次数。
- 大群在线成员集合大小。
- 大群按节点分发耗时。
- 大群实时推送成功率和补拉率。
- 极光 Push 触发数、成功数和失败数。
- 配置刷新成功和失败次数。
- Presence TTL 过期导致的离线数量。
## 12. 第一阶段落地范围
第一阶段建议只做以下内容:
- Redis 基础配置和 client 初始化。
- 用户在线、离线、心跳刷新、跨节点查询。
- 验证码、临时 token、限流计数等短期状态迁移到 Redis。
- 系统配置、字典、租户配置、第三方配置改为进程内 TTL 缓存刷新。
- 万人大群实时推送纳入 Redis 改造近期实现范围,先完成 Redis key、在线成员索引、节点分片和跨节点实时分发链路。
- 明确消息正文不进入 Redis 长期存储;万人大群 24 小时热消息可进入 Redis 做读加速。
- 群成员分页快照和用户总未读 Redis 热缓存已完成基础接入。
后续再逐步推进好友/黑名单全量预热、群成员分页快照、未读数等缓存改造。
@@ -0,0 +1,446 @@
# IM bootstrap 入口与未读数改造需求文档
## 1. 背景
当前 `GET /v1/im/bootstrap` 已返回登录后的功能开关、权限、工作台模块、系统 Topic 和安全摘要。前端同事在接入时提出以下问题:
1. 新朋友未处理数、群列表、文件助手和系统消息入口是否都由 bootstrap 控制显示。
2. 未读数字段分别从哪里读取。
3. 工作台是否需要总未读数。
4. 文件助手和系统消息是否完全依赖 WebSocket Topic 推送,还是 bootstrap 也要返回入口和初始未读。
5. 文档需要给出完整响应示例,避免入口字段和里层结构继续口头确认。
现有前后端对接文档已有基础约定:
1. 登录后客户端必须执行 `sub me`,从 `meta.sub` 获取每个会话 Topic 的 `seq/read/recv`。
2. 普通聊天会话未读数使用 `max(seq - read, 0)`。
3. `bootstrap` 返回固定入口、功能开关、权限和系统 Topic。
4. `workbench.modules[].unread_count` 已在结构中定义,但当前实现固定为 0。
缺口在于:bootstrap 没有明确返回通讯录入口、系统消息入口、文件助手入口、工作台总未读和业务 badge;文档也没有明确哪些未读来自 Topic,哪些未读来自业务表统计。
## 2. 目标
1. 明确 `bootstrap` 是登录后首屏入口配置和初始 badge 的聚合接口。
2. 明确 Topic 类未读和业务类未读的分工。
3. 为前端提供稳定响应结构,减少后续沟通成本。
4. 保持聊天实时链路轻量:普通聊天、群聊、文件助手和系统消息的实时增量仍通过 Tinode WebSocket Topic 更新。
5. 为后续工作台 badge、系统通知和好友申请的实时事件留出兼容空间。
## 3. 非目标
1. 不把所有会话列表塞入 bootstrap;普通会话列表仍通过 `sub me` 和会话搜索/列表能力获取。
2. 不用 bootstrap 替代 WebSocket 增量更新。
3. 不让客户端根据本地按钮状态绕过服务端权限校验。
4. 不要求本期实现复杂实时聚合服务;可先返回准确初始值,再通过已有 Topic 或系统事件增量更新。
## 4. 概念定义
| 名称 | 含义 |
| --- | --- |
| 入口 entry | 登录后首页、通讯录、工作台等固定可见入口配置 |
| Topic 未读 | 由 Tinode Topic 的 `seq/read/recv` 推导出的消息未读 |
| 业务 badge | 不能用 Topic `seq/read` 表达的业务待处理数量,例如新朋友待处理申请 |
| 工作台模块未读 | `modules[].unread_count`,表示单个工作台模块的业务未读 |
| 工作台总未读 | 所有可见工作台模块未读汇总,供底部 Tab 或一级入口展示 |
| 系统消息 | 服务端可信系统通知,展示页可查 HTTP 列表,实时通知可走系统 Topic 或系统事件 |
| 文件助手 | 用户自己的固定会话入口,用于收藏/文件转发/个人文件消息;若实现为 Topic,则未读遵循 Topic 规则 |
## 5. 未读数来源规则
### 5.1 Topic 类未读
以下未读原则上来自 Tinode Topic:
1. 单聊会话。
2. 群聊会话。
3. 系统消息入口,如果系统消息以 `system_topic.topic` 作为可信 Topic 承载。
4. 文件助手入口,如果文件助手以固定 Topic 承载。
计算规则:
```text
topic_unread_count = max(seq - read, 0)
```
客户端登录后必须执行:
```text
WebSocket Upgrade -> hi -> token login -> sub me -> get sub/data
```
初始会话列表从 `meta.sub` 获取,后续通过 `{data}`、`{meta}`、`{pres}`、`{info}` 和 `note read/recv` 更新本地状态。
### 5.2 业务类未读
以下未读不适合用 Topic `seq/read` 计算,应由 HTTP 业务接口或 bootstrap 返回:
1. 新朋友待处理申请数。
2. 工作台模块待处理数,例如会议、日报、月报、总经理信箱。
3. 未来的审批、公告、任务等非聊天会话类 badge。
业务类未读的真源在业务表,bootstrap 返回登录后的初始聚合值;后续增量可通过系统事件通知前端刷新或局部更新。
## 6. bootstrap 响应结构扩展
### 6.1 顶层新增字段
`GET /v1/im/bootstrap` 的 `data` 建议新增:
```json
{
"entry_badges": {
"contacts_total_unread": 2,
"new_friends_pending_count": 2,
"groups_unread_count": 0,
"file_assistant_unread_count": 0,
"system_unread_count": 0,
"workbench_total_unread": 5
},
"entries": {
"new_friends": {
"enabled": true,
"route": "/contacts/new-friends",
"unread_count": 2
},
"groups": {
"enabled": true,
"route": "/contacts/groups",
"unread_count": 0
},
"file_assistant": {
"enabled": true,
"route": "/chat/file-assistant",
"topic": "slf",
"unread_count": 0,
"unread_source": "topic"
},
"system_messages": {
"enabled": true,
"route": "/chat/system",
"topic": "sys",
"unread_count": 0,
"unread_source": "business"
}
}
}
```
字段说明:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `entry_badges.contacts_total_unread` | int | 通讯录一级入口红点总数;本期可等于 `new_friends_pending_count` |
| `entry_badges.new_friends_pending_count` | int | 当前用户待处理入站好友申请数 |
| `entry_badges.groups_unread_count` | int | 群入口业务未读;若只是群聊消息未读,则可为 0,由会话列表负责 |
| `entry_badges.file_assistant_unread_count` | int | 文件助手入口初始未读 |
| `entry_badges.system_unread_count` | int | 系统消息入口初始未读 |
| `entry_badges.workbench_total_unread` | int | 工作台可见模块未读总和 |
| `entries.*.enabled` | bool | 是否展示入口 |
| `entries.*.route` | string | 前端内部路由,只允许白名单内部路径 |
| `entries.*.topic` | string | 若入口对应 Topic,则返回 Topic 名;没有 Topic 时省略 |
| `entries.*.unread_count` | int | 当前入口初始未读或待处理数 |
| `entries.*.unread_source` | string | `topic` 或 `business`,用于前端判断后续更新来源 |
### 6.2 unread_source 约定
| unread_source | 含义 | 后续更新方式 |
| --- | --- | --- |
| `topic` | 未读来自 Topic `seq/read` | WebSocket Topic 增量和 `note read/recv` |
| `business` | 未读来自业务表统计 | 业务 HTTP 刷新或系统事件 |
| `none` | 入口无未读概念 | 不展示 badge |
`new_friends.unread_source` 可省略,默认按 `business` 处理。若为了统一,也可以显式返回:
```json
{
"enabled": true,
"route": "/contacts/new-friends",
"unread_count": 2,
"unread_source": "business"
}
```
## 7. 工作台 unread 规则
当前 `modules` 结构保留:
```json
{
"code": "meetings",
"name": "会议通知",
"icon_file_id": "",
"route": "/meetings",
"unread_count": 3,
"enabled": true,
"permission": "enabled"
}
```
新增规则:
1. `modules[].unread_count` 是模块级业务未读。
2. `entry_badges.workbench_total_unread` 是所有可见且 `enabled=true` 模块的未读总和。
3. 隐藏或无权限模块不计入总未读。
4. 后续增量事件使用 `workbench.badge.updated`:
```json
{
"type": "system_event",
"event_type": "workbench.badge.updated",
"event_id": "evt_...",
"created_at": "2026-08-20T12:00:00.000Z",
"params": {
"module_code": "meetings",
"unread_count": 4,
"workbench_total_unread": 6,
"workbench_version": 1787153039
}
}
```
前端收到后只更新对应模块和工作台总未读;如果 `workbench_version` 大于本地版本,应重新调用 `GET /v1/im/workbench` 或 `GET /v1/im/bootstrap`。
## 8. 系统消息规则
系统消息有两个视角:
1. 列表查询:`GET /v1/im/system-notifications?cursor=` 返回历史通知列表。
2. 实时增量:通过服务端可信系统事件推送。
bootstrap 返回:
```json
{
"system_topic": {
"topic": "sys",
"type": "system"
},
"entries": {
"system_messages": {
"enabled": true,
"route": "/chat/system",
"topic": "sys",
"unread_count": 0,
"unread_source": "business"
}
}
}
```
实现要求:
1. 普通用户不能向系统 Topic 发布可信系统消息。
2. 客户端不能把普通用户伪造的 `system_event` 渲染为可信通知。
3. 本期系统消息未读使用 `im_system_notifications.read_at` 统计,`unread_source` 固定为 `business`,并由服务端返回准确 `system_unread_count`。
本期约定:系统消息入口保留 `topic:"sys"` 作为入口标识和后续事件通道标识,初始未读数以系统通知业务表为准。若后续确认普通用户不能订阅 `sys`,应改为每用户系统通知 Topic 或纯 HTTP 通知列表 + business badge。
## 9. 文件助手规则
文件助手需要先明确产品实现形态:
### 9.1 Topic 形态
如果文件助手是用户自己的固定 Topic,例如 `slf` 或专用 `file_assistant_topic`:
```json
{
"file_assistant": {
"enabled": true,
"route": "/chat/file-assistant",
"topic": "slf",
"unread_count": 0,
"unread_source": "topic"
}
}
```
未读通过 `sub me` 中该 Topic 的 `seq/read` 计算。
### 9.2 纯入口形态
如果文件助手只是本地入口,不对应消息 Topic:
```json
{
"file_assistant": {
"enabled": true,
"route": "/files/assistant",
"unread_count": 0,
"unread_source": "none"
}
}
```
此时不展示消息未读,只展示入口。
本期建议采用 Topic 形态,便于复用消息、文件、转发、未读和多端同步能力。
## 10. 通讯录入口规则
通讯录页包含以下固定入口:
1. 新朋友。
2. 群聊。
3. 文件助手。
4. 系统消息。
bootstrap 控制入口展示和初始 badge。具体列表仍由各自接口负责:
| 入口 | 展示控制 | 初始 badge | 列表数据 |
| --- | --- | --- | --- |
| 新朋友 | `entries.new_friends.enabled` | `new_friends_pending_count` | `GET /v1/im/friend-requests` |
| 群聊 | `entries.groups.enabled` | 群业务 badge,群消息未读走 Topic | 群列表接口或 `sub me` 中群 Topic |
| 文件助手 | `entries.file_assistant.enabled` | Topic 或 none | Topic 消息或文件助手专用接口 |
| 系统消息 | `entries.system_messages.enabled` | Topic 或 business | `GET /v1/im/system-notifications` |
## 11. 完整 bootstrap 示例
```json
{
"code": 0,
"data": {
"features": {
"create_group": true,
"message_edit": true,
"message_recall": true,
"scan_qr": true,
"system_notifications": true,
"user_search": true
},
"permissions": {
"create_group": true,
"scan_qr": true,
"user_search": true
},
"entry_badges": {
"contacts_total_unread": 2,
"new_friends_pending_count": 2,
"groups_unread_count": 0,
"file_assistant_unread_count": 0,
"system_unread_count": 1,
"workbench_total_unread": 5
},
"entries": {
"new_friends": {
"enabled": true,
"route": "/contacts/new-friends",
"unread_count": 2,
"unread_source": "business"
},
"groups": {
"enabled": true,
"route": "/contacts/groups",
"unread_count": 0,
"unread_source": "business"
},
"file_assistant": {
"enabled": true,
"route": "/chat/file-assistant",
"topic": "slf",
"unread_count": 0,
"unread_source": "topic"
},
"system_messages": {
"enabled": true,
"route": "/chat/system",
"topic": "sys",
"unread_count": 1,
"unread_source": "business"
}
},
"modules": [
{
"code": "organization",
"enabled": true,
"icon_file_id": "",
"name": "组织架构",
"permission": "enabled",
"route": "/organization",
"unread_count": 0
},
{
"code": "meetings",
"enabled": true,
"icon_file_id": "",
"name": "会议通知",
"permission": "enabled",
"route": "/meetings",
"unread_count": 3
}
],
"workbench_version": 1787153039,
"system_topic": {
"topic": "sys",
"type": "system"
},
"message_policy": {
"edit_window_seconds": 300,
"recall_window_seconds": 120,
"supported_actions": ["copy", "quote", "delete", "recall", "edit"]
},
"security": {
"realname": {
"allow_skip": true,
"remind_after": "24h",
"required": false,
"status": "unverified"
}
}
},
"request_id": "req_xxx",
"server_time": "2026-08-20T12:00:00.000Z"
}
```
## 12. 前端处理规则
1. bootstrap 成功后,先渲染固定入口和初始 badge。
2. 随后完成 WebSocket `sub me`,用 Topic `seq/read` 覆盖 Topic 类入口未读。
3. `unread_source=topic` 的入口以后以 Topic 状态为准。
4. `unread_source=business` 的入口以后以业务接口刷新或系统事件为准。
5. 切租户、退出登录、切账号时必须清空本地入口和 badge 缓存。
6. 前端不得把 `tenant_id`、内部数据库 ID 或未在白名单中的 route 写入跳转。
## 13. 服务端实现要求
1. `serveIMBootstrap` 增加 `entry_badges` 和 `entries`。
2. `serveIMBootstrap` 的入口数据必须按当前 Token 的租户和用户权限裁剪。
3. `route` 必须走服务端白名单,不能从数据库任意透传外部 URL。
4. 新朋友待处理数按当前租户、当前用户、入站、待处理状态统计。
5. 工作台 `modules[].unread_count` 从业务统计或模块 unread provider 获取,不再固定为 0。
6. `workbench_total_unread` 只汇总当前用户可见模块。
7. 文件助手和系统消息的 `topic` 必须是当前租户和当前用户有权限访问的 Topic。
8. 如果某入口当前未实现,仍可返回 `enabled:false`,前端隐藏入口。
## 14. 后端建议实现顺序
1. 扩展 bootstrap 响应结构,先返回固定入口和 0 badge,确保前端结构稳定。
2. 增加新朋友待处理数统计。
3. 增加工作台模块 unread provider 和总未读。
4. 明确文件助手 Topic,补齐入口 topic 和未读来源。
5. 明确系统消息未读来源;若 `sys` 不适合普通用户订阅,则改为业务 unread。
6. 增加系统事件 `workbench.badge.updated` 和入口 badge 更新事件。
7. 更新前后端对接文档,把本需求中的响应示例合并到 bootstrap 章节。
## 15. 验收用例
1. 新用户登录调用 bootstrap,返回 `entries`、`entry_badges`、`modules`、`workbench_version`。
2. 有 2 条待处理好友申请时,`new_friends_pending_count=2`,`entries.new_friends.unread_count=2`。
3. 没有待处理好友申请时,新朋友入口仍展示但 unread 为 0。
4. 工作台有两个可见模块未读分别为 3 和 2 时,`workbench_total_unread=5`。
5. 隐藏模块未读不计入 `workbench_total_unread`。
6. `unread_source=topic` 的入口在 `sub me` 返回后可用 Topic `seq/read` 覆盖初始 unread。
7. 切换租户后 bootstrap 返回另一租户入口和 badge,本地旧 badge 不复用。
8. 普通用户构造外部 route 或系统消息字段不能影响 bootstrap 返回。
9. 未实现入口返回 `enabled:false` 时前端隐藏入口。
10. 接口响应字段保持向后兼容,旧客户端忽略新增字段仍可正常工作。
## 16. 待确认问题
1. 文件助手最终使用 `slf` Topic,还是新增专用文件助手 Topic。
2. 普通用户系统消息入口是否继续使用 `sys`,还是改为每用户系统通知 Topic 或纯 HTTP 通知列表。
3. 群聊入口是否需要业务未读,还是完全由群 Topic 会话列表承担。
4. 工作台每个模块的 unread provider 由 IM 直接查库,还是由 HotGo/业务库提供统计。
5. 是否需要新增统一 badge 增量事件,例如 `entry.badge.updated`。
@@ -0,0 +1,343 @@
# IM 个人/企业注册登录前后端对接说明
## 1. 目标
当前客户端没有独立的企业码进入页面,登录页和注册页由用户选择“个人用户”或“企业用户”。前端提交登录和注册时必须显式带类型,后端不能再通过企业码是否为空来推断个人/企业。
核心规则:
1. 前端新增字段 `registration_type`,类型为 int,取值只能是 `0` 或 `1`。
2. `registration_type=1` 时,后端按个人注册处理,内部租户使用 `default`,数据写入 `default` 租户业务库。
3. `registration_type=0` 时,前端必须提交企业码,后端必须校验企业存在、启用且允许注册。
4. 登录新增字段 `login_type`,类型为 int,取值同样是 `0` 企业、`1` 个人。
5. 企业登录或企业注册都不允许使用 `default`。
6. `default` 是系统保留的个人租户码,不允许作为企业码输入。
一句话规则:**注册类型由 `registration_type` 决定,登录类型由 `login_type` 决定;个人落 `default`,企业必须校验企业码。**
## 2. 前端注册字段
注册页提交发码和最终注册时都带同一组注册上下文字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `registration_type` | int | 是 | `0` 表示企业注册,`1` 表示个人注册 |
| `tenant_code` | string | 企业注册必填 | 企业码;个人注册不填或传空字符串 |
前端规则:
| `registration_type` | `tenant_code` | 后端处理 |
| --- | --- | --- |
| `1` | 空 | 允许提交,后端内部使用 `default` |
| `1` | 非空 | 后端拒绝,避免误入库 |
| `0` | 空 | 后端拒绝,提示请输入企业码 |
| `0` | `default`、`Default`、`DEFAULT` | 后端拒绝 |
| `0` | 其他值 | 后端在注册链路中校验 |
用户在企业注册模式输入 `default` 时,建议提示:
```text
default 是系统保留企业码。个人注册请选择个人用户,企业注册请输入企业提供的企业码。
```
## 3. 企业码校验
不新增单独的企业码校验接口,也不要求前端在注册前调用 `/v1/im/tenants/resolve`。
后端在注册发码和最终注册链路中校验:
1. `registration_type=1`
- 不要求企业码。
- 即使请求 Header 或内部路由使用 `default`,也属于服务端内部租户,不是用户输入企业码。
- 创建用户时写入 `default` 租户业务库。
2. `registration_type=0`
- `tenant_code` 必填。
- `tenant_code` 大小写不敏感等于 `default` 时拒绝。
- 查询企业是否存在、启用、允许注册。
- 通过后在对应企业租户业务库创建用户。
企业不存在、停用或不允许注册时,客户端统一提示:
```text
企业不存在或不可用
```
客户端不要区分不存在、停用、禁用注册等内部原因。
`/v1/im/tenants/resolve` 只保留给服务发现、品牌配置、历史企业入口等场景使用;当前注册页不依赖它完成主流程。
## 4. 前端登录字段
登录页提交 WebSocket `{login}` 时带登录上下文字段:
| 字段 | 类型 | 必填 | 说明 |
| --- | --- | --- | --- |
| `login_type` | int | basic 登录必填 | `0` 表示企业登录,`1` 表示个人登录 |
| `tenant_code` | string | 企业登录必填 | 企业码;个人登录不填、传空字符串或内部传 `default` |
登录规则:
| `login_type` | `tenant_code` / `hi.tenant` | 后端处理 |
| --- | --- | --- |
| `1` | `default` | 个人登录,允许 |
| `1` | 非 `default` | 拒绝,避免个人登录误进企业租户 |
| `0` | 空 | 拒绝,提示请输入企业码 |
| `0` | `default`、`Default`、`DEFAULT` | 拒绝,统一提示“企业不存在或不可用” |
| `0` | 其他值 | 企业登录,后端按该租户认证 |
企业用户如果在企业入口输入 `default`,后端返回策略错误,错误参数为 `what=tenant_code`、`reason=TENANT_CODE_FORBIDDEN`;客户端统一展示:
```text
企业不存在或不可用
```
个人登录 `{login}` 示例:
```json
{
"login": {
"id": "login-1",
"scheme": "basic",
"login_type": 1,
"tenant_code": "",
"secret": "base64(account:password)"
}
}
```
企业登录 `{login}` 示例:
```json
{
"login": {
"id": "login-1",
"scheme": "basic",
"login_type": 0,
"tenant_code": "acme",
"secret": "base64(account:password)"
}
}
```
## 5. 个人注册流程
个人注册必须显式提交 `registration_type=1`。
1. 用户选择个人注册。
2. 前端提交 `registration_type=1`,`tenant_code` 为空。
3. 注册发码、验证码校验、WebSocket `hi`、`acc` 注册链路使用服务端内部租户 `default`。
4. 服务端将用户创建到 `default` 租户对应的业务库。
发码示例:
```http
POST /v1/im/auth/verification-codes
X-MeetingIM-APIKey: <api_key>
X-IM-Schema: 1
X-Tenant-Code: default
X-Request-ID: <uuid>
```
```json
{
"scene": "register",
"registration_type": 1,
"tenant_code": "",
"phone": "19122421333"
}
```
WebSocket 握手示例:
```json
{
"hi": {
"id": "hi-1",
"ver": "0.25",
"ua": "ios/1.0.0",
"platf": "ios",
"lang": "zh-CN",
"tenant": "default"
}
}
```
最终注册 `{acc}` 示例:
```json
{
"acc": {
"id": "acc-1",
"user": "new",
"scheme": "basic",
"registration_type": 1,
"tenant_code": "",
"username": "henry",
"phone": "19122421333",
"secret": "base64(henry:password)",
"login": true,
"cred": [
{
"meth": "tel",
"val": "19122421333",
"resp": "verification_token",
"params": {"challenge_id": "challenge-id"}
}
]
}
}
```
## 6. 企业注册流程
企业注册必须显式提交 `registration_type=0`。
1. 用户选择企业注册。
2. 用户填写企业码,例如 `acme`。
3. 前端直接提交用户输入的企业码,不做企业码存在性或保留字拦截。
4. 获取验证码、提交注册时都带 `registration_type=0` 和 `tenant_code=acme`。
5. 后端在注册链路中校验企业是否存在且可用。
6. 校验通过后,服务端在该企业租户业务库中创建用户;校验不通过时返回“企业不存在或不可用”。
注册发码请求:
```http
POST /v1/im/auth/verification-codes
X-MeetingIM-APIKey: <api_key>
X-IM-Schema: 1
X-Tenant-Code: acme
X-Request-ID: <uuid>
```
```json
{
"scene": "register",
"registration_type": 0,
"tenant_code": "acme",
"phone": "19122421333"
}
```
WebSocket `hi`:
```json
{
"hi": {
"id": "hi-1",
"ver": "0.25",
"ua": "android/1.0.0",
"platf": "android",
"lang": "zh-CN",
"tenant": "acme"
}
}
```
最终注册 `{acc}` 示例:
```json
{
"acc": {
"id": "acc-1",
"user": "new",
"scheme": "basic",
"registration_type": 0,
"tenant_code": "acme",
"username": "henry",
"phone": "19122421333",
"secret": "base64(henry:password)",
"login": true,
"cred": [
{
"meth": "tel",
"val": "19122421333",
"resp": "verification_token",
"params": {"challenge_id": "challenge-id"}
}
]
}
}
```
## 7. 后端校验规则
后端负责注册链路的最终校验,前端不做企业码拦截。
1. `registration_type` 缺失或不是 `0|1`
- 拒绝注册。
- 返回 `MALFORMED_REQUEST`,提示客户端升级或检查参数。
2. `registration_type=1`
- 不从空企业码推断,而是由该字段明确进入个人注册。
- 内部租户码设置为 `default`。
- 拒绝用户提交的非空 `tenant_code`,避免误入库。
- 注册时写入 `default` 租户业务库。
3. `registration_type=0`
- `tenant_code` 必填。
- 先 trim 首尾空格。
- 如果大小写不敏感等于 `default`,拒绝注册。
- 否则查询租户是否存在、是否启用、是否允许注册。
- 校验通过后绑定对应租户。
建议错误:
```json
{
"code": 30000,
"reason": "MALFORMED_REQUEST",
"message": "invalid registration_type or tenant_code"
}
```
企业不可用时,客户端展示:
```text
企业不存在或不可用
```
## 8. 缓存和本地数据隔离
客户端本地缓存、Token、会话、草稿、附件缓存必须继续按 `tenant_code + user_id` 隔离。
个人注册用户登录后,缓存键中的 `tenant_code` 使用 `default`。企业用户登录后,使用对应企业码。切换个人/企业身份时必须清理当前连接并重新 `hi`,不能在同一个 WebSocket 中切换租户。
## 9. 验收用例
| 场景 | 预期 |
| --- | --- |
| `login_type=1`,`hi.tenant=default` | 个人登录成功进入 `default` 租户认证 |
| `login_type=1`,`hi.tenant=acme` | 拒绝登录 |
| `login_type=0`,企业码空 | 拒绝登录,提示请输入企业码 |
| `login_type=0`,企业码为 `default` | 拒绝登录,提示企业不存在或不可用 |
| `login_type=0`,企业码有效 | 在该企业租户认证 |
| `registration_type=1`,企业码空,手机号注册 | 成功进入 `default` 租户注册链路 |
| `registration_type` 缺失 | 拒绝注册,返回参数错误 |
| `registration_type=1`,企业码非空 | 拒绝或忽略,建议拒绝,避免误入库 |
| `registration_type=0`,企业码空 | 拒绝注册,提示请输入企业码 |
| `registration_type=0`,企业码为 `default` | 拒绝注册 |
| `registration_type=0`,企业码不存在 | 发码或注册失败,提示企业不存在或不可用 |
| `registration_type=0`,企业码已停用 | 统一提示企业不存在或不可用 |
| `registration_type=0`,企业码有效 | 注册写入该企业租户库 |
| 个人用户注册成功后登录 | `tenant_code=default`,可正常登录、收发消息 |
## 10. 对客户端的明确要求
1. 登录页和注册页都必须明确区分个人和企业。
2. basic 登录必须提交 `login_type`。
3. 发码和最终注册都必须提交 `registration_type`。
4. 个人登录/注册提交类型 `1`,不要让用户输入或看到 `default`。
5. 企业登录/注册提交类型 `0` 和用户输入的企业码。
6. 企业用户手动输入 `default` 也直接提交,由后端返回错误,客户端按后端错误展示提示。
7. 不需要为了注册主流程调用 `/v1/im/tenants/resolve`。
## 11. 对后端的明确要求
1. basic 登录链路必须以 `login_type` 作为个人/企业登录的判定来源。
2. 注册链路必须以 `registration_type` 作为个人/企业注册的判定来源。
3. 不允许仅根据企业码是否为空推断个人注册或个人登录。
4. 登录、注册、发码、验证码校验、WebSocket `hi/login/acc` 链路保持同一租户上下文。
5. 显式企业码 `default` 必须拒绝,防止用户绕过企业码校验。
6. 企业登录/注册的企业码必须校验存在、启用、允许登录/注册。
7. 日志中要能看出来源是 `personal_default` 还是 `enterprise_code`,方便排查误入库问题。
+186 -6
View File
@@ -1,6 +1,6 @@
# IM 产品原型需求文档
> 本文档根据 `/Users/superysh/im/第二项目/` 中的 156 张产品原型图整理。功能按业务域归类,每张图以原文件名作为标题,图片下方给出功能说明与界面内容描述。
> 本文档根据 `/Users/superysh/im/第二项目/` 中的 160 张产品原型图整理。功能按业务域归类,每张图以原文件名作为标题,图片下方给出功能说明与界面内容描述。
## 产品总体定位
@@ -18,13 +18,45 @@
提供企业租户入口。页面展示产品标识、企业码输入框和“进入企业”按钮;提交后应校验企业码是否存在、是否启用,并将用户导向对应企业的登录环境。
### 1.1.1 企业注册-登录入口.png
<img src="images/企业注册-登录入口.png" alt="企业注册-登录入口" width="360">
功能说明:
企业码登录页右上角提供“企业注册”入口,底部提供“遇到问题?”反馈入口。点击“企业注册”进入企业资料提交流程;点击“遇到问题?”进入问题反馈页。入口文案应保持可见且易点击,避免与状态栏或背景装饰重叠。
### 1.1.2 企业注册-填写注册信息.png
<img src="images/企业注册-填写注册信息.png" alt="企业注册-填写注册信息" width="360">
功能说明:
企业注册页按步骤展示“填写企业信息、确定信息、提交审核”,当前处于第 1 步。用户需填写 18 位统一社会信用代码,支持扫描识别;可选上传营业执照,并填写企业邮箱用于接收审核结果及企业通知。提交前应校验统一社会信用代码格式、邮箱格式、图片大小和文件类型,并提示资料提交后预计审核时长。
### 1.1.3 企业注册-提交成功提示.png
<img src="images/企业注册-提交成功提示.png" alt="企业注册-提交成功提示" width="360">
功能说明:
企业注册资料提交成功后,在当前页展示明确 Toast 提示“提交成功,我们会尽快联系你”。提交成功状态应防止重复提交,并记录申请单状态;审核结果需通过企业邮箱或后续通知渠道触达。页面底部审核时长文案应与实际运营规则一致。
### 1.1.4 企业注册-问题反馈.png
<img src="images/企业注册-问题反馈.png" alt="企业注册-问题反馈" width="360">
功能说明:
企业入口问题反馈页支持填写问题描述并上传最多 9 张截图,便于定位企业码、注册资料、审核通知等问题。提交时应携带设备、版本、租户线索和可选联系方式;图片上传需显示数量限制、失败重试和大小校验,提交成功后给出明确受理反馈。
### 1.2 登录页面.png
<img src="images/登录页面.png" alt="登录页面" width="360">
功能说明:
展示用户名/手机号和密码登录表单,支持密码显隐、忘记密码、注册跳转以及用户协议/隐私政策确认。登录按钮在必填信息和协议条件满足后才可有效提交。
展示用户名/手机号和密码登录表单,支持密码显隐、忘记密码、注册跳转以及用户协议/隐私政策确认。登录按钮在必填信息和协议条件满足后才可有效提交。登录成功后安全保存 Token 和过期时间;应用重启或断线重连时优先使用未过期 Token 自动登录,并用服务端返回的新 Token 覆盖旧值。Token 过期或失效后返回账号密码登录。
### 1.3 登录页面-密码错误.png
@@ -40,7 +72,17 @@
功能说明:
提供用户名/手机号、短信验证码、密码和确认密码输入。需支持获取验证码、密码显隐、两次密码一致性校验、已有账号返回登录以及协议勾选。
提供独立的用户名和手机号输入框,两个字段二选一,允许同时填写但手机号优先。需支持获取验证码、密码显隐、已有账号返回登录以及协议确认。
业务规则:
1. 普通用户名只允许 1~10 位小写英文字母或数字,允许纯数字,不允许大写、中文、空格和符号。
2. 手机号只允许 11 位数字,服务端固定补 `+86`。
3. 手机号非空时走手机号方式并忽略用户名输入;注册后 `username` 和 `phone` 均保存规范化手机号。手机号为空时才走用户名方式,`phone=NULL`。
4. 邀请码固定为 5 位大小写英文字母,区分大小写,不允许数字和符号。
5. 用户名方式不发送短信,必须填写有效邀请码;注册前提示“未绑定手机号无法自助找回密码,忘记密码后只能联系企业管理员重置”。
6. 短信开关开启时,手机号方式必须完成验证码验证,邀请码不能绕过验证码。
7. 短信开关关闭时,点击获取验证码仍显示正常倒计时,但服务端不发送短信;手机号方式必须填写有效邀请码才能无验证码注册。
### 1.5 注册页面-倒计时.png
@@ -48,7 +90,7 @@
功能说明:
展示验证码发送后的倒计时状态。倒计时期间禁止重复发送,归零后恢复“获取验证码”;后端需同时执行发送频控和验证码有效期校验。
展示验证码发送后的倒计时状态。倒计时期间禁止重复发送,归零后恢复“获取验证码”;后端需同时执行发送频控和验证码有效期校验。短信开关关闭时前端交互和倒计时保持不变,但用户不会收到短信,最终是否允许注册由服务端结合邀请码判断。
### 1.6 忘记密码.png
@@ -56,7 +98,7 @@
功能说明:
通过手机号、验证码、新密码和再次确认完成密码重置。应校验账号归属、验证码正确性及新密码规则,成功后应废弃旧令牌并引导用户重新登录。
通过手机号、验证码、新密码和再次确认完成密码重置,仅适用于已保存手机号的账号。应校验账号归属、验证码正确性及新密码规则,成功后应废弃旧令牌并引导用户重新登录;无手机号的用户名账号应提示联系当前企业管理员重置密码。
### 1.7 身份验证(安卓).png
@@ -976,7 +1018,7 @@
功能说明:
工作台聚合组织内部的常用办公应用和业务入口,包括组织架构、日报/月报、会议通知、总经理信箱等模块。应用入口应根据企业配置和用户权限动态展示,并支持未读数量提示。
工作台聚合组织内部的常用办公应用和业务入口,包括组织架构、日报/月报、会议通知、会议笔记、总经理信箱等模块。应用入口应根据企业配置和用户权限动态展示,并支持未读数量提示。
### 9.2 部门组织框架.png
@@ -1074,6 +1116,144 @@
页面通过提示文案说明信箱用途、处理方式及隐私边界。提示内容应由企业后台可配置,并在用户提交前清晰告知实名/匿名规则和信息接收方。
### 9.14 会议笔记
会议笔记是企业工作台下的办公协同模块,用于围绕会议记录会议资料、纪要文件和相关附件。功能需要和会议通知、参会人、会议状态及文件能力联动,保证发起人、会议记录员、参会人和普通成员在不同权限下看到合适的入口、文件列表和操作按钮。
通知与刷新规则:
1. 工作台底部 Tab 的红点数字需要与工作台内可接收通知的模块上下对应。当前原型中的红点 `3` 表示工作台下共有 3 条未读通知,这 3 条通知只归属到“会议通知”模块。
2. 当前只有“会议通知”模块接收移动端通知,并承担会议创建、会议变更、会议提醒、参会反馈等通知展示。
3. 打卡、月报、日报、部门框架、举报信箱、会议笔记等其他工作台模块不做移动端通知,也不产生独立通知红点。
4. 除会议通知外,其他模块在用户进入模块时调用各自接口获取最新数据并刷新页面;客户端不得依赖通知事件来更新这些模块。
5. 会议笔记的文件新增、预览、下载状态只在会议笔记列表/详情页内刷新展示,不向工作台 Tab 追加通知数。
### 9.14.1 会议笔记-工作台入口.png
<img src="images/会议笔记-工作台入口.png" alt="会议笔记-工作台入口" width="360">
功能说明:
工作台新增“会议笔记”入口,与打卡、月报、日报、部门框架、举报信箱、会议通知等企业应用并列展示。入口点击后进入会议笔记列表页。
业务规则:
1. 会议笔记入口应由企业工作台配置和用户权限控制,未启用或无权限时不展示。
2. 工作台底部 Tab 红点只展示可通知模块的未读通知总数;当前只统计“会议通知”,不统计会议笔记、日报、月报、打卡、部门框架和举报信箱。
3. 入口排序、图标和名称应支持后台配置,但客户端需限制为白名单内部路由,不能跳转任意外部地址。
### 9.14.2 会议笔记-列表.png
<img src="images/会议笔记-列表.png" alt="会议笔记-列表" width="360">
功能说明:
会议笔记列表展示“与我有关”的会议记录,顶部提供会议搜索。每条会议卡片展示会议主题、发起人、会议时间、参会人、会议时长和会议状态,并提供“会议笔记”按钮进入详情;部分卡片支持“查看更多”查看完整会议信息或参会人信息。
业务规则:
1. “与我有关”仅展示当前用户作为发起人、会议记录员、参会人或具备组织查看权限的会议,且会议必须是已结束状态;未开始、进行中和已取消会议不进入会议笔记列表。
2. 列表按会议开始时间倒序展示,今天、昨天、前天等相对日期应由客户端按本地时区展示,排序仍以后端时间戳为准。
3. 搜索支持按会议主题、发起人、参会人和会议地点检索;搜索结果仍必须受会议可见权限限制。
4. 已结束但会议记录员尚未上传笔记的会议仍需要在会议笔记列表展示,卡片只展示会议基础信息和已结束状态,进入详情后展示“暂无笔记”空状态。
5. 已结束会议可查看有权限访问的会议笔记;会议记录员进入详情可上传文件,普通参会人进入详情不展示上传按钮且只能预览已上传文件。
6. 参会人过多时列表只展示前几名并省略,点击“查看更多”进入详情查看完整信息。
### 9.14.3 会议笔记-发布页面.png
<img src="images/会议笔记-发布页面.png" alt="会议笔记-发布页面" width="360">
功能说明:
会议发布页用于创建会议并指定会议记录员。页面包含发起人、地点、会议记录员、参会人、开始时间、时长、会议主题等字段,底部通过“发起会议”按钮提交。
业务规则:
1. 发起人默认取当前登录用户,不允许普通用户手动伪造;具备代发权限时应明确展示实际发起人和创建人。
2. 发布页面字段顺序中,会议记录员应位于参会人上方;会议记录员默认取会议发起人,用户可按权限改选其他成员。
3. 会议记录员、参会人、开始时间、时长和会议主题为必填项;地点可选。
4. 参会人可选择“所有人”或指定成员。选择所有人时应按组织权限和企业规模做确认提示,避免误发全员会议。
5. 会议记录员默认拥有上传、替换、删除本人上传文件和查看全部会议笔记的权限;普通参会人只能查看和预览已上传文件,不提供上传入口。
6. 创建成功后应进入会议详情或会议笔记列表;需要通知参会人时统一走“会议通知”模块,会议笔记模块本身不产生移动端通知。重复点击“发起会议”需防抖和幂等处理。
### 9.14.4 会议笔记-详情页.png
<img src="images/会议笔记-详情页.png" alt="会议笔记-详情页" width="360">
功能说明:
会议详情页展示会议主题、状态、发起人、时间、地点、会议记录员、参会人和会议时长。下方“全部笔记(文件)”展示会议相关文件,文件项包含类型图标、文件名、大小、上传人、上传日期和下载按钮。底部提供“上传文件”按钮。
业务规则:
1. 会议基础信息应与会议通知模块共用同一份会议数据,避免会议时间、参会人或状态在两个模块展示不一致。
2. 文件列表按上传时间倒序展示;同名文件允许存在,但应保留唯一文件 ID、上传人和上传时间。
3. 文件类型需识别 PDF、Office 文档、图片、压缩包等常见格式;未知格式展示通用文件图标。
4. “上传文件”按钮只对会议记录员展示;如后续扩展管理员代传能力,必须由服务端显式下发上传权限。
5. 当前用户是会议记录员时,详情页展示上传文件按钮;当前用户只是参会人时,不展示上传按钮,文件操作只保留预览能力。
6. 下载和预览都必须校验当前用户是否仍具备会议可见权限,不能仅凭文件 ID 访问。
### 9.14.5 会议笔记-详情页(普通成员).png
<img src="images/会议笔记-详情页(普通成员).png" alt="会议笔记-详情页(普通成员)" width="360">
功能说明:
普通成员或普通参会人进入会议笔记详情时,可查看会议基础信息;当没有可见笔记文件时,文件区域展示空状态“暂无笔记”。普通参会人页面不展示上传按钮,已上传文件仅提供预览入口。
业务规则:
1. 普通参会人只可查看对自己开放的会议笔记文件;无文件或无权限时展示统一空状态。
2. 空状态应区分“暂无笔记”和“无权限查看”两类业务原因,但默认文案可统一为不泄露权限细节的提示。
3. 普通参会人不能通过前端构造请求上传、删除、替换或下载文件;服务端必须再次校验角色和会议权限。
4. 若会议后续新增了对该成员可见的笔记文件,列表和详情应能刷新出新文件。
### 9.14.6 会议笔记-详情页-在线预览.png
<img src="images/会议笔记-详情页-在线预览.png" alt="会议笔记-详情页-在线预览" width="360">
功能说明:
当文件支持在线预览时,文件项右侧展示“预览”按钮,文件状态显示“在线预览”。点击后进入文件预览页;不支持预览的文件仍展示下载按钮。
业务规则:
1. 支持预览的文件类型至少包括 PDF 和常见图片,Office 文档是否预览取决于后端转换能力。
2. 预览状态由服务端返回,客户端不得仅按扩展名判断是否可预览。
3. 预览入口和下载入口都需要受文件权限控制;预览失败时应允许用户回到详情页并选择下载。
4. 预览文件应使用短期授权地址或受控文件接口,禁止暴露长期有效的公开下载链接。
### 9.14.7 会议笔记-详情页-文件下载中.png
<img src="images/会议笔记-详情页-文件下载中.png" alt="会议笔记-详情页-文件下载中" width="360">
功能说明:
文件下载过程中,当前文件项展示“正在下载”、进度条和“取消”按钮;其他文件仍保持可下载状态。
业务规则:
1. 每个文件需独立展示下载进度、成功、失败和取消状态。
2. 下载中允许取消,取消后应停止网络请求并恢复可下载状态;已完成文件可进入系统打开或本地文件管理流程。
3. 下载失败需展示可重试状态,并保留失败原因用于问题排查。
4. 后台、断网、弱网场景下应保存下载任务状态,恢复网络后按产品策略继续或提示重试。
### 9.14.8 会议笔记-文件预览.png
<img src="images/会议笔记-文件预览.png" alt="会议笔记-文件预览" width="360">
功能说明:
文件预览页展示文档内容,顶部提供返回、标题和更多菜单。预览区域展示页码进度,例如 `1/213`;更多菜单包含举报和分享操作。
业务规则:
1. 文件预览页应支持多页文档翻页、缩放、滑动浏览和加载失败重试。
2. 页码、加载状态和错误状态必须与当前文件一致,切换文件或返回详情页后应清理旧状态。
3. 分享权限由会议和文件权限共同决定;无分享权限时隐藏分享入口或提示无权限。
4. 举报入口用于举报文件内容或会议资料,提交后应关联会议 ID、文件 ID、上传人和当前查看人。
5. 预览页不得缓存超出权限有效期的文件内容;退出登录、切换企业或权限被收回后必须清理本地可访问副本。
## 10. 个人资料、通用设置、隐私与青少年模式
### 10.1 我的.png
+66
View File
@@ -0,0 +1,66 @@
-- IM 企业入口注册与问题反馈 SQL
-- 更新时间:2026-08-24
--
-- 执行范围:hotgo 控制库。
-- 说明:未登录企业注册、入口问题反馈发生在租户绑定前,因此公共附件和申请单不放入租户业务库。
-- 文件对象通过 IM 服务端当前 media handler 存储;生产环境应配置 media.use_handler=oss。
USE hotgo;
CREATE TABLE IF NOT EXISTS im_public_uploads (
id BIGINT NOT NULL AUTO_INCREMENT,
file_id VARCHAR(64) NOT NULL,
purpose VARCHAR(64) NOT NULL,
mime_type VARCHAR(128) NOT NULL DEFAULT '',
size BIGINT NOT NULL DEFAULT 0,
etag VARCHAR(128) NOT NULL DEFAULT '',
location VARCHAR(2048) NOT NULL DEFAULT '',
expires_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
uploaded_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_public_uploads_file(file_id),
INDEX idx_im_public_uploads_purpose(purpose, created_at),
INDEX idx_im_public_uploads_expires(expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS im_tenant_applications (
id BIGINT NOT NULL AUTO_INCREMENT,
application_id VARCHAR(64) NOT NULL,
credit_code VARCHAR(18) NOT NULL,
enterprise_email VARCHAR(255) NOT NULL DEFAULT '',
license_file_id VARCHAR(64) NOT NULL DEFAULT '',
tenant_code_hint VARCHAR(64) NOT NULL DEFAULT '',
client_context JSON,
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
estimated_review_hour INT NOT NULL DEFAULT 24,
submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_tenant_applications_app(application_id),
UNIQUE INDEX uk_im_tenant_applications_idempotency(idempotency_key),
INDEX idx_im_tenant_applications_credit(credit_code),
INDEX idx_im_tenant_applications_status(status, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS im_tenant_entry_feedbacks (
id BIGINT NOT NULL AUTO_INCREMENT,
feedback_id VARCHAR(64) NOT NULL,
description TEXT NOT NULL,
contact VARCHAR(255) NOT NULL DEFAULT '',
tenant_code_hint VARCHAR(64) NOT NULL DEFAULT '',
screenshot_file_ids JSON,
client_context JSON,
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
submitted_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_tenant_entry_feedbacks_feedback(feedback_id),
UNIQUE INDEX uk_im_tenant_entry_feedbacks_idempotency(idempotency_key),
INDEX idx_im_tenant_entry_feedbacks_status(status, id),
INDEX idx_im_tenant_entry_feedbacks_hint(tenant_code_hint, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -0,0 +1,147 @@
# IM 会议笔记产品原型需求文档
> 本文档根据 `docs/note/` 中的 8 张会议笔记产品原型图整理。文档格式与 `IM-产品原型需求文档.md` 保持一致,后续可合并到第 9 章“企业工作台与办公协同”中,建议插入在总经理信箱之后、个人资料与设置之前。
## 9.14 会议笔记
会议笔记是企业工作台下的办公协同模块,用于围绕会议记录会议资料、纪要文件和相关附件。功能需要和会议通知、参会人、会议状态及文件能力联动,保证发起人、会议记录员、参会人和普通成员在不同权限下看到合适的入口、文件列表和操作按钮。
通知与刷新规则:
1. 工作台底部 Tab 的红点数字需要与工作台内可接收通知的模块上下对应。当前原型中的红点 `3` 表示工作台下共有 3 条未读通知,这 3 条通知只归属到“会议通知”模块。
2. 当前只有“会议通知”模块接收移动端通知,并承担会议创建、会议变更、会议提醒、参会反馈等通知展示。
3. 打卡、月报、日报、部门框架、举报信箱、会议笔记等其他工作台模块不做移动端通知,也不产生独立通知红点。
4. 除会议通知外,其他模块在用户进入模块时调用各自接口获取最新数据并刷新页面;客户端不得依赖通知事件来更新这些模块。
5. 会议笔记的文件新增、预览、下载状态只在会议笔记列表/详情页内刷新展示,不向工作台 Tab 追加通知数。
### 9.14.1 会议笔记-工作台入口.png
<img src="images/会议笔记-工作台入口.png" alt="会议笔记-工作台入口" width="360">
功能说明:
工作台新增“会议笔记”入口,与打卡、月报、日报、部门框架、举报信箱、会议通知等企业应用并列展示。入口点击后进入会议笔记列表页。
业务规则:
1. 会议笔记入口应由企业工作台配置和用户权限控制,未启用或无权限时不展示。
2. 工作台底部 Tab 红点只展示可通知模块的未读通知总数;当前只统计“会议通知”,不统计会议笔记、日报、月报、打卡、部门框架和举报信箱。
3. 入口排序、图标和名称应支持后台配置,但客户端需限制为白名单内部路由,不能跳转任意外部地址。
### 9.14.2 会议笔记-列表.png
<img src="images/会议笔记-列表.png" alt="会议笔记-列表" width="360">
功能说明:
会议笔记列表展示“与我有关”的会议记录,顶部提供会议搜索。每条会议卡片展示会议主题、发起人、会议时间、参会人、会议时长和会议状态,并提供“会议笔记”按钮进入详情;部分卡片支持“查看更多”查看完整会议信息或参会人信息。
业务规则:
1. “与我有关”仅展示当前用户作为发起人、会议记录员、参会人或具备组织查看权限的会议,且会议必须是已结束状态;未开始、进行中和已取消会议不进入会议笔记列表。
2. 列表按会议开始时间倒序展示,今天、昨天、前天等相对日期应由客户端按本地时区展示,排序仍以后端时间戳为准。
3. 搜索支持按会议主题、发起人、参会人和会议地点检索;搜索结果仍必须受会议可见权限限制。
4. 已结束但会议记录员尚未上传笔记的会议仍需要在会议笔记列表展示,卡片只展示会议基础信息和已结束状态,进入详情后展示“暂无笔记”空状态。
5. 已结束会议可查看有权限访问的会议笔记;会议记录员进入详情可上传文件,普通参会人进入详情不展示上传按钮且只能预览已上传文件。
6. 参会人过多时列表只展示前几名并省略,点击“查看更多”进入详情查看完整信息。
### 9.14.3 会议笔记-发布页面.png
<img src="images/会议笔记-发布页面.png" alt="会议笔记-发布页面" width="360">
功能说明:
会议发布页用于创建会议并指定会议记录员。页面包含发起人、地点、会议记录员、参会人、开始时间、时长、会议主题等字段,底部通过“发起会议”按钮提交。
业务规则:
1. 发起人默认取当前登录用户,不允许普通用户手动伪造;具备代发权限时应明确展示实际发起人和创建人。
2. 发布页面字段顺序中,会议记录员应位于参会人上方;会议记录员默认取会议发起人,用户可按权限改选其他成员。
3. 会议记录员、参会人、开始时间、时长和会议主题为必填项;地点可选。
4. 参会人可选择“所有人”或指定成员。选择所有人时应按组织权限和企业规模做确认提示,避免误发全员会议。
5. 会议记录员默认拥有上传、替换、删除本人上传文件和查看全部会议笔记的权限;普通参会人只能查看和预览已上传文件,不提供上传入口。
6. 创建成功后应进入会议详情或会议笔记列表;需要通知参会人时统一走“会议通知”模块,会议笔记模块本身不产生移动端通知。重复点击“发起会议”需防抖和幂等处理。
### 9.14.4 会议笔记-详情页.png
<img src="images/会议笔记-详情页.png" alt="会议笔记-详情页" width="360">
功能说明:
会议详情页展示会议主题、状态、发起人、时间、地点、会议记录员、参会人和会议时长。下方“全部笔记(文件)”展示会议相关文件,文件项包含类型图标、文件名、大小、上传人、上传日期和下载按钮。底部提供“上传文件”按钮。
业务规则:
1. 会议基础信息应与会议通知模块共用同一份会议数据,避免会议时间、参会人或状态在两个模块展示不一致。
2. 文件列表按上传时间倒序展示;同名文件允许存在,但应保留唯一文件 ID、上传人和上传时间。
3. 文件类型需识别 PDF、Office 文档、图片、压缩包等常见格式;未知格式展示通用文件图标。
4. “上传文件”按钮只对会议记录员展示;如后续扩展管理员代传能力,必须由服务端显式下发上传权限。
5. 当前用户是会议记录员时,详情页展示上传文件按钮;当前用户只是参会人时,不展示上传按钮,文件操作只保留预览能力。
6. 下载和预览都必须校验当前用户是否仍具备会议可见权限,不能仅凭文件 ID 访问。
### 9.14.5 会议笔记-详情页(普通成员).png
<img src="images/会议笔记-详情页(普通成员).png" alt="会议笔记-详情页(普通成员)" width="360">
功能说明:
普通成员或普通参会人进入会议笔记详情时,可查看会议基础信息;当没有可见笔记文件时,文件区域展示空状态“暂无笔记”。普通参会人页面不展示上传按钮,已上传文件仅提供预览入口。
业务规则:
1. 普通参会人只可查看对自己开放的会议笔记文件;无文件或无权限时展示统一空状态。
2. 空状态应区分“暂无笔记”和“无权限查看”两类业务原因,但默认文案可统一为不泄露权限细节的提示。
3. 普通参会人不能通过前端构造请求上传、删除、替换或下载文件;服务端必须再次校验角色和会议权限。
4. 若会议后续新增了对该成员可见的笔记文件,列表和详情应能刷新出新文件。
### 9.14.6 会议笔记-详情页-在线预览.png
<img src="images/会议笔记-详情页-在线预览.png" alt="会议笔记-详情页-在线预览" width="360">
功能说明:
当文件支持在线预览时,文件项右侧展示“预览”按钮,文件状态显示“在线预览”。点击后进入文件预览页;不支持预览的文件仍展示下载按钮。
业务规则:
1. 支持预览的文件类型至少包括 PDF 和常见图片,Office 文档是否预览取决于后端转换能力。
2. 预览状态由服务端返回,客户端不得仅按扩展名判断是否可预览。
3. 预览入口和下载入口都需要受文件权限控制;预览失败时应允许用户回到详情页并选择下载。
4. 预览文件应使用短期授权地址或受控文件接口,禁止暴露长期有效的公开下载链接。
### 9.14.7 会议笔记-详情页-文件下载中.png
<img src="images/会议笔记-详情页-文件下载中.png" alt="会议笔记-详情页-文件下载中" width="360">
功能说明:
文件下载过程中,当前文件项展示“正在下载”、进度条和“取消”按钮;其他文件仍保持可下载状态。
业务规则:
1. 每个文件需独立展示下载进度、成功、失败和取消状态。
2. 下载中允许取消,取消后应停止网络请求并恢复可下载状态;已完成文件可进入系统打开或本地文件管理流程。
3. 下载失败需展示可重试状态,并保留失败原因用于问题排查。
4. 后台、断网、弱网场景下应保存下载任务状态,恢复网络后按产品策略继续或提示重试。
### 9.14.8 会议笔记-文件预览.png
<img src="images/会议笔记-文件预览.png" alt="会议笔记-文件预览" width="360">
功能说明:
文件预览页展示文档内容,顶部提供返回、标题和更多菜单。预览区域展示页码进度,例如 `1/213`;更多菜单包含举报和分享操作。
业务规则:
1. 文件预览页应支持多页文档翻页、缩放、滑动浏览和加载失败重试。
2. 页码、加载状态和错误状态必须与当前文件一致,切换文件或返回详情页后应清理旧状态。
3. 分享权限由会议和文件权限共同决定;无分享权限时隐藏分享入口或提示无权限。
4. 举报入口用于举报文件内容或会议资料,提交后应关联会议 ID、文件 ID、上传人和当前查看人。
5. 预览页不得缓存超出权限有效期的文件内容;退出登录、切换企业或权限被收回后必须清理本地可访问副本。
## 合并建议
1. 合并到 `IM-产品原型需求文档.md` 时,建议将本章插入到第 9 章“企业工作台与办公协同”的 `9.13 总经理信箱-文案提示.png` 之后。
2. 如果主文档继续按图片顺序编号,可将本章编号调整为 `9.14` 至 `9.21`;如果按业务模块编号,可保留 `9.14.x` 子编号。
3. 本文档引用的图片已经复制到 `docs/images/`,文件名均以 `会议笔记-` 开头,避免与现有 `工作台.png` 等图片重名。
@@ -0,0 +1,181 @@
# IM 会议笔记前后端逐图对接协议
> 版本:v1.0
> 日期:2026-09-05
> 需求依据:[IM 会议笔记产品原型需求文档](./IM-会议笔记产品原型需求文档.md)
> 公共协议依据:[IM 前后端逐图对接协议](./IM-前后端对接文档.md)
> 说明:本文档的图片标题和顺序与会议笔记产品原型完全一致。后续合并时建议作为第 9 章的 `9.14 会议笔记`,原主文档 `9.14 接口示例` 顺延为 `9.15 接口示例`。
## 9.14 会议笔记
会议笔记属于企业工作台模块,复用会议主数据和统一文件能力。服务端负责判断会议是否结束、当前用户是否与会议有关、是否为会议记录员,以及文件查看和操作权限;客户端只根据响应中的状态与 `permissions` 展示页面,不自行推导权限。
本模块遵循主文档第 0 章的公共约定:产品 HTTP 使用 `X-MeetingIM-APIKey`、`X-IM-Schema: 1`、`X-Request-ID` 和 `Authorization: Bearer <token>`;写操作额外使用 `Idempotency-Key`;成功和失败响应均使用统一包裹结构。
会议笔记不产生移动端通知、不写入会议通知未读数,也不增加工作台底部 Tab 红点。客户端进入会议笔记列表或详情页时主动请求最新数据;从上传、预览或下载页面返回后,只刷新当前会议笔记页面。
### 9.14.1 会议笔记-工作台入口.png
<img src="images/会议笔记-工作台入口.png" alt="会议笔记-工作台入口" width="360">
**状态:工作台接口需改造。**
- `GET /v1/im/workbench` 的 `modules` 增加 `code="meeting_notes"`、`route="/meeting-notes"`。服务端按租户功能开关和当前用户权限决定是否返回该入口。
- 会议笔记模块固定返回 `unread_count=0`,不参与 `GET /v1/im/bootstrap` 的 `entry_badges.workbench_total_unread` 汇总。
- 当前工作台红点只对应“会议通知”模块的未读通知。日报、月报、打卡、部门框架、举报信箱和会议笔记均不产生工作台通知数。
- 客户端点击入口后请求 `GET /v1/im/meeting-notes`。切换企业后必须清空原租户的会议笔记缓存并重新加载。
### 9.14.2 会议笔记-列表.png
<img src="images/会议笔记-列表.png" alt="会议笔记-列表" width="360">
**状态:新增。**
- 列表:`GET /v1/im/meeting-notes?query=&cursor=&limit=20`。接口只返回当前用户作为发起人、会议记录员或参会人的已结束会议,不返回未开始、进行中或已取消会议。
- `query` 支持匹配会议主题、发起人、会议记录员、参会人和地点;搜索范围仍受当前用户会议可见权限限制。
- 列表按 `end_at` 倒序排列,分页使用不透明 `cursor`。客户端不得用本地时间过滤会议状态或自行扩大搜索范围。
- 已结束但尚未上传笔记的会议仍返回,使用 `note_status="empty"`、`file_count=0`;客户端展示会议基础信息,进入详情后显示“暂无笔记”。
- `permissions.can_view=true` 决定是否允许进入详情;`permissions.can_upload` 只对当前会议记录员为 `true`。普通参会人可以进入详情,但不显示上传按钮。
- `state="ended"` 是服务端按会议状态及服务端当前时间计算或持久化的有效状态。服务端必须以 `end_at` 和取消状态为准,不能信任客户端传入的“已结束”条件。
### 9.14.3 会议笔记-发布页面.png
<img src="images/会议笔记-发布页面.png" alt="会议笔记-发布页面" width="360">
**状态:现有会议创建接口需改造。**
- 页面字段顺序为发起人、地点、会议记录员、参会人、开始时间、时长、会议主题。会议记录员默认显示当前会议发起人。
- 创建仍使用 `POST /v1/im/meetings`,请求新增 `recorder_user_id`。客户端未传或传空字符串时,服务端默认使用当前登录用户;客户端不能通过 `organizer_id` 伪造发起人。
- 服务端校验会议记录员属于当前租户且账号有效。首版允许会议记录员是发起人或参会人;若选择了非参会人,服务端自动加入参会人列表并在响应中返回最终名单。
- 服务端保存的会议必须有 `recorder_user_id`;创建请求可省略该字段以使用发起人默认值。`attendee_user_ids`、`start_at`、`end_at` 和 `title` 为必填业务数据,`location` 可为空,且 `end_at` 必须晚于 `start_at`。
- 创建成功后的会议通知仍统一归属“会议通知”模块;会议笔记模块不额外生成通知或未读数。
- `PATCH /v1/im/meetings/{meeting_id}` 支持在会议结束前修改 `recorder_user_id`,并使用 `version` 做乐观锁。会议结束后返回 `MEETING_CLOSED`。
### 9.14.4 会议笔记-详情页.png
<img src="images/会议笔记-详情页.png" alt="会议笔记-详情页" width="360">
**状态:新增。**
- 详情:`GET /v1/im/meetings/{meeting_id}/notes`,一次返回会议基础信息、发起人、会议记录员、参会人、文件列表、笔记状态和当前用户权限。
- 会议基础信息与会议通知页面共用 `im_meetings` 和 `im_meeting_participants` 数据,不复制会议标题、时间和成员快照作为另一份可编辑主数据。
- 当前用户为会议记录员且会议已结束时,`permissions.can_upload=true`,客户端展示“上传文件”;其他用户不展示该按钮。
- 上传文件先调用 `POST /v0/file/u/`,Multipart 携带 `purpose=meeting_note` 和 `meeting_id`;上传成功取得 `file_id` 后,再调用 `POST /v1/im/meetings/{meeting_id}/notes/files` 建立会议笔记文件记录。
- 文件登记成功后才进入详情列表。上传成功但登记失败的临时文件不得展示,并由服务端按临时文件 TTL 回收。
- 当前会议记录员可替换或删除自己登记的文件;所有写操作均需 `Idempotency-Key`,服务端记录操作审计。
### 9.14.5 会议笔记-详情页(普通成员).png
<img src="images/会议笔记-详情页(普通成员).png" alt="会议笔记-详情页(普通成员)" width="360">
**状态:新增权限响应。**
- 普通参会人调用与 9.14.4 相同的详情接口。响应中 `permissions.can_view=true`、`can_upload=false`、`can_replace=false`、`can_delete=false`。
- 没有已上传文件时返回 `note_status="empty"` 和空数组 `files=[]`,客户端展示“暂无笔记”,不把空状态当作请求失败。
- 有文件时普通参会人只展示文件名、大小、类型、上传人、上传时间和预览入口,不展示上传、替换、删除按钮。
- 服务端必须在文件登记、替换、删除、预览和下载接口逐次校验会议关系及角色。隐藏客户端按钮不能代替服务端鉴权。
- 与会议无关、已离开可见范围或跨租户的用户统一返回 `RESOURCE_NOT_FOUND` 或 `FORBIDDEN`,不得通过 `meeting_id`、`note_file_id` 或 `file_id` 探测资源。
### 9.14.6 会议笔记-详情页-在线预览.png
<img src="images/会议笔记-详情页-在线预览.png" alt="会议笔记-详情页-在线预览" width="360">
**状态:基础文件读取已实现;会议笔记 ACL 和预览转换新增。**
- 文件项返回 `preview.status=ready|processing|unsupported|failed`、`preview.mode=native|converted|download_only` 和可选 `preview_file_id`,客户端根据状态选择直接预览、轮询转换状态或提示暂不支持。
- 原生可预览文件使用受认证的 `GET /v0/file/s/{file_id}`;转换预览使用 `GET /v0/file/s/{preview_file_id}`。两个接口都必须校验当前用户仍有会议笔记查看权限。
- 服务端不得返回长期有效的公共下载 URL。客户端缓存键至少包含 `tenant_code + user_id + file_id + etag`,退出登录或切换企业时清理受控缓存。
- `preview.status="processing"` 时客户端可在当前详情页低频刷新,建议 2 秒后开始、最长 30 秒;离开页面立即停止轮询。
### 9.14.7 会议笔记-详情页-文件下载中.png
<img src="images/会议笔记-详情页-文件下载中.png" alt="会议笔记-详情页-文件下载中" width="360">
**状态:复用统一下载任务;会议笔记 ACL 新增。**
- 下载使用 `GET /v0/file/s/{file_id}`,Header 传 API Key 与 `X-MeetingIM-Auth: token <base64_token>`,并支持 `Range` 和 `If-Range` 断点续传。
- 下载任务保存 `file_id,etag,total_bytes,downloaded_bytes,temp_path,state`。Token 更新后使用新 Token 续传,禁止把 Token 写入任务文件或日志。
- 服务端在每次下载及续传时重新校验会议可见权限。返回 `FILE_FORBIDDEN` 时客户端停止重试;返回 `FILE_UNAVAILABLE` 时标记文件失效并刷新详情。
- 下载进度属于客户端本地状态,不更新会议笔记业务状态、不产生工作台通知,也不写入未读数。
### 9.14.8 会议笔记-文件预览.png
<img src="images/会议笔记-文件预览.png" alt="会议笔记-文件预览" width="360">
**状态:客户端预览页;服务端复用 9.14.6 文件读取协议。**
- 预览页展示服务端返回的安全文件名和实际内容;标题不得直接信任未经清理的上传文件名。
- 图片、PDF、文本等受支持类型在应用内预览;Office 等类型优先使用受控转换文件,无法转换时按 `preview.mode="download_only"` 提示下载后打开。
- 文件被删除、审核拒绝、转换失败或权限撤销时,服务端返回稳定 `reason`,客户端关闭预览并刷新详情,不能继续显示旧的授权链接。
- 从预览页返回详情后调用 `GET /v1/im/meetings/{meeting_id}/notes` 获取最新文件状态。
### 9.14.9 接口示例
以下示例用于会议笔记 HTTP 联调快速查阅。成功响应统一包裹为 `{"code":0,"data":示例,"request_id":"...","server_time":"..."}`,下表只展示 `data`。
#### 9.14.9.1 工作台、会议记录员与会议笔记
| 接口 | 输入示例 | 成功 `data` 示例 |
| --- | --- | --- |
| `GET /v1/im/workbench` 需改造 | Query only | `{"layout_version":2,"modules":[{"code":"meeting_notifications","name":"会议通知","route":"/meetings","unread_count":3,"enabled":true,"permission":"enabled"},{"code":"meeting_notes","name":"会议笔记","route":"/meeting-notes","unread_count":0,"enabled":true,"permission":"enabled"}]}` |
| `POST /v1/im/meetings` 需改造 | Header `Idempotency-Key`;Body `{"title":"项目周会","start_at":"2026-09-05T10:00:00+08:00","end_at":"2026-09-05T10:30:00+08:00","location":"会议室 A","recorder_user_id":"usr_me","attendee_user_ids":["usr_2"],"reminder_minutes":[10]}` | `{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","state":"scheduled","participants":[{"user_id":"usr_me","response":"accepted"},{"user_id":"usr_2","response":"pending"}],"permissions":{"can_update":true,"can_cancel":true},"version":1}` |
| `PATCH /v1/im/meetings/{meeting_id}` 需改造 | Header `Idempotency-Key`;Body `{"recorder_user_id":"usr_2","version":1}` | `{"meeting_id":"mtg_1","recorder":{"user_id":"usr_2","display_name":"李四"},"version":2,"updated_at":"2026-09-05T01:30:00Z"}` |
| `GET /v1/im/meeting-notes?query=&cursor=&limit=20` 新增 | Query only | `{"items":[{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","location":"会议室 A","state":"ended","participant_count":2,"participant_names":["张三","李四"],"note_status":"empty","file_count":0,"permissions":{"can_view":true,"can_upload":true}}],"next_cursor":"","has_more":false}` |
| `GET /v1/im/meetings/{meeting_id}/notes` 新增 | Path meeting ID | `{"meeting":{"meeting_id":"mtg_1","title":"项目周会","organizer":{"user_id":"usr_me","display_name":"张三"},"recorder":{"user_id":"usr_me","display_name":"张三"},"start_at":"2026-09-05T02:00:00Z","end_at":"2026-09-05T02:30:00Z","location":"会议室 A","state":"ended","participants":[{"user_id":"usr_me","display_name":"张三"},{"user_id":"usr_2","display_name":"李四"}]},"note_status":"available","files":[{"note_file_id":"mnote_1","file_id":"file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf","uploader":{"user_id":"usr_me","display_name":"张三"},"created_at":"2026-09-05T03:00:00Z","version":1,"preview":{"status":"ready","mode":"native","preview_file_id":"file_1"},"permissions":{"can_preview":true,"can_download":true,"can_replace":true,"can_delete":true}}],"permissions":{"can_view":true,"can_upload":true},"version":1}` |
| `POST /v0/file/u/` | Multipart 文件;字段 `purpose=meeting_note`、`meeting_id=mtg_1`;Header `X-MeetingIM-Auth` | `{"ctrl":{"code":200,"params":{"file_id":"file_1","url":"/v0/file/s/file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf"}}}` |
| `POST /v1/im/meetings/{meeting_id}/notes/files` 新增 | Header `Idempotency-Key`;Body `{"file_id":"file_1"}` | `{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_1","name":"会议纪要.pdf","size":204800,"mime":"application/pdf","created_at":"2026-09-05T03:00:00Z","version":1,"preview":{"status":"processing","mode":"converted"}}` |
| `PUT /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}` 新增 | Header `Idempotency-Key`;Body `{"file_id":"file_2","version":1}` | `{"note_file_id":"mnote_1","meeting_id":"mtg_1","file_id":"file_2","name":"会议纪要-v2.pdf","version":2,"updated_at":"2026-09-05T03:10:00Z"}` |
| `DELETE /v1/im/meetings/{meeting_id}/notes/files/{note_file_id}` 新增 | Header `Idempotency-Key`;Query `version=2` | `{"note_file_id":"mnote_1","deleted":true,"deleted_at":"2026-09-05T03:20:00Z"}` |
| `GET /v0/file/s/{file_id}` | Header `X-MeetingIM-Auth`;可带 `Range: bytes=0-` | 返回文件字节流及 `Content-Type`、`Content-Length`、`ETag`、`Accept-Ranges`;失败返回 `FILE_FORBIDDEN` 或 `FILE_UNAVAILABLE` |
### 9.14.10 字段与状态约定
| 字段 | 取值 | 前端处理 |
| --- | --- | --- |
| `meeting.state` | `scheduled`、`in_progress`、`ended`、`canceled` | 会议笔记列表只展示 `ended` |
| `note_status` | `empty`、`available` | `empty` 展示“暂无笔记”,`available` 展示文件列表 |
| `preview.status` | `ready`、`processing`、`unsupported`、`failed` | 分别进入预览、短时刷新、仅下载提示、失败提示 |
| `preview.mode` | `native`、`converted`、`download_only` | 使用原文件、转换文件或仅提供下载 |
| `permissions.can_view` | Boolean | 是否可以查看当前会议笔记 |
| `permissions.can_upload` | Boolean | 是否显示并允许使用上传按钮,仅会议记录员为 `true` |
| `permissions.can_replace` | Boolean | 是否允许替换当前笔记文件 |
| `permissions.can_delete` | Boolean | 是否允许删除当前笔记文件 |
| `permissions.can_preview` | Boolean | 是否允许预览当前文件 |
| `permissions.can_download` | Boolean | 是否允许下载当前文件;首版普通参会人若产品要求“只能预览”,该值返回 `false` |
客户端必须以服务端返回的 `permissions` 为准。特别是普通参会人“只能预览”的要求,由文件项 `can_preview=true,can_download=false` 表达;即使预览底层使用文件读取接口,客户端也不得提供导出、系统分享或保存到相册入口。
### 9.14.11 错误码与客户端处理
| `reason` | 典型 HTTP 状态 | 场景 | 客户端处理 |
| --- | --- | --- | --- |
| `MEETING_NOT_ENDED` | 409 | 未结束会议上传或登记笔记 | 提示“会议结束后可上传笔记”,刷新会议详情 |
| `MEETING_CLOSED` | 409 | 会议结束后修改记录员或会议数据 | 切换为只读并刷新详情 |
| `NOT_MEETING_RECORDER` | 403 | 非记录员上传、替换或删除文件 | 隐藏操作入口并刷新权限 |
| `MEETING_NOTE_VERSION_CONFLICT` | 409 | 替换或删除时版本过期 | 拉取最新详情,不自动覆盖 |
| `MEETING_NOTE_FILE_INVALID` | 400 | 文件未上传完成、用途不符或不属于当前租户 | 提示重新上传 |
| `FILE_PREVIEW_PROCESSING` | 409 | 转换预览尚未完成 | 保持当前页并按退避策略刷新 |
| `FILE_PREVIEW_UNSUPPORTED` | 422 | 文件类型不支持在线预览 | 根据 `can_download` 决定是否提供下载 |
| `FILE_FORBIDDEN` | 403 | 当前用户无文件访问权限 | 关闭预览或停止下载并刷新详情 |
| `FILE_UNAVAILABLE` | 404/410 | 文件被删除、失效或审核拒绝 | 标记不可用并刷新文件列表 |
| `RESOURCE_NOT_FOUND` | 404 | 会议或笔记不可见、跨租户访问 | 返回列表,不提示资源是否真实存在 |
### 9.14.12 前后端联调与验收要点
1. 发起会议时不传 `recorder_user_id`,响应中的记录员必须默认为当前发起人;指定其他记录员时,响应返回最终记录员和参会人名单。
2. 会议笔记列表只包含与当前用户有关的已结束会议;已结束但无文件的会议仍展示,详情为空状态。
3. 记录员进入详情显示上传按钮,普通参会人进入同一会议不显示上传按钮;直接调用写接口仍必须被服务端拒绝。
4. 普通参会人只允许在线预览时,响应为 `can_preview=true,can_download=false`,客户端不展示下载、分享或导出入口。
5. 文件上传成功但会议笔记登记失败时,详情列表不出现该文件;重复登记同一幂等键返回首次结果。
6. 文件替换和删除必须校验 `version`;旧版本请求返回冲突,不能覆盖新文件。
7. 进入列表、进入详情以及从上传/预览/下载页面返回时均主动刷新;断开 WebSocket 不影响会议笔记数据获取。
8. 会议笔记新增、替换、删除均不产生 Push、会议通知未读或工作台红点;工作台红点只随会议通知模块变化。
9. 跨租户、无关用户和已撤销权限用户无法通过会议 ID 或文件 ID 查看、预览、下载会议笔记。
10. 切换企业或退出登录后,会议笔记列表、详情、预览文件和下载临时数据按租户及账号隔离清理。
## 合并建议
1. 将本文 `9.14 会议笔记` 整段插入现有《IM 前后端逐图对接协议》第 9 章的 `9.13 总经理信箱-文案提示.png` 之后。
2. 将现有 `9.14 接口示例` 及其子章节顺延为 `9.15`,并把本文 `9.14.9.1` 的接口行合并到新的第 9 章接口示例表中。
3. 同步扩展会议数据模型:`im_meetings` 增加会议记录员字段,并新增会议笔记文件关联表;具体表结构在系统设计和数据库变更文档中统一维护。
4. 主文档第 9 章现有会议创建示例需要补充 `recorder_user_id`,工作台示例需要增加 `meeting_notes` 模块并明确其 `unread_count=0`。
File diff suppressed because it is too large Load Diff
+227
View File
@@ -0,0 +1,227 @@
# IM 剩余问题修复方案
整理时间:2026-09-18
本文记录当前暂未完成的三个后端问题:青少年模式业务限制、Web 平台版本检查、消息搜索召回稳定性。
更新记录:
- 2026-09-18:P1 第一阶段已处理 Web 平台版本检查与消息搜索召回兜底;青少年模式已完成第一阶段策略层、夜间通话限制和消息内容过滤接入,剩余支付守护、好友/群邀请、Push 等全链路限制继续按 P2 增强。
## 1. 青少年模式业务限制未强制执行
### 现状
当前后端已经具备青少年模式的基础状态能力:
- `GET /v1/im/settings/youth-mode` 可返回 `enabled`、`policy_version`、`restrictions`、`restriction_items`。
- `POST /v1/im/settings/youth-mode/enable` 和 `disable` 已完成开启、关闭和独立密码校验链路。
- `im_youth_mode_settings` 已保存用户维度的启用状态、策略版本、限制项和密码状态。
第一阶段已将 `night_quiet` 接入音视频呼叫发起限制,将 `content_filter` 接入消息发送前内容安全审核,并把这两项 `restriction_items[].enforced` 改为 `true`。`payment_guard` 以及好友申请、群邀请、系统通知/Push 等更大范围入口仍未完整接入。
### 问题影响
如果只保存开关、不做服务端限制,客户端隐藏入口不能形成安全边界。用户可以通过旧客户端、重新安装、直接调用接口或构造 WebSocket 消息绕过限制。
### 建议修复方案
建议将青少年模式拆成一个统一策略检查层,例如 `imYouthPolicy` 或 `impolicy/youth`,所有关键入口调用同一套判断逻辑。
第一阶段建议先落这几个限制:
| 限制项 | 建议拦截点 | 修复方式 |
| --- | --- | --- |
| `night_quiet` 深夜免打扰 | 发消息、音视频呼叫、系统通知/Push | 服务端读取用户青少年状态和当前时间段,限制指定时段内发起呼叫或降低通知等级。 |
| `content_filter` 内容过滤 | 发文本、发链接、上传后发布文件、资料/群资料修改 | 复用现有内容安全/风险审核能力;命中高风险时拒绝或进入审核状态。 |
| `payment_guard` 支付守护 | 充值、购买、付费功能入口 | 当前 IM 暂无支付主链路,先预留统一错误码和策略接口,后续支付接口接入时强制调用。 |
建议新增统一返回:
```json
{
"code": 30005,
"reason": "YOUTH_MODE_RESTRICTED",
"message": "restricted by youth mode",
"details": {
"restriction": "night_quiet",
"policy_version": 1
}
}
```
### 建议改造点
- `server/im_http.go`
- HTTP 发消息、转发、收藏转发、好友申请、群邀请、音视频相关接口前置检查。
- `youthModeRestrictionItems` 中已真正接入的规则将 `enforced` 改为 `true`。
- `server/topic.go`
- 原生 WebSocket `{pub}` 保存消息前执行相同策略,避免绕过 HTTP。
- `server/store/store.go`
- 增加按用户读取青少年模式的轻量缓存,短 TTL 即可。
- `docs/IM-青少年模式前后端对接文档.md`
- 补充哪些接口会返回 `YOUTH_MODE_RESTRICTED`。
### 验收建议
- 开启青少年模式后,旧客户端直接发消息不能绕过限制。
- HTTP 和 WebSocket 两条链路限制结果一致。
- 关闭青少年模式后限制即时解除。
- `restriction_items[].enforced` 只对已经服务端强制执行的规则返回 `true`。
### 当前处理状态
已新增统一策略判断 `imYouthModePolicyDecision`。当前行为:
- `night_quiet`:青少年模式开启且带该限制时,22:00-06:00 发起音视频呼叫返回 `YOUTH_MODE_RESTRICTED`。
- `content_filter`:普通 WS 消息发送前进入内容安全审核,文本、文件名、链接 URL 等可展示内容会参与提取。
- `payment_guard`:仍为预留项,返回 `enforced=false`,后续支付或付费功能接入时再强制调用。
## 2. Web 平台版本检查接口缺失
### 现状
当前版本检查接口为:
```http
GET /v1/im/app/releases/latest?platform=ios|android&current_version=1.0.0&channel=stable
```
当前实现和文档主要覆盖 `ios`、`android`。Web 客户端如果调用 `platform=web`,会因为平台枚举未支持而无法得到可用版本配置。
### 问题影响
Web 端无法统一读取版本策略、升级提示、最低支持版本和发布说明。后续如果 Web 端需要灰度、强刷、维护提示或版本兼容控制,只能靠前端硬编码。
### 建议修复方案
将 `web` 加入 App Release 平台枚举。平台建议统一为:
```text
ios, android, web
```
Web 响应建议保持与移动端同结构:
```json
{
"current_version": "1.0.0",
"has_update": false,
"force_update": false,
"platform": "web",
"channel": "stable",
"latest_version": "1.0.0",
"min_supported_version": "1.0.0",
"update_type": "none",
"package_url": "",
"store_url": "",
"release_notes": "当前已是最新版本。",
"remind_after_seconds": 0
}
```
Web 没有安装包时,`package_url` 和 `store_url` 可以为空;如果需要强刷或维护提示,可通过 `update_type=required` 和 `release_notes` 传达。
### 建议改造点
- `server/im_http.go`
- 放开 `platform=web` 参数校验。
- 保持 `current_version` 必传。
- `server/db/mysql/adapter.go` 或 App Release 相关存储实现
- 确认 `im_app_releases.platform` 没有限制 `web`。
- 如数据库通过字典/配置初始化版本记录,需要补默认 Web 记录。
- `docs/IM-App版本更新前后端对接文档.md`
- 补充 Web 平台示例。
- `docs/IM-前后端对接文档.md`
- 将 `platform=ios|android` 更新为 `ios|android|web`。
### 验收建议
- `platform=web&current_version=1.0.0&channel=stable` 返回 200。
- `platform=invalid` 仍返回参数错误。
- Web 无更新、推荐更新、强制更新三类数据都能按同一结构返回。
### 当前处理状态
已在 `serveIMAppReleaseLatest` 放开 `platform=web`,并补充 Web 平台单元测试。对接文档已同步更新为 `ios|android|web`。
## 3. 消息搜索召回稳定性待复核
### 现状
当前接口为:
```http
GET /v1/im/topics/{topic}/messages/search?q=&type=text&cursor=
GET /v1/im/topics/{topic}/messages/search?type=file&q=&cursor=
```
现有后端已经能返回 `seq`、`from`、`ts`、`snippet`、`matches`、`snapshot`、`availability` 等字段。但问题清单中仍记录了“部分明显应该命中的关键词搜索返回空结果”的现象。该问题目前属于待复核项,可能来源包括:
- 搜索范围只覆盖部分消息内容字段,没有覆盖所有文本来源。
- 结构化消息中正文可能存在于 `content.text`、`content.txt`、`content.title`、`content.summary`、Drafty 或文件快照字段中,索引/过滤口径不一致。
- 中文分词、大小写、空格、标点归一化不一致。
- 分页游标与过滤顺序不一致,导致命中项被跳过。
- 热消息、分表消息和历史消息的查询路径不一致。
### 问题影响
搜索结果不稳定会直接影响聊天记录检索、文件检索、收藏定位和问题复盘。用户明知道某条消息存在,但搜索不到,会被认为是数据丢失。
### 建议修复方案
第一阶段先做“确定性兜底”,不急于引入复杂搜索引擎:
1. 明确搜索字段规范。
- 文本消息:统一提取 `content.text`、`content.txt`、`content.content`、`content.title`、`content.summary`、字符串 content。
- 文件消息:统一提取文件名、mime、文件说明、消息文本。
- 链接/名片/合并转发:提取标题、摘要、展示文本。
2. 增加 `message_search_text` 生成函数。
- 写入消息时生成可搜索文本。
- 老数据可查询时动态 fallback。
3. 查询顺序固定。
- 先按用户权限和 topic 过滤可见消息。
- 再按搜索文本匹配。
- 最后按 `seq desc` 或 `created_at desc` 分页。
4. 中文和符号归一化。
- 至少做大小写归一、前后空格、连续空白压缩、全角半角基础处理。
5. 增加回归测试。
- 普通文本、中文关键词、文件名、链接标题、转发快照、撤回/删除消息、分页边界都要覆盖。
第二阶段如果数据量和体验要求更高,再考虑引入专用搜索索引:
- MySQL FULLTEXT/ngram。
- OpenSearch/Elasticsearch。
- 独立 `im_message_search_index` 表,按 topic、seq、search_text 存储轻量索引。
### 建议改造点
- `server/im_http.go`
- `messageText` 和 `messageSearchPayload` 的提取逻辑继续收口,保证响应与查询使用同一文本口径。
- `server/db/mysql/adapter.go` 或消息查询实现
- 检查 `TenantMessageSearch` 的 SQL 过滤条件和分页顺序。
- 确认群消息分表 `im_group_messages_YYYYMM` 与私聊消息表搜索路径一致。
- `server/store/types`
- 如新增搜索索引表,补结构体和迁移 SQL。
- `docs/IM-前后端对接文档.md`
- 明确搜索匹配字段、排序规则和 `availability` 行为。
### 验收建议
- 同一 topic 下创建 20 条混合消息,逐条用关键词可搜到。
- 搜索中文短词、英文大小写、文件名、链接标题均有稳定结果。
- 翻页时不丢边界记录、不重复。
- 被删除/撤回消息按 `availability` 返回或按产品规则隐藏,行为一致。
- 私聊和群聊搜索结果字段一致。
### 当前处理状态
已完成第一阶段确定性兜底:私聊和群聊搜索不再只依赖 SQL `LIKE` 命中消息 JSON 原文,改为先按权限、Topic、类型和游标读取固定窗口,再对结构化文本做统一提取和归一化匹配。当前覆盖文本、文件名、标题、摘要、链接、卡片和转发快照等字段,并补充结构化内容召回测试。
## 建议排期
| 优先级 | 项目 | 原因 |
| --- | --- | --- |
| P1 | Web 平台版本检查 | 改动小,边界清楚,适合快速补齐。 |
| P1 | 消息搜索召回复核 | 影响用户感知,需要先补测试锁定问题。 |
| P2 | 青少年模式业务限制 | 涉及多个入口和策略口径,需要独立设计和联调,不能只改一个接口。 |
@@ -0,0 +1,256 @@
# IM 原生 Cluster 与 Redis Fanout 职责边界
## 1. 背景
IM Server 当前同时存在两套跨 Pod 能力:
- Tinode 原生 Cluster:以 Topic master 为中心,负责把 Topic 请求路由到唯一 master 节点,由 master 统一处理 Topic 状态机。
- Redis Fanout:以 Redis Pub/Sub 为跨 Pod 实时总线,负责把服务端生成的实时事件广播到其他 Pod,再由各 Pod 投递给本机 WebSocket session。
测试环境目前是多 Pod standalone 部署,日志表现为 `Cluster: running as a standalone server.`,因此当前实际跨 Pod 主链路是 Redis Fanout。目标架构需要启用原生 Cluster,把 Topic 热路径、强顺序状态机和实时通话状态迁回 Topic master;Redis Fanout 保留为大数据量、缓存、补偿和用户级控制能力。
本文定义两套机制的职责边界,避免后续同时启用后出现重复投递、重复状态变更或状态源不一致。
## 2. 总原则
1. 同一类 Topic 事件只能有一个实时主链路。
2. 原生 Cluster 启用时,Topic 状态机相关事件优先由 Cluster 处理。
3. Redis Fanout 不作为 Topic master 的替代品;它是跨 Pod 投递、通知和 session control 总线。
4. 关键业务状态必须以 MySQL、Redis 原子状态或 Kafka 顺序消费作为权威,WebSocket fanout 只负责实时通知。
5. 可丢的临时状态可以只走 fanout;不可丢的状态必须有历史补拉或条件更新兜底。
6. 能利用原生 Cluster 内存态、高性能、强一致和强顺序的 Topic 热路径,应优先交给原生 Cluster。
7. 数据量大、需要广播、缓存、聚合、补偿或跨业务模块共享的数据,应优先交给 Redis/MySQL/Kafka。
## 3. 分工原则
两套机制不是互斥关系,而是按数据特征分工:
```text
强状态、强顺序、小范围热路径 -> 原生 Cluster
大数据量、跨节点广播、缓存、补偿 -> Redis / MySQL / Kafka
```
### 3.1 优先使用原生 Cluster 的场景
以下场景应尽量使用原生 Cluster,发挥 Topic master 的内存态和串行状态机能力:
- 单个 Topic 内的订阅、离开、权限变更和在线 session 状态。
- 单聊/群聊 Topic 内的 `pub`、`read`、`recv`、`get/set meta`。
- 通话实时状态机:`ringing`、`accept`、`hang-up`、`offer/answer/ice-candidate`。
- 对顺序敏感的 Topic 内临时事件,例如正在输入、同 Topic 内 presence 派发。
- 需要立即反馈客户端、且依赖 Topic 当前内存状态的操作。
这些能力的共同特点是:
- 数据范围集中在单个 Topic。
- 需要强顺序或强一致。
- 更适合由唯一 Topic master 串行处理。
- 不适合广播给所有 Pod 后各自判断。
### 3.2 优先使用 Redis / MySQL / Kafka 的场景
以下场景应优先使用 Redis、MySQL 或 Kafka,不强行塞进原生 Cluster:
- 全局用户在线状态查询、好友列表在线状态展示。
- 群成员在线节点集合、大群在线成员筛选。
- 大群 fanout、热点消息缓存、未读聚合、离线补偿。
- 用户级 session control:踢端、全端下线、同平台挤旧端、密码重置清退。
- 限流、短状态、幂等、分布式去重。
- 历史消息、通话记录、审计、后台统计。
- Push、文件安全、后台管理命令等异步任务。
这些能力的共同特点是:
- 数据量大或跨 Topic。
- 需要被 HTTP、Kafka、后台任务等多入口共享。
- 可以最终一致,或可以通过历史补拉/条件更新补偿。
- 不适合放在某个 Topic master 的内存里长期维护。
## 4. 目标启用模式
目标模式为:
```text
原生 Cluster Topic master + Redis 大数据量支撑 + MySQL/Kafka 权威状态
```
目标分工:
- 原生 Cluster 负责单个 Topic 内的实时请求路由、内存状态机、强顺序处理和低延迟下发。
- Redis 负责全局在线状态、大群在线成员集合、热点缓存、大数据量 fanout、限流、幂等和 session control。
- MySQL 负责最终历史、通话记录、关系状态和可补偿查询。
- Kafka 负责后台异步任务、Push、审计、文件安全和跨服务事件。
启用原生 Cluster 后的核心原则:
```text
Topic 内强状态、强顺序、小范围热路径,必须优先走原生 Cluster
跨 Topic、大数据量、可最终一致或需补偿的数据,继续走 Redis/MySQL/Kafka
```
当前 standalone + Redis Fanout 是过渡状态。切换到原生 Cluster 前,需要先完成部署形态、职责收敛、重复链路关闭和压测验收。
## 5. 职责边界表
| 能力 | 当前 standalone + Redis Fanout 过渡模式 | 目标原生 Cluster 模式 | 说明 |
| --- | --- | --- | --- |
| Topic master 路由 | 不启用 | Cluster 负责 | 原生 Cluster 通过一致性哈希确定 Topic master。 |
| `{sub}` 订阅 | 本 Pod Hub/Topic 处理,跨 Pod 投递依赖 Redis | Cluster `ProxyReqJoin` | 启 Cluster 后不要再用 Redis 模拟订阅路由。 |
| `{leave}` 离开 | 本 Pod 处理 | Cluster `ProxyReqLeave` | 与订阅同源。 |
| `{pub}` 消息写入 | 本 Pod 处理,生成 data 后 Redis fanout | Cluster `ProxyReqBroadcast` 到 Topic master | 启 Cluster 后 Topic data 不应再额外 Redis topic fanout。 |
| `{get/set}` meta | 本 Pod 处理 | Cluster `ProxyReqMeta` | 强依赖 Topic 状态时应由 master 处理。 |
| `read/recv` 回执 | Redis topic/user fanout 下发结果 | Cluster 路由 note 到 master | 启 Cluster 后回执状态由 master 生成,避免双份 info。 |
| 通话 `ringing/accept/hang-up` | Redis `call_event` fanout + DB 条件更新兜底 | Cluster `ProxyReqCall` | 启 Cluster 后 Redis `call_event` 必须跳过。 |
| `offer/answer/ice-candidate` | 当前可由现有通话状态机处理;声网模式下不走 SDP/ICE | Cluster 由 Topic master 转发 | 首版声网主要关注 accept/hang-up。 |
| Topic 内 data/info/pres 下发 | Redis realtime fanout | Cluster proxy response | 两者不能同时作为主链路。 |
| 用户在线状态查询 | Redis presence | 可继续 Redis presence | 即使启 Cluster,也建议 Redis 保持全局在线查询缓存。 |
| 群在线成员缓存 | Redis topic member presence | 可继续 Redis 缓存 | 用于大群本节点在线成员查找和展示。 |
| 踢端/全端下线 | Redis session control | Redis session control | 用户级控制不属于单个 Topic,建议继续 Redis。 |
| 密码重置清退 | Redis session control | Redis session control | 与 Cluster 不冲突。 |
| 同平台新登录挤旧端 | 本 Pod + Redis platform evict | 本 Pod + Redis platform evict | 继续用 Redis 保证跨 Pod 生效。 |
| 好友/群业务事件 | 本地投递 + Redis user/topic fanout | 可继续 Redis user fanout | 业务事件通常不是 Topic 状态机请求。 |
| 系统消息通知 | 本地投递 + Redis fanout | 视消息写入入口决定 | 如果写入会话 Topic,应避免和 Cluster topic 下发重复。 |
| 群消息 seq/热消息缓存 | Redis / MySQL | 可继续 Redis / MySQL | 大数据量缓存和历史权威不依赖原生 Cluster。 |
| Kafka 后台事件 | Kafka 负责异步持久化、Push、审计 | Kafka 继续负责 | Kafka 不做实时 WS fanout 主链路。 |
## 6. Redis Fanout 主链路范围
在当前 standalone 过渡模式下,Redis Fanout 是跨 Pod 实时投递主链路。启用原生 Cluster 后,Redis Fanout 需要收敛为配套能力,负责:
1. `message/user`:用户维度的系统事件、业务事件、me presence 下发。
2. `session_control/user|device|platform`:踢端、清退、同平台挤旧端。
3. presence 缓存:用户在线状态、节点 session 信息、群成员在线节点信息。
4. 大群在线成员筛选、热点消息缓存、未读聚合和历史补偿辅助。
5. 限流、短状态、幂等、分布式去重。
仅在 standalone 过渡模式下,Redis Fanout 额外负责:
1. `message/topic`:Topic 维度的 `data`、`info`、`pres` 跨 Pod下发。
2. `call_event/topic`:通话 `ringing`、`accept`、`hang-up` 进入 Topic 状态机。
这些 Topic 状态机相关 fanout 在原生 Cluster 启用后必须关闭或跳过。
Redis Fanout 不负责:
- 作为消息历史权威。
- 作为通话最终状态权威。
- 保证同一 Topic 的全局严格顺序。
- 替代 MySQL/Kafka 的审计、补偿和持久化。
## 7. 原生 Cluster 主链路范围
启用原生 Cluster 后,它应负责:
1. Topic owner/master 分配。
2. Topic 请求路由:`sub`、`leave`、`pub`、`get/set`、`note`。
3. Topic 内状态机:订阅、权限、回执、通话、presence 派发。
4. Topic master 到 proxy session 的响应和广播。
5. 节点故障后的 proxy session 清理与订阅失效通知。
原生 Cluster 不建议负责:
- 用户级 session control。
- 全局在线状态查询缓存。
- 业务后台事件可靠投递。
- Kafka 异步消费、Push、审计。
- Redis 热消息缓存、限流、短状态。
- 大群全量成员 fanout、全局未读聚合、历史数据扫描等大数据量任务。
## 8. 双开时必须避免的重复链路
如果 `globals.cluster != nil`,以下 Redis Fanout 应跳过或降级:
| Redis 入口 | Cluster 启用时处理 |
| --- | --- |
| `publishRealtimeTopicMessage` | 跳过。Topic data/info/pres 由 Cluster proxy response 下发。 |
| `publishRealtimeCallEvent` | 跳过。通话 note 由 `ProxyReqCall` 路由到 Topic master。 |
| 群消息在线投递 fanout | 需要专项确认;如果同一 data 已由 Cluster 下发,不得再广播。 |
| 会话 Topic 系统消息 fanout | 如果消息进入 Topic 状态机,应避免再次 topic fanout。 |
以下 Redis Fanout 可继续保留:
| Redis 入口 | 保留原因 |
| --- | --- |
| `publishRealtimeEvictUser` | 用户级清退,不归属单一 Topic。 |
| `publishRealtimeEvictDevice` | 设备级清退,不归属单一 Topic。 |
| `publishRealtimeEvictUserPlatform` | 同平台挤旧端,需要跨 Pod 控制 session。 |
| 用户 presence 缓存 | 全局在线查询和好友列表展示仍需要。 |
| 业务 user event | 多数业务事件不是 Topic 状态机请求。 |
## 9. 状态权威与顺序规则
### 9.1 消息顺序
- 权威顺序来自 `topic + seq`。
- Redis Fanout 到达乱序时,客户端必须按 seq 去重和重排。
- 服务端历史读取按 seq 返回,作为重连补偿来源。
### 9.2 通话状态
通话状态必须使用条件更新保证单向流转:
```text
started -> accepted -> finished
started -> missed
started -> declined
accepted -> disconnected
```
规则:
- `accept` 只能从 `started` 写入 `accepted`。
- `missed` 只能从 `started` 且 `accepted_at IS NULL` 写入。
- `finished` 只能结束未结束通话。
- 已 `accepted` 的通话不得被 `missed` 或 `declined` 覆盖。
- WebSocket 下发失败不能改变最终状态,客户端可通过通话记录或会话消息补偿。
### 9.3 Session Control
session control 以 Redis 短状态和 token/auth_version 为兜底:
- Redis 实时踢端用于立即关闭在线 WS。
- token/auth_version 用于防止旧 token 重新登录。
- stale session-control fanout 必须丢弃,避免旧事件踢掉新 session。
## 10. 启用原生 Cluster 的前置条件
启用原生 Cluster 前必须满足:
1. K8s 部署从普通 Deployment 改为适合稳定网络身份的形态,例如 StatefulSet 或稳定 Pod DNS。
2. 每个 Pod 配置唯一且稳定的 `cluster_self`。
3. 配置完整 `cluster_config.nodes`,包括节点名和可达地址。
4. 开放并验证 gRPC/cluster 节点间通信。
5. 明确 Redis Fanout 哪些保留、哪些关闭。
6. 压测 `sub/pub/read/recv/call/group` 跨节点链路。
7. 验证节点重启、扩缩容、Topic rehash、proxy subscription 失效和客户端重订阅。
8. 验证不会重复投递 data/info/pres/call event。
## 11. 观测与验收
必须保留以下观测指标:
- Redis fanout publish/receive/deliver 计数。
- Redis fanout `skip_native_cluster` 计数。
- Redis fanout `no_local_topic`、`no_local_session`、`hub_full` 计数。
- 通话状态转换计数:started、accepted、finished、missed、declined。
- DB 条件更新失败计数,用于识别迟到事件和乱序事件。
- Cluster 模式下 proxy queue、TopicMaster RPC 失败、cluster unreachable、rehash 次数。
验收用例至少覆盖:
1. A、B 连接到不同 Pod,P2P 消息实时到达。
2. 接收方未订阅当前会话,仅在线 me,能收到未读提醒或补偿。
3. 通话 accept/hang-up 跨 Pod 到达,且不会被 missed 覆盖。
4. 多端登录同一账号,所有端收到业务事件。
5. 密码重置或后台踢端能清退所有 Pod 上的 session。
6. 大群在线成员分布在多 Pod,消息只投递在线成员,离线靠 seq 补拉。
7. Redis 短暂不可用时,关键状态不写坏,客户端可补偿。
## 12. 当前结论
当前阶段职责划分如下:
- 当前测试环境仍是 standalone + Redis Fanout,这是过渡状态。
- 目标是启用原生 Cluster,让 Topic master 承担内存高性能、强一致、强顺序的实时状态机。
- Redis 不退出架构,而是转为大数据量、缓存、补偿、全局 presence、session control 和异步支撑能力。
- 启用原生 Cluster 时,Topic 状态机相关的 Redis topic/call fanout 必须关闭或降级,避免 data/info/pres/call event 双路投递。
+458
View File
@@ -0,0 +1,458 @@
# IM 原生 Cluster 运维部署说明
## 1. 目标
本文给运维说明 IM Server 如何部署才能真正启用 Tinode 原生 Cluster。
目标效果:
- 多个 IM Pod 组成同一个 Tinode Cluster,不再是 standalone。
- 每个 Pod 拥有稳定、唯一的节点名。
- 节点之间通过 `cluster_config.nodes.addr` 建立内部 TCP RPC 通信。
- Topic 请求可以路由到唯一 Topic master,由 master 处理 Topic 内状态机、顺序和实时下发。
如果部署后日志仍出现:
```text
Cluster: running as a standalone server.
```
说明原生 Cluster 没有启用,跨 Pod Topic 状态机不会生效。
## 2. 原生 Cluster 启用条件
必须同时满足以下条件:
1. `cluster_self` 非空。
2. `cluster_self` 的值必须等于 `cluster_config.nodes` 中某个节点的 `name`。
3. `cluster_config.nodes` 必须包含所有 IM 节点,包括当前节点。
4. 每个节点的 `addr` 必须是其他 Pod 可访问的内部地址。
5. 每个节点必须监听自己的 `addr` 端口。
6. 节点之间网络策略、安全组、Service 必须放通 cluster 内部端口。
7. 至少 2 个节点才算有效 Cluster,生产建议 3 个节点起。
当前代码启动参数由 `docker/tinode/entrypoint.sh` 传入:
```bash
./tinode --config=${CONFIG} --static_data=$STATIC_DIR --cluster_self=$CLUSTER_SELF --pprof_url=$PPROF_URL
```
所以运维侧必须确保每个 Pod 的环境变量 `CLUSTER_SELF` 有值,并且值稳定。
## 3. 推荐部署形态
推荐使用 Kubernetes `StatefulSet`,不要用普通 `Deployment` 直接滚动多个副本。
原因:
- 原生 Cluster 依赖稳定节点名,例如 `im-server-0`、`im-server-1`、`im-server-2`。
- 原生 Cluster 依赖稳定 DNS,例如 `im-server-0.im-server-cluster.default.svc.cluster.local`。
- 普通 Deployment 的 Pod 名会变化,不适合作为 `cluster_self`。
- StatefulSet 配合 Headless Service 可以给每个 Pod 提供固定网络身份。
推荐结构:
```text
Headless Service: im-server-cluster
StatefulSet: im-server
Pod DNS:
im-server-0.im-server-cluster.<namespace>.svc.cluster.local
im-server-1.im-server-cluster.<namespace>.svc.cluster.local
im-server-2.im-server-cluster.<namespace>.svc.cluster.local
```
## 4. 端口规划
至少需要区分三类端口:
| 端口 | 用途 | 是否对公网暴露 | 说明 |
| --- | --- | --- | --- |
| `6060` | HTTP/WebSocket/API | 通过 Ingress/SLB 暴露 | 客户端连接入口。 |
| `16060` | Tinode gRPC 客户端端口 | 通常不对公网暴露 | 管理或内部 gRPC 客户端使用。 |
| `12000` | 原生 Cluster 内部 RPC | 不对公网暴露,只在 Pod 间放通 | `cluster_config.nodes.addr` 使用。 |
注意:`cluster_config.nodes.addr` 使用的是原生 Cluster 内部 TCP RPC 地址,不是 `grpc_listen` 的 `16060`。
建议统一所有 Pod 的 cluster 监听端口为 `12000`,通过 Pod DNS 区分节点:
```json
{
"name": "im-server-0",
"addr": "im-server-0.im-server-cluster.default.svc.cluster.local:12000"
}
```
不要沿用 docker compose 示例里的 `tinode-0:12000`、`tinode-1:12001`、`tinode-2:12002`,除非是在同一主机或 docker compose 网络里部署。
## 5. 配置文件要求
`server/tinode.conf` 默认配置是:
```json
"cluster_config": {
"self": "",
"nodes": [],
"failover": {
"enabled": false
}
}
```
这个配置不会启用 Cluster。
生产或测试环境需要提供完整配置。建议 3 节点配置如下:
```json
"cluster_config": {
"self": "",
"nodes": [
{
"name": "im-server-0",
"addr": "im-server-0.im-server-cluster.default.svc.cluster.local:12000"
},
{
"name": "im-server-1",
"addr": "im-server-1.im-server-cluster.default.svc.cluster.local:12000"
},
{
"name": "im-server-2",
"addr": "im-server-2.im-server-cluster.default.svc.cluster.local:12000"
}
],
"failover": {
"enabled": true,
"heartbeat": 100,
"vote_after": 8,
"node_fail_after": 16
}
}
```
说明:
- `self` 可以保持为空,由启动参数 `--cluster_self=$CLUSTER_SELF` 覆盖。
- `CLUSTER_SELF` 必须分别为 `im-server-0`、`im-server-1`、`im-server-2`。
- `nodes` 必须所有 Pod 完全一致。
- `addr` 必须能被其他 Pod 访问。
- 3 节点建议开启 failover;2 节点不建议开启 failover。
## 6. Kubernetes 示例
以下示例只展示 Cluster 相关关键配置,实际部署时需要合并现有镜像、环境变量、Secret、资源限制、探针、Ingress 和业务配置。
### 6.1 Headless Service
```yaml
apiVersion: v1
kind: Service
metadata:
name: im-server-cluster
namespace: default
spec:
clusterIP: None
selector:
app: im-server
ports:
- name: cluster
port: 12000
targetPort: 12000
- name: http
port: 6060
targetPort: 6060
- name: grpc
port: 16060
targetPort: 16060
```
### 6.2 客户端入口 Service
客户端入口可以继续使用普通 Service 或 Ingress,负载到所有 Pod:
```yaml
apiVersion: v1
kind: Service
metadata:
name: im-server
namespace: default
spec:
sessionAffinity: None
selector:
app: im-server
ports:
- name: http
port: 6060
targetPort: 6060
```
### 6.3 StatefulSet
```yaml
apiVersion: apps/v1
kind: StatefulSet
metadata:
name: im-server
namespace: default
spec:
serviceName: im-server-cluster
replicas: 3
selector:
matchLabels:
app: im-server
template:
metadata:
labels:
app: im-server
spec:
containers:
- name: im-server
image: <im-server-image>
ports:
- name: http
containerPort: 6060
- name: grpc
containerPort: 16060
- name: cluster
containerPort: 12000
env:
- name: CLUSTER_SELF
valueFrom:
fieldRef:
fieldPath: metadata.name
- name: EXT_CONFIG
value: /etc/tinode/tinode.conf
- name: CLUSTER_PROXY_TO_MASTER_BUFFER
value: "4096"
- name: CLUSTER_PROXY_TO_MASTER_WORKERS
value: "16"
- name: CLUSTER_PROXY_TO_MASTER_ENQUEUE_TIMEOUT_MS
value: "300"
- name: CLUSTER_ROUTE_RETRIES
value: "3"
- name: CLUSTER_ROUTE_RETRY_BACKOFF_MS
value: "25"
resources:
requests:
cpu: "1"
memory: 512Mi
limits:
cpu: "2"
memory: 1Gi
readinessProbe:
httpGet:
path: /healthz
port: 6060
timeoutSeconds: 3
livenessProbe:
httpGet:
path: /livez
port: 6060
timeoutSeconds: 3
volumeMounts:
- name: tinode-config
mountPath: /etc/tinode
readOnly: true
volumes:
- name: tinode-config
configMap:
name: im-server-tinode-conf
```
重点:
- `serviceName` 必须指向 Headless Service。
- `CLUSTER_SELF` 直接取 Pod 名,StatefulSet 下会稳定为 `im-server-0`、`im-server-1`、`im-server-2`。
- ConfigMap 中的 `cluster_config.nodes.name` 必须和 Pod 名一致。
- ConfigMap 中的 `cluster_config.nodes.addr` 建议使用 StatefulSet Pod DNS。
## 7. NetworkPolicy / 安全组要求
如果集群启用了 NetworkPolicy,需要允许 IM Pod 之间访问 cluster 端口:
```yaml
apiVersion: networking.k8s.io/v1
kind: NetworkPolicy
metadata:
name: im-server-cluster-allow
namespace: default
spec:
podSelector:
matchLabels:
app: im-server
policyTypes:
- Ingress
- Egress
ingress:
- from:
- podSelector:
matchLabels:
app: im-server
ports:
- protocol: TCP
port: 12000
egress:
- to:
- podSelector:
matchLabels:
app: im-server
ports:
- protocol: TCP
port: 12000
```
如果没有 NetworkPolicy,可以跳过此项,但仍要确认云厂商安全组、服务网格策略没有拦截 Pod 间 TCP `12000`。
## 8. 启动顺序
建议:
1. 先部署 ConfigMap、Headless Service、普通入口 Service。
2. 再部署 StatefulSet。
3. 第一个 Pod 可以执行数据库初始化和升级。
4. 其他 Pod 设置 `NO_DB_INIT=true`,避免多 Pod 同时初始化数据库。
5. 等所有 Pod Ready 后再接入客户端流量。
如果当前镜像沿用 `docker/tinode/entrypoint.sh` 的逻辑:
- 第一个 Pod 可以保留 `NO_DB_INIT=false`。
- 其他 Pod 建议设置 `NO_DB_INIT=true`。
- 如果所有 Pod 同时启动,要确认数据库初始化脚本具备幂等能力,否则建议改成 init Job 单独执行。
## 9. 发布步骤建议
### 9.1 灰度前检查
执行:
```bash
kubectl -n default get pod -l app=im-server -o wide
kubectl -n default get svc im-server-cluster
kubectl -n default exec im-server-0 -- sh -c 'echo $CLUSTER_SELF'
kubectl -n default exec im-server-1 -- sh -c 'echo $CLUSTER_SELF'
kubectl -n default exec im-server-2 -- sh -c 'echo $CLUSTER_SELF'
```
期望:
```text
im-server-0
im-server-1
im-server-2
```
验证节点 DNS:
```bash
kubectl -n default exec im-server-0 -- sh -c 'nc -vz im-server-1.im-server-cluster.default.svc.cluster.local 12000'
kubectl -n default exec im-server-0 -- sh -c 'nc -vz im-server-2.im-server-cluster.default.svc.cluster.local 12000'
```
### 9.2 启动后日志检查
查看日志:
```bash
kubectl -n default logs im-server-0 | grep -i cluster
kubectl -n default logs im-server-1 | grep -i cluster
kubectl -n default logs im-server-2 | grep -i cluster
```
不能出现:
```text
Cluster: running as a standalone server.
```
如果出现该日志,优先检查:
- `CLUSTER_SELF` 是否为空。
- `CLUSTER_SELF` 是否不在 `cluster_config.nodes.name` 中。
- 配置文件是否确实被容器读取。
- `EXT_CONFIG` 是否指向了正确文件。
- `cluster_config.nodes` 是否为空。
如果出现节点连接失败,优先检查:
- `cluster_config.nodes.addr` DNS 是否能解析。
- 端口 `12000` 是否监听。
- Pod 间 NetworkPolicy 是否放通。
- Service `targetPort` 是否正确。
### 9.3 功能验收
至少验证:
1. A、B 两个账号分别连到不同 Pod,P2P 消息实时互达。
2. A、B 分别连到不同 Pod,语音通话 `ringing`、`accept`、`hang-up` 状态实时互达。
3. 同一个账号多端连接不同 Pod,`recv/read` 回执正常。
4. 群聊成员分布在不同 Pod,群消息实时下发正常。
5. 重启任意一个 Pod 后,客户端能重连,Topic 可重新订阅。
6. 日志无持续 cluster unreachable、proxy timeout、dial timeout。
## 10. 与 Redis Fanout 的关系
启用原生 Cluster 后,Redis 仍然保留,但职责要收敛:
- 保留:全局 presence、session control、踢端、同平台挤旧端、大群在线缓存、热点缓存、限流、幂等、补偿。
- 收敛或关闭:Topic data/info/pres 的 Redis topic fanout。
- 收敛或关闭:通话 `call_event/topic` 的 Redis fanout。
否则会出现同一个 Topic 事件既由 Cluster 下发、又由 Redis Fanout 下发,导致重复 data/info/pres/call event。
对应职责边界参考:
- `docs/IM-原生Cluster与RedisFanout职责边界.md`
## 11. 常见问题
### 11.1 日志仍然是 standalone
原因通常是 `cluster_self` 为空或配置文件里的 `nodes` 为空。
处理:
1. 确认 Pod 环境变量 `CLUSTER_SELF`。
2. 确认启动参数包含 `--cluster_self=$CLUSTER_SELF`。
3. 确认容器读取的是带完整 `cluster_config.nodes` 的配置。
### 11.2 Pod 能启动,但跨 Pod 消息还是不通
原因通常是 cluster 内部端口不通,或节点地址写错。
处理:
1. 从任意 Pod `nc -vz <peer-pod-dns> 12000`。
2. 检查 Headless Service selector 是否匹配 Pod label。
3. 检查 NetworkPolicy 是否放通 egress/ingress。
4. 检查 `cluster_config.nodes.addr` 是否写成了只在 docker compose 里可用的短域名。
### 11.3 扩缩容后部分 Topic 异常
原生 Cluster 的 Topic master 基于节点集合计算。扩缩容会改变节点集合,可能触发 Topic 重新路由。
建议:
- 生产先固定 3 节点。
- 不要频繁自动扩缩容 IM Server。
- 扩缩容必须配合完整压测和重连验证。
- 如果要从 3 节点扩到 5 节点,先更新所有 Pod 的 `cluster_config.nodes`,再按发布计划滚动。
### 11.4 2 节点能不能用
2 节点可以启用基础 Cluster,但不建议开启 failover,也不建议生产使用。
生产推荐 3 节点起,便于 leader 心跳和故障判断。
## 12. 运维上线检查清单
- [ ] 使用 StatefulSet,不使用普通 Deployment 承载 Cluster 节点身份。
- [ ] 创建 Headless Service。
- [ ] 每个 Pod 的 `CLUSTER_SELF` 等于 Pod 名。
- [ ] `cluster_config.nodes` 包含所有 Pod。
- [ ] 所有 Pod 使用同一份 `cluster_config.nodes`。
- [ ] 每个 `nodes.addr` 可从其他 Pod 访问。
- [ ] 放通 cluster 内部 TCP 端口 `12000`。
- [ ] 普通 HTTP/WebSocket 入口仍通过 Service/Ingress 负载到所有 Pod。
- [ ] 只有一个入口负责数据库初始化,或数据库初始化已改为独立 Job。
- [ ] 启动日志不再出现 `Cluster: running as a standalone server.`。
- [ ] 跨 Pod P2P、群聊、read/recv、通话状态已验收。
- [ ] 原生 Cluster 启用后,Topic 相关 Redis Fanout 已按职责边界关闭或降级。
+6 -3
View File
@@ -223,10 +223,11 @@
1. 用户列表、搜索、详情。
2. 创建、启用、停用、离职、恢复用户。
3. 重置密码、撤销 Token、踢下线。
3. 重置密码、撤销 Token、踢下线;无手机号账号忘记密码时只能由当前租户管理员执行重置。
4. 查看登录设备、登录 Session、安全事件。
5. 查看用户实名状态、青少年状态、隐私设置摘要。
6. 支持同手机号在不同租户独立存在,本租户后台只看到本租户用户。
7. 支持注册邀请码单个/批量生成、有效期设置、撤销、明文精确查询和导出;邀请码固定为 5 位大小写英文字母,区分大小写且租户内不可重复。
建议接口:
@@ -244,8 +245,10 @@
验收点:
1. 停用用户不影响其他租户相同手机号或相同设备用户。
2. 重置密码必须递增 `auth_version` 并撤销旧 Token。
3. 查看敏感信息必须脱敏,证件号、手机号、Token 不写日志。
2. 管理员只能重置当前租户用户,不得根据用户名跨租户查找或操作。
3. 重置密码必须递增 `auth_version`,撤销旧 Token 和在线 Session,并写入包含操作者、目标用户、时间和结果的安全审计。
4. 查看敏感信息必须脱敏,证件号、手机号、Token 不写日志。
5. 邀请码明文查看、查询和导出必须校验独立权限并写审计;普通日志不得记录邀请码。
### 5.3 组织架构管理
+638
View File
@@ -0,0 +1,638 @@
# IM 增强项第一批开发
> 编写日期:2026-08-21
> 来源依据:`docs/IM-未实现后续增强项.md`、`docs/IM-前后端对接文档.md`、当前 `server/main.go` 和 `server/im_http.go` 路由实现。
> 目标:从后续增强项中筛选出当前可以直接开发、且不依赖短信服务商、FCM/APNs、TURN/STUN、SFU、病毒扫描服务等第三方能力的内容,作为第一批排期范围。
## 1. 筛选原则
第一批只纳入满足以下条件的增强项:
1. 只依赖当前 IM 服务、Tinode/IM 租户库、HotGo 控制库或已有 Redis/MySQL 基础设施。
2. 不依赖真实短信供应商、离线推送供应商、音视频媒体服务、病毒扫描引擎等第三方服务。
3. 优先补齐前后端对接文档中已定义,但当前代码未实现或实现不完整的接口。
4. 优先选择能直接提升前端联调完整度、生产可用性和接口一致性的项目。
以下内容不进入第一批:
1. 真实短信发送、短信模板、供应商回执和供应商失败重试。
2. FCM/APNs 离线 Push 投递和供应商错误码处理。
3. TURN/STUN 实际服务接入、SFU、群音视频媒体房间。
4. 第三方病毒扫描或文件内容安全服务。
5. 必须依赖 HotGo 管理后台页面完整改造才能闭环的运营后台功能。
## 2. 第一批开发范围总览
### 2.1 当前开发进度
> 进度更新:2026-08-21
| 开发项 | 状态 | 备注 |
| --- | --- | --- |
| 组织全局成员接口 | 已实现 | 新增 `GET /v1/im/organization/members`,支持部门、关键词、分页和 `has_more`。 |
| 月报 reopen | 已实现 | 新增 `POST /v1/im/reports/monthly/{id}/reopen`,按本人报告和 version 回退草稿。 |
| 实名更正申诉 | 已实现 | 新增 `POST /v1/im/security/realname/appeals` 和 `im_realname_appeals` 表。 |
| 群邀请外部接受 | 已实现 | 新增 `POST /v1/im/group-invitations/{id}/accept`。 |
| 主动创建/获取 P2P 会话 | 已实现 | 新增 `POST /v1/im/conversations/p2p`,复用好友/黑名单策略。 |
| 会话设置 | 已实现 | 新增 `GET/PATCH /v1/im/conversations/{topic}/settings` 和 `im_conversation_settings` 表。 |
| 多端云草稿 | 已实现 | 新增 `GET/PUT /v1/im/topics/{topic}/draft` 和 `im_topic_drafts` 表。 |
| 群文件列表、登记、删除 | 已实现 | 新增 `GET/POST/DELETE /v1/im/groups/{topic}/files`,复用现有群文件表。 |
| 普通消息转发 | 未实现 | 待下一步接入消息快照、目标权限校验和消息落库。 |
| 合并转发 Bundle 详情 | 未实现 | 待下一步新增 bundle 表和详情接口。 |
| 登出与指定会话撤销 | 部分实现 | `POST /v1/im/auth/logout` 已通过 Redis 精准撤销当前 token;按 session ID 撤销指定设备仍待统一 session 模型。 |
| 列表 `has_more` 统一 | 部分实现 | 新增/触达接口已补,历史接口需继续横向清理。 |
| 风控 token 单次消费接入业务接口 | 未实现 | 待和具体业务动作一起接入。 |
### 2.2 范围表
| 模块 | 优先级 | 开发项 | 是否依赖第三方 | 当前状态 |
| --- | --- | --- | --- | --- |
| 账号安全 | P0 | 登出与指定会话撤销 | 否 | 当前 token 登出已实现;指定 session 撤销待实现 |
| 消息转发 | P0 | 普通消息转发 | 否 | 仅转发目标接口已实现 |
| 消息转发 | P0 | 合并转发 Bundle 详情 | 否 | 文档有接口,代码未见路由 |
| 群聊 | P0 | 群文件列表、登记、删除 | 否 | 路径已不误兜底,但业务未实现 |
| 单聊 | P1 | 主动创建/获取 P2P 会话 | 否 | 文档有接口,代码未见路由 |
| 会话 | P1 | 会话设置 | 否 | 文档有接口,代码未见路由 |
| 会话 | P1 | 多端云草稿 | 否 | 文档有接口,代码未见路由 |
| 群聊 | P1 | 群邀请外部接受 | 否 | 群内邀请已有,外部接受接口未见路由 |
| 安全实名 | P1 | 实名更正申诉 | 否 | 实名提交/查询已有,申诉未见路由 |
| 组织架构 | P1 | 组织全局成员接口 | 否 | 部门成员和组织搜索已有,独立全局成员接口未见路由 |
| 办公协同 | P1 | 月报 reopen | 否 | 月报草稿/提交/详情已有,reopen 未见路由 |
| 协议一致性 | P1 | 列表 `has_more` 统一 | 否 | 部分列表仍只返回 `items,next_cursor` |
| 安全风控 | P1 | 风控 token 单次消费接入业务接口 | 否 | 可签发 `risk_action_token`,业务消费仍需补齐 |
## 3. 详细需求
### 3.1 登出与指定会话撤销
#### 目标
补齐客户端退出登录和安全页踢出指定设备能力,避免只能通过 `auth_version` 做全量失效。
#### 接口
1. `POST /v1/im/auth/logout`
- 退出当前登录设备。
- 需要使当前 token 对应 session 失效。
- 不影响同账号其他设备。
2. `DELETE /v1/im/security/sessions/{id}`
- 撤销指定 session。
- 只能撤销本人 session。
- 撤销后该 session 对应 token 立即不可用。
#### 数据建议
如当前没有独立 session 表,建议新增:
```sql
CREATE TABLE im_user_sessions (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
session_id VARCHAR(64) NOT NULL,
device_id VARCHAR(128) NOT NULL DEFAULT '',
platform VARCHAR(32) NOT NULL DEFAULT '',
token_hash VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'active',
last_seen_at DATETIME(3) NULL,
revoked_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_im_user_sessions_session (tenant_id, session_id),
KEY idx_im_user_sessions_user_status (tenant_id, user_id, status)
);
```
主键使用已有雪花 ID 工具生成。
#### 验收点
1. 当前设备登出后,当前 token 访问登录态接口返回未登录或 token 失效。
2. 其他设备 token 不受影响。
3. 删除指定 session 后,被删除设备无法继续访问。
4. 重复登出或重复删除返回幂等成功或稳定错误码。
### 3.2 普通消息转发
#### 目标
补齐普通聊天消息转发能力。现有 `GET /v1/im/forward-targets` 只解决目标选择,不等同于真正转发。
#### 接口
`POST /v1/im/messages/forward`
建议请求字段:
```json
{
"source_topic": "grp_xxx",
"message_seqs": [101, 102],
"target_topics": ["usrA-usrB", "grp_yyy"],
"mode": "single",
"client_mid": "uuid"
}
```
`mode` 建议支持:
1. `single`:逐条转发。
2. `bundle`:合并转发,生成转发卡片和 bundle。
#### 关键规则
1. 校验当前用户对源消息有查看权限。
2. 校验当前用户对每个目标会话有发送权限。
3. 不允许转发已被策略禁止转发的消息类型。
4. 文件消息需要重新校验文件访问权限。
5. 使用 HTTP 幂等能力,避免重复转发。
#### 验收点
1. 单条消息可转发到好友、群和文件助手。
2. 多条消息可逐条转发。
3. 无权限源消息、无权限目标会话返回稳定错误。
4. 同一幂等键重试不会重复投递。
### 3.3 合并转发 Bundle
#### 目标
支持合并转发卡片详情查看,使客户端可以打开“聊天记录”样式的转发包。
#### 接口
`GET /v1/im/forward-bundles/{id}`
建议响应结构:
```json
{
"id": "fb_123",
"title": "聊天记录",
"source_summary": {
"topic": "grp_xxx",
"message_count": 12
},
"items": [],
"next_cursor": "",
"has_more": false
}
```
#### 数据建议
建议新增:
```sql
CREATE TABLE im_forward_bundles (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
bundle_no VARCHAR(64) NOT NULL,
owner_user_id BIGINT NOT NULL,
source_topic VARCHAR(128) NOT NULL,
title VARCHAR(128) NOT NULL DEFAULT '',
message_count INT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_im_forward_bundles_no (tenant_id, bundle_no),
KEY idx_im_forward_bundles_owner (tenant_id, owner_user_id, created_at)
);
CREATE TABLE im_forward_bundle_items (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
bundle_id BIGINT NOT NULL,
source_topic VARCHAR(128) NOT NULL,
source_seq INT NOT NULL,
sender_user_id BIGINT NOT NULL,
content_json JSON NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
KEY idx_im_forward_bundle_items_bundle (tenant_id, bundle_id, id)
);
```
#### 验收点
1. 合并转发后生成 bundle。
2. 接收方可分页查看 bundle 内容。
3. Bundle 内容使用生成时快照,不因源消息后续不可见而展示异常。
4. 原始会话无权限用户不能直接借 bundle 访问源会话。
### 3.4 群文件
#### 目标
补齐群文件列表、上传登记和删除管理能力。当前 `/groups/{topic}/files` 已不再误返回群详情,但业务仍缺失。
#### 接口
1. `GET /v1/im/groups/{topic}/files`
- 支持分页、关键词、文件类型筛选。
2. `POST /v1/im/groups/{topic}/files`
- 将已上传文件登记为群文件。
3. `DELETE /v1/im/groups/{topic}/files/{file_id}`
- 删除或移除群文件。
#### 数据建议
可复用现有文件上传表,并新增群文件索引表:
```sql
CREATE TABLE im_group_files (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
topic VARCHAR(128) NOT NULL,
file_id BIGINT NOT NULL,
uploader_user_id BIGINT NOT NULL,
name VARCHAR(255) NOT NULL DEFAULT '',
mime VARCHAR(128) NOT NULL DEFAULT '',
size BIGINT NOT NULL DEFAULT 0,
status VARCHAR(16) NOT NULL DEFAULT 'active',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
deleted_at DATETIME(3) NULL,
KEY idx_im_group_files_topic (tenant_id, topic, status, created_at),
UNIQUE KEY uk_im_group_files_file (tenant_id, topic, file_id)
);
```
#### 权限规则
1. 只有群成员可查看群文件。
2. 只有群成员可登记群文件。
3. 删除权限建议第一批按群主、管理员、上传者放行。
4. 退群后不可继续通过群文件入口访问。
#### 验收点
1. 群成员可分页查看群文件。
2. 上传者登记文件后列表可见。
3. 普通成员不能删除其他人的文件,管理员和群主可以删除。
4. 非群成员访问返回无权限。
### 3.5 主动创建/获取 P2P 会话
#### 目标
补齐前端从好友资料页或通讯录进入单聊时的显式会话创建入口。
#### 接口
`POST /v1/im/conversations/p2p`
建议请求:
```json
{
"peer_user_id": "usr_xxx"
}
```
#### 规则
1. 必须校验双方好友关系。
2. 必须校验黑名单状态。
3. 如果 P2P topic 已存在,直接返回已有 topic。
4. 如果不存在,创建或确保 Tinode P2P 会话关系存在。
#### 验收点
1. 好友之间调用返回稳定 P2P topic。
2. 非好友或黑名单关系返回不可发起会话。
3. 重复调用返回同一个 topic。
### 3.6 会话设置
#### 目标
支持置顶、免打扰、会话备注等跨端同步设置。
#### 接口
1. `GET /v1/im/conversations/{topic}/settings`
2. `PATCH /v1/im/conversations/{topic}/settings`
建议字段:
```json
{
"pinned": true,
"muted": false,
"remark": "",
"version": 3
}
```
#### 数据建议
```sql
CREATE TABLE im_conversation_settings (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
topic VARCHAR(128) NOT NULL,
pinned TINYINT(1) NOT NULL DEFAULT 0,
muted TINYINT(1) NOT NULL DEFAULT 0,
remark VARCHAR(128) NOT NULL DEFAULT '',
version BIGINT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_im_conversation_settings_user_topic (tenant_id, user_id, topic)
);
```
#### 验收点
1. 设置后同账号其他端读取一致。
2. 版本冲突返回稳定错误或按后写覆盖策略处理。
3. 非会话参与者不可设置。
### 3.7 多端云草稿
#### 目标
支持同账号多端同步草稿。
#### 接口
`PUT /v1/im/topics/{topic}/draft`
建议请求:
```json
{
"content": "draft text",
"entities": [],
"version": 1
}
```
#### 数据建议
```sql
CREATE TABLE im_topic_drafts (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
topic VARCHAR(128) NOT NULL,
content TEXT NULL,
entities_json JSON NULL,
version BIGINT NOT NULL DEFAULT 1,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
UNIQUE KEY uk_im_topic_drafts_user_topic (tenant_id, user_id, topic)
);
```
#### 验收点
1. 保存草稿后其他端可读取。
2. 清空草稿后其他端同步清空。
3. 非会话参与者不可写入草稿。
### 3.8 群邀请外部接受
#### 目标
支持被邀请人从邀请卡片或外部入口接受群邀请。
#### 接口
`POST /v1/im/group-invitations/{id}/accept`
#### 规则
1. 校验邀请存在、未过期、未失效。
2. 校验当前登录用户是被邀请人。
3. 按群治理策略决定直接入群或进入审批。
4. 重复接受应幂等返回当前状态。
#### 验收点
1. 被邀请人可接受有效邀请。
2. 非被邀请人不可接受。
3. 已过期或已撤销邀请返回稳定错误。
4. 需要审批的群不会绕过审批。
### 3.9 实名更正申诉
#### 目标
实名审核驳回或用户发现实名信息有误时,可提交更正申诉。
#### 接口
`POST /v1/im/security/realname/appeals`
建议请求:
```json
{
"reason": "证件姓名填写错误",
"attachments": ["file_1"]
}
```
#### 数据建议
```sql
CREATE TABLE im_realname_appeals (
id BIGINT NOT NULL PRIMARY KEY,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
reason VARCHAR(500) NOT NULL DEFAULT '',
attachments_json JSON NULL,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
reviewer_id BIGINT NULL,
review_comment VARCHAR(500) NOT NULL DEFAULT '',
reviewed_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
KEY idx_im_realname_appeals_user (tenant_id, user_id, created_at),
KEY idx_im_realname_appeals_status (tenant_id, status, created_at)
);
```
#### 验收点
1. 用户可提交申诉。
2. 重复待处理申诉按策略限制。
3. 申诉记录可供后续后台审核使用。
### 3.10 组织全局成员接口
#### 目标
补齐前端需要全局组织成员列表时的统一入口,避免只能从部门成员或搜索接口拼装。
#### 接口
`GET /v1/im/organization/members`
建议查询参数:
1. `department_id`
2. `keyword`
3. `cursor`
4. `limit`
#### 规则
1. 复用组织成员表和组织搜索能力。
2. 按租户、部门、成员状态过滤。
3. 字段返回遵循现有隐私策略。
#### 验收点
1. 不传部门时返回全局成员分页。
2. 传部门时返回指定部门成员。
3. keyword 可搜索姓名、账号、手机号等允许搜索字段。
4. 返回 `items,next_cursor,has_more`。
### 3.11 月报 reopen
#### 目标
支持已提交月报按权限退回草稿,便于用户修正后重新提交。
#### 接口
`POST /v1/im/reports/monthly/{id}/reopen`
#### 规则
1. 只能 reopen 本人的月报,或由具备管理权限的用户 reopen。
2. 已归档、已锁定周期不可 reopen。
3. reopen 后状态回到草稿或待修改状态。
4. 保留审计记录。
#### 验收点
1. 已提交月报可按权限 reopen。
2. reopen 后可以继续保存草稿并重新提交。
3. 无权限用户不可 reopen 他人月报。
### 3.12 列表 `has_more` 统一
#### 目标
统一所有 HTTP 列表接口分页协议,减少前端针对每个接口单独判断。
#### 范围
第一批建议优先覆盖新增接口,以及以下已有高频接口:
1. 好友列表、新朋友列表、黑名单列表。
2. 群列表、群成员、群申请、群公告。
3. 会议列表、日报列表、月报列表。
4. 系统通知、举报/反馈相关列表。
5. 搜索类接口。
#### 响应要求
```json
{
"items": [],
"next_cursor": "",
"has_more": false
}
```
#### 验收点
1. 空列表返回 `items: []`、`has_more: false`。
2. 最后一页返回 `has_more: false`。
3. 有下一页时返回非空 `next_cursor` 和 `has_more: true`。
### 3.13 风控 token 单次消费
#### 目标
当前风控确认接口可签发 `risk_action_token`,第一批补齐业务接口消费该 token 的闭环。
#### 范围
优先接入以下业务动作:
1. 打开或发送高风险 URL。
2. 高频添加好友。
3. 高频群邀请或入群申请。
4. 举报提交中的高风险目标。
#### 规则
1. token 必须绑定租户、用户、动作、目标和过期时间。
2. token 只能消费一次。
3. token 只能用于原业务动作,不可跨动作复用。
#### 验收点
1. 未确认风险时业务接口返回 challenge。
2. 确认后携带 token 可继续原动作。
3. token 重复使用返回已消费。
4. token 用于其他动作返回无效。
## 4. 建议开发顺序
### 第一阶段:低风险补齐路由
1. 组织全局成员接口。
2. 月报 reopen。
3. 实名更正申诉。
4. 群邀请外部接受。
这些接口业务边界相对清楚,可以最快完成前端缺口补齐。
### 第二阶段:会话能力
1. 主动创建/获取 P2P 会话。
2. 会话设置。
3. 多端云草稿。
4. 登出与指定会话撤销。
这些能力涉及用户状态和多端一致性,建议一起设计数据表和事件同步。
### 第三阶段:消息与文件增强
1. 普通消息转发。
2. 合并转发 Bundle。
3. 群文件。
4. 收藏分享权限二次校验。
5. 文件下载短时签名 URL。
这些能力涉及消息权限、文件权限和幂等,建议放在同一阶段统一处理。
### 第四阶段:协议一致性与风控闭环
1. 列表 `has_more` 统一。
2. 风控 token 单次消费。
3. 好友申请、入群申请、@ 全体等限频。
4. 二维码吊销与扫码审计。
5. 关系操作审计。
这一阶段适合在主要业务接口补齐后做横向治理。
## 5. 本批次不处理但需保留的增强项
以下增强项仍然重要,但不纳入第一批:
1. 真实短信供应商发送、失败重试和发送审计。
2. FCM/APNs 离线 Push 投递。
3. TURN/STUN 配置实际接入和 SFU 群通话。
4. 病毒扫描和第三方内容安全检测。
5. HotGo 管理后台完整审核流、组织维护界面、信箱后台流转界面。
6. 大规模性能压测和专项压测报告。
这些项目建议作为第二批或外部依赖就绪后的专项处理。
## 6. 文档同步要求
每完成一个开发项,需要同步更新:
1. `docs/IM-前后端对接文档.md`
2. `docs/IM-系统设计文档.md`
3. `docs/IM-未实现后续增强项.md`
4. 必要时补充数据库变更到 `docs/IM-数据库变更汇总.sql`
已经完成的增强项应从 `IM-未实现后续增强项.md` 的待办中移除或标记为已进入基线,避免重复排期。
@@ -0,0 +1,162 @@
# IM 多开跨 Pod 实时投递压测方案
日期:2026-09-13
> 006 商户、2000 账号、两台压测机的可执行场景与统计口径见
> [IM-006商户双机消息压力测试方案](./IM-006商户双机消息压力测试方案.md)。本文保留为通用阶梯与指标说明。
## 1. 目标
验证 IM Server 使用 Redis realtime fanout 后,在多 Pod、多端登录、群消息和群业务事件场景下的实时投递能力。
本次压测重点观察:
1. 跨 Pod 在线消息是否能投递到所有目标 WS 连接。
2. 群消息在多 Pod 下的端到端延迟。
3. Redis Pub/Sub 发布、接收、投递失败和本地无连接的比例。
4. Hub 队列满、WS 写队列满、Redis 发布失败等异常是否可观测。
5. Pod 扩缩容、单 Pod 重启时是否出现明显丢消息或延迟尖刺。
## 2. 前置环境
建议至少准备:
- IM Server:3 个 Pod。
- Redis:独立实例或 Redis Cluster,开启 `group_fanout_enabled`。
- MySQL:使用压测租户库。
- 监控:开启 `/metrics`,采集 Prometheus 指标。
- 账号:准备足够多的群成员账号 CSV,格式为 `username,password[,token]`。
- 群 topic:提前创建一个大群,并把压测账号加入该群。
每个 Pod 必须配置不同的节点标识:
```json
"node_id": "im-pod-1"
```
或通过环境变量:
```shell
IM_NODE_ID=im-pod-1
```
## 3. 新增服务端指标
本次改造新增 Prometheus 指标:
```text
realtime_fanout_events_total{stage,kind,target_type,status}
realtime_fanout_delivery_duration_seconds_bucket{kind,target_type}
```
常用查询:
```promql
sum by (stage, kind, target_type, status) (rate(realtime_fanout_events_total[1m]))
```
```promql
histogram_quantile(
0.95,
sum by (le, kind, target_type) (rate(realtime_fanout_delivery_duration_seconds_bucket[5m]))
)
```
重点关注 status:
- `publish/ok`:本 Pod 成功发布到 Redis。
- `publish/error`:Redis 发布失败。
- `publish/skip_native_cluster`:启用了 Tinode 原生 cluster,跳过 Redis topic fanout。
- `receive/ok`:其他 Pod 收到 Redis fanout。
- `receive/malformed`:收到无法解析的消息。
- `deliver/local_delivered`:本 Pod 找到本地 WS 并投递。
- `deliver/no_local_session`:本 Pod 没有目标本地连接,属于正常情况。
- `deliver/hub_full`:Hub routeSrv 队列满,需要重点告警。
- `deliver/skip_no_session_store`:session store 未初始化。
## 4. 压测脚本
脚本位置:
```shell
loadtest/realtime_fanout_probe.go
```
运行示例:
```shell
go run ./loadtest/realtime_fanout_probe.go \
-ws "ws://im-pod-1.example.com/v0/channels,ws://im-pod-2.example.com/v0/channels,ws://im-pod-3.example.com/v0/channels" \
-apikey "$IM_API_KEY" \
-accounts loadtest/users.csv \
-topic grpXXXXXXXXXXXX \
-subscribers 3000 \
-publishers 10 \
-rate 200 \
-duration 10m \
-ramp 2m
```
脚本行为:
1. 按 CSV 账号建立 WS 连接。
2. 将连接均匀分布到 `-ws` 提供的多个 Pod 地址。
3. 每个连接登录并订阅同一个群 topic。
4. 从前 `-publishers` 个连接中选择发布者。
5. 按 `-rate` 总速率发布带 `probe_id` 和 `sent_at_unix_nano` 的群消息。
6. 所有订阅连接统计收到的 `{data}`,输出接收数、丢失率、p50/p95/p99/max 延迟。
输出示例:
```text
==== realtime fanout probe summary ====
published=120000 active_subscribers=3000 expected_deliveries=360000000 received=359998200 lost=1800 loss_rate=0.0005% unique_messages_seen=120000 errors=0 ctrl_acks=120000
latency p50=18ms p95=65ms p99=140ms max=900ms samples=359998200
```
## 5. 压测阶梯
建议按下面阶梯逐步推进,每个阶梯至少跑 10 分钟:
| 阶段 | Pod | 在线连接 | 发布者 | 总 QPS | 目标 |
| --- | --- | ---: | ---: | ---: | --- |
| S1 | 3 | 300 | 1 | 10 | 验证链路正确性 |
| S2 | 3 | 3000 | 10 | 100 | 验证常规群聊压力 |
| S3 | 3 | 10000 | 20 | 300 | 验证大群在线 fanout |
| S4 | 6 | 30000 | 50 | 500 | 验证扩容后的 Redis fanout |
| S5 | 6 | 30000 | 50 | 1000 | 探测上限和队列瓶颈 |
## 6. 通过标准
建议初始通过标准:
- `publish/error` 为 0。
- `receive/malformed` 为 0。
- `deliver/hub_full` 为 0。
- 压测脚本统计丢失率低于 0.01%。
- p95 端到端延迟低于 200ms。
- p99 端到端延迟低于 500ms。
- Pod CPU、内存、GC 无持续上升趋势。
- Redis CPU 不超过 70%,网络出入带宽有余量。
## 7. 故障注入
基础压测通过后执行:
1. 压测中重启一个 IM Pod,观察其他 Pod 是否继续投递。
2. 压测中扩容 Pod 数,观察 `receive/ok` 是否按 Pod 数增加。
3. 临时降低 Hub/WS 队列容量或制造慢客户端,确认 `hub_full`、WS 写失败日志可见。
4. Redis 短暂不可用,确认 `publish/error` 增加,恢复后新消息继续投递。
## 8. 结果记录
每轮记录:
- Git commit。
- IM Pod 数量和规格。
- Redis 规格。
- 在线连接数、发布 QPS、消息大小。
- 脚本输出摘要。
- Prometheus p95/p99。
- `publish/error`、`deliver/hub_full`、WS 写失败数量。
- Pod 重启/扩容期间的最大延迟和丢失率。
@@ -0,0 +1,488 @@
# IM 多开与跨 Pod 消息投递改造方案
> 日期:2026-09-12
> 适用范围:IMServer、Admin Server / HotGo、Redis、Kafka、JPush
> 关联文档:`docs/对接文档最新/IM-前后端对接文档.md`、`docs/IM-服务端架构与新功能开发指南.md`、`docs/IM-Redis三层数据分层改造方案.md`
## 1. 背景与目标
多开指同一用户在多设备、多 App 实例、多浏览器窗口或同设备多连接同时在线。服务端必须保证任意一个 Pod 收到的实时消息、业务通知、在线状态变化和账号治理命令,都能正确投递到同一租户内该用户在所有相关 Pod 上的 WebSocket / Long Polling 连接。
当前代码已有以下基础:
- `SessionStore` 管理本 Pod 内的实时连接。
- Redis presence 记录用户在线状态、session 集合和节点维度在线用户。
- 群消息已有 `redis_fanout.go`,通过 Redis Pub/Sub 通知其他 Pod。
- 群消息已拆到 `im_group_messages`,并具备 Redis seq、`client_mid` 幂等和 Kafka 异步持久化基础。
- `im_business_events.go` 仍使用租户库 business event outbox 做本地轮询投递。
本功能仍处于开发完善阶段,尚未上线。本次改动按“完整补齐多开跨 Pod 能力”处理,不按线上热修补或灰度兼容旧缺陷的思路处理。
本次改造目标:
1. 不再依赖租户库 business event outbox 作为 C 端实时通知主链路。
2. 建立统一的跨 Pod 实时投递总线,覆盖个人消息、群消息、系统消息、好友申请、好友状态、群治理、账号治理等需要多开同步的数据。
3. IMServer 只把本 Pod 连接保存在内存,全局在线、节点路由和跨 Pod 投递统一依赖 Redis。
4. Admin Server 不直连 IM 租户库,不直接操作用户 WS;后台写操作通过 Kafka Command 到 IMServer,由 IMServer 更新租户库并通过实时投递总线通知在线端。
5. 离线 Push 只做提醒和唤醒,不作为消息正文或业务状态真源。
## 1.1 前置确认
实施前必须确认生产部署是否启用 Tinode 原生 `cluster_config`:
- 如果未启用,Redis realtime dispatcher 是跨 Pod 实时投递唯一主链路。
- 如果启用,Topic 消息不要同时走 Tinode 原生 Topic master/proxy 和 Redis 广播式 dispatcher,否则可能重复投递。当前代码实现策略是:`globals.cluster != nil` 时,Topic 消息 fanout 不再额外发布 Redis realtime;session control 仍可通过 Redis 做幂等控制补充。
未启用原生 cluster 时,每个 Pod 必须配置唯一节点 ID。优先级为:
1. `node_id` 配置项。
2. `IM_NODE_ID` 环境变量。
3. `HOSTNAME` 环境变量。
4. 本地开发兜底值 `local`。
多 Pod 环境不能让所有实例都使用 `local`,否则 Redis fanout 会误认为其他 Pod 是本节点并跳过投递。
## 2. 核心结论
跨 Pod 实时投递使用 Redis Pub/Sub 或 Redis Stream,不使用数据库 outbox,也不使用 Kafka 作为 WebSocket fanout 主链路。
Kafka 仍保留以下职责:
- Admin Server 到 IMServer 的后台写命令。
- IMServer 到 Admin Server 的命令结果、审核结果、摘要同步。
- 私聊/群聊消息异步持久化。
- 文件安全检测、离线 Push 任务、审计、死信。
Redis 实时投递总线负责:
- 这个 Pod 收到消息后,通知其他 Pod。
- 其他 Pod 根据本地 `SessionStore` 找到真实 WS 连接并投递。
- 通过事件 ID / 消息 ID / `topic+seq` 做短期去重,优先复用已有 `ClaimShortState` / `ClaimShortStateValue` Redis 原语。
MySQL 负责:
- 最终状态和历史补偿。
- 用户、好友、群、系统通知、消息、文件、设备会话等可追溯数据。
- 客户端重连后按 `topic+seq`、通知 ID 或业务 ID 拉取缺口。
## 3. 总体架构
```text
Client A
-> Pod-1 WebSocket
-> Session / Topic / HTTP handler
-> 权限、幂等、seq、状态写入
-> 投递本 Pod 本地 sessions
-> Redis realtime bus
channel/stream: im:{tenant_id}:rt:{kind}
payload: envelope + server message
-> Pod-2 / Pod-3 subscriber
-> 按 tenant + target user/topic 找本 Pod sessions
-> queueOut 到对应 WS / Long Polling
Client B reconnect
-> hi -> token login -> sub me/topics
-> 按 seq/read/recv 或 notification cursor 从 MySQL/Redis 热缓存补齐
```
Admin Server 写操作:
```text
Admin Server
-> 校验后台权限
-> 写 hotgo 命令状态
-> Kafka im.admin.command
-> IMServer 任一 Pod 消费
-> 更新租户 MySQL / Redis / Topic 权限快照
-> Redis realtime bus 通知在线 C 端
-> Kafka im.admin.command.result 回写 Admin
```
## 4. 跨 Pod 实时投递总线设计
### 4.1 统一 Envelope
所有需要投递到其他 Pod 的实时事件统一包装:
```json
{
"event_id": "rt_01K...",
"tenant_id": "1",
"tenant_code": "default",
"kind": "message|topic_event|user_event|presence|session_control",
"target_type": "user|topic|device|session",
"target": "usr_xxx 或 grp_xxx 或 p2p_xxx",
"from": "usr_sender",
"source_node": "im-pod-1",
"skip_sid": "sid_sender",
"dedup_key": "message:grp_xxx:123",
"occurred_at": "2026-09-08T10:00:00.000Z",
"payload": {}
}
```
字段约定:
- `event_id`:全局唯一,消费者短期去重。
- `tenant_id`:必填,Redis key 和投递过滤必须带租户。
- `kind`:决定消费者如何解释 payload。
- `target_type=user`:投递给某用户在本 Pod 的所有连接。
- `target_type=topic`:投递给本 Pod 已订阅该 Topic 的连接。
- `target_type=device`:投递或控制某用户指定设备连接。
- `target_type=session`:精确控制某个 Session,主要用于本 Pod 本地,跨 Pod 可选。
- `skip_sid`:避免给发起连接重复投递,除非业务要求多端含当前端也收到。
- `dedup_key`:短期幂等键,例如 `msg:topic:seq`、`friend_request:id:version`。
- `payload`:可以是 Tinode `ServerComMessage` 的 JSON,也可以是业务刷新事件。
### 4.2 Redis Channel 建议
首版可以使用 Pub/Sub,要求客户端重连补偿兜底:
```text
im:{tenant_id}:rt:user
im:{tenant_id}:rt:topic
im:{tenant_id}:rt:presence
im:{tenant_id}:rt:session_control
```
如果需要更强的可观测和短暂重放,可升级为 Redis Stream:
```text
im:{tenant_id}:stream:rt
consumer_group = imserver:{node_id}
```
选择建议:
- 在线消息 fanout:Pub/Sub 足够,丢失后客户端按 seq 补偿。
- 踢设备、封禁、强制退出:建议 Pub/Sub + Redis control key 双写,避免订阅瞬断导致控制命令遗漏。
- Admin 重要业务变更:状态已落 MySQL,实时事件可以 Pub/Sub;客户端重进页面重新 GET。
### 4.3 本 Pod 投递规则
收到跨 Pod envelope 后:
1. 丢弃 `source_node == currentNodeID()` 的回环事件,除非显式需要自消费。
2. 使用 Redis `rt:dedup:{event_id}` 或本地 LRU 做短期去重。
3. 校验 `tenant_id`,不得跨租户投递。
4. 根据 `target_type` 选择本地 sessions:
- `user`:`session.uid == target && session.tenantID == tenantID`
- `topic`:`session.hasSub(target) && session.tenantID == tenantID`
- `device`:`session.uid == uid && session.deviceID == device_id`
5. 对每个匹配 Session 调用 `queueOut`。
6. 队列满时记录指标,不阻塞 Redis 订阅协程;客户端靠重连补偿。
## 5. 哪些数据需要多开处理
| 类别 | 场景 | 是否需要跨 Pod 实时投递 | 目标 | 最终补偿来源 | 说明 |
| --- | --- | --- | --- | --- | --- |
| 个人消息 | P2P 文本、图片、语音、文件、名片、通话邀请消息 | 是 | P2P Topic 在线成员、发送人其他端 | 私聊消息表 / Topic seq | Pod-1 收到发送后,本 Pod 投递并通过 Redis 投递到接收人和发送人其他端所在 Pod。 |
| 群消息 | 群文本、媒体、文件、@、合并转发 | 是 | 群 Topic 在线成员 | `im_group_messages` / Redis 热消息 | 已有群 fanout 雏形,需要纳入统一 envelope 和节点去重。 |
| 发送回执 | `pub` ack、消息发送成功 seq | 当前连接本地为主,发送人其他端可选 | 发送人其他端 | 消息表 / seq | 当前连接必须收到 `ctrl 202`;其他端同步消息用 `{data}`,不依赖 ctrl。 |
| 送达/已读 | `note recv/read` | 是 | 对端所有在线端、本人其他端 | subscription 游标 | 多端 read/recv 取最大值,所有端刷新气泡状态和未读数。 |
| 正在输入 | `kp/kpa` 临时状态 | 是,但 TTL 短 | 对端在线端或群内订阅端 | 无 | 不落库,允许丢失,限频。 |
| 消息撤回/编辑 | recall、revision | 是 | Topic 在线成员 | 消息版本表 / 消息状态 | HTTP 成功后发 topic 事件,在线端刷新 `topic+seq+version`。 |
| 删除/清空记录 | 当前用户视图删除 | 是,用户维度 | 本人所有端 | 删除游标 / del 记录 | 只影响操作者多端,不影响对方。 |
| 系统消息 | 系统通知列表新增、已读 | 是 | 目标用户所有端 | `im_system_notifications` | 新增需要提醒所有端;已读要同步清红点。 |
| 好友申请 | 创建、同意、拒绝 | 是 | 申请人、被申请人所有端 | 好友申请表、系统通知表 | 刷新新朋友列表、bootstrap badge、会话 context。 |
| 好友关系 | 添加、删除好友、拉黑/取消拉黑、备注 | 是 | 双方或操作者所有端 | 好友/黑名单表 | 输入区权限、会话标题、通讯录和黑名单需要同步。 |
| 好友在线状态 | 上线、离线、最近活跃 | 是 | 订阅相关 P2P/me Topic 的在线端 | Redis presence | 不应扫库;presence 以 Redis TTL 为准。 |
| 群成员 | 入群、退群、踢人、邀请审批 | 是 | 群 Topic 在线成员、相关用户所有端 | 群成员表、订阅表 | 同步群人数、成员列表、会话权限和系统提示。 |
| 群角色 | 群主转让、管理员增删 | 是 | 群 Topic 在线成员、目标用户所有端 | 群成员角色表 | 必须刷新 Topic `perUser` 权限快照和 Redis 成员缓存。 |
| 群禁言 | 全员禁言、成员禁言/解禁 | 是 | 群 Topic 在线成员、被禁言用户所有端 | 群治理/禁言表 | 输入框权限不能只靠前端本地状态。 |
| 群公告 | 发布、更新、已读 | 是 | 群 Topic 在线成员;已读为本人所有端 | 群公告表、公告已读表 | 顶部入口、公告详情和红点同步。 |
| 群资料 | 名称、头像、简介 | 是 | 群 Topic 在线成员 | 群表 / Topic desc | 会话标题、列表摘要、成员页都要刷新。 |
| 群文件 | 上传登记、删除、审核结果 | 是 | 群 Topic 在线成员 | 群文件表、文件引用表 | 文件正文不走 Redis,只投递文件项变化事件。 |
| 会话设置 | 置顶、免打扰、群昵称、草稿 | 是,用户维度 | 本人所有端 | subscription/private 设置表 | 不影响其他用户。 |
| 未读数/入口红点 | bootstrap badge、会话 unread、系统通知 unread | 是 | 本人所有端 | seq/read、系统通知表、业务表 | 实时事件只触发刷新;服务端 GET 返回最终值。 |
| 设备会话 | 新设备登录、退出当前设备、踢设备、全端退出 | 是 | 被控制用户所有端或指定设备 | 设备会话表、Token auth_version、Redis control key | 需要跨 Pod 关闭 WS。 |
| 账号治理 | 封禁、解封、冻结、注销、密码重置 | 是 | 目标用户所有端 | 用户状态、auth_version | Admin command 触发,IM 执行后踢在线并清 Token。 |
| 安全风控 | 风险弹窗确认/取消 | 是,用户维度 | 操作者所有端 | 风控挑战表 / Redis action token | 防止多端同时继续敏感操作。 |
| 会议/工作台 | 会议通知、日报月报提醒、工作台红点 | 是 | 目标用户所有端 | 对应业务表 | 会议笔记不产生通知,但页面返回后主动刷新。 |
| 文件安全 | 审核通过/拒绝、文件失效 | 是 | 文件引用可见用户 | 文件元数据/引用表 | 通知在线端刷新消息卡片或文件列表。 |
| 离线 Push | 用户离线、后台或不可实时投递 | 不属于 WS 多开,但需去重 | JPush alias / registration_id | Push 任务表或 Kafka | 多设备允许同 alias,payload 只带定位字段。 |
## 6. IMServer 改造方案
### 6.1 新增 Realtime Dispatcher
建议新增模块:
```text
server/realtime_dispatcher.go
server/store/realtime_runtime.go
```
职责:
- 发布跨 Pod envelope 到 Redis。
- 订阅 Redis envelope 并投递到本 Pod `SessionStore`。
- 提供 `PublishToUser`、`PublishToTopic`、`PublishSessionControl` 等方法。
- 统一 `event_id`、`dedup_key`、`skip_sid`、租户校验和指标。
接口示意:
```go
type RealtimeTarget struct {
TenantID types.TenantID
Type string // user/topic/device/session
Target string
SkipSid string
}
func PublishRealtime(target RealtimeTarget, msg *ServerComMessage) error
func PublishRealtimeEvent(target RealtimeTarget, event IMRealtimeEvent) error
```
现有 `redis_fanout.go` 后续可以改为调用统一 dispatcher,保留兼容封装。
### 6.2 个人消息跨 Pod
当前群消息已有专用 fanout,个人消息也需要同等级处理。
发送流程:
1. WS 收到 P2P `pub`。
2. Session 完成租户、登录、Topic 归一化。
3. Topic 校验好友关系、黑名单、风控、文件 ACL。
4. 使用 `client_mid` 做幂等,生成 Topic seq。
5. 本 Pod 投递:
- 当前连接返回 `ctrl 202`。
- 发送人其他端收到同一 `{data}`。
- 接收人在本 Pod 的端收到 `{data}`。
6. Redis realtime bus 投递到其他 Pod:
- `target_type=topic`,`target=p2p...`
- 或拆成 `target_type=user` 给双方用户。
7. 异步持久化到 MySQL。
8. 不在线设备只生成 Push 任务,客户端重连按 seq 补齐。
关键点:
- `noecho=false` 时发送人多端应同步消息。
- `skip_sid` 只能跳过当前连接的重复 `{data}`,不能跳过发送人其他端。
- P2P 传入 `usr...` 时必须先归一化为真实 P2P Topic。
### 6.3 群消息跨 Pod
保留当前 Redis seq、`client_mid` 幂等、热消息和 `im_group_messages` 最终落库模型。
需要补强:
- `redis_fanout.go` 使用统一 envelope。
- Redis channel 从全局 `im:0:group_fanout` 调整为带租户或 envelope 必带 `tenant_id`。
- 投递时优先使用 `topic:{topic}:node:{node_id}:online_members`,避免所有 Pod 都扫描全部 session。
- 消费端支持 topic 已加载和未加载两种路径:
- Topic 已加载:投递到 `hub.routeSrv`,由 Topic 统一处理。
- Topic 未加载:直接扫描本 Pod 已订阅该 Topic 的 session 投递。
- Redis Pub/Sub 丢失不补发,客户端重连通过 `since=last_seq+1` 补齐。
### 6.4 业务状态事件跨 Pod
替换 `im_business_events.go` 的数据库 outbox 轮询模式。
HTTP handler 或 Store 写成功后:
1. 同步写 MySQL 最终状态。
2. 失效 Redis 缓存和 Topic 权限快照。
3. 调用 realtime dispatcher 发布事件。
4. 如需通知离线用户,投 Kafka Push 任务。
业务事件 payload 示例:
```json
{
"info": {
"topic": "usr_target",
"from": "usr_actor",
"what": "im",
"event": "friend.request.created",
"payload": {"request_id": "frq_1"},
"ts": "2026-09-08T10:00:00Z"
}
}
```
群事件:
```json
{
"info": {
"topic": "grp_xxx",
"from": "usr_actor",
"what": "group",
"event": "group.role.updated",
"payload": {"topic": "grp_xxx", "user_id": "usr_2", "role": "admin"},
"ts": "2026-09-08T10:00:00Z"
}
}
```
### 6.5 Session Control 跨 Pod
设备退出、踢设备、全端退出、密码重置、账号封禁必须跨 Pod 生效。
建议 Redis key:
```text
im:{tenant_id}:session_control:user:{uid}:version
im:{tenant_id}:session_control:device:{uid}:{device_id}:version
im:{tenant_id}:session_control:event:{event_id}
```
流程:
1. IMServer 或 Admin command 更新 MySQL 用户状态、设备会话状态或 `auth_version`。
2. 写 Redis control version,TTL 可 1 到 7 天。
3. 发布 `target_type=user/device` 的 `session_control` envelope。
4. 各 Pod 收到后调用 `SessionStore.EvictUser` 或 `EvictUserDevice`。
5. Session 心跳或上行业务包也检查 Redis control version,防止 Pub/Sub 丢失。
### 6.6 Presence 多开规则
Redis presence 是全局在线真源:
- 用户登录成功:增加 session,写 `presence:user:{uid}`、`presence:user:{uid}:sessions`、`presence:node:{node_id}:users`。
- 订阅群 Topic:写 `topic:{topic}:online_members` 和 `topic:{topic}:node:{node_id}:online_members`。
- 心跳:刷新 session TTL、user TTL、已订阅群成员 TTL。
- 断线:移除 session;最后一个 session 离线时标记 `online=0`。
- 节点异常:依赖 TTL 自动过期。
好友在线状态变化:
- 不扫描所有用户。
- 只通知与该用户有订阅关系或正在会话的在线端。
- 客户端展示最近在线状态时,HTTP/WS 查询均以 Redis presence 为准。
## 7. Admin Server 改造方案
### 7.1 Admin 不直接做 WS 投递
Admin Server / HotGo 只负责后台权限、配置、审核和命令生产,不直接连接 IMServer 内存态,也不直接写租户库。
Admin 写操作统一:
1. 校验后台管理员权限。
2. 写 hotgo 命令表,记录 `command_id/event_id/idempotency_key/actor/request_id`。
3. 发送 Kafka `im.admin.command`。
4. 等待 IMServer `im.admin.command.result` 更新命令状态。
### 7.2 Admin 命令触发的多开场景
| Admin 操作 | Kafka Command | IMServer 动作 | C 端实时投递 |
| --- | --- | --- | --- |
| 封禁用户 | `user.disable` | 更新用户状态、递增 `auth_version`、踢在线 | `session_control user.disabled` 给目标用户所有端 |
| 解封用户 | `user.enable` | 更新用户状态、失效缓存 | 可选 `user.enabled` 给本人和相关管理视图 |
| 重置密码 | `user.password.reset` | 更新密码、递增 `auth_version`、撤销 Token | `session_control security.password_reset` |
| 踢设备 | `user.device.revoke` | 撤销设备会话 | `session_control device.revoked` |
| 解散群 | `group.delete` | 解散 Topic、撤销订阅和二维码 | `group.deleted` 给群在线成员 |
| 踢群成员 | `group.member.remove` | 更新成员状态和 Topic 订阅 | `group.member.removed` 给群成员和被踢用户多端 |
| 设置管理员 | `group.admin.set` | 更新角色、刷新权限快照 | `group.role.updated` |
| 删除管理员 | `group.admin.remove` | 降级角色、刷新权限快照 | `group.role.updated` |
| 禁言成员 | `group.member.mute` | 写禁言表、刷新权限 | `group.mute.updated` |
| 撤回/处置消息 | `message.moderate` | 更新消息状态、审核记录 | `message.recalled` 或 `message.moderated` |
| 文件审核处置 | `file.audit.update` | 更新文件状态和引用可见性 | `file.unavailable` 给引用可见用户 |
| 举报处理结果 | `abuse.report.resolve` | 更新举报状态、生成系统通知 | `system_notification.created` 给举报人 |
### 7.3 Admin 摘要同步
Admin 页面需要列表、统计和审核台数据,不应通过实时总线获得。
推荐:
- Admin 查询租户详情:调用 IM 内部只读 API。
- Admin 高频列表:维护 hotgo 摘要表。
- IM 状态变更后通过 Kafka result/event 同步摘要给 Admin。
这些摘要同步不属于 C 端多开实时投递,不要求发到用户 WS。
## 8. Redis、Kafka、MySQL 边界
| 组件 | 用途 | 不应用于 |
| --- | --- | --- |
| Redis Pub/Sub / Stream | 在线跨 Pod WS 投递、session control、presence 变化、短期去重 | 最终历史、长事务、后台命令可靠执行 |
| Redis KV/Set/Hash | presence、节点在线用户、Topic 在线成员、短期幂等、热消息 | 长期业务主数据 |
| Kafka | Admin command/result、消息持久化、Push、文件安全、审计、死信 | WebSocket 实时 fanout、验证码、在线状态 |
| MySQL 租户库 | 最终状态、历史查询、重连补偿、审计 | 在线连接表、实时 fanout |
| hotgo | Admin 后台主数据、配置、命令状态、摘要 | C 端 IM 消息和租户业务最终状态 |
## 9. 渐进实施计划
### 阶段一:统一跨 Pod 投递
1. 新增 realtime dispatcher。
2. 先增加 P2P 个人消息跨 Pod 投递。当前 P2P 是完全空白,应优先于群消息迁移。
3. 将现有群消息 Redis fanout 改为 dispatcher,并修复 `ServerComMessage` 内部路由字段 `json:"-"` 导致跨 Pod 丢 `TenantID/RcptTo/SkipSid` 的问题。
4. 增加用户维度事件投递,先覆盖好友申请、系统通知、已读同步。
5. 增加 session control 跨 Pod 踢设备/全端退出。
6. 同步上线 Redis 订阅健康检查、发布/消费/队列满指标和基础告警。
### 阶段二:替换 business event outbox
1. 已停止把 C 端在线通知写入 `im_business_events_outbox`,本次改造直接替换为 realtime dispatcher。
2. `publishIMRelationEvent`、`publishIMMeetingEvent` 等改为:
- 需要历史展示的系统通知写 `im_system_notifications`。
- 在线刷新直接调用 realtime dispatcher。
- 离线提醒投 Kafka Push。
3. 已移除 `startIMExternalBusinessEventPoller`。
4. `registration.invite_used` 改为注册提交后直接同步邀请码使用聚合;`security.password_reset` 改为密码重置后直接跨 Pod 踢线。
### 阶段三:Admin 联动
1. Admin Server 后台写操作全部通过 Kafka command。
2. IMServer command consumer 执行业务状态更新。
3. IMServer 执行成功后通过 realtime dispatcher 通知 C 端。
4. IMServer 通过 Kafka result/event 回写 Admin 命令状态和摘要。
### 阶段四:可观测和压测
1. 完善跨 Pod 投递指标:发布数、消费数、投递 session 数、丢弃数、队列满数、延迟。
2. 完善 Redis 订阅健康检查和告警。
3. 压测同一用户 10 端、多 Pod P2P 消息、千人群/万人群消息。
4. 验证 Pub/Sub 丢失后重连补偿路径。
## 10. 验收用例
| 用例 | 步骤 | 预期 |
| --- | --- | --- |
| 同用户多端收个人消息 | 用户 A 两端分别连 Pod-1、Pod-2,用户 B 发 P2P 消息到 Pod-1 | A 两端都收到消息,B 其他端也同步,消息只落一条。 |
| 群消息跨 Pod | 群成员分布在 Pod-1、Pod-2、Pod-3,Pod-1 收到群消息 | 所有在线订阅成员实时收到;离线成员重连按 seq 补齐。 |
| 重复发送幂等 | 断线后使用同一 `client_mid` 重发 | 返回同一 `seq`,不重复落库,不重复 Push。 |
| 已读多端同步 | A 在手机读到 seq=100 | A 其他端未读清除,B 多端看到 read 更新。 |
| 好友申请多端同步 | B 向 A 发好友申请 | A 所有在线端刷新新朋友红点和申请列表。 |
| 系统通知已读同步 | A 在 Pod-1 上把系统通知标记已读 | A 在 Pod-2 的端红点同步消失。 |
| 群角色变更 | Admin 将 A 设为管理员 | A 所有端群管理入口出现;群在线成员看到角色标识刷新。 |
| 禁言即时生效 | Admin 禁言 A,A 另一个端在 Pod-2 继续发消息 | 服务端返回 `GROUP_MUTED`,不能成功发送。 |
| 踢设备 | Admin 踢 A 的 device-1 | device-1 在任意 Pod 的连接被关闭,其他设备保持在线。 |
| 全端退出 | 密码重置或封禁 A | A 所有 Pod 上的连接关闭,旧 Token 后续登录失败。 |
| Pub/Sub 瞬断补偿 | 人为断开 Pod-2 Redis 订阅后发送消息,再恢复并重连客户端 | 实时期间可能漏收,但重连后按 seq 补齐,最终状态正确。 |
## 11. 代码落点建议
IMServer:
- `server/realtime_dispatcher.go`:统一跨 Pod 发布与订阅。
- `server/redis_fanout.go`:改造成群消息 dispatcher 适配层。
- `server/sessionstore.go`:补充按租户、用户、设备、Topic 的本地投递辅助方法。
- `server/session.go`:登录、心跳、断线时刷新 Redis presence 和 control version 检查。
- `server/topic.go`:个人/群消息发布成功后调用 dispatcher。
- `server/im_http.go`:好友、系统通知、群治理、会话设置等 HTTP 写成功后调用 dispatcher。
- `server/kafka_admin_commands.go`:Admin command 执行成功后调用 dispatcher。
- `server/store/group_message_runtime.go`:补充 realtime bus Redis 发布、订阅、去重 key。
Admin Server / HotGo:
- 后台写操作保留 command/outbox 到 Kafka 的可靠链路。
- 不新增任何直接调用 IM Pod WS 内存态的接口。
- 命令结果和摘要通过 Kafka result/event 回写 hotgo。
- 后台实时页面如需刷新,使用 Admin 自己的 WebSocket/SSE,从 hotgo 命令结果或摘要表驱动,不复用 C 端 WS。
## 12. 风险与注意事项
1. Pub/Sub 不保证离线重放,因此所有实时投递都必须有最终补偿来源。
2. `event_id` 去重不能代替消息 `client_mid` 幂等;二者层级不同。
3. 群消息 seq 只能有一个发号源,Redis 不可用时不能临时改用 MySQL 发号。
4. Session control 不能只靠 Pub/Sub,必须在心跳/上行业务包检查版本兜底。
5. Topic 权限变化后必须刷新内存快照和 Redis 缓存,否则多开端会出现权限漂移。
6. Push payload 不得带敏感正文,只带 `tenant_code/topic/seq/notification_id` 等定位字段。
7. Admin command 成功但实时投递失败时,不回滚业务状态;客户端通过重连或重新 GET 补偿。
8. 跨租户 Redis key、channel、payload 都必须带 `tenant_id`,不允许使用裸 `topic` 做全局路由。
@@ -0,0 +1,484 @@
# IM 工作台服务拆分迁移稳定方案
> 目标:将 `im_server` 中的工作台业务拆到独立仓库 `/Users/youshunlai/go_workspace/im_workbench`,先形成独立服务和独立发布边界;数据库暂不迁移,等接口全部稳定后再单独做工作台库迁移。
## 1. 结论
建议采用“独立仓库 + 分阶段迁移 + Nginx 分流 + Redis 统一 Token 鉴权”的方式:
1. `im_server` 保留 IM 核心能力:登录态校验、聊天协议、单聊/群聊、消息存储、长连接、在线状态、Redis 实时 fanout、Kafka 消息消费。
2. `im_workbench` 承接工作台能力:工作台入口、组织架构、考勤、会议、会议笔记、日报、月报、总经理信箱、工作台红点。
3. 当前阶段不迁移数据库,`im_workbench` 先连接现有租户库读取和写入工作台相关表。
4. 等 `im_workbench` 接口、鉴权、Kafka、监控全部稳定后,再单独启动数据库迁移专项。
5. 工作台接口直接切换为 `/v1/workbench/*`,由 Nginx/API Gateway 按路径分发到 `im_workbench`;`im_server` 不再代理工作台请求。
6. 迁移采用“先复制迁出、旧代码保留”的方式;只有 `im_workbench` 全部接口、数据库、Kafka、监控联调通过后,才删除 `im_server` 中的工作台旧代码。
一句话:网关分流,Redis 鉴权,先拆服务和接口,数据库后迁,最后清理 `im_server` 中的工作台旧代码。
## 2. 拆分范围
### 2.1 迁出到 im_workbench
以下业务从 `im_server` 迁移到 `im_workbench`:
| 模块 | 当前接口示例 | 迁移归属 |
|---|---|---|
| 工作台入口 | `GET /v1/workbench` | im_workbench |
| 组织架构 | `/v1/workbench/organization/*` | im_workbench |
| 考勤打卡 | `/v1/workbench/attendance/*` | im_workbench |
| 会议通知 | `/v1/workbench/meetings*` | im_workbench |
| 会议笔记 | `/v1/workbench/meeting-notes`、`/v1/workbench/meetings/{id}/notes*` | im_workbench |
| 日报 | `/v1/workbench/reports/daily*` | im_workbench |
| 月报 | `/v1/workbench/reports/monthly*` | im_workbench |
| 总经理信箱 | `/v1/workbench/mailbox*` | im_workbench |
| 工作台红点 | `workbench_total_unread`、模块未读 | im_workbench 计算,im_server 聚合透出 |
### 2.2 留在 im_server
以下能力继续留在 `im_server`:
| 模块 | 说明 |
|---|---|
| 登录与 Token 签发 | C 端登录仍由 IM 负责,登录成功后把 Token 会话写入 Redis。 |
| 聊天消息 | 单聊、群聊、消息 seq、消息落库、Redis fanout、WS 下发。 |
| 群治理 | 群资料、群成员、禁言、进群审批、群公告、群文件。 |
| 文件上传入口 | `POST /v0/file/u/` 暂留 im_server,工作台只保存业务引用;后续可单独拆文件服务。 |
| 系统通知投递 | 最终给 C 端的在线通知仍由 im_server 通过 WS 下发。 |
| Kafka 消费到 C 端 | Admin 或 Workbench 产生的业务事件,由 im_server 消费后找在线用户下发。 |
## 3. 目标架构
```text
C 端 App
|
| HTTPS
v
Nginx / API Gateway
|-- /v1/im/*、/v0/channels、/v0/file/u/ -> im_server
|-- /v1/workbench/* -> im_workbench
im_workbench
|
| MySQL
v
现有租户库中的工作台表
im_workbench
|
| Kafka: workbench 业务事件
v
im_server
|
| WebSocket
v
C 端在线用户
```
稳定边界:
1. `im_workbench` 不直接操作 IM 消息表、在线连接、WebSocket。
2. `im_server` 不直接写工作台业务表,也不代理工作台 HTTP 请求。
3. `im_server` 登录成功后将 Token 会话写入 Redis,`im_workbench` 每次请求从 Redis 校验 Token。
4. 两边共享用户 ID 口径:内部管理和工作台接口使用数据库 `BIGINT/int64`;聊天协议层仍可使用 `usr...`。
5. 工作台需要通知 C 端时投 Kafka 事件,由 `im_server` 统一下发。
## 4. 数据库处理方案
### 4.0 当前阶段原则
当前阶段数据库先不迁移。`im_workbench` 独立服务启动后,仍访问现有租户库中的工作台相关表,先保证新服务接口、Redis Token 鉴权、Nginx 路由、Kafka 事件和监控全部跑通。
代码层面禁止服务启动时自动建库建表。`im_server` 和 `im_workbench` 都只负责连接已有数据库和执行正常业务 SQL;所有 `CREATE DATABASE`、`CREATE TABLE`、`ALTER TABLE`、索引变更、初始化字典和种子数据,统一整理为 SQL 文件,经审核后由人工或运维发布流程执行。
当前 MySQL adapter 已禁用 `CreateDb` 自动建库建表和 `UpgradeDb` 自动升级 DDL。服务启动时如果数据库不存在会直接连接失败;数据库版本不一致时只报错提示执行人工 SQL,不再自动补表、补字段或补索引。
数据库迁移作为后置专项处理,触发条件是:
1. `/v1/workbench/*` 全部接口在本地、测试环境、前端联调中稳定。
2. `im_server` 中工作台旧接口确认没有客户端继续访问。
3. 工作台事件、通知、红点、文件引用链路全部验收通过。
4. 运维确认新库命名、备份、监控、账号权限和迁移窗口。
### 4.1 当前工作台相关表
当前这些表仍保留在租户库 `tinode` 中,暂不迁移;后续数据库专项时再纳入迁移范围:
| 表 | 用途 | 迁移建议 |
|---|---|---|
| `im_workbench_apps` | 工作台模块配置、排序、入口 | 暂留租户库;后续迁到工作台库或由 Admin 配置后同步。 |
| `im_org_departments` | 组织部门树 | 暂留租户库;后续迁到工作台库。 |
| `im_org_members` | 组织成员、岗位、联系方式 | 暂留租户库;保留 `user_id BIGINT` 关联 IM 用户。 |
| `im_org_visibility_overrides` | 组织可见范围覆盖 | 暂留租户库。 |
| `im_attendance_rules` | 考勤规则 | 暂留租户库。 |
| `im_attendance_shifts` | 考勤班次 | 暂留租户库。 |
| `im_attendance_records` | 考勤日记录 | 暂留租户库。 |
| `im_attendance_checkins` | 打卡流水 | 暂留租户库。 |
| `im_attendance_appeals` | 补卡/异常申诉 | 暂留租户库。 |
| `im_meetings` | 会议主表 | 暂留租户库。 |
| `im_meeting_participants` | 会议参与人 | 暂留租户库。 |
| `im_meeting_note_files` | 会议笔记文件引用 | 暂留租户库;文件实体仍在文件表/OSS。 |
| `im_daily_reports` | 日报 | 暂留租户库。 |
| `im_monthly_reports` | 月报 | 暂留租户库。 |
| `im_mailbox_config` | 总经理信箱配置 | 暂留租户库;后续可迁到工作台库或 Admin 公共库配置后同步。 |
| `im_mailbox_submissions` | 总经理信箱提交 | 暂留租户库。 |
### 4.2 不迁出的表
| 表/数据 | 原因 |
|---|---|
| `users`、认证、会话、设备 | 属于 IM 登录和账号基础能力。 |
| `topics`、`subscriptions`、`messages`、`im_group_messages` | 属于聊天主链路。 |
| 好友、黑名单、群成员、群治理表 | 属于 IM 社交关系和群聊能力。 |
| 文件实体表、OSS 元数据 | 暂不迁移,工作台只保存 `file_id` 引用。 |
| `im_system_notifications` | 建议先留在 im_server,作为统一 C 端通知中心。后续如要拆,可单独设计通知服务。 |
| `im_business_events_outbox` | 已废弃,不纳入迁移;当前代码不再自动建表,工作台拆分后事件统一走 Kafka 或实时投递链路。 |
### 4.3 后续目标库建议
后续接口稳定后,再给工作台单独库,命名按环境区分:
```text
本地:im_workbench
测试:im_workbench_test 或按运维规范命名
生产:im_workbench
```
如果继续保持“一个租户一个库”,则工作台也应支持租户库路由:
```text
hotgo.im_tenant
- code
- im_db_name
- workbench_db_name
- im_internal_base_url
- workbench_internal_base_url
```
正式方向建议在 `hotgo.im_tenant` 或租户配置表里增加工作台库配置,而不是让 `im_server` 读取工作台库 DSN。
### 4.4 后续数据迁移顺序
迁库等接口全部稳定后再做,不要和本轮服务拆分混在一起。后续建议分四步:
1. 建新库新表:在 `im_workbench` 库手动执行审核后的 SQL,创建同结构表,先不接线上流量。
2. 全量复制:从租户库复制历史工作台表数据到工作台库。
3. 增量同步:迁移窗口内对工作台写操作做双写,或由旧库 binlog/脚本同步到新库。
4. 切读切写:先灰度读新库,再切写新库,最后关闭旧库工作台写入。
迁移校验至少包括:
| 校验项 | 说明 |
|---|---|
| 行数校验 | 每张表按租户统计行数。 |
| 主键/业务唯一键校验 | `meeting_id`、`report_id`、`submission_id`、`department_id` 等必须一致。 |
| 更新时间校验 | 对比 `updated_at` 最大值,确认增量无遗漏。 |
| 抽样内容校验 | 抽样会议、日报、月报、考勤记录详情。 |
| 外键引用校验 | 会议参与人、会议笔记文件、组织成员部门引用不能断。 |
### 4.5 回滚原则
当前阶段没有数据库迁移,回滚以服务和路由为主。后续真正迁库时,旧库数据至少保留一个发版周期,不删除旧表。
回滚策略:
1. 如果服务拆分异常:关闭 `/v1/workbench/*` 路由,前端回退旧路径或旧版本。
2. 如果 `im_workbench` 业务异常:保留 `im_server` 旧工作台代码作为临时回退能力。
3. 如果 Redis 鉴权异常:停止工作台新路由,优先恢复登录态校验。
4. 如果 Kafka 通知异常:业务写入不回滚,只降级为客户端主动拉取和后台补偿推送。
5. 后续如果新库读写异常:再按迁库专项方案切回旧库。
## 5. 路由与鉴权迁移方案
### 5.1 Nginx 分流规则
工作台接口直接使用新路径 `/v1/workbench/*`,Nginx/API Gateway 按路径分流:
```text
/v1/im/* -> im_server
/v0/channels -> im_server
/v0/file/u/ -> im_server
/v1/workbench/* -> im_workbench
```
建议工作台目标路由:
| 原 IM 路径 | 新 Workbench 路径 |
|---|---|
| `GET /v1/im/workbench` | `GET /v1/workbench` |
| `/v1/im/organization/*` | `/v1/workbench/organization/*` |
| `/v1/im/attendance/*` | `/v1/workbench/attendance/*` |
| `/v1/im/meetings*` | `/v1/workbench/meetings*` |
| `/v1/im/meeting-notes*` | `/v1/workbench/meeting-notes*` |
| `/v1/im/meetings/{id}/notes*` | `/v1/workbench/meetings/{id}/notes*` |
| `/v1/im/reports/daily*` | `/v1/workbench/reports/daily*` |
| `/v1/im/reports/monthly*` | `/v1/workbench/reports/monthly*` |
| `/v1/im/mailbox*` | `/v1/workbench/mailbox*` |
不建议让 `im_server` 继续代理工作台请求。路由由 Nginx 分流,服务各自鉴权、各自访问自己的数据库。
### 5.2 Redis Token 鉴权
登录仍由 `im_server` 负责。登录成功后,`im_server` 将 Token 会话写入 Redis,`im_workbench` 直接从 Redis 校验登录态。
Redis Key 建议:
```text
im:session:token:{token_hash}
```
Value 建议使用 JSON:
```json
{
"user_id": 42,
"tenant_id": 1,
"tenant_code": "default",
"device_id": "ios-device-1",
"auth_level": "auth",
"state": "active",
"expires_at": "2026-09-13T18:00:00+08:00",
"issued_at": "2026-09-13T10:00:00+08:00"
}
```
请求 Header 保持 C 端统一:
| Header | 说明 |
|---|---|
| `Authorization` | `Bearer {token}`,两个服务都从 Redis 校验。 |
| `X-MeetingIM-APIKey` | 应用 API Key。 |
| `X-Tenant-Code` | 租户编码。 |
| `X-IM-Schema` | 协议版本。 |
| `X-Request-ID` | 链路追踪 ID。 |
校验流程:
1. 从 `Authorization` 取 Bearer Token,先做格式校验。
2. 对 Token 做 hash 后查 Redis,避免 Redis 明文 Key 暴露原始 Token。
3. 校验会话存在、未过期、`state=active`。
4. 校验 `X-Tenant-Code` 与 Redis 会话中的 `tenant_code` 一致。
5. 校验 API Key 是否可用于当前租户和客户端类型。
6. 将 `tenant_id`、`tenant_code`、`user_id`、`device_id` 写入请求上下文。
Token 失效场景:
| 场景 | 处理 |
|---|---|
| 用户主动退出 | 删除当前 Token Key。 |
| 踢设备 | 删除指定 `device_id` 对应 Token Key。 |
| 冻结/封禁账号 | 删除用户全部 Token Key,并通过 Kafka 通知在线连接断开。 |
| 修改密码 | 删除用户全部 Token Key。 |
| Token 过期 | Redis TTL 自动过期。 |
### 5.3 共享鉴权代码
建议抽一个轻量公共包,两个服务复用:
```text
meetingim_common/auth/session
```
公共包只放稳定协议:
1. Redis Key 生成。
2. Token hash 规则。
3. Session JSON 结构。
4. Header 解析。
5. 错误码定义。
不要把数据库访问、HTTP handler、业务权限放进公共包,避免公共包变成新的耦合点。
## 6. 事件与通知
工作台拆出后,工作台业务变更不再直接调用 `im_server` 代码函数,而是投 Kafka。
建议 Topic:
| Topic | 生产者 | 消费者 | 用途 |
|---|---|---|---|
| `im.workbench.event` | im_workbench | im_server | 工作台事件、红点、系统通知、WS 下发。 |
| `im.cache.invalidate` | im_workbench/Admin | im_server/im_workbench | 组织、工作台模块、配置变更后的缓存失效。 |
| `im.audit.event` | im_workbench | Admin/审计消费者 | 工作台敏感操作审计。 |
`im.workbench.event` 消息建议字段:
```json
{
"event_id": "wb_default_meeting_mtg_1_created_v1",
"tenant_id": 1,
"tenant_code": "default",
"actor_user_id": 42,
"module": "meeting",
"action": "meeting.created",
"resource_id": "mtg_1",
"target_user_ids": [42, 43],
"title": "会议通知",
"summary": "项目周会",
"data": {
"meeting_id": "mtg_1",
"badge_delta": 1,
"version": 1
},
"occurred_at": "2026-09-13T13:30:00+08:00"
}
```
消费原则:
1. `event_id` 全局幂等,消费端记录处理日志。
2. 系统通知写入失败可重试,超过次数进入死信。
3. WS 下发失败不影响业务写入,客户端重连后通过工作台接口和 bootstrap 补偿。
4. 工作台红点以服务端查询为准,Kafka 只做实时增量提醒。
## 7. 代码迁移步骤
### 7.1 第一阶段:服务骨架
在 `/Users/youshunlai/go_workspace/im_workbench` 新建 Go 服务:
```text
im_workbench/
cmd/workbench/
internal/http/
internal/store/
internal/kafka/
internal/config/
docs/
sql/
```
第一阶段只做:
1. 配置文件、日志、MySQL、Redis、Kafka 初始化。
2. 健康检查:`GET /healthz`、`GET /readyz`。
3. Redis Token 鉴权中间件。
4. 租户上下文解析。
5. 复制工作台接口 DTO 和响应结构。
### 7.2 第二阶段:迁移业务代码
从 `im_server` 迁出以下代码:
| 当前位置 | 迁移内容 |
|---|---|
| `server/im_http.go` | `serveIMWorkbench`、Organization、Attendance、Meetings、MeetingNotes、DailyReports、MonthlyReports、Mailbox 相关 handler。 |
| `server/store/store.go` | 工作台相关 Persistence Interface 和 mapper。 |
| `server/store/types/*.go` | `workbench.go`、`organization.go`、`attendance.go`、`meeting.go`、`reports.go`、`mailbox.go` 等类型。 |
| `server/db/mysql/tenant_adapter.go` | 工作台相关 SQL 实现。 |
| `server/im_business_events.go` | 工作台事件生产逻辑拆到 `im_workbench`,只保留 IM 消费下发。 |
| `server/kafka_workbench_audit.go` | 工作台事件生产侧迁出,IM 侧保留消费或下发处理。 |
迁移时要保留测试:
1. 原 handler 单测迁到 `im_workbench`。
2. `im_server` 原有工作台测试暂时保留,作为旧链路回归保护。
3. `im_workbench` 新增同等接口测试,确认新路径行为一致。
4. Kafka 事件用例拆为 Workbench 生产端测试和 IM 消费端测试。
### 7.3 第三阶段:Nginx 路由切换
`im_server` 中原工作台 handler 暂时不删除,不做主链路代理。新请求由 Nginx/API Gateway 直接分流到 `im_workbench`:
```text
客户端请求 /v1/workbench
-> Nginx/API Gateway
-> im_workbench Redis Token 鉴权
-> return response
```
路由层必须支持:
1. `/v1/workbench/*` 全量转发到 `im_workbench`。
2. `/v1/im/*`、`/v0/channels`、`/v0/file/u/` 继续转发到 `im_server`。
3. 透传 `Authorization`、`X-MeetingIM-APIKey`、`X-Tenant-Code`、`X-IM-Schema`、`X-Request-ID`。
4. 工作台服务超时建议 3 到 5 秒。
5. 工作台不可用时返回明确 `WORKBENCH_UNAVAILABLE` 或网关 502/504,不影响聊天主链路。
旧代码保留策略:
1. `im_server` 工作台旧 handler、store、SQL 先保留。
2. 前端切新路径后,测试环境持续观察一个发版周期。
3. `im_workbench` 全部接口、数据库、Kafka、监控确认稳定后,再从 `im_server` 删除旧工作台代码。
4. 删除前要确保没有客户端继续访问 `/v1/im/workbench`、`/v1/im/meetings*` 等旧路径。
### 7.4 第四阶段:接口稳定验收
当前阶段只做接口稳定验收,不做数据库切换。建议保留以下服务开关:
| 配置 | 说明 |
|---|---|
| `workbench.service.enabled` | 是否启用工作台独立服务。 |
| `workbench.route.enabled` | Nginx/API Gateway 是否启用 `/v1/workbench/*` 路由。 |
| `workbench.kafka.enabled` | 是否启用工作台事件 Kafka。 |
后续迁库专项再增加数据库切换开关:
```text
workbench.db.read_from=old|new|shadow
workbench.db.write_to=old|dual|new
```
## 8. 发布与回归
### 8.1 发布前检查
1. `im_workbench` 健康检查通过。
2. `im_workbench` 可连接现有租户库,并只访问工作台相关表。
3. Redis Token 鉴权与 `im_server` 登录态一致。
4. Nginx/API Gateway 工作台路由灰度开关默认关闭或仅测试环境开放。
5. Kafka Topic、消费组、死信队列已建。
6. 监控已覆盖 HTTP 成功率、耗时、Kafka 积压、DB 错误、网关转发失败。
7. 本阶段不要求新工作台库建表和数据复制。
### 8.2 必测用例
| 模块 | 用例 |
|---|---|
| 工作台入口 | 登录后返回模块排序、开关、未读角标。 |
| 组织架构 | 普通用户、手机号注册用户、邀请用户、测试账号可见范围正确。 |
| 考勤 | 配置查询、打卡、重复幂等、记录列表、申诉创建。 |
| 会议 | 创建、列表、详情、修改、取消、提醒、参会响应。 |
| 会议笔记 | 上传文件、绑定文件、详情、替换、删除、权限校验。 |
| 日报 | 创建草稿、保存、提交、列表、统计、提醒。 |
| 月报 | 创建、保存、提交、reopen、详情、统计。 |
| 总经理信箱 | 配置查询、匿名/实名提交、附件引用。 |
| 通知 | 工作台事件生成系统通知,在线用户收到 WS,离线用户重登可补偿。 |
| 回滚 | 关闭 `/v1/workbench/*` 路由后,前端回退旧版本或网关明确降级;旧代码未删前可切回旧路径。 |
## 9. 风险点
| 风险 | 处理 |
|---|---|
| 一次性拆代码和迁库导致回归面过大 | 本阶段不迁库,只拆服务和接口;数据库等接口稳定后单独迁移。 |
| 工作台依赖 IM 用户资料 | 通过 IM 内部查询 API 获取用户摘要,禁止直连 IM 库。 |
| IM 需要工作台红点 | 通过工作台内部接口或 Kafka 事件聚合,禁止 im_server 查工作台库。 |
| 文件上传仍在 im_server | 工作台保存 `file_id` 引用,文件服务后续再独立拆。 |
| Kafka 事件丢失 | 生产端直接投 Kafka,必要时使用业务失败表补偿;消费端使用幂等和死信队列。 |
| 前端路径切换风险 | 前端切到 `/v1/workbench/*` 前必须完成接口对照、灰度和回归。 |
| 数据不一致 | 当前阶段继续读写同一套租户库,避免双写不一致;后续迁库再做影子读和校验。 |
## 10. 推荐落地顺序
1. 在 `im_workbench` 建服务骨架、配置、健康检查、Redis Token 鉴权。
2. 迁移工作台类型、store interface、MySQL 实现和单测。
3. 迁移 `/v1/workbench`、组织架构、考勤接口。
4. 迁移会议、会议笔记、日报、月报、总经理信箱接口。
5. Nginx/API Gateway 增加 `/v1/workbench/*` 路由,默认关闭或只对测试环境开放。
6. 本地环境用新路径跑通全部工作台接口。
7. 测试环境启用新路由灰度,只切少量测试账号。
8. 全量接口稳定后,确认没有客户端访问旧 `/v1/im/*` 工作台路径。
9. 清理 `im_server` 内工作台业务实现,只保留 IM 主链路。
10. 单独启动数据库迁移专项,新建工作台库、复制数据、影子读、切读写。
## 11. 当前建议先做的第一步
第一步不要急着删 `im_server` 代码,建议先在 `im_workbench` 完成最小可运行服务:
1. 监听端口建议:`7070`。
2. 支持 `GET /healthz`、`GET /readyz`。
3. 支持 Redis Token 鉴权和统一 Header 校验。
4. 接入本地 MySQL、Redis、Kafka 配置。
5. 先迁 `GET /v1/workbench` 一个读接口做样板。
6. Nginx/API Gateway 加 `/v1/workbench` 测试路由,只开放样板接口。
这样第一步上线风险最低,也能验证服务拆分、Redis 鉴权、链路追踪、配置和部署是否顺畅。
+331 -66
View File
@@ -7,6 +7,7 @@
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
-- 5. Non-auto-increment BIGINT primary keys are generated by the server Snowflake generator.
SET NAMES utf8mb4;
@@ -54,12 +55,14 @@ CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config (
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
auth_version INT NOT NULL DEFAULT 1,
password_updated_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
PRIMARY KEY (id),
UNIQUE INDEX uk_im_user_security_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
@@ -107,24 +110,10 @@ CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications (
INDEX idx_im_realname_id_hash (id_number_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_action_tokens (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
token_id VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
target VARCHAR(128) NOT NULL DEFAULT '',
token_hash CHAR(64) NOT NULL,
state SMALLINT NOT NULL,
expires_at DATETIME(3) NOT NULL,
consumed_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_action_tokens_token (token_id),
INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
DROP TABLE IF EXISTS tinode.im_action_tokens;
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
purpose VARCHAR(64) NOT NULL,
password_hash VARBINARY(255) NOT NULL,
@@ -136,7 +125,8 @@ CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
locked_until DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id, purpose)
PRIMARY KEY (id),
UNIQUE INDEX uk_im_youth_mode_settings_user_purpose (user_id, purpose)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
@@ -159,43 +149,9 @@ CREATE TABLE IF NOT EXISTS tinode.im_system_notifications (
INDEX idx_im_system_notifications_inbox (user_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_http_idempotency (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
method VARCHAR(8) NOT NULL,
route VARCHAR(512) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
body_hash CHAR(64) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'processing',
response_status INT NOT NULL DEFAULT 0,
response_body MEDIUMBLOB,
expires_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_http_idempotency_scope (user_id, method, route, idempotency_key),
INDEX idx_im_http_idempotency_expiry (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_business_events_outbox (
id BIGINT NOT NULL AUTO_INCREMENT,
event_id VARCHAR(64) NOT NULL,
target_type VARCHAR(16) NOT NULL,
target VARCHAR(128) NOT NULL,
actor_user_id BIGINT NOT NULL DEFAULT 0,
event_type VARCHAR(64) NOT NULL,
payload JSON,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
next_retry_at DATETIME(3),
delivered_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_business_events_outbox_event (event_id),
INDEX idx_im_business_events_outbox_pending (status, next_retry_at, id),
INDEX idx_im_business_events_outbox_target (target_type, target, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- HTTP write idempotency is Redis-backed in the latest code.
DROP TABLE IF EXISTS meeting_im.im_http_idempotency;
DROP TABLE IF EXISTS tinode.im_http_idempotency;
CREATE TABLE IF NOT EXISTS tinode.im_message_revisions (
id BIGINT NOT NULL AUTO_INCREMENT,
@@ -234,6 +190,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_friend_requests (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
peer_id BIGINT NOT NULL,
status VARCHAR(16) NOT NULL,
@@ -241,16 +198,19 @@ CREATE TABLE IF NOT EXISTS tinode.im_friendships (
topic CHAR(25) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id, peer_id),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_friendships_pair (user_id, peer_id),
INDEX idx_im_friendships_peer (peer_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
id BIGINT NOT NULL,
blocker_id BIGINT NOT NULL,
blocked_id BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (blocker_id, blocked_id),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_user_blocks_pair (blocker_id, blocked_id),
INDEX idx_im_user_blocks_blocked (blocked_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -434,10 +394,24 @@ CREATE TABLE IF NOT EXISTS tinode.im_org_members (
CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_org_visibility_overrides (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
visibility_scope VARCHAR(16) NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
remark VARCHAR(256) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_org_visibility_user (user_id),
INDEX idx_im_org_visibility_scope (visibility_scope, state)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_meetings (
id BIGINT NOT NULL AUTO_INCREMENT,
meeting_id VARCHAR(64) NOT NULL,
organizer_id BIGINT NOT NULL,
recorder_user_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
start_at DATETIME(3) NOT NULL,
end_at DATETIME(3) NOT NULL,
@@ -453,6 +427,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_meetings (
UNIQUE INDEX uk_im_meetings_meeting (meeting_id),
UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key),
INDEX idx_im_meetings_organizer (organizer_id, start_at),
INDEX idx_im_meetings_recorder (recorder_user_id, end_at),
INDEX idx_im_meetings_state_time (state, start_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -471,6 +446,54 @@ CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants (
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'im_meetings' AND COLUMN_NAME = 'recorder_user_id') = 0,
'ALTER TABLE tinode.im_meetings ADD COLUMN recorder_user_id BIGINT NOT NULL DEFAULT 0 AFTER organizer_id',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
UPDATE tinode.im_meetings SET recorder_user_id = organizer_id WHERE recorder_user_id = 0;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'im_meetings' AND INDEX_NAME = 'idx_im_meetings_recorder') = 0,
'CREATE INDEX idx_im_meetings_recorder ON tinode.im_meetings(recorder_user_id, end_at)',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
CREATE TABLE IF NOT EXISTS tinode.im_meeting_note_files (
id BIGINT NOT NULL AUTO_INCREMENT,
note_file_id VARCHAR(64) NOT NULL,
meeting_id VARCHAR(64) NOT NULL,
file_id VARCHAR(64) NOT NULL,
uploader_id BIGINT NOT NULL,
file_name VARCHAR(255) NOT NULL DEFAULT '',
mime_type VARCHAR(128) NOT NULL DEFAULT '',
file_size BIGINT NOT NULL DEFAULT 0,
file_etag VARCHAR(128) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL DEFAULT 'active',
preview_status VARCHAR(16) NOT NULL DEFAULT 'ready',
preview_mode VARCHAR(16) NOT NULL DEFAULT 'native',
preview_file_id VARCHAR(64) NOT NULL DEFAULT '',
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
deleted_by BIGINT,
deleted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_meeting_note_files_id (note_file_id),
INDEX idx_im_meeting_note_files_file (meeting_id, file_id, status),
UNIQUE INDEX uk_im_meeting_note_files_idempotency (meeting_id, uploader_id, idempotency_key),
INDEX idx_im_meeting_note_files_meeting (meeting_id, status, id DESC),
INDEX idx_im_meeting_note_files_uploader (uploader_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_daily_reports (
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
@@ -553,6 +576,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions (
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
nickname VARCHAR(64) NOT NULL DEFAULT '',
@@ -560,26 +584,116 @@ CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
signature VARCHAR(256) NOT NULL DEFAULT '',
gender VARCHAR(16) NOT NULL DEFAULT '',
region VARCHAR(128) NOT NULL DEFAULT '',
email VARCHAR(128) NOT NULL DEFAULT '',
public_account VARCHAR(64) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
PRIMARY KEY (id),
UNIQUE INDEX uk_im_user_profiles_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Existing database migration (the application performs the same migration
-- idempotently at startup):
-- 1. Add im_users.phone VARCHAR(32) NULL and im_users.username VARCHAR(32).
-- 2. Backfill username from im_auth.uname after removing the basic: prefix;
-- use phone or reserved_<id> only when no Basic login exists.
-- 3. Add im_user_profiles.email VARCHAR(128) NOT NULL DEFAULT ''.
-- 4. Backfill phone/email from validated im_credentials first, then use
-- tel:/email: rows in im_usertags as a legacy fallback.
-- 5. Verify uniqueness, then make username NOT NULL and add UNIQUE(username).
-- 6. Drop im_auth.uname and im_auth.scheme; add UNIQUE(userid).
-- 7. Drop im_usertags and im_users.tags only after the backfill is verified.
-- im_topics.tags and im_topictags are topic discovery data and must be retained.
CREATE TABLE IF NOT EXISTS hotgo.im_registration_invite_batches (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
quantity INT UNSIGNED NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
expires_at DATETIME(3) NOT NULL,
created_by BIGINT NOT NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX uk_im_registration_invite_batch_tenant_idempotency (tenant_id, idempotency_key),
INDEX idx_im_registration_invite_batch_tenant_created (tenant_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS hotgo.im_registration_invite_codes (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
batch_id BIGINT,
code CHAR(5) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'active',
expires_at DATETIME(3) NOT NULL,
usage_count INT UNSIGNED NOT NULL DEFAULT 0,
last_used_at DATETIME(3),
created_by BIGINT NOT NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX uk_im_registration_invite_code_tenant (tenant_id, code),
INDEX idx_im_registration_invite_tenant_status_expires (tenant_id, status, expires_at),
INDEX idx_im_registration_invite_tenant_batch (tenant_id, batch_id),
INDEX idx_im_registration_invite_tenant_created (tenant_id, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_registration_invite_uses (
id BIGINT NOT NULL,
invite_code_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
request_id VARCHAR(64) NOT NULL,
used_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
INDEX idx_im_registration_invite_use_code (invite_code_id),
UNIQUE INDEX uk_im_registration_invite_use_user (user_id),
INDEX idx_im_registration_invite_use_request (request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Legacy default-tenant migration. For additional tenant databases, repeat
-- this copy with that database name and its hotgo.im_tenant.id before drop.
SET @im_sql := IF(
(SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='tinode' AND TABLE_NAME='im_registration_invite_batches') > 0,
'INSERT INTO hotgo.im_registration_invite_batches (id,tenant_id,quantity,idempotency_key,expires_at,created_by,remark,created_at,updated_at) SELECT id,1,quantity,idempotency_key,expires_at,created_by,remark,created_at,updated_at FROM tinode.im_registration_invite_batches ON DUPLICATE KEY UPDATE updated_at=VALUES(updated_at)',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql := IF(
(SELECT COUNT(*) FROM information_schema.TABLES WHERE TABLE_SCHEMA='tinode' AND TABLE_NAME='im_registration_invite_codes') > 0,
'INSERT INTO hotgo.im_registration_invite_codes (id,tenant_id,batch_id,code,status,expires_at,usage_count,last_used_at,created_by,remark,created_at,updated_at) SELECT c.id,1,c.batch_id,c.code,CASE WHEN c.status=''used'' THEN IF(c.expires_at<=NOW(3),''expired'',''active'') ELSE c.status END,c.expires_at,COUNT(u.id),MAX(u.used_at),c.created_by,c.remark,c.created_at,c.updated_at FROM tinode.im_registration_invite_codes c LEFT JOIN tinode.im_registration_invite_uses u ON u.invite_code_id=c.id GROUP BY c.id ON DUPLICATE KEY UPDATE usage_count=VALUES(usage_count),last_used_at=VALUES(last_used_at),updated_at=VALUES(updated_at)',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
DROP TABLE IF EXISTS tinode.im_registration_invite_codes;
DROP TABLE IF EXISTS tinode.im_registration_invite_batches;
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
add_me_methods JSON,
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
read_receipts TINYINT NOT NULL DEFAULT 1,
friend_request_verification TINYINT NOT NULL DEFAULT 1,
group_invite_verification TINYINT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
PRIMARY KEY (id),
UNIQUE INDEX uk_im_user_privacy_settings_user (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
-- 全局客户端版本策略属于 Admin 控制库,不按租户拆分。
CREATE TABLE IF NOT EXISTS hotgo.im_app_releases (
id BIGINT NOT NULL AUTO_INCREMENT,
platform VARCHAR(32) NOT NULL,
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
@@ -599,6 +713,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
id BIGINT NOT NULL,
device_id VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
provider VARCHAR(32) NOT NULL,
@@ -607,7 +722,8 @@ CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
platform VARCHAR(32) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (device_id),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_device_push_tokens_device (device_id),
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -706,6 +822,113 @@ CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions (
INDEX idx_im_feedback_submissions_user (user_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Attendance.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_attendance_rules (
id BIGINT NOT NULL AUTO_INCREMENT,
rule_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
workday TINYINT NOT NULL DEFAULT 1,
location_required TINYINT NOT NULL DEFAULT 0,
wifi_required TINYINT NOT NULL DEFAULT 0,
allow_outside_checkin TINYINT NOT NULL DEFAULT 1,
appeal_allowed TINYINT NOT NULL DEFAULT 1,
effective_from DATE,
effective_to DATE,
state VARCHAR(16) NOT NULL DEFAULT 'active',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_attendance_rules_rule (rule_id),
INDEX idx_im_attendance_rules_state (state, effective_from, effective_to)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_attendance_shifts (
id BIGINT NOT NULL AUTO_INCREMENT,
shift_id VARCHAR(64) NOT NULL,
rule_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
work_start TIME NOT NULL,
work_end TIME NOT NULL,
checkin_start TIME NOT NULL,
checkout_end TIME NOT NULL,
state VARCHAR(16) NOT NULL DEFAULT 'active',
sort_order INT NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_attendance_shifts_shift (shift_id),
INDEX idx_im_attendance_shifts_rule (rule_id, state, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_attendance_records (
id BIGINT NOT NULL AUTO_INCREMENT,
record_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
record_date DATE NOT NULL,
rule_id VARCHAR(64) NOT NULL,
shift_id VARCHAR(64) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'not_checked',
exceptions JSON,
appeal_status VARCHAR(32) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_attendance_records_record (record_id),
UNIQUE INDEX uk_im_attendance_records_user_date_shift (user_id, record_date, shift_id),
INDEX idx_im_attendance_records_user_id (user_id, id DESC),
INDEX idx_im_attendance_records_date_status (record_date, status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_attendance_checkins (
id BIGINT NOT NULL AUTO_INCREMENT,
checkin_id VARCHAR(64) NOT NULL,
record_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
record_date DATE NOT NULL,
action VARCHAR(32) NOT NULL,
rule_id VARCHAR(64) NOT NULL,
shift_id VARCHAR(64) NOT NULL,
client_time DATETIME(3) NOT NULL,
server_time DATETIME(3) NOT NULL,
result VARCHAR(32) NOT NULL,
location JSON,
wifi JSON,
device JSON,
idempotency_key VARCHAR(128) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_attendance_checkins_checkin (checkin_id),
UNIQUE INDEX uk_im_attendance_checkins_idempotency (user_id, idempotency_key),
INDEX idx_im_attendance_checkins_record (record_id, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_attendance_appeals (
id BIGINT NOT NULL AUTO_INCREMENT,
appeal_id VARCHAR(64) NOT NULL,
record_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
appeal_type VARCHAR(64) NOT NULL,
reason VARCHAR(512) NOT NULL,
target_action VARCHAR(32) NOT NULL DEFAULT '',
target_time DATETIME(3),
attachment_file_ids JSON,
status VARCHAR(32) NOT NULL DEFAULT 'pending',
version INT NOT NULL DEFAULT 1,
idempotency_key VARCHAR(128) NOT NULL,
submitted_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_attendance_appeals_appeal (appeal_id),
UNIQUE INDEX uk_im_attendance_appeals_idempotency (user_id, idempotency_key),
INDEX idx_im_attendance_appeals_record (record_id),
INDEX idx_im_attendance_appeals_user_status (user_id, status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Seed data.
-- ============================================================
@@ -724,11 +947,13 @@ ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission)
VALUES
('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'),
('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'),
('attendance', '打卡', '/attendance', 10, 'attendance', 1, 'enabled'),
('monthly_reports', '月报', '/reports/monthly', 20, 'monthly_reports', 1, 'enabled'),
('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'),
('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'),
('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled')
('organization', '部门框架', '/organization', 40, 'organization', 1, 'enabled'),
('executive_mailbox', '举报信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled'),
('meetings', '会议通知', '/meetings', 60, 'meetings', 1, 'enabled'),
('meeting_notes', '会议笔记', '/meeting-notes', 70, '', 1, 'enabled')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
route = VALUES(route),
@@ -736,11 +961,19 @@ ON DUPLICATE KEY UPDATE
unread_key = VALUES(unread_key),
updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_attendance_rules(rule_id, name, workday, location_required, wifi_required, allow_outside_checkin, appeal_allowed)
VALUES ('att_rule_default', '默认考勤规则', 1, 0, 0, 1, 1)
ON DUPLICATE KEY UPDATE updated_at = updated_at;
INSERT INTO tinode.im_attendance_shifts(shift_id, rule_id, name, work_start, work_end, checkin_start, checkout_end, sort_order)
VALUES ('shift_default', 'att_rule_default', '默认班次', '09:00:00', '18:00:00', '08:30:00', '20:00:00', 10)
ON DUPLICATE KEY UPDATE updated_at = updated_at;
INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content)
VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。')
ON DUPLICATE KEY UPDATE updated_at = updated_at;
INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds)
INSERT INTO hotgo.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds)
VALUES
('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0)
@@ -769,5 +1002,37 @@ INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_pol
VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。')
ON DUPLICATE KEY UPDATE updated_at = updated_at;
-- ============================================================
-- Shared database runtime fixes for meeting_im.
-- These are required after moving default/system tenants into the shared DB.
-- ============================================================
CREATE TABLE IF NOT EXISTS meeting_im.im_user_account_sequences (
tenant_id BIGINT NOT NULL,
id TINYINT UNSIGNED NOT NULL,
next_value INT UNSIGNED NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (tenant_id, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
ALTER TABLE meeting_im.im_user_account_sequences
DROP PRIMARY KEY,
ADD PRIMARY KEY (tenant_id, id);
INSERT INTO meeting_im.im_user_account_sequences(tenant_id, id, next_value)
SELECT tenant_id, 1, GREATEST(COALESCE(MAX(account_no), 999), 999)
FROM meeting_im.im_users
WHERE tenant_id > 0
GROUP BY tenant_id
ON DUPLICATE KEY UPDATE
next_value = GREATEST(next_value, VALUES(next_value));
INSERT IGNORE INTO meeting_im.im_user_account_sequences(tenant_id, id, next_value)
SELECT id, 1, 999
FROM hotgo.im_tenant
WHERE state = 1;
DROP TABLE IF EXISTS meeting_im.im_login_lockouts;
-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go.
UPDATE tinode.im_kvmeta SET `value` = 123 WHERE `key` = 'version';
@@ -0,0 +1,213 @@
# IM 服务端主动推送消息梳理
> 本文先按当前代码实现梳理,后续确认产品口径后再合并到《IM-前后端对接文档》。
## 1. 术语和通道
服务端主动下行分两层:
1. Tinode 原生实时消息:通过 WebSocket/Long Polling 下发 `{data}`、`{info}`、`{pres}`、`{meta}` 等帧,覆盖聊天消息、回执、输入中、在线状态、通话信令。
2. IM 产品业务事件:HTTP 写操作或业务事务写入 `im_business_events_outbox` / 群业务 outbox 后,由服务端投递为 `{info}` 帧,前端据此刷新业务接口或局部状态。
当前产品业务事件不是聊天消息,不占普通会话消息 `seq`。系统通知列表是否出现记录,取决于是否额外写入 `im_system_notifications`。
## 2. 产品业务事件统一帧格式
### 2.1 用户维度事件
来自 `im_business_events_outbox`,投递到用户自己的在线会话。
```json
{
"info": {
"topic": "usr_target",
"from": "usr_actor",
"what": "im",
"event": "friend.request.accepted",
"payload": {
"user_id": "usr_actor"
},
"ts": "2026-08-25T10:00:00Z"
}
}
```
字段说明:
| 字段 | 说明 |
| --- | --- |
| `info.topic` | 目标用户 ID,即事件接收人 |
| `info.from` | 操作人用户 ID;系统事件可能为空 |
| `info.what` | 固定为 `im` |
| `info.event` | 产品事件类型 |
| `info.payload` | 事件载荷,按事件类型变化 |
投递机制:
- 写入 outbox 后会立即尝试投递。
- Hub 不可用或队列满时保持 pending,后续 poller 每秒按在线租户重试。
- 投递成功后标记 delivered。
- `registration.invite_used` 与 `security.password_reset` 是内部消费事件,不作为普通 WS 业务事件下发。
### 2.2 群维度事件
来自群业务 outbox,投递到群 Topic,在线群成员收到。
```json
{
"info": {
"topic": "grp_xxx",
"from": "usr_actor",
"what": "group",
"event": "group.member.joined",
"payload": {
"topic": "grp_xxx",
"user_id": "usr_xxx"
},
"ts": "2026-08-25T10:00:00Z"
}
}
```
当前群事件投递时,还会给除操作人外的群成员写入一条 `im_system_notifications`,标题为“群聊动态”,summary 暂为事件类型。
## 3. 好友和关系事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 创建好友申请 `POST /v1/im/friend-requests` | 被申请人 | `info.what="im"`, `event="friend.request.created"` | `{"user_id":"申请人"}` | 有,写给被申请人,标题“新的好友申请” |
| 同意好友申请 `POST /v1/im/friend-requests/{id}/accept` | 申请人 | `info.what="im"`, `event="friend.request.accepted"` | `{"user_id":"通过人"}` | 当前有,写给申请人,标题“好友申请已处理” |
| 同意好友申请 | 通过人本人 | `info.what="im"`, `event="friend.request.accepted"` | `{"user_id":"通过人"}` | 当前无 |
| 拒绝好友申请 `POST /v1/im/friend-requests/{id}/reject` | 申请人 | `info.what="im"`, `event="friend.request.rejected"` | `{"user_id":"拒绝人"}` | 当前有,写给申请人,标题“好友申请已处理” |
| 拒绝好友申请 | 拒绝人本人 | `info.what="im"`, `event="friend.request.rejected"` | `{"user_id":"拒绝人"}` | 当前无 |
| 删除好友 `DELETE /v1/im/friends/{user_id}` | 被删除方 | `info.what="im"`, `event="friend.deleted"` | `{"user_id":"操作人"}` | 有,标题“好友关系已解除” |
| 删除好友 | 操作人本人 | `info.what="im"`, `event="friend.deleted"` | `{"user_id":"操作人"}` | 当前无 |
| 修改好友备注 `PATCH /v1/im/friends/{user_id}` | 操作人本人 | `info.what="im"`, `event="friend.remark.updated"` | `{"user_id":"操作人"}` | 当前无 |
| 拉黑用户 `PUT /v1/im/blocks/{user_id}` | 操作人本人 | `info.what="im"`, `event="relationship.blocked"` | `{"user_id":"操作人"}` | 当前无 |
| 取消拉黑 `DELETE /v1/im/blocks/{user_id}` | 操作人本人 | `info.what="im"`, `event="relationship.unblocked"` | `{"user_id":"操作人"}` | 当前无 |
前端建议动作:
- 收到 `friend.request.created`:刷新新朋友列表 `GET /v1/im/friend-requests`,刷新 `GET /v1/im/bootstrap` 的 `entry_badges.new_friends_pending_count`。
- 收到 `friend.request.accepted/rejected`:刷新新朋友列表;accepted 时刷新好友列表、P2P 会话 context。
- 收到 `friend.deleted/relationship.blocked/relationship.unblocked`:刷新好友列表、黑名单列表、会话 context,重新判断输入区 `can_send`。
- 收到 `friend.remark.updated`:刷新好友列表和会话头显示名。
当前待确认点:
- 好友申请通过/拒绝、删除好友等操作人本人是否也要写入系统消息列表。当前代码只给对方或申请人写系统通知,操作人本人主要靠 WS 业务事件刷新。
- 对接文档现有示例写成 `sys` topic `{data}` 的 `system_event`,但当前代码实际实时推送为 `{info what:"im"}`。后续需要统一口径。
## 4. 消息编辑和撤回事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 撤回消息 `POST /v1/im/messages/{topic}/{seq}/recall` | Topic 在线成员 | `info.what="im"`, `event="message.recalled"` | `{"topic":"...","seq":1,"version":2}` | 无 |
| 编辑消息 `POST /v1/im/messages/{topic}/{seq}/revisions` | Topic 在线成员 | `info.what="im"`, `event="message.edited"` | `{"topic":"...","seq":1,"version":2}` | 无 |
前端建议动作:
- 收到 `message.recalled`:按 `topic + seq + version` 重新拉取或本地标记该消息为已撤回。
- 收到 `message.edited`:按 `topic + seq + version` 重新拉取消息详情或修订记录,更新气泡内容。
注意:
- 这两个事件当前 target_type 是 `topic`,投递到 Topic,而不是某一个用户。
- 数据库层和 HTTP 层都可能写入同类 outbox,依赖 `event_id` 幂等避免重复。
## 5. 群业务事件
| 触发点 | 接收方 | WS 事件 | payload 主要字段 | 系统通知 |
| --- | --- | --- | --- | --- |
| 创建群 | 群成员 | `info.what="group"`, `event="group.created"` | `topic`、群基础信息 | 除操作人外写入 |
| 更新群设置 | 群成员 | `event="group.settings.updated"` | `topic` | 除操作人外写入 |
| 成员资料/权限更新 | 群主、管理员、本人 | `event="group.member.updated"` | `user_id`、`member_sync=single` | 不写群系统消息 |
| 成员加入 | 群成员 | `event="group.member.joined"` | `user_id`、`topic`、`member_sync=single` | 写入群消息并参与个人未读与离线 Push |
| 入群申请创建 | 群主、管理员、申请人 | `event="group.join_request.created"` | `request_id`、`applicant_user_id`、`topic` | 普通成员不可见 |
| 入群申请处理 | 群主、管理员、申请人 | `event="group.join_request.accepted/rejected"` | `request_id`、`status`、`topic` | 普通成员不可见;接受时另发全群加入消息 |
| 群公告更新 | 群成员 | `event="group.announcement.updated"` | `topic`、公告信息 | 除操作人外写入 |
| 群禁言更新 | 群主、管理员、被操作人 | `event="group.mute.updated"` | `topic`、禁言信息、`member_sync` | 普通成员不可见 |
| 成员离群/移除 | 群主、管理员、离群成员 | `event="group.member.left"` | `user_id`、`member_sync=remove` | 普通成员不可见 |
| 群角色更新 | 群主、管理员、被操作人 | `event="group.role.updated"` | `user_id`、角色信息、`member_sync` | 普通成员不可见 |
| 群主转让 | 群成员 | `event="group.owner.transferred"` | `previous_owner`、`new_owner` | 除操作人外写入 |
前端建议动作:
- 首次进入、断线重连、本地无快照或收到 `member_sync=full`:分页拉取完整群成员。
- 收到 `member_sync=single`:调用 `GET /v1/im/groups/{topic}/members/{user_id}` 只刷新一个成员。
- 收到 `member_sync=remove`:从本地成员快照删除 `user_id`;若本地状态无法确认,再全量分页同步。
- 入群申请变化:管理员刷新入群申请列表和群 summary 待处理数。
- 群公告变化:刷新公告区。
- 成员加入/离开:刷新群成员数、成员列表和会话系统提示。
注意:
- 当前群事件实时帧 `what="group"`,不是 `what="im"`。
- 群系统通知的 summary 当前主要是事件类型,文案还比较粗,需要产品确认后优化。
## 6. 会议事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 创建会议 | 所有参会人 | `info.what="im"`, `event="meeting.created"` | `{"meeting_id":"...","version":1,"state":"scheduled"}` | 除操作者外写入,标题“会议通知” |
| 更新会议 | 所有参会人 | `event="meeting.updated"` | `meeting_id/version/state` | 除操作者外写入 |
| 取消会议 | 所有参会人 | `event="meeting.canceled"` | `meeting_id/version/state` | 除操作者外写入 |
| 发送会议提醒 | 待响应参会人 | `event="meeting.reminded"` | `meeting_id/version/state` | 除操作者外写入 |
| 更新参会响应 | 所有参会人 | `event="meeting.response.updated"` | `meeting_id/version/state` | 除操作者外写入 |
前端建议动作:
- 收到会议事件后刷新会议详情或会议列表。
- `meeting.reminded` 可触发本地提醒 UI。
## 7. 安全事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 风险挑战确认/取消 | 操作人本人 | `info.what="im"`, `event="security.risk.continue"` 或 `security.risk.cancel` | `{"risk_challenge_id":"...","decision":"continue|cancel"}` | 有,标题“安全操作确认” |
| 密码重置成功 | 无普通 WS 事件 | 内部事件 `security.password_reset` | `{"user_id":123}` | 当前用于清退该用户在线 session |
前端建议动作:
- 收到 `security.risk.*`:刷新安全验证页面状态。
- 密码重置后旧 token/session 会被服务端清退,客户端按登录失效流程处理。
## 8. 邀请码统计事件
| 触发点 | 接收方 | WS 事件 | payload | 系统通知 |
| --- | --- | --- | --- | --- |
| 注册成功使用邀请码 | 不下发给普通客户端 | 内部事件 `registration.invite_used` | `{"tenant_id":1,"invite_code_id":123,"user_id":456}` | 无 |
说明:
- 该事件当前由 IM 侧消费,用于把租户库 `im_registration_invite_uses` 聚合回 HotGo 控制库的邀请码统计。
- 普通 App 前端不需要监听。
## 9. Tinode 原生实时下行
这类不是产品 outbox 事件,但也是服务端主动下行,前端需要持续处理。
| 帧类型 | 触发点 | 接收方 | 前端动作 |
| --- | --- | --- | --- |
| `{data}` | 普通聊天消息、文件消息、通话邀请 `webrtc=started`、服务端替换通话结果消息 | Topic 订阅者 | 写入/更新会话时间线、更新未读 |
| `{info what:"read"}` | 对端已读回执 | Topic 订阅者 | 更新消息已读状态 |
| `{info what:"recv"}` | 对端送达回执 | Topic 订阅者 | 更新送达状态 |
| `{info what:"kp"}` | 对端输入中 | Topic 订阅者 | 显示输入中状态,超时自动消失 |
| `{info what:"call"}` | WebRTC `ringing/accept/offer/answer/ice-candidate/hang-up` | 通话对端 | 驱动通话状态机和 SDP/ICE 交换 |
| `{pres}` | 订阅、离线、在线、权限/订阅变化等 presence | 相关订阅者 | 更新在线状态、订阅状态、必要时刷新列表 |
| `{meta}` | 服务端响应 get/sub 中的 desc/sub/data/del/cred 等查询结果 | 请求方 | 合并服务端状态 |
| `{ctrl}` | 客户端请求结果 | 请求方 | 更新请求状态、错误提示、幂等结果 |
## 10. 当前文档与实现差异
当前《IM-前后端对接文档》部分位置把好友关系变化描述为 `sys` topic 的 `{data head.x-im-type="system_event"}`。代码当前更准确的模型是:
- 实时刷新事件:`{info what:"im"}` 或 `{info what:"group"}`。
- 系统通知列表:通过 `im_system_notifications` 暴露给 `GET /v1/im/system-notifications`,并不是所有实时事件都会写入。
- 聊天/系统 Topic `{data}` 仍用于普通消息时间线和 Tinode 原生 Topic 消息,不等同于业务 outbox 实时事件。
建议后续合并到对接文档时明确:
1. 前端必须监听所有 `{info}`,按 `what + event` 分发业务刷新。
2. 系统消息入口的未读数来自 `im_system_notifications`,不是 `{info}` 事件数量。
3. 好友申请通过后是否双方都写系统通知,需要产品确认;若确认双方都有,需要同步补代码。
@@ -0,0 +1,470 @@
# IM 服务端架构与新功能开发指南
> 文档状态:按 2026-09-08 代码实现整理
> 适用项目:`im_server`
> 文档目标:让新接手的开发者能够快速判断一个功能应放在哪里、如何调用、如何存储、如何做多节点一致性,以及如何验收。
## 1. 系统定位
`im_server` 是基于 Tinode 扩展的多租户 IM 业务服务,同时承担两类职责:
1. IM 实时核心:WebSocket 连接、登录会话、Topic 订阅、消息路由、在线状态、音视频信令。
2. C 端产品业务:注册、安全、好友、群治理、收藏、文件、工作台、会议、日报、举报等 HTTP API。
系统不是传统的 `controller -> service -> repository` 三层单体。它的核心是“Topic Actor 实时模型 + HTTP 业务扩展层 + Store 统一持久化边界”。开发新功能时必须先判断它属于实时 Topic 链路,还是 HTTP 业务链路,或者同时涉及两者。
## 2. 系统边界
### 2.1 服务与数据库归属
| 服务 | 直连数据库 | 负责数据 |
| --- | --- | --- |
| IM Server | 租户业务库,默认库为 `tinode` | 用户、认证、Topic、订阅、私聊/群聊消息、群业务、好友关系、文件元数据、C 端业务数据 |
| Admin / HotGo | `hotgo` | 租户注册表、系统配置、字典、后台账号、菜单权限、后台命令 Outbox、管理端摘要数据 |
原则:服务与库一一对应,Admin 不直连租户库,IM 不直连 `hotgo`。跨系统写操作通过 Kafka 命令/事件,必须同步返回的少量查询通过受控的内部 HTTP API。
### 2.2 租户隔离
目标模型是“一个租户一个业务库”:
- `tenant_id` 是请求上下文和路由键,不要因为租户库内没有 `tenant_id` 列就在代码中丢掉租户上下文。
- 所有业务 Store 调用必须显式传递 `types.TenantID`。
- `store.ForTenant` 和 `tenantBusinessAdapter` 负责验证租户与选择租户适配器。
- MySQL 的 `TenantRoutedAdapter.TenantAdapterFor` 负责将租户路由到对应连接池。
- 任何无租户上下文的业务查询都应失败关闭,不允许回退到旧的无租户查询。
## 3. 技术栈
| 领域 | 当前实现 | 用途 |
| --- | --- | --- |
| 语言 | Go 1.26 | 服务端主体 |
| 实时协议 | Tinode JSON Protocol、WebSocket、Long Polling、gRPC | 长连接、Topic 订阅、消息和集群通信 |
| HTTP | `net/http` + `http.ServeMux` | C 端 REST API、文件 API、内部 API |
| 数据库 | MySQL + `database/sql` + `sqlx` | 租户业务最终状态 |
| 热状态/缓存 | Redis 7,`go-redis/v9` | 在线状态、Seq、幂等、群热消息、对象缓存、关系 Set、限流、跨节点 fanout |
| 异步事件 | Kafka,`segmentio/kafka-go` | 跨系统命令、消息异步持久化、审计、缓存失效、失败重试与死信 |
| 文件 | 阿里云 OSS,保留 FS/S3 适配器 | 文件对象存储,业务库只存元数据和引用 |
| 文件安全 | 阿里云内容安全 | 上传完成后的异步审核 |
| 离线推送 | JPush,同时保留 FCM/TNPG 插件能力 | 离线提醒;客户端重连后按 Topic seq 补偿 |
| 音视频 | Tinode 通话信令 + 声网 RTC Token | 呼叫邀请/接听/挂断与 RTC 加入 |
| 认证 | basic/token/code/rest 可插拔认证器 | 密码、Token、验证码和外部认证 |
| 可观测性 | Go `log`、expvar、Prometheus exporter、pprof | 日志、运行指标和性能诊断 |
## 4. 运行时架构
```text
Client
|-- WebSocket /v0/channels
| -> Session -> Hub -> Topic actor -> Store/Redis
| |-> local sessions
| `-> Redis fanout -> other IM nodes -> WebSocket
|
|-- HTTP /v1/im/*
| -> protocol middleware -> bearer/API key/tenant
| -> handler -> Store mapper -> tenant adapter -> tenant MySQL
| |-> Redis cache/short state
| `-> business event -> online WebSocket/Kafka
|
`-- Upload/Download
-> media handler -> OSS
-> file metadata/link -> tenant MySQL
`-> file security task -> Kafka -> Aliyun security
Admin / HotGo
|-- Kafka admin command/config event -> IM consumer -> tenant business mutation
`-- signed internal HTTP <-------> IM internal read API
```
### 4.1 启动顺序
`server/main.go` 是服务入口,主要顺序为:
1. 读取 JSON with comments 格式的 `meetingim.conf`/`tinode.conf`。
2. 初始化日志、expvar、pprof 和集群 worker ID。
3. 打开 Store,校验数据库版本与租户适配能力,同时初始化 Redis runtime。
4. 初始化 Kafka、IM 内部 API 和 Admin 内部配置 API 客户端。
5. 初始化认证器、验证器、文件处理器、Push 和音视频。
6. 创建 `SessionStore` 和 `Hub`,启动 Redis 群消息 fanout 订阅。
7. 启动 Kafka Admin Command、Config Event、Cache Invalidate、Message Persist 和 Dead Letter 消费者。
8. 启动集群、gRPC、HTTP/WebSocket 路由和优雅退出。
任何新增的基础组件都应明确放在这个顺序中,初始化失败时应快速终止启动,不要让必需依赖在不可用状态下继续运行。
## 5. 核心业务模型
### 5.1 Session
`Session` 表示一条客户端连接,保存租户、用户、设备、认证等级、已订阅 Topic 和输出队列。一个用户可以同时有多个 Session。
登录和断线时需同步 Redis 在线状态;心跳用于刷新 presence TTL。不应仅依赖本进程 Session 判断全局在线状态。
### 5.2 Hub
`Hub` 是 Topic 路由中心,核心通道包括:
| 通道 | 用途 |
| --- | --- |
| `join` | 加载/创建 Topic,将 Session 附加到 Topic |
| `routeCli` | 客户端消息路由到 Topic |
| `routeSrv` | 服务端业务消息路由到 Topic/Session |
| `meta` | 未订阅 Topic 的元数据查询 |
| `unreg` | 卸载或删除 Topic |
| `userStatus` | 用户停用/恢复等状态变更 |
Topic 索引不是纯字符串,而是 `types.TopicKey{TenantID, Topic}`,以防止不同租户的同名 Topic 串数据。
### 5.3 Topic Actor
`Topic` 是会话和消息处理单元,通过 channel 串行处理加入、退出、客户端消息、服务端消息和元数据操作。主要分类包括:
| Topic | 含义 |
| --- | --- |
| `me` | 用户主 Topic,会话列表和用户级 presence |
| `fnd` | 搜索/发现 Topic |
| `usr...` | 客户端可见的 P2P 对端标识,入库前需归一化为真实 P2P Topic |
| `grp...` | 群聊 Topic |
| `chn...` | 频道 Topic |
| `slf...` | 个人收藏/保存消息 Topic |
| `sys...` | 系统通知 Topic |
涉及消息发送、已读、撤回、编辑、通话信令、在线投递时,优先复用 Topic/Session 链路,不要另造一套无状态 HTTP 投递逻辑。
## 6. 四类标准调用链路
### 6.1 WebSocket 实时消息
```text
Client packet
-> hdl_websock.go 解码
-> Session 认证/租户上下文
-> Hub.join / Hub.routeCli
-> Topic 权限和业务校验
-> Redis 原子 Seq + client_mid 幂等
-> 本地 Session 实时投递
-> Redis Pub/Sub 跨节点 fanout
-> Kafka persist 异步落 MySQL
```
群消息使用 `im_group_messages`,私聊仍使用私聊消息表。`SeqId` 全链路必须使用 `int64/BIGINT`,群 Topic seq 由 Redis 原子递增,数据库只作基线与最终持久化。
群热消息可在 Redis 保留 24 小时左右,历史补偿最终以 MySQL 为准。离线用户只接收 Push 提醒,重连后按 Topic seq 拉取缺口。
### 6.2 C 端 HTTP 业务请求
```text
/v1/im/*
-> withIMHTTPProtocol
-> API key + X-IM-Schema + X-Request-ID
-> Bearer token -> principal{tenant_id, uid}
-> 写请求 Idempotency-Key 中间件
-> serveIM... handler
-> store.<Domain> mapper
-> TenantBusinessAdapter
-> tenant MySQL / Redis
-> writeIMSuccess / writeIMError
```
所有已认证的 `POST/PUT/PATCH/DELETE` 默认要求 `Idempotency-Key`,幂等结果保留 24 小时。同一 key 使用不同请求体必须返回冲突,已完成请求应重放原响应。
HTTP 统一响应包含 `code/data/reason/message/details/request_id/server_time`。业务错误应返回稳定 `reason`,不应将 SQL 或内部异常直接暴露给客户端。
### 6.3 Admin 后台写操作
```text
Admin API
-> hotgo 本地权限校验
-> 同事务写 command + outbox
-> Kafka im.admin.command
-> IM consumer 校验 tenant/event/actor
-> event_id + consumer_group 幂等抢占
-> 租户库业务变更
-> 缓存失效 + Topic/Session 同步
-> Kafka im.admin.command.result
-> Admin 更新命令结果
```
不可重试的参数、权限、版本冲突应直接返回稳定失败原因。临时故障按退避策略重试,超过上限写 `im.dead.letter` 和租户失败表。人工重放必须生成新 `event_id`,并使用 `parent_event_id` 串起审计链。
### 6.4 文件链路
```text
Client request upload
-> IM 创建文件元数据
-> OSS 上传/回调完成
-> 租户库更新文件状态
-> 绑定消息/群文件/业务引用
-> Kafka im.file.security
-> 阿里云安全检测
-> 更新审核状态并通知用户
```
文件字节流不经 Kafka,Kafka 只传递文件 ID、OSS 对象信息和审核任务。任何业务文件都必须建立引用记录,否则垃圾回收可能将它当作孤儿文件删除。
## 7. 数据分层与一致性
### 7.1 进程内缓存
适合低频变更、全局或租户级、可短时间最终一致的数据:
- 租户基本配置和状态。
- 注册短信开关。
- 字典和第三方配置(OSS、文件安全、短信、JPush、Agora)。
更新来源为定时刷新和 Kafka `im.config.event`/`im.cache.invalidate`。进程内缓存不应与 Redis 重复缓存同一份高频业务对象。
### 7.2 Redis
Redis 用于跨节点共享且高频的数据:
- 用户在线状态、Session 和群成员节点索引。
- 群消息原子 `SeqId`、`client_mid` 唯一占位和状态。
- 24 小时左右的群热消息。
- 用户、Topic、Subscription、群设置、群成员、成员分页快照和未读数缓存。
- 好友/黑名单 Set。
- 验证码、Action Token、临时占位、限流等 TTL 状态。
- 群消息 Redis Pub/Sub 跨节点实时 fanout。
Redis key 必须包含统一前缀和租户维度,不允许跨租户共用业务 key。写 MySQL 成功后必须失效或刷新对应 Redis 数据,多节点通过 Kafka 失效事件收敛。
### 7.3 MySQL
MySQL 是最终业务状态、历史查询和审计依据。以下数据不得仅存 Redis:
- 用户、凭据、好友、黑名单、Topic、订阅和群治理最终状态。
- 私聊/群聊历史消息、撤回和编辑版本。
- 文件元数据、业务引用和安全审核结果。
- HTTP 幂等结果、Kafka 消费幂等、死信和审计数据。
### 7.4 一致性顺序
普通业务写的推荐顺序:
1. 校验租户、身份、权限、幂等键和业务版本。
2. 在同一事务中更新 MySQL 业务状态及 outbox(需要可靠事件时)。
3. 提交事务。
4. 失效本节点 Redis/进程内缓存。
5. 投递 Kafka 失效或业务事件,通知其他节点和客户端。
如果业务要求“先实时到达,再异步落库”,则必须先完成 Redis 幂等占位和 seq 分配,投递 Kafka 持久化事件,并为失败准备重试和死信补偿。
## 8. Kafka 责任边界
| Topic | 主要用途 |
| --- | --- |
| `im.admin.command` | Admin 到 IM 的业务写命令 |
| `im.admin.command.result` | IM 回传命令结果 |
| `im.config.event` | 租户、字典、第三方配置变更 |
| `im.business.event` | 好友、群、会议、工作台等 C 端业务事件 |
| `im.message.persist` | 私聊消息异步落库 |
| `im.group.message.persist` | 群聊消息异步落库 |
| `im.message.push` | 离线 Push 任务,属增强链路 |
| `im.file.security` | 文件安全检测 |
| `im.cache.invalidate` | 多节点缓存失效 |
| `im.audit.event` | 跨系统审计 |
| `im.dead.letter` | 超过重试上限的事件 |
Kafka 不用于 WebSocket 建连、验证码、在线状态、实时群消息 fanout、OSS 字节流或必须当场返回结果的查询。
统一 Envelope 至少包含:`event_id`、`trace_id`、`tenant_id/tenant_code`、`source_service`、`target_service`、`event_type`、`biz_type`、`biz_id`、`idempotency_key`、`version`、`occurred_at`、`actor`、`payload`。
## 9. 代码组织
| 路径 | 职责 | 新功能常见修改 |
| --- | --- | --- |
| `server/main.go` | 进程入口、组件初始化、HTTP 路由注册 | 新增路由或新基础组件启动 |
| `server/session.go` | 连接、登录、请求分发、设备和 Session 状态 | 账号登录或连接级功能 |
| `server/hub.go` | Topic 加载与全局消息路由 | 路由、Topic 生命周期 |
| `server/topic.go` / `init_topic.go` | Topic actor、权限、订阅和消息处理 | 消息、群权限、Topic 内实时行为 |
| `server/datamodel.go` | WebSocket 客户端/服务端协议结构 | 协议字段或服务端推送类型 |
| `server/im_http.go` | C 端产品 HTTP API 的路由处理与通用约定 | 大部分 C 端业务入口 |
| `server/internal_api.go` | IM 向 Admin 提供的签名内部查询 API | 后台需要同步读取租户数据 |
| `server/admin_internal_config.go` | IM 调用 Admin 内部配置 API | IM 需要 hotgo 配置兜底查询 |
| `server/kafka_*.go` | Kafka runtime、生产、消费、重试、死信和各业务 handler | 跨系统或异步事件 |
| `server/im_business_events.go` | 业务事件持久化与在线投递 | 需主动推给 C 端的状态变更 |
| `server/redis_fanout.go` | 群消息跨节点实时投递 | 群大并发/未加载 Topic 投递 |
| `server/store/types/` | 业务实体、值对象、查询参数和稳定错误 | 先定义领域数据结构 |
| `server/store/store.go` | Store mapper、参数验证、租户边界、缓存编排 | 对 handler 提供统一持久化 API |
| `server/store/infrastructure.go` | HTTP 幂等、业务事件、Kafka 幂等/死信 Store | 可靠基础链路 |
| `server/store/group_message_runtime.go` | Redis seq、唯一 key、热消息、presence、缓存和限流 | Redis 新 key 或新的 TTL 状态 |
| `server/db/adapter.go` | 持久化能力接口 | 新增 Store 数据能力 |
| `server/db/mysql/tenant_adapter.go` | MySQL 租户业务 SQL 实现 | 业务 CRUD/事务 |
| `server/db/mysql/*_schema.go` | 扩展业务表建表/升级逻辑 | 新表、列、索引 |
| `server/media/` | FS/OSS/S3 文件处理插件 | 文件存储能力 |
| `server/filesafety/` | 文件安全适配器 | 文件检测能力 |
| `server/push/` | 离线 Push 插件 | 厂商 Push 接入 |
| `server/auth/` / `server/validate/` | 认证和凭据验证插件 | 登录或验证方式 |
| `server/*_test.go` | 单元与链路测试 | handler、Topic、Kafka、Redis 逻辑测试 |
| `server/db/mysql/*_test.go` | MySQL schema 和 SQL 回归 | 数据库变更测试 |
## 10. 新功能放置决策
| 问题 | 选择 |
| --- | --- |
| 是否是客户端建连、订阅、发消息或通话实时信令? | WebSocket + Session/Hub/Topic |
| 是否是 C 端业务 CRUD、列表、搜索、设置或审批? | `/v1/im/*` HTTP + Store |
| 是否是 Admin 对租户业务的写操作? | Admin 本地 command/outbox + Kafka + IM consumer |
| Admin 是否必须当场读取租户详情? | IM 受控内部 GET API |
| IM 是否需要读取 hotgo 的低频配置? | 进程内缓存 + Kafka 刷新 + Admin 内部 API 兜底 |
| 是否需要跨节点高频共享、TTL 或原子计数? | Redis |
| 是否需要最终可追溯的业务状态? | 租户 MySQL |
| 是否跨系统、可异步、需重试/削峰/审计? | Kafka |
| 是否为在线群消息实时扩散? | Redis Pub/Sub + WS,不用 Kafka 做主链路 |
| 是否为文件本体? | OSS;MySQL 只存元数据/引用 |
## 11. 新功能标准开发流程
### 11.1 需求分析
1. 从前后端对接文档提取请求、响应、权限、状态机、幂等、并发和通知要求。
2. 标出数据归属:`hotgo` 公共数据或租户库业务数据。
3. 选择入口:HTTP、WebSocket、Kafka Admin Command 或内部 API。
4. 列出需要同步的状态:MySQL、Redis、Topic 内存、进程内缓存、C 端推送。
5. 明确失败类型:可重试、不可重试、并发冲突、部分成功和补偿。
### 11.2 领域与数据库
1. 在 `server/store/types/` 按业务域定义实体、状态常量、查询参数和结果。
2. 在 `server/db/adapter.go` 增加最小必要的租户持久化接口,方法必须携带 `TenantID`。
3. 在 MySQL 适配器实现 SQL;复杂写操作使用事务,状态迁移在 SQL `WHERE` 中校验旧状态/版本。
4. 在对应 `*_schema.go` 添加幂等建表/升级 SQL,为高频查询、唯一幂等键和状态扫描建索引。
5. 在 `server/store/store.go` 增加 mapper,负责参数验证、调用 adapter 和缓存编排;handler 不应直接拼 SQL。
表命名遵循当前租户库风格,类型必须与 Go 全链路一致,尤其是 ID、seq、version 使用 `BIGINT/int64`。面向 JavaScript 客户端的 64 位 ID 优先以字符串输出,避免精度丢失。
### 11.3 API 或协议层
HTTP 功能:
1. 在 `server/im_http.go` 定义独立 request/response 结构,不要直接暴露数据库 row。
2. 使用 `decodeIMJSONBody`、`authenticateIMHTTPBearer`、`requireIMGroupMember` 等公共函数做解码、认证和权限检查。
3. 在 `server/main.go` 注册具体路由,路由顺序要避免通配前缀吞掉更具体的路径。
4. 使用 `writeIMSuccess`/`writeIMError` 统一输出,新增稳定业务 `reason`。
5. 资源更新需并发保护时使用 version/`If-Match`,不允许用 `version=0` 绕过乐观锁。
WebSocket 功能:
1. 在 `datamodel.go` 增加最小协议字段,考虑 JSON 和 protobuf 转换兼容。
2. 在 Session 做连接级验证,在 Topic 做资源权限与业务状态验证。
3. 所有可重发消息必须有 `client_mid`/业务幂等键。
4. 成功后同时考虑本节点 Session、跨节点 fanout、离线 Push 与历史补偿。
### 11.4 缓存、事件与审计
1. 对高频读对象选择 cache-aside:先 Redis,miss 后读 MySQL 并回填。
2. 所有写方法列出需失效的 key,包括对象 key、关系 Set、分页快照和未读数。
3. 需可靠异步处理时同事务写 outbox,消费端使用 `event_id + consumer_group` 幂等。
4. 影响管理权限、账号安全、消息内容、文件处置或人工重放的操作要写审计。
5. 业务状态变更需立即告知 C 端时,创建业务事件并通过已认证服务端链路投递,不信任客户端伪造的系统消息。
### 11.5 测试与文档
每个功能至少覆盖:
1. 正常路径、无认证、跨租户、无权限、资源不存在和方法错误。
2. 重复 `Idempotency-Key`/`client_mid` 不产生重复数据。
3. 同一幂等键配不同 payload 返回冲突。
4. version 冲突和并发状态迁移。
5. MySQL 成功后 Redis/Topic 缓存及在线客户端状态正确刷新。
6. Kafka 重复消费、临时失败重试、不可重试失败和死信重放。
7. 64 位 ID/seq 在 JSON 编解码时不丢失精度。
8. 更新前后端对接文档、SQL 变更集和未实现增强项。
## 12. 关键约定
### 12.1 ID 与时间
- 用户 ID 由 Snowflake UID 体系生成,对外使用 `usr...` 编码形式或协议规定的字符串。
- 业务 ID 必须全局或租户内唯一,不能用秒级时间戳直接代替唯一 ID。
- `SeqId`、数据库主键和 version 使用 `int64/BIGINT`。
- 持久化与对外时间使用 UTC,HTTP 输出 RFC3339/RFC3339Nano。
### 12.2 Topic 归一化
客户端传入的 `usr...`、`slf` 等可见 Topic 不一定是数据库真实 Topic。历史查询、上下文、收藏、转发、媒体、回执等接口在查库前必须统一归一化,并在鉴权、查询和响应之间保持明确的 Topic 语义。
### 12.3 权限
- HTTP handler 负责验证主体和请求参数,Store/adapter 仍应使用租户和业务条件限制写入。
- Topic 管理员、群主、普通成员权限以服务端为准,不接受客户端声明的角色。
- 群主转让、设管理员、禁言、踢人等变更后,必须刷新 Topic 内 `perUser` 权限快照和 Redis 订阅/成员缓存,不能等待重连后生效。
- 封禁、冻结、踢设备、全端退出和注销等账号治理必须同步处理已存在 Session。
### 12.4 可观测性
- 日志使用 `server/logs` 的 `Info/Warn/Err`,默认输出 stderr,由容器日志系统收集。
- 日志行前缀为 `I/W/E` 加 Go log flags 生成的日期时间;敏感值、Token、密码、验证码、证件号和第三方密钥不得记录。
- HTTP 日志和异步事件应携带 `request_id/trace_id`、租户、业务类型和业务 ID。
- 新增核心链路应增加请求数、失败数、延迟、队列深度/积压和死信指标。
## 13. 禁止的实现方式
1. 在 HTTP handler 内直接打开 MySQL 或拼接 SQL。
2. 因租户库表没有 `tenant_id` 列,就不向 Store 传递 `TenantID`。
3. Admin 直连 `tinode` 或 IM 直连 `hotgo`。
4. 将 Kafka 当作 HTTP RPC 或实时 WebSocket fanout 使用。
5. 将业务最终状态只存在 Redis。
6. 为同一份高频数据同时增加进程内缓存和 Redis 缓存。
7. 业务写成功后不失效 Redis、Topic 权限快照和成员分页快照。
8. 重试请求没有 `Idempotency-Key/client_mid/event_id`。
9. 使用 `int` 或 JSON number 承载可能超过 JavaScript 安全范围的 ID/seq。
10. 直接信任客户端构造的系统通知、管理员角色或审计信息。
11. 文件上传成功后不创建业务引用和安全审核任务。
12. 新增数据表只在开发者本机手工执行,不写幂等 schema SQL、升级逻辑和回归测试。
## 14. 开发与验收命令
MySQL 构建使用 build tag:
```bash
go build -tags mysql -o bin/tinode ./server
go build -tags mysql -o bin/init-db ./tinode-db
```
建议测试顺序:
```bash
# 先跑修改模块的定向测试
go test ./server -run 'TestName'
# Store 与 MySQL schema/adapter
go test -tags mysql ./server/store/... ./server/db/mysql/...
# 完整服务端回归
go test -tags mysql ./server/...
```
发版前至少检查:
- 配置文件可解析,MySQL、Redis、Kafka、OSS 等必需依赖可连通。
- 数据库版本与代码一致,所有新表/列/索引已在目标环境幂等执行。
- Kafka Topic、消费组、重试和死信链路可用。
- 单节点和多节点下的在线状态、群消息、权限变更均一致。
- 新增接口与前后端对接文档一致,无密钥、密码或测试 Token 进入 Git。
## 15. 新功能提交清单
开发者在提交 PR/代码前应逐项确认:
- [ ] 已确定功能入口是 HTTP、WebSocket、Kafka 还是内部 API。
- [ ] 已确定数据归属 `hotgo` 还是租户库。
- [ ] 所有 Store/adapter 调用都显式携带 `TenantID`。
- [ ] 已设计幂等键、唯一索引、业务版本和状态机。
- [ ] 已明确 MySQL、Redis、进程内缓存的使用和失效顺序。
- [ ] 需跨系统可靠投递时使用 outbox + Kafka,消费端已幂等。
- [ ] 已处理 Topic 内存权限快照和多节点同步。
- [ ] 文件已存 OSS,业务引用和安全审核已建立。
- [ ] `SeqId`/ID/version 已在 Go、JSON、Kafka 和 MySQL 全链路使用 64 位安全类型。
- [ ] 已增加正常、权限、跨租户、幂等、并发、缓存和重试测试。
- [ ] 已补充 API 对接文档、SQL 变更和未完成增强项。
## 16. 相关文档
- `docs/IM-前后端对接文档.md`:C 端业务 API 和交互协议。
- `docs/IM-系统设计文档.md`:原始产品系统设计。
- `docs/IM-Redis三层数据分层改造方案.md`:进程内、Redis、MySQL 分层细节。
- `docs/IM-Kafka接入方案.md`:Admin/IM 边界、Topic、幂等、重试和死信。
- `docs/IM-Kafka接入范围识别.md`:按业务场景判断 Kafka 接入范围。
- `docs/IM-Kafka接入联调测试文档.md`:Admin 与 IM 联调用例。
- `docs/third/IM-文件安全第三方接入记录.md`:OSS 和文件安全链路。
- `docs/IM-未实现后续增强项.md`:已知未完成能力和深度优化项。
+170 -110
View File
@@ -1,116 +1,163 @@
# IM 第一至十一章后续增强项
> 更新日期:2026-08-17
> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章群消息与 @、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构、第十章个人设置与客户端能力、第十一章反诈安全/举报/反馈
> 用途:记录当前已完成基线之外的增强项,后续统一排期处理。
> 状态说明:2026-08-17 已完成的 P0/P1 已移入各章“当前已完成基线”,不再作为后续待办。完整修复记录见 `IM-需求实现情况审核报告.md`。
> 更新日期:2026-08-25
> 核对依据:`docs/IM-前后端对接文档.md`、`server/main.go` 当前 `/v1/im/*` 路由、`server/im_http.go` 当前 handler。
> 范围:第一章登录注册与安全、第二章消息首页/导航/系统通知、第三章单聊关系、第四章消息输入/转发/检索、第五章通讯录/好友/黑名单、第六章群聊创建/资料/治理、第七章文件/收藏/聊天记录检索、第八章实时语音与视频通话、第九章企业工作台与组织架构、第十章个人设置与客户端能力、第十一章反诈安全/举报/反馈。
> 用途:记录当前代码基线之外仍需排期的增强项。已经落地的 P0/P1 不再作为待办保留;已经抽入专项开发文档的项目,在本文中仅保留来源和现状说明,不再作为未分流待办处理。
## 0. 本次核对结论
### 0.1 已从待办移入基线
1. 注册、找回密码和敏感操作验证码在短信未接入时均可用固定测试码 `123456` 完成联调;真实短信通道仍是后续增强项。
2. `GET /v1/im/bootstrap` 已返回 `entries`、`entry_badges`、`modules`、`message_policy`、`system_topic`、`security` 和 `limits.max_file_size/max_text_length/max_forward_targets`。
3. `GET /v1/im/forward-targets` 已新增,返回文件助手和可转发 P2P/群会话目标。
4. 总经理信箱配置与提交的 500 已修复,`GET /v1/im/executive-mailbox/config`、`POST /v1/im/executive-mailbox/submissions` 已可联调。
5. 举报原因 500 已修复,`GET /v1/im/reports/abuse/reasons` 已按 `items` 返回。
6. 群文件和群公告路径已从群详情兜底中拆出,群文件基础接口已落地,未实现的子路径会返回明确错误。
7. 注册数据模型已改为 `im_users.username`、`im_users.phone`、`im_user_profiles.email`,不再依赖用户 `tags` 保存手机号/邮箱。
8. 邀请码主数据在 HotGo 库,注册使用明细在 IM 租户库;邀请码可多人复用,不再“核销”。
9. 用户搜索、用户主页、用户二维码已支持 `user_id/username/public_account` 作为 identifier。
10. 第一批增强中的群文件列表/登记/删除、群邀请外部接受、实名更正申诉、组织全局成员接口、月报 reopen 已落地。
11. 第一批增强中的主动创建/获取 P2P 会话、会话设置、多端云草稿已落地。
### 0.2 当前仍明确缺口及分流状态
| 优先级 | 缺口 | 对接文档位置 | 当前代码状态 | 分流状态 |
| --- | --- | --- | --- | --- |
| P0 | 真实短信供应商发送、失败重试、发送审计 | 1.4、1.7、1.10 | 当前测试码 `123456` 可联调,但生产短信未完成 | 依赖第三方,暂留后续增强池 |
| P0 | 消息转发落地接口 | 4.18、4.19、4.20 | 仅 `forward-targets` 已实现,`POST /v1/im/messages/forward`、`GET /v1/im/forward-bundles/{id}` 未实现 | 已抽入 `IM-增强项第一批开发.md` |
| P0 | 指定会话撤销 | 10.7 | `POST /v1/im/auth/logout` 已实现;`DELETE /v1/im/security/sessions/{id}` 未见路由 | 已抽入 `IM-增强项第一批开发.md` |
### 0.3 已抽入第一批开发的项目
以下项目已经从本文档的普通后续增强池中抽出,形成专项开发文档 `docs/IM-增强项第一批开发.md`。其中已落地项已移入各章“当前已完成基线”;仍未完全落地的项目继续以第一批开发文档作为实现、验收和同步更新依据。
1. 仍待落地:按 session ID 撤销指定设备;当前 token 登出已完成。
2. 仍待落地:普通消息转发。
3. 仍待落地:合并转发 Bundle 详情。
4. 已落地:群文件列表、登记和删除。
5. 已落地:主动创建/获取 P2P 会话。
6. 已落地:会话设置。
7. 已落地:多端云草稿。
8. 已落地:群邀请外部接受。
9. 已落地:实名更正申诉。
10. 已落地:组织全局成员接口。
11. 已落地:月报 reopen。
12. 仍待统一回归:列表 `has_more` 统一。
13. 仍待落地:风控 token 单次消费接入业务接口。
## 1. 第一章:登录、注册与安全
### 1.1 当前已完成基线
1. 企业码解析、租户状态校验和公开租户配置返回。
2. 注册、登录前验证码发送与验证。
3. 注册验证码 Token 消费,避免绕过验证码直接注册。
4. 忘记密码挑战验证与重置确认。
5. Token 绑定 `auth_version`,密码重置后旧 Token 失效。
6. 登录后敏感操作二次验证挑战。
7. 实名认证状态查询与提交,证件信息脱敏/保护存储。
8. 独立密码设置与验证,包含弱密码规则和失败锁定。
9. 登录后 bootstrap 返回实名、安全与菜单基础能力。
10. 产品 HTTP 已统一校验 API Key、Schema 和 Request ID。
11. 登录态写接口已接入租户库幂等记录,支持请求摘要冲突检测和首次响应重放。
12. 风险确认已生成可重试安全事件和用户系统通知。
2. WebSocket `hi` 租户绑定、`basic` 登录和 `{acc user:new}` 注册已具备。
3. 注册支持用户名/手机号二选一;手机号注册写入 `im_users.phone`,用户名写入 `im_users.username`。
4. 用户名格式校验、手机号规范化、服务端随机用户名生成规则已具备。
5. 注册验证码发码、校验、一次性 `verification_token` 消费已具备。
6. 找回密码挑战验证与重置确认已具备。
7. 敏感操作二次验证挑战和 `action_token` 签发已具备。
8. 短信未接入时注册、找回密码、敏感操作均可用测试码 `123456` 联调。
9. 邀请码主数据在 HotGo 控制库,注册明细在 IM 租户库,支持可复用邀请码。
10. Token 绑定 `auth_version`,密码重置后旧 Token 失效。
11. 实名状态查询与提交已具备,证件信息脱敏/保护存储。
12. 独立密码设置与验证已具备,包含弱密码规则和失败锁定。
13. 产品 HTTP 已统一校验 API Key、Schema 和 Request ID。
14. 登录态写接口已接入租户库幂等记录,支持请求摘要冲突检测和首次响应重放。
15. 风险确认可生成安全事件和系统通知。
16. 实名更正申诉 `POST /v1/im/security/realname/appeals` 已具备,申诉记录进入 pending 状态。
### 1.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | 验证码真实发送通道 | 当前验证码生成、校验和限频链路已具备,短信/邮件供应商发送仍需接入真实服务。 | 验证码可通过供应商下发;供应商失败有可观测错误和重试策略。 |
| P0 | 验证码限频维度补强 | 需要按租户、手机号、IP、设备、场景组合限频,并落库或接入 Redis。 | 高频请求返回 `VERIFICATION_RATE_LIMITED`,不同场景互不误伤。 |
| P1 | 安全事件通知覆盖扩展 | 风险确认已接入统一事件和系统通知;密码重置、实名审核、独立密码变更、新设备登录仍需补齐。 | 操作成功后 `/v1/im/system-notifications` 能看到对应事件且重试不重复入箱。 |
| P1 | 实名审核工作流 | 当前支持提交和状态查询,后续需要后台审核、驳回原因、通过后状态变更。 | 审核通过后 `realname.status=verified`;驳回可再次提交。 |
| P1 | 独立密码用途扩展 | 当前按 `purpose` 存储,可继续接入青少年模式关闭、敏感设置修改等业务入口。 | 不同 `purpose` 相互隔离;错误次数和锁定互不串扰。 |
| P1 | 设备与会话管理 | 需要登录设备列表、主动踢出设备、Token 刷新/撤销审计。 | 修改密码或踢设备后对应 Token 立即不可用。 |
| P0 | 真实短信供应商发送通道 | 当前为测试码联调模式;生产必须接短信服务商、模板、签名、失败重试和发送审计。 | 真实手机号收到验证码;供应商失败返回稳定 JSON,告警可观测。 |
| P0 | 验证码多维限频 | 需要按租户、手机号、IP、设备、场景组合限频,并接 Redis 或落库。 | 高频请求返回 `VERIFICATION_RATE_LIMITED`,不同场景互不误伤。 |
| P0 | 指定会话撤销 | `POST /v1/im/auth/logout` 已通过 Redis 撤销当前 token;`DELETE /v1/im/security/sessions/{id}` 尚未实现。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 |
| P1 | 实名后台审核流转 | 当前支持实名提交、状态查询和申诉提交,后续需要后台审核、驳回原因、通过状态变更和申诉处理工单。 | 审核通过后 `realname.status=verified`;驳回可再次提交;申诉有处理状态。 |
| P1 | 安全事件通知覆盖扩展 | 风险确认已接入通知;密码重置、实名审核、独立密码变更、新设备登录仍需补齐。 | `/v1/im/system-notifications` 可看到对应事件且重试不重复入箱。 |
| P1 | API Key 轮换策略 | 当前 API Key 校验兼容现有逻辑,尚未实现最低可接受序号或撤销表。 | 旧序号 Key 可按配置禁用,新 Key 不影响正常客户端。 |
| P2 | 风控与反诈策略接入 | 第一章安全动作后续应接入风险评分、反诈弹窗和二次确认策略。 | 高风险动作触发二次验证或反诈提示,低风险动作直通。 |
| P2 | 风控与反诈策略接入登录注册 | 安全动作后续应接入风险评分、反诈弹窗和二次确认策略。 | 高风险注册/登录/改密触发二次验证或反诈提示。 |
## 2. 第二章:消息首页、全局导航与系统通知
### 2.1 当前已完成基线
1. `GET /v1/im/bootstrap` 返回首页菜单、能力开关、权限和 `message_policy`。
2. `GET /v1/im/search/conversations` 支持会话搜索和基础分页。
3. WebSocket 发消息支持 `Head["x-im-client-mid"]` 幂等,重连重发不重复落库/广播。
4. 消息 Header 已按产品协议校验 `x-im-schema`、`x-im-type`、`x-im-client-mid`,并兼容旧 `type`。
5. 普通用户禁止发布 `sys` Topic,且禁止伪造 `system`/`system_event`。
6. `GET /v1/im/topics/{topic}/messages/{seq}/receipts` 返回群消息送达/已读基础列表。
7. `POST /v1/im/topics/{topic}/messages/{seq}/recall` 支持版本校验、权限校验和时限内撤回。
8. `POST /v1/im/topics/{topic}/messages/{seq}/revisions` 支持文本消息编辑、版本递增和修订记录。
9. `GET /v1/im/system-notifications` 和 `POST /v1/im/system-notifications/{id}/read`。
10. 消息编辑/撤回已在事务内写入通用 Outbox,并向 Topic 在线成员派发可去重事件。
11. 编辑/撤回已接入全局 HTTP 幂等和响应重放。
12. 好友、群、会议和安全模块已可信生产系统通知。
1. `GET /v1/im/bootstrap` 返回首页菜单、能力开关、权限、入口、红点和 `message_policy`。
2. `bootstrap.limits` 已返回 `max_file_size`、`max_text_length`、`max_forward_targets`。
3. `GET /v1/im/search/conversations` 支持会话搜索和基础分页。
4. WebSocket 发消息支持 `Head["x-im-client-mid"]` 幂等,重连重发不重复落库/广播。
5. 消息 Header 已按产品协议校验 `x-im-schema`、`x-im-type`、`x-im-client-mid`,并兼容旧 `type`。
6. 普通用户禁止发布 `sys` Topic,且禁止伪造 `system`/`system_event`。
7. `GET /v1/im/topics/{topic}/messages/{seq}/receipts` 返回群消息送达/已读基础列表。
8. 撤回、编辑接口已具备,并接入幂等与 Outbox。
9. `GET /v1/im/system-notifications` 和 `POST /v1/im/system-notifications/{id}/read` 已具备。
10. 好友、群、会议和安全模块已可信生产系统通知。
### 2.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | 消息变更查询占位策略 | 已撤回消息在普通拉取中应返回撤回占位,而不是仅靠客户端本地事件。 | 新设备登录拉历史时能看到撤回占位和版本。 |
| P1 | 群回执隐私和角色策略 | 当前返回基础 read/recv 列表,后续需按租户策略、群规模、角色决定是否展示具体成员。 | 普通成员/管理员在不同策略下看到不同粒度回执。 |
| P1 | 群回执 cursor 精确分页 | 当前 cursor 是基础位置分页,后续可升级为稳定游标,避免成员变动造成跳页。 | 成员新增/删除时分页不重复、不漏主要数据。 |
| P0 | 万人大群离线提醒与重连补偿 | 离线只走极光 Push 提醒,客户端重连后按 Topic seq 拉取补偿。 | 离线用户收到极光提醒;重连后按 seq 补齐且不丢不重。 |
| P0 | 系统通知未读聚合实时化 | `bootstrap.entry_badges.system_unread_count` 已有,后续需确认真源与增量事件。 | 读通知后 bootstrap 和 WS badge 均一致。 |
| P1 | 列表 `has_more` 统一 | 部分接口仍只返回 `items,next_cursor`,对接文档要求可统一补 `has_more`。 | 前端所有列表分页逻辑可复用同一协议。 |
| P1 | 群回执隐私和角色策略 | 当前返回基础 read/recv 列表,后续需按租户策略、群规模、角色裁剪。 | 普通成员/管理员在不同策略下看到不同粒度回执。 |
| P1 | `message_policy` 租户配置化 | 当前 recall/edit 窗口和菜单能力是固定值,后续需要来自租户配置。 | 不同租户可配置不同撤回/编辑时限和菜单能力。 |
| P1 | 系统通知 action 白名单 | `action.type` 和参数需要统一注册、校验和版本化。 | 非白名单 action 写入失败;客户端只收到可识别跳转。 |
| P1 | 会话搜索排序与类型过滤 | 当前按现有订阅数据过滤,后续需支持会话类型、置顶、免打扰、最近活跃排序等。 | 搜索结果与消息首页排序、筛选规则一致。 |
| P1 | 会话搜索排序与类型过滤 | 当前按订阅数据过滤,后续需支持会话类型、置顶、免打扰、最近活跃排序。 | 搜索结果与消息首页排序、筛选规则一致。 |
| P1 | 群成员分页快照深度优化 | 当前已接入 Redis 短 TTL 响应快照,后续大群需要 sorted set、稳定游标和成员变更主动失效。 | 万人大群成员翻页不重复不漏,角色/昵称/退群变更后缓存能及时失效。 |
| P1 | 未读数 Redis 深度优化 | 当前已接入用户总未读 Redis 热缓存,后续需要 Topic 维度未读计数、bootstrap 聚合缓存和主动失效事件。 | 首页红点、会话未读和多端 read/recv 后的未读数实时一致。 |
| P2 | 离线重连恢复编排测试 | 需要端到端覆盖 `hi -> token login -> sub me -> 恢复 Topic -> get data`。 | 断网重连后未丢消息、未重复消息、未错乱回执。 |
| P2 | 消息 Header schema 版本演进 | 当前做基础格式/枚举校验,后续需要按 `x-im-schema` 版本维护兼容矩阵。 | 老客户端消息可兼容,新客户端非法字段被拒绝。 |
## 3. 第三章:单聊关系、权限与会话上下文
### 3.1 当前已完成基线
1. 单聊会话上下文接口返回双方关系、发送能力、对端公开资料和 P2P Topic。
1. `GET /v1/im/conversations/{topic}/context` 返回双方关系、发送能力、对端公开资料和 P2P Topic。
2. P2P 发送路径接入好友关系校验,非好友、拉黑状态下禁止直接发送。
3. 好友申请、通过、拒绝、删除好友与单聊关系状态联动。
4. 拉黑/解除拉黑后关系上下文即时反映 `blocked_by_me`、`blocked_by_peer` 等状态。
5. 会话侧可根据 `can_send` 锁定输入框,并引导重新添加好友。
6. 好友申请、通过/拒绝、删除、拉黑/解除和备注已生成稳定关系事件,相关对端通知可去重。
7. 好友申请/审批关键路径已在业务事务内写入通知和 Outbox,其他可重复关系写由全局 HTTP 幂等保证可补写。
8. 非好友资料查询已执行 `profile_visibility`。
5. 好友申请、通过/拒绝、删除、拉黑/解除和备注已生成稳定关系事件,相关对端通知可去重。
6. 非好友资料查询已执行 `profile_visibility`。
7. 主动创建/获取 P2P 会话 `POST /v1/im/conversations/p2p` 已具备。
8. 会话设置 `GET/PATCH /v1/im/conversations/{topic}/settings` 已具备。
9. 多端云草稿 `GET/PUT /v1/im/topics/{topic}/draft` 已具备。
### 3.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | P2P 策略统一入口 | 当前核心路径已校验好友/黑名单,后续需要把发消息、通话、名片、文件、历史查看统一收敛到同一策略服务。 | 任一 P2P 业务入口对同一关系状态返回一致权限。 |
| P1 | Pending 会话体验增强 | 待处理好友申请对应的单聊页应携带申请摘要、来源、过期时间和快速处理入口。 | 进入 pending 会话可直接通过/拒绝,不需要跳转新朋友页。 |
| P1 | 黑名单历史消息策略 | 当前发送/通话阻断已具备,后续需明确拉黑后历史消息、资料、文件和已读回执可见范围。 | 被拉黑人在不同策略下看到一致的历史与资料裁剪结果。 |
| P1 | 资料策略继续扩展 | 好友/非好友可见性已执行,企业字段级可见性和青少年模式仍需纳入同一策略。 | 企业受限字段与青少年用户无法从其他入口绕过策略。 |
| P1 | 会话策略与草稿联调回归 | P2P 会话、会话设置和云草稿接口已具备,仍需覆盖多端、冲突、非好友/黑名单状态下的联调回归。 | 多端草稿同步、版本冲突和权限拒绝都符合协议。 |
| P1 | Pending 会话体验增强 | 待处理好友申请对应单聊页应携带申请摘要、来源、过期时间和快速处理入口。 | 进入 pending 会话可直接通过/拒绝。 |
| P1 | 黑名单历史消息策略 | 需明确拉黑后历史消息、资料、文件和已读回执可见范围。 | 被拉黑人在不同策略下看到一致的历史与资料裁剪结果。 |
| P1 | 好友关系审计 | 删除、拉黑、解除拉黑、申请处理等敏感关系操作写审计日志。 | 后台可按租户、操作者、目标用户查询操作流水。 |
| P2 | 关系状态迁移测试矩阵 | 需要覆盖 none、pending_inbound、pending_outbound、friend、blocked_by_me、blocked_by_peer 的全迁移。 | 任意状态迁移后 DB、HTTP 上下文、WS 权限一致。 |
## 4. 第四章:群消息、群成员与 @ 能力
## 4. 第四章:消息输入、媒体内容、转发与检索
### 4.1 当前已完成基线
1. 群成员列表接口支持基础分页、搜索和 `mentionable` 过滤。
2. 返回成员角色、公开资料、ACL 和是否可被 @。
3. 群 @ 消息头部字段校验,支持 `mentioned_user_ids`、`mention_all` 等基础结构。
4. 群 @ 权限接入成员 ACL,管理员/群主才具备 @ 全体能力。
5. 群消息读取链路保留 @ 元数据,客户端可据此展示提醒。
6. 群昵称、角色、禁言、成员和公告等群业务变更已写入群 Outbox,并生成在线事件与系统通知。
1. 群 @ 消息头部字段校验,支持 `mentioned_user_ids`、`mention_all` 等基础结构。
2. 群 @ 权限接入成员 ACL,管理员/群主才具备 @ 全体能力。
3. 群成员列表支持基础分页、搜索和 `mentionable` 过滤。
4. `GET /v1/im/forward-targets` 已具备,返回文件助手和可转发的会话目标。
5. 消息撤回、编辑、回执、搜索、上下文定位、媒体列表接口已挂在 `/v1/im/topics/` 下。
6. 收藏入口与收藏转发基础接口已具备。
### 4.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | @ 实时通知与离线 Push | 当前消息元数据已落库,后续需要对被 @ 成员生成高优先级提醒和离线 Push。 | 被 @ 用户在在线、离线、多端场景均收到一次提醒。 |
| P0 | @ 全体频控 | @ 全体需要按租户、群、角色、时间窗口限频,避免刷屏。 | 超限返回稳定错误码,管理员可配置限额。 |
| P0 | 群成员稳定游标分页 | 当前成员列表以基础 cursor 分页,后续应使用稳定游标避免成员变动跳页。 | 成员入群/退群时翻页不重复、不漏主要数据。 |
| P1 | 群成员资料隐私裁剪 | 群内非好友主页需要按群关系、好友关系、隐私设置裁剪资料字段。 | 同一成员从群内和好友主页进入时字段符合策略差异。 |
| P0 | 消息转发接口 | 对接文档有 `POST /v1/im/messages/forward`,当前未见路由;收藏转发不等同普通消息转发。 | 单条/多条消息可分别转发到好友、群和文件助手。 |
| P0 | 合并转发 Bundle | 对接文档有 `GET /v1/im/forward-bundles/{id}`,当前未见路由。 | 合并转发卡片可分页查看,权限隔离原会话。 |
| P0 | @ 实时通知与离线 Push | 当前消息元数据已落库,后续需要对被 @ 成员生成高优先级提醒和离线 Push。 | 被 @ 用户在线、离线、多端场景均收到一次提醒。 |
| P0 | 离线 Push Kafka 化 | `im.message.push` 已纳入 Kafka 方案,但本轮暂不实现;离线提醒仍按后续极光 Push 统一接入。 | 消息、@、通话、系统通知等离线场景统一投递 Kafka Push 任务,失败可重试和补偿。 |
| P0 | @ 全体频控 | @ 全体需要按租户、群、角色、时间窗口限频。 | 超限返回稳定错误码,管理员可配置限额。 |
| P1 | 群成员稳定游标分页 | 当前成员列表以基础 cursor 分页,后续应使用稳定游标避免成员变动跳页。 | 成员入群/退群时翻页不重复、不漏主要数据。 |
| P1 | @ 候选排序优化 | @ 面板候选应支持拼音首字母、最近互动、角色优先和本地缓存协同。 | 大群搜索响应稳定,候选排序符合产品规则。 |
| P1 | 群治理策略配置化 | 成员私聊、成员邀请、全员禁言、上传权限等应统一由群治理配置控制。 | 不同群可配置不同治理能力,HTTP 与 WS 权限一致。 |
| P2 | 大群性能压测 | 需要覆盖 500、2000、10000 成员下的成员列表、@ 搜索和发送性能。 | 达到目标延迟和资源占用阈值。 |
| P1 | 消息检索稳定游标 | 搜索和媒体列表需要稳定游标,避免新消息插入造成跳页。 | 搜索期间新增消息不影响当前翻页结果。 |
## 5. 第五章:通讯录、好友关系与黑名单
@@ -120,26 +167,25 @@
2. 新朋友列表、好友申请、通过、拒绝链路已具备。
3. 用户搜索支持账号、精准账号和手机号类型,返回公开资料、关系和 `action`。
4. 添加好友二维码支持短期签名 Token 生成与解析。
5. 好友个人主页返回关系、资料和操作能力。
5. 个人主页支持 `user_id/username/public_account` identifier,返回关系、资料和操作能力。
6. 群内非好友个人主页返回群角色、关系、资料和操作能力。
7. 好友备注更新、删除好友、拉黑/解除拉黑已具备。
8. 黑名单列表和批量移出黑名单已具备。
9. 好友/黑名单写操作已统一接入 HTTP 幂等、关系事件和必要的对端系统通知。
10. 用户搜索和好友申请已执行 `add_me_methods`,手机号搜索不会返回禁止该方式的非好友。
11. 好友个人主页已执行 `profile_visibility`。
11. 用户搜索已改用昵称、公开账号、登录账号和 `im_users.phone` 等结构化字段,不再依赖用户标签。
### 5.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P1 | 用户搜索租户策略扩展 | 个人添加方式已执行,后续还需增加租户级 `allow_user_search`、通讯录授权和存在性防探测策略。 | 租户关闭搜索或未授权通讯录时,客户端无法绕过且不泄露用户存在性。 |
| P0 | 二维码吊销与一次性策略 | 当前二维码是短期签名 Token,后续如需主动失效、刷新覆盖、扫码审计,需要落库或接入 Redis。 | 用户刷新二维码后旧码按策略失效;后台可查询扫码记录。 |
| P1 | 好友资料字段精细裁剪 | 基础 `everyone/friends/nobody` 可见性已执行,后续需按群内关系、黑名单、实名/青少年等规则裁剪到字段级。 | 不同入口返回字段符合隐私矩阵。 |
| P1 | 通讯录排序与拼音索引 | 当前支持基础 initial,后续需维护昵称/备注拼音、特殊字符分组和本地增量同步。 | 好友列表与客户端通讯录排序一致,中文首字母稳定。 |
| P1 | 二维码吊销与扫码审计 | 当前二维码是短期签名 Token,主动失效、刷新覆盖、扫码审计需要落库或接 Redis。 | 用户刷新二维码后旧码按策略失效;后台可查询扫码记录。 |
| P1 | 好友资料字段精细裁剪 | 基础可见性已执行,后续需按群关系、黑名单、实名/青少年等规则裁剪到字段级。 | 不同入口返回字段符合隐私矩阵。 |
| P1 | 通讯录排序与拼音索引 | 当前支持基础 initial,后续需维护昵称/备注拼音、特殊字符分组和本地增量同步。 | 好友列表与客户端通讯录排序一致。 |
| P1 | 黑名单占位资料 | 资料删除、离职或不可见时,黑名单列表仍需返回可解除的受控占位用户。 | 黑名单中失效用户可展示占位并成功移出。 |
| P1 | 好友申请防骚扰 | 需要按目标用户、申请人、IP、设备、来源和群来源限频,并支持拒收策略。 | 高频申请被限制;群来源申请校验双方仍在群内。 |
| P1 | 关系操作审计 | 关键关系系统通知已接入,添加、删除、拉黑、解除和二维码解析仍需独立审计流水。 | 后台可按租户、操作者和目标用户追溯。 |
| P2 | 批量接口部分失败语义增强 | 批量移出黑名单目前返回逐项结果,后续需要统一批量接口错误码、重试建议和幂等结果缓存。 | 部分失败时客户端能只重试失败项。 |
| P1 | 关系操作审计 | 添加、删除、拉黑、解除和二维码解析仍需独立审计流水。 | 后台可按租户、操作者和目标用户追溯。 |
## 6. 第六章:群聊创建、资料维护与成员治理
@@ -148,25 +194,26 @@
1. 群列表、创建群、群资料查询与更新、群头像和群简介保存接口已具备。
2. 群设置、个人群设置、退出群、群昵称接口已具备。
3. 群二维码生成与扫码解析支持 `join_group` 场景。
4. 群邀请、进群申请、申请列表、同意/拒绝、群 summary 申请计数已具备。
4. 群内邀请、进群申请、申请列表、同意/拒绝、群 summary 申请计数已具备。
5. 群公告最新查询、发布/更新、详情和已读接口已具备。
6. 群治理配置 `GET/PATCH /governance` 已具备。
7. 管理员候选、管理员上限、批量添加管理员、删除管理员和群主转让接口已具备。
8. 禁言候选、批量禁言、禁言列表和解除禁言接口已具备。
9. 12 张 `im_group_*` 业务表已纳入 MySQL 租户库自动初始化,独立租户库模式不保存 `tenant_id`。
10. 群邀请、入群申请/审批、成员变更、角色变更、群主转让和退群已收口到 MySQL 事务。
11. 群写操作已使用 `im_group_idempotency` 和 `im_group_events_outbox`,提交后通过 Hub 派发,失败保留 pending 重试。
12. WebSocket 群消息发布已校验成员禁言和全员禁言,支持过期恢复及管理员豁免。
13. 群公告已使用独立表并记录成员已读时间。
9. 群写操作已使用 `im_group_idempotency` 和 `im_group_events_outbox`,提交后通过 Hub 派发,失败保留 pending 重试。
10. WebSocket 群消息发布已校验成员禁言和全员禁言,支持过期恢复及管理员豁免。
11. 群文件列表、登记和删除接口 `GET/POST/DELETE /v1/im/groups/{topic}/files` 已具备。
12. 群邀请外部接受 `POST /v1/im/group-invitations/{id}/accept` 已具备。
### 6.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | 群文件权限与下载闭环 | 群文件基础接口已具备,后续需与文件 ACL、退群后下载阻断、审核状态、OSS 短链失效联动。 | 群文件列表可搜索/分页,上传登记和删除权限正确,退群后不可下载旧 URL。 |
| P1 | 群邀请外部接受体验回归 | 外部接受接口已具备,仍需覆盖邀请卡、多端重复接受、过期和审批模式切换。 | 被邀请人可通过邀请卡入群或进入审批;重复接受幂等返回稳定结果。 |
| P1 | 进群申请隐私与限频 | 申请入口需要按群治理、租户策略、用户状态、来源、IP/设备限频。 | 高频申请被限制;普通成员不可看到敏感申请人信息。 |
| P1 | 管理员/群主角色层级策略 | 管理员互相操作、管理员禁言、群主转让二次认证等需要完整策略矩阵。 | 群主、管理员、普通成员在每个治理动作上的权限稳定一致。 |
| P1 | 群文件索引 | 6.27-6.28 需要独立群文件表,绑定上传文件、群成员权限、下载状态和删除权限。 | 群文件列表可搜索/分页,退群后不可下载旧 URL。 |
| P1 | 群解散与二维码吊销 | 群解散、二维码撤销、邀请失效需要状态联动和权限回收。 | 解散后 Topic、二维码、邀请、文件新增权限全部失效。 |
| P1 | 群成员 `user_id` 完整性回归 | 测试报告曾发现 owner 成员 `user_id` 为空,需要专门回归确认。 | 群成员列表所有成员均返回稳定 `usr...`。 |
| P2 | 大群性能与稳定分页 | 成员、申请、公告、禁言、文件列表都需要稳定游标和大群压测。 | 大群场景下分页不重复不漏,接口延迟达标。 |
## 7. 第七章:文件下载、收藏管理与聊天记录检索
@@ -174,26 +221,25 @@
### 7.1 当前已完成基线
1. 收藏创建、列表、详情和删除接口已具备,支持文本、文件、语音、链接等类型。
2. 收藏批量创建和基础分享前的数据结构已具备。
3. Topic 消息关键词搜索、文件/图片/视频等媒体类型检索接口已具备。
4. 消息上下文定位接口可围绕指定 seq 返回前后消息。
5. 文件访问已接入用户与 Topic 权限校验,下载前可判断资源可见性。
6. 收藏等登录态 HTTP 写接口已接入全局幂等响应重放。
2. 收藏批量创建、批量删除和收藏转发接口已具备。
3. 转发目标列表 `GET /v1/im/forward-targets` 已具备。
4. Topic 消息关键词搜索、文件/图片/视频等媒体类型检索接口已具备。
5. 消息上下文定位接口可围绕指定 seq 返回前后消息。
6. 文件访问已接入用户与 Topic 权限校验,下载前可判断资源可见性。
7. 收藏等登录态 HTTP 写接口已接入全局幂等响应重放。
### 7.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P1 | 收藏业务去重策略 | 同一 HTTP 请求的超时重试已由全局幂等保证;使用不同幂等键重复收藏同一消息、文件或链接时,仍需业务唯一性策略。 | 重复点击收藏不会产生重复记录,并返回已有收藏 ID。 |
| P0 | 普通消息转发与合并转发 | `/favorites/forward` 已有,但 `/messages/forward`、`/forward-bundles/{id}` 未实现。 | 普通消息可单条/多条/合并转发,Bundle 可分页查看。 |
| P0 | 收藏分享权限二次校验 | 收藏文件或消息分享给好友/群时,需要重新校验接收方是否可访问原资源或生成受控副本。 | 无权限接收方不能通过收藏分享拿到原会话附件。 |
| P0 | 文件下载短时签名 URL | 当前权限判断已具备,后续应生成短时下载 URL,并记录下载审计。 | URL 过期不可访问;退群/拉黑后旧 URL 失效或受策略限制。 |
| P0 | 文件安全扫描与 MIME 校验 | 上传后的文件需要扩展名、MIME、内容嗅探、病毒扫描和图片/视频解码安全检查。 | 高风险文件不可下载或转发,客户端收到稳定错误码。 |
| P1 | 收藏业务去重策略 | 使用不同幂等键重复收藏同一消息、文件或链接时,仍需业务唯一性策略。 | 重复点击收藏不会产生重复记录,并返回已有收藏 ID。 |
| P1 | 收藏分类与全文索引 | 收藏列表需要按类型、来源、时间、关键词、发送人等维度检索。 | 大量收藏下搜索响应稳定,类型筛选结果准确。 |
| P1 | 聊天记录稳定游标 | 消息搜索和媒体列表需要稳定游标,避免新消息插入导致翻页重复或漏项。 | 搜索期间新增消息不影响当前翻页结果。 |
| P1 | 历史消息权限快照策略 | 用户退群、删除好友、拉黑后,历史消息和附件的可见范围需要统一规则。 | 各入口对同一历史资源权限判断一致。 |
| P1 | 下载状态与断点续传 | 大文件下载需要断点续传、失败重试、进度状态和下载完成事件。 | 网络中断后可续传;下载完成状态可跨端同步或按端隔离。 |
| P2 | 聊天记录高亮与命中片段 | 搜索结果可返回命中摘要、高亮范围和消息类型摘要。 | 客户端无需二次解析即可展示命中片段。 |
| P2 | 收藏导出与批量管理 | 后续可支持收藏批量删除、批量转发、导出和云端容量统计。 | 批量操作有部分失败语义和可重试结果。 |
## 8. 第八章:实时语音与视频通话
@@ -211,53 +257,54 @@
| --- | --- | --- | --- |
| P0 | 跨设备抢答状态机 | 同一账号多端收到来电时,需要一个设备接听后自动取消其他端待接状态。 | A 手机接听后,A 平板和 Web 端立即变为已在其他设备接听。 |
| P0 | 通话状态强一致与幂等 | invite、accept、hang-up 等事件需要幂等键或事件序号,避免乱序/重复导致状态回退。 | 重复 hang-up 不改变最终状态;迟到 accept 不会覆盖已结束通话。 |
| P0 | 通话事件 Outbox 与通知 | 来电、未接、挂断、忙线等需要可信事件和 Push,DB 状态与通知通过 Outbox 衔接。 | 离线用户收到一次来电/未接提醒,失败可补偿。 |
| P0 | 通话事件 Outbox 与离线 Push | 来电、未接、挂断、忙线等需要可信事件和 Push,DB 状态与通知通过 Outbox 衔接。 | 离线用户收到一次来电/未接提醒,失败可补偿。 |
| P0 | TURN/STUN 配置与媒体准入 | 服务端应按租户/区域返回可用 ICE 配置,并限制未授权用户创建媒体会话。 | 客户端拉取到有效 TURN;无权限通话被服务端拒绝。 |
| P1 | 结束原因标准化 | 需要统一 `missed`、`declined`、`busy`、`timeout`、`network_error` 等原因和客户端展示文案。 | 不同端同一结束原因展示一致,统计口径一致。 |
| P1 | 通话质量指标 | 需要采集建立耗时、ICE 成功率、丢包、抖动、断连等指标。 | 后台可按租户和版本查看通话质量趋势。 |
| P1 | 群通话/会议房间拆分 | 群音视频不宜复用 P2P call,需要会议房间、参与人状态和 SFU 接入。 | 群通话有独立 room_id、成员列表和退出/重进状态。 |
| P1 | 通话权限统一策略 | 通话入口应复用 P2P/群关系策略、黑名单、禁言、青少年模式和企业策略。 | 被拉黑、非好友或受限制用户无法发起通话。 |
| P2 | 通话记录聚合与清理 | 通话记录可按会话、日期、未接分类聚合,并支持保留周期清理。 | 记录列表分页稳定,超过保留期按策略清理。 |
## 9. 第九章:企业工作台与办公协同
### 9.1 当前已完成基线
1. 工作台接口 `GET /v1/im/workbench` 已具备,返回模块、布局版本和企业功能开关。
2. 工作台模块表 `im_workbench_apps` 已具备,默认包含组织架构、会议通知、日报、月报、总经理信箱入口。
3. 服务端对工作台 route 做内部白名单过滤,避免客户端执行任意服务端 URL。
4. 组织架构部门表 `im_org_departments` 和成员表 `im_org_members` 已具备。
5. 部门列表 `GET /v1/im/organization/departments`、部门成员 `GET /v1/im/organization/departments/{id}/members`、组织搜索 `GET /v1/im/organization/search` 已具备。
6. 会议通知 `POST/GET/PATCH/CANCEL/REMIND/MY-RESPONSE` 已具备基线,包含参与人和版本冲突控制。
7. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。
8. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。
9. 工作台 `tenant_code` 已返回租户注册表的公开编码,不再暴露内部 ID。
2. `GET /v1/im/bootstrap` 已返回工作台总未读 `entry_badges.workbench_total_unread`。
3. 工作台模块表 `im_workbench_apps` 已具备,默认包含组织架构、会议通知、日报、月报、总经理信箱入口。
4. 服务端对工作台 route 做内部白名单过滤,避免客户端执行任意服务端 URL。
5. 组织架构部门表 `im_org_departments` 和成员表 `im_org_members` 已具备。
6. 部门列表、部门成员、组织搜索已具备。
7. 会议通知创建、列表、详情、更新、取消、提醒、参会回应已具备基线。
8. 日报/月报草稿、提交、详情、列表、统计与提醒基线已具备。
9. 总经理信箱配置与提交接口已具备,包含匿名策略、文案版本和幂等键。
10. 会议创建、变更、取消、提醒和参会回应已生成在线事件与系统通知,关键变更在事务内写 Outbox。
11. 组织全局成员接口 `GET /v1/im/organization/members` 已具备。
12. 月报 reopen `POST /v1/im/reports/monthly/{id}/reopen` 已具备。
13. 组织架构可见范围已按账号来源裁剪:邀请码注册默认仅本人,手机号注册默认本人和好友组织路径,上架测试账号可通过 `im_org_visibility_overrides.visibility_scope=all` 放开全量。
### 9.2 后续增强项
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | 工作台权限与功能开关配置化 | 当前模块基线可返回,后续需要按租户、部门、角色、后台配置动态裁剪。 | 不同用户登录同一租户看到不同授权模块。 |
| P0 | 工作台未读角标事件 | 文档要求 `workbench.badge.updated`,后续需要模块未读聚合和实时增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 |
| P0 | 工作台未读角标实时事件 | `workbench_total_unread` 已有,后续需要模块未读聚合和 `workbench.badge.updated` 增量事件。 | 会议、日报、信箱未读变化后工作台角标实时更新。 |
| P0 | 组织数据后台维护链路 | 当前前台读接口和表已具备,后续需要后台导入、增删改部门、调整成员和离职状态。 | 后台修改组织后前台接口版本变化,客户端可刷新。 |
| P0 | 组织权限与可见字段裁剪 | 成员手机号、邮箱、职位等字段需要按组织角色、隐私和企业策略裁剪。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
| P1 | 组织搜索索引优化 | 当前基础 LIKE 搜索可用,后续需要拼音、工号、手机号脱敏匹配和稳定排序。 | 大组织下搜索延迟达标,结果排序符合组织规则。 |
| P1 | 组织版本与增量同步 | 部门和成员返回版本,后续需要全局组织版本、变更事件和客户端增量刷新。 | 切换部门或人员变更后客户端无需全量重拉。 |
| P1 | 会议离线 Push 联调 | 会议在线事件、系统通知和 Outbox 已完成,后续需使用真实 Firebase 凭据和移动端验证离线通知。 | 参与人离线时收到一次提醒,点击可跳转至正确会议。 |
| P0 | 组织字段级隐私策略 | 组织架构范围裁剪已具备,成员手机号、邮箱、职位等字段后续仍需按组织角色、隐私和企业策略进一步细化。 | 普通员工和管理员看到的成员字段不同且不可绕过。 |
| P1 | 组织全局成员接口深度联调 | `GET /v1/im/organization/members` 已具备,仍需按部门、关键词、权限和分页做测试环境回归。 | 前端可直接分页查询并按部门过滤,字段裁剪符合权限。 |
| P1 | 会议列表完整联调 | 对接文档标注 `GET /v1/im/meetings?cursor=` 待补列表;代码有 `serveIMMeetings`,仍需用测试数据确认返回结构。 | 会议列表、详情、响应、取消端到端一致。 |
| P1 | 月报 reopen 审计与权限扩展 | reopen 接口已具备,后续需要后台权限、原因记录、审计和通知闭环。 | 已提交月报可按权限退回草稿并保留审计。 |
| P1 | 日报/月报后台统计口径 | 当前基础统计可用,后续需要按部门、周期、迟交、补交、未读等维度扩展。 | 管理员看板与业务口径一致。 |
| P1 | 总经理信箱后台流转 | 当前前台配置/提交已具备,后续需要后台处理、回复、转派、归档和通知。 | 信件全生命周期可追踪,匿名身份受保护。 |
| P1 | 办公协同审计 | 组织查询、会议变更、报告查看、信箱提交等敏感动作需要审计。 | 后台可按租户、用户、对象查询办公操作流水。 |
| P2 | 办公模块统一附件策略 | 会议、日报、月报、信箱附件需要复用第七章文件 ACL、用途和病毒扫描。 | 附件上传、查看、下载权限与业务状态一致。 |
## 10. 个人设置与客户端能力
## 10. 第十章:个人设置与客户端能力
### 10.1 当前已完成基线
1. 个人资料 `GET/PATCH /v1/im/me/profile`、个人页 `GET /v1/im/me` 已具备。
2. 隐私设置 `GET/PATCH /v1/im/me/privacy` 已具备,支持版本冲突控制。
3. 最新版本检查 `GET /v1/im/app/releases/latest` 已具备,支持未登录按租户解析调用。
4. 青年模式查询、开启、关闭接口已具备,复用敏感操作 `action_token` 和独立密码校验链路。
3. 最新版本检查 `GET /v1/im/app/releases/latest` 已具备,版本策略为全局配置,不依赖租户解析。
4. 青少年模式查询、开启、关闭接口已具备,复用敏感操作 `action_token` 和独立密码校验链路。
5. 设备 Push Token 覆盖写入 `PUT /v1/im/devices/{device_id}/push-token` 已具备。
6. FCM 已优先使用产品 `im_device_push_tokens` 数据源,并支持通过环境变量注入 Firebase 凭据。
7. 用户搜索/加好友、个人资料、消息回执及 Tinode P2P 最后在线输出已执行对应隐私设置。
@@ -266,15 +313,15 @@
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | 指定会话撤销 | `auth/logout` 已通过 Redis token 摘要黑名单精准实现;`security/sessions/{id}` 仍未实现。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 |
| P0 | 个人资料变更实时同步 | 资料、头像、隐私变更后需要向多端推送 `profile.updated`、`privacy.updated`。 | 同账号多端无需重登即可刷新设置。 |
| P0 | 精准登出与会话撤销 | 现有 token 主要靠 `auth_version` 批量失效,后续需要 access/refresh/session 表支持当前设备登出和指定 session 删除。 | 当前设备退出不影响其他设备;删除指定 session 后该设备 token 立即失效。 |
| P0 | 青年模式策略配置化 | 当前返回默认 restrictions,后续需要后台按租户配置可用时段、内容限制、消费保护等规则。 | 不同租户返回不同限制策略且能热更新。 |
| P0 | 青少年模式策略配置化 | 当前返回默认 restrictions,后续需要后台按租户配置可用时段、内容限制、消费保护等规则。 | 不同租户返回不同限制策略且能热更新。 |
| P0 | Push Token 生命周期 | 当前可覆盖写入,后续需要 token 失效回收、设备解绑、平台厂商错误码处理。 | 推送失败后自动禁用无效 token,重新登录可恢复。 |
| P1 | 版本灰度发布 | 当前按 platform/channel 查最新版本,后续需要按租户、用户、灰度比例、最低版本强制升级。 | 灰度用户收到新版本,非灰度用户保持稳定版本。 |
| P1 | 隐私策略入口扩展 | 产品主要搜索、资料、回执和最后在线入口已执行隐私设置,组织架构字段、文件、通话和青少年模式仍需纳入统一策略。 | 非授权用户无法通过任一业务入口绕过隐私配置。 |
| P1 | 隐私策略入口扩展 | 组织架构字段、文件、通话和青少年模式仍需纳入统一策略。 | 非授权用户无法通过任一业务入口绕过隐私配置。 |
| P2 | 客户端能力上报 | 后续增加设备能力、通知权限、厂商通道状态和前台活跃状态上报。 | 后台可定位推送不可达和版本兼容问题。 |
## 11. 反诈安全、举报与意见反馈
## 11. 第十一章:反诈安全、举报与意见反馈
### 11.1 当前已完成基线
@@ -300,7 +347,20 @@
## 12. 统一处理建议
1. 优先剩余 P0:验证码真实发送与多维限频、消息撤回历史占位、P2P 文件/通话策略统一、@通知与频控、文件安全与短时授权、通话强状态机、工作台/组织权限和风控 token 业务消费。
2. 再做剩余 P1:租户策略配置、实名审核、设备/会话管理、字段级隐私矩阵、关系审计、群文件与解散状态机、通话质量指标、报表/信箱后台流转和真实 FCM 离线联调。
3. 最后做 P2:端到端重连测试、schema 版本演进、大群压测、批量接口体验优化、收藏导出、通话记录聚合和办公模块附件统一策略。
4. 每个增强项落地后同步更新本文档、`IM-系统设计文档.md`、`IM-前后端对接文档.md` 和 `IM-需求实现情况审核报告.md`,避免已完成项继续出现在待办清单。
1. 第一优先级处理生产可用性 P0:真实短信、登出/会话撤销、普通消息转发/合并转发、文件安全、通话强状态机、工作台权限、风控 token 消费。
2. 第二优先级补齐已落地接口的联调闭环:P2P 会话、会话设置、云草稿、群文件、群邀请外部接受、实名申诉、组织全局成员、月报 reopen。
3. 第三优先级统一协议和治理能力:所有列表补齐 `has_more`、稳定游标、审计流水、字段级隐私矩阵、租户配置化策略。
4. 万人大群未实现增强项仅保留离线能力:离线只走极光 Push 提醒,客户端重连后按 Topic seq 拉取补偿;实时推送能力归入 `IM-Redis三层数据分层改造方案.md` 后续直接实现。
5. Redis 读性能深度优化继续保留:群成员分页 sorted set/稳定游标/主动失效、Topic 维度未读计数、bootstrap 聚合缓存和主动失效事件。
6. 每个增强项落地后同步更新本文档、`IM-系统设计文档.md`、`IM-前后端对接文档.md` 和接口审核报告,避免已完成项继续出现在待办清单。
## 13. Kafka 联调后续增强项
来源:`IM-Kafka接入联调测试文档.md` 10.2。本轮已完成后端 Kafka 主链路联调,但端到端页面与真实客户端仍需专项补测;同时 Kafka 断线恢复暴露出进程稳定性问题。
| 优先级 | 增强项 | 说明 | 建议验收点 |
| --- | --- | --- | --- |
| P0 | Admin Kafka 断线自动恢复 | 本地联调中停止 Kafka 后,Admin consumer 报 `client has run out of available brokers` 并 fatal 退出。 | Kafka 停止/恢复期间 Admin 进程不退出,consumer 退避重连,恢复后继续消费积压消息。 |
| P0 | IM Kafka consumer 自动恢复 | Kafka 恢复后 IM 仍 healthy,但 consumer 未立即消费新事件,重启 IM 后才补偿消费。 | Kafka broker 重启后无需重启 IM,`im.admin.command`、`im.message.persist`、`im.cache.invalidate` 等 Topic 自动恢复消费。 |
| P1 | Admin 前端页面人工验收 | 后端接口、Kafka、数据库状态已测;仍需在浏览器逐个菜单验收操作单、死信、审计和状态流转展示。 | 后台页面可展示 pending/running/succeeded/failed/dead,死信可查询、忽略、解决、重放,审计列表与详情可查看。 |
| P1 | 真实 C 端 WebSocket 联调 | 后台命令已生成业务事件/系统通知;仍需真实客户端在线观察 WS 实时刷新。 | 在线客户端收到群治理、用户治理、工作台、系统通知等事件;断线重连后可按 seq/bootstrap 补偿。 |
@@ -0,0 +1,550 @@
# IM 注册、邀请码与认证数据模型改造需求文档
## 0. 统一验证码输入变更(2026-09-16)
本节为最新规则,覆盖本文后续章节中“独立邀请码输入框”“短信开启时邀请码不能替代验证码”等旧描述:
1. 注册页面取消独立邀请码输入框,邀请码与短信验证码共用同一个输入框。
2. 5 位大小写英文字母按邀请码处理,6 位数字按短信验证码处理,其他格式拒绝。
3. 有效邀请码可以作为注册放行凭证;注册成功后写入 `im_registration_invite_uses`,建立邀请码与新用户的推荐使用关系。
4. 短信验证码注册会建立已完成的 `tel credential`;邀请码注册只保存手机号,不把手机号标记为短信已验证。
5. 新客户端不再提交独立 `invite_code`,而是在 `{acc}.cred[].resp` 中提交用户在统一输入框填写的 5 位邀请码。旧 `invite_code` 字段仅保留向后兼容。
6. `POST /v1/im/auth/verification-codes/{challenge_id}/verify` 输入邀请码时返回 `{"verification_mode":"invite_code","invite_accepted":true}`;输入短信验证码时继续返回一次性 `verification_token`。
> 版本:V1.0 评审稿
> 编写日期:2026-08-18
> 适用范围:IM 客户端、IM 服务端、HotGo 管理后台、MySQL
> 关联文档:IM-前后端对接文档.md 1.4、IM-系统设计文档.md
## 1. 文档目的
本文统一定义以下改造:
1. 注册账号支持“用户名、手机号、用户名加手机号”三种输入组合。
2. 手机号注册支持短信验证,后端固定补中国国家号 +86。
3. HotGo 后台增加注册短信发送开关。
4. 邀请码作为无短信验证时的注册放行凭证。
5. 邀请码支持单个生成、批量生成、自定义有效期、撤销和使用审计。
6. im_users 增加非空 username,phone 改为可空。
7. im_auth 删除数据库字段 scheme 和 uname,只按 userid 保存 Basic 密码凭据。
## 2. 核心业务定义
| 名称 | 定义 |
| --- | --- |
| 用户名输入 | 注册页面独立的用户名输入框 |
| 手机号输入 | 注册页面独立的手机号输入框,只接受 11 位 ASCII 数字 |
| 放行优先级 | 手机号非空且短信开关开启时以验证码为注册放行依据;短信开关关闭或未填写手机号时以邀请码为注册放行依据 |
| 已验证手机号 | 已成功消费 register 场景 verification_token,并建立已完成 tel credential 的手机号 |
| 用户名注册 | 保存用户填写的 username;未填写手机号时依靠有效邀请码放行,phone 为 NULL |
| 手机号注册 | 手机号字段非空的注册;保存用户填写的 username 和规范化 phone,未填写 username 时由服务端随机生成 username;短信验证状态由 tel credential 区分 |
| 注册短信开关 | 控制服务端是否真正调用短信服务商的全局后台配置 |
| 邀请码 | 租户内生成的 5 位大小写英文字母明文码,区分大小写、不可重复,有效期内可供多人使用、可撤销 |
## 3. 用户账号数据规则
### 3.1 im_users 字段
目标字段:
| 字段 | 约束 | 说明 |
| --- | --- | --- |
| username | 非空、租户内唯一 | 主登录账号;用户填写的普通用户名必须匹配 `^[a-z0-9]{1,10}$`。未填写 username 但填写手机号时,由服务端随机生成 3 位小写字母 + 6 位数字的系统 username |
| phone | 可空、租户内唯一 | 用户填写手机号时的注册号码,统一保存 E.164;是否完成短信验证由 tel credential 判断 |
落库规则:
| 注册方式 | username | phone |
| --- | --- | --- |
| 用户名加手机号,验证码注册 | alice | +8613800138000 |
| 仅手机号,验证码注册 | abc123456 | +8613800138000 |
| 普通用户名加邀请码注册 | alice | NULL |
| 用户名加手机号,短信关闭邀请码注册 | alice | +8613800138000 |
| 仅手机号,短信关闭邀请码注册 | abc123456 | +8613800138000 |
普通用户名注册规则:
1. 必须匹配 `^[a-z0-9]{1,10}$`,长度为 1~10 位。
2. 只允许 ASCII 小写英文字母 `a-z` 和数字 `0-9`,允许纯字母、字母数字组合或纯数字。
3. 大写字母、中文、空格、点、下划线、冒号及其他符号均不允许;服务端校验原始输入,不通过静默转小写来放行不合规注册。
4. 前端按相同规则限制输入并提示,服务端必须再次校验,不能只依赖客户端。
5. 该规则只约束用户填写的普通用户名。服务端随机生成的系统 username 固定为 9 位 `^[a-z]{3}[0-9]{6}$`,属于系统值例外;存量迁移生成的内部保留账号也不适用此正则,不能作为普通用户名注册。
### 3.2 字段保存和手机号规范化
1. 页面分别展示用户名输入框和手机号输入框。
2. 用户名字段非空时必须按原值校验并保存到 `im_users.username`,不能因为同时填写手机号而忽略用户名。
3. 手机号字段非空时必须校验并保存到 `im_users.phone`;手机号字段为空时 `phone` 保持 NULL。
4. 手机号字段使用 ^[0-9]{11}$ 校验;手机号非空但格式错误时直接拒绝,不能降级为用户名方式。
5. 发送短信和落库前,后端把 11 位数字规范化为 +86xxxxxxxxxxx。
6. 用户名和手机号同时填写时,`username` 保存用户填写的用户名,`phone` 保存规范化手机号。
7. 仅手机号注册且 username 为空时,服务端随机生成 `^[a-z]{3}[0-9]{6}$` 格式的 username,检查租户内唯一,冲突时重新生成;`phone` 保存规范化手机号。
8. 不提供国家和区号选择,本期固定中国 +86。
9. 登录页面仍使用统一的“用户名/手机号”输入框;先精确查询 username,未命中时将 11 位输入补 +86 后查询 phone。
10. 注册、绑定手机号时必须检查 username 和 phone 两个命名空间,防止同一输入命中两个用户。
11. 普通用户名注册必须先校验 `^[a-z0-9]{1,10}$`;用户名字段非空且格式错误时必须拒绝,不能因手机号合法而跳过用户名校验。
12. 注册创建公开资料时,账户昵称暂时默认等于最终保存的 username;客户端未传 `desc.public.fn` 或传入值为空时,服务端使用最终 username 填充昵称。
## 4. 注册放行规则
### 4.1 决策表
| 用户名字段 | 手机号字段 | 短信开关 | 有效验证票据 | 邀请码 | 服务端结果 |
| --- | --- | --- | --- | --- | --- |
| 已填 | 空 | 任意 | 不适用 | 有效 | 放行,保存 username,phone=NULL |
| 已填 | 空 | 任意 | 不适用 | 未填或无效 | 拒绝 |
| 已填 | 11 位 | 开 | 有 | 未填 | 放行,保存 username 和 phone |
| 已填 | 11 位 | 开 | 有 | 有效 | 放行,保存 username 和 phone,并记录邀请码使用关系 |
| 空 | 11 位 | 开 | 有 | 未填或有效 | 放行,随机生成 username,phone 写手机号 |
| 任意 | 11 位 | 开 | 无 | 任意 | 拒绝,邀请码不能绕过已开启的短信验证 |
| 已填 | 11 位 | 关 | 无 | 有效 | 放行,保存 username 和 phone |
| 空 | 11 位 | 关 | 无 | 有效 | 放行,随机生成 username,phone 写手机号 |
| 任意 | 11 位 | 关 | 无 | 未填或无效 | 拒绝 |
| 已填 | 格式错误 | 任意 | 任意 | 任意 | 拒绝,不能降级为用户名方式 |
| 空 | 空 | 任意 | 任意 | 任意 | 拒绝,必须填写用户名或手机号 |
补充规则:
1. 有效 verification_token 的优先级高于当前短信开关。开关关闭不能让已验证票据失效。
2. 短信开关开启时,邀请码不能绕过手机号验证。
3. 用户主动填写邀请码后,服务端必须校验;不能在邀请码无效时静默忽略。
4. 手机号字段非空且短信开关开启时,以验证码为准,邀请码可不填;手机号字段为空或短信开关关闭时,必须填写有效邀请码。
5. 短信关闭时邀请码替代短信验证作为注册授权,但不建立已完成的 tel credential。
6. 用户创建、租户用户配额校验、邀请码使用记录、手机号票据消费应原子完成。
### 4.2 客户端页面行为
1. 页面分别显示“请输入用户名”和“请输入手机号”。
2. 用户名和手机号是独立字段;两个都填写时都参与注册,用户名保存为主登录账号,手机号保存为绑定号码。
3. 手机号为空时不发送短信,使用用户名加邀请码方式注册。
4. 手机号为 11 位数字时允许点击“获取验证码”。
5. 用户名方式不发送短信,验证码字段不作为提交项。
6. 邀请码字段始终显示,文案为“请输入邀请码(非必填)”。
7. 短信开关关闭时,发码接口仍返回正常倒计时,前端进入与开关开启时相同的倒计时状态。
8. 前端不自行决定最终是否可以免验证码注册,最终放行完全由服务端判断。
9. 用户名方式注册前提示:未绑定手机号时无法自助找回密码,只能联系企业管理员重置。
## 5. 注册短信发送开关
### 5.1 HotGo 配置
HotGo 后台增加全局配置:
~~~text
group = im
key = register_sms_enabled
type = bool
value = 1 / 0
~~~
实现约定:
1. 管理界面的开关选项复用 HotGo 已有 sys_switch 字典。
2. 当前开关值保存到 hg_sys_config;字典只负责提供开/关选项,不作为配置值真源。
3. 只有平台管理员可以修改。
4. 修改操作写 HotGo 后台审计日志。
5. IM 服务端在可控缓存时间内读取最新值。
6. 本期为全局开关;未来需要按企业控制时再迁移到 im_tenant_config。
### 5.2 开关开启
1. 服务端生成强随机验证码。
2. 创建 register 场景挑战并执行限频。
3. 调用短信服务商发送验证码。
4. 短信服务商失败时返回稳定错误 SMS_DELIVERY_FAILED。
5. 禁止继续使用固定测试码 123456。
### 5.3 开关关闭
1. 发码接口仍创建限频挑战。
2. 返回 challenge_id、expires_in_seconds 和 resend_after_seconds。
3. 不调用短信服务商。
4. 不生成客户端可猜测或可验证的固定验证码。
5. 前端仍正常显示倒计时,但用户不会收到短信。
6. 记录内部 delivery_suppressed 审计事件,不向客户端暴露开关状态。
## 6. 邀请码管理
### 6.1 基本规则
1. 邀请码按租户隔离,不能跨企业使用。
2. 邀请码固定为 5 位 ASCII 大小写英文字母,格式为 `^[A-Za-z]{5}$`,不包含数字和符号。
3. 邀请码区分大小写,`AbCdE` 与 `abcde` 是两个不同的邀请码;客户端和服务端不得自动转换大小写。
4. 邀请码由服务端使用密码学安全随机数从 `A-Z`、`a-z` 字符集中生成,不能使用递增数字、用户 ID 或批次 ID。
5. 同一租户内邀请码不能重复,数据库唯一索引是最终约束;单个或批量生成发生冲突时必须重新生成,不得返回重复码。
6. 邀请码以明文落库,并使用完整明文进行大小写敏感的精确查询和注册校验。
7. 每个邀请码在有效期内可以成功注册多个账号,不设置使用次数上限。
8. 管理员创建时自行设置 expires_at,必须晚于当前时间。
9. 状态包括 active、expired、revoked,不存在 used 状态。
10. expired、revoked 状态不可恢复为 active。
11. 邀请码校验必须同时检查租户、完整明文、状态和到期时间,不因已有其他用户使用记录而失效。
### 6.2 单个生成
请求示例:
~~~json
{
"expires_at": "2026-09-01T00:00:00+08:00",
"remark": "新员工入职"
}
~~~
服务端返回一个 5 位邀请码。具备邀请码查看权限的租户管理员可在后续列表和详情中查看完整明文。
### 6.3 批量生成
请求示例:
~~~json
{
"quantity": 100,
"expires_at": "2026-09-01T00:00:00+08:00",
"remark": "2026年8月新员工入职"
}
~~~
规则:
1. 一次批量生成共用 batch_id 和 expires_at。
2. 每个邀请码均为独立生成的 5 位大小写英文字母,租户内唯一且区分大小写。
3. 批量请求必须支持幂等键,重试不能生成重复批次。
4. 单批数量由后台配置限制,建议默认上限 1000。
5. 生成成功后支持明文导出;后续再次查询或导出必须校验邀请码查看/导出权限并写审计。
### 6.4 查询、撤销和审计
管理后台支持:
1. 按完整邀请码进行大小写敏感的精确查询。
2. 按状态、批次、创建人、创建时间、到期时间查询。
3. 列表和详情返回邀请码明文,接口必须校验当前租户的邀请码查看权限。
4. 单个撤销仍处于 active 的邀请码;撤销不删除历史使用记录。
5. 按 batch_id 批量撤销仍处于 active 的邀请码。
6. 查看邀请码的累计使用次数、使用用户、使用时间和请求 ID。
7. 所有生成、查询、查看、导出、撤销操作写后台审计日志。
### 6.5 HotGo 管理接口
| 方法 | 路径 | 说明 |
| --- | --- | --- |
| POST | /imInvite/create | 单个生成 |
| POST | /imInvite/batchCreate | 批量生成 |
| POST | /imInvite/list | 分页或明文精确查询 |
| POST | /imInvite/revoke | 单个撤销 |
| POST | /imInvite/batchRevoke | 批量撤销 |
| GET | /imInvite/batchView | 批次详情 |
| POST | /imInvite/export | 按过滤条件导出 CSV |
HotGo 根据可信登录上下文取得 `im_tenant.id`,通过 `im_tenant.db_dsn/db_name` 选择租户业务库并直接维护邀请码管理数据。浏览器不能提交可信 DSN 或决定实际租户库。
IM 不再提供 `/admin/v1/im/*` HMAC 管理接口。注册校验和 `im_registration_invite_uses` 写入仍由 IM 在用户注册事务中完成,HotGo 不参与注册事务。
管理员重置密码时,HotGo 在同一个租户库事务中更新 `im_auth.secret`、递增 `im_user_security.auth_version`,并写入 `im_business_events_outbox` 的 `security.password_reset` 事件。IM 当前轮询此事件并清退在线会话;后续迁移 Kafka 时保持事件类型和 payload 不变,只替换投递与消费通道。
## 7. 客户端注册协议
### 7.1 发码
~~~http
POST /v1/im/auth/verification-codes
~~~
~~~json
{
"scene": "register",
"phone": "13800138000"
}
~~~
后端补 +86 后发送并保存手机号哈希。开关关闭时返回字段和倒计时保持一致。
### 7.2 提交注册
在现有 WebSocket acc 消息中增加 username、phone 和 invite_code:
~~~json
{
"acc": {
"id": "acc-1",
"user": "new",
"scheme": "basic",
"username": "alice",
"phone": "",
"secret": "base64(account:password)",
"invite_code": "AbCdE",
"login": true,
"cred": []
}
}
~~~
用户名方式时,phone 为空,secret 使用 username:password,cred 为空且 invite_code 必须有效。
手机号字段非空时,短信开关开启则必须通过验证码放行,cred 传递一次性 verification_token;短信开关关闭则走邀请码放行,cred 可为空且 invite_code 必须有效。用户名字段非空时保存用户名并使用 username:password 生成 secret;用户名字段为空时由服务端随机生成 3 位小写字母 + 6 位数字的 username,客户端提交 secret 时仍使用 phone:password。短信关闭路径不建立已完成的 tel credential。
服务端必须校验 secret 中的登录名与客户端可确定的注册账号一致:username 非空时必须匹配 username;username 为空且 phone 非空时必须匹配规范化手机号。服务端生成的随机 username 在账号创建后返回给客户端,并作为后续登录主账号。username 和 phone 独立传递是注册路由信息,不能只根据 secret 是否为 11 位数字推断,因为普通用户名允许使用纯数字。
账户昵称暂时与最终 username 保持一致:服务端创建用户资料时以最终 username 写入 `desc.public.fn` 或资料表昵称;仅手机号注册时昵称等于随机生成的 username,不使用手机号作为昵称。
客户端不得提交 phone_account、sms_enabled、skip_verification 等服务端决策字段。
## 8. im_auth 改造
### 8.1 改造边界
当前只有 Basic 密码凭据写入 im_auth:
1. token 是无状态签名,不写 im_auth。
2. code 使用临时缓存,不写 im_auth。
3. anonymous 不保存凭据。
4. rest 调用外部认证服务。
因此数据库中的 scheme=basic 是冗余信息。登录账号真源迁移到 im_users 后,uname 和 basic: 前缀同样冗余。
协议层仍保留 acc.scheme 和 login.scheme,用于选择 basic 或 token 认证处理器。本次只删除数据库字段。
这里删除的 uname 仅指 `im_auth.uname` 数据库列。Basic 协议仍从 `secret` 中解析登录账号和密码,登录账号改为查询 `im_users.username` 或 `im_users.phone`;除非后续同时改造客户端和协议消息结构,否则 `account:password` 格式及用户名禁止冒号的技术约束继续保留。
### 8.2 目标表结构
~~~sql
CREATE TABLE im_auth (
id INT NOT NULL AUTO_INCREMENT,
userid BIGINT NOT NULL,
authlvl INT NOT NULL,
secret VARCHAR(255) NOT NULL,
expires DATETIME,
PRIMARY KEY (id),
UNIQUE INDEX auth_userid (userid),
CONSTRAINT fk_im_auth_user
FOREIGN KEY (userid) REFERENCES im_users(id)
);
~~~
变更内容:
1. 删除 scheme。
2. 删除 uname。
3. 删除 auth_uname 和 auth_userid_scheme。
4. 增加 UNIQUE(userid)。
5. 保留 authlvl,以兼容普通用户和 ROOT 账号。
6. 保留 secret 和 expires。
### 8.3 登录和密码处理
1. Basic 登录先规范化账号输入。
2. 先从 im_users.username 查询 userid。
3. 未命中且输入为 11 位数字时,补 +86 后从 im_users.phone 查询 userid。
4. 按 userid 从 im_auth 读取 bcrypt 密码摘要。
5. 修改密码、重置密码、删除认证和 ROOT 提升都按 userid 操作。
6. Store 和 Adapter 接口不再传递 scheme、unique 或 basic: 前缀。
### 8.4 密码找回
1. im_users.phone 非空的账号可以使用短信找回密码。
2. 找回流程向 im_users.phone 发送 reset_password 场景验证码,成功消费验证码后才允许设置新密码。
3. 如果该手机号注册时未建立完成状态的 tel credential,成功完成密码找回验证后可以补建该凭证。
4. im_users.phone 为 NULL 的用户名账号不提供短信或其他自助找回方式。
5. 无手机号账号忘记密码后,只能联系当前租户管理员执行后台密码重置。
6. 管理员重置密码必须递增 auth_version、撤销旧 Token 和在线 Session,并写安全审计。
7. 管理员只能重置当前租户用户,不能通过用户名跨租户查找或操作。
### 8.5 密码与登录 Token
1. 客户端按现有 Basic 协议提交 `base64(account + ":" + password)`。Base64 只是协议编码,不是密码加密;账号注册和登录必须使用 HTTPS/WSS。
2. 服务端解析出密码后使用 bcrypt 单向哈希,当前成本参数为 `bcrypt.DefaultCost=10`;`im_auth.secret` 只保存 bcrypt 哈希,不保存可逆密文或明文。
3. 登录 Token 继续沿用现有 Tinode HMAC 签名 Token,默认有效期为 14 天,服务端不保存 Token 明文。
4. 客户端保存登录响应中的 `params.token` 和 `params.expires`,Token 必须放入 iOS Keychain 或 Android Keystore,不得写入普通数据库、日志或埋点。
5. 每次 Basic 登录或有效 Token 登录成功后,服务端都返回新的 14 天 Token;客户端必须用新 Token 覆盖旧 Token。
6. 当前不增加 Refresh Token 和独立刷新接口。Token 未过期时可通过 `login.scheme=token` 登录并取得新 Token;Token 已过期或校验失败时必须重新进行账号密码登录。
7. Token 内绑定 `tenant_id`、`uid`、`auth_version` 和过期时间。密码重置、管理员重置或账号安全状态变化后递增 `auth_version`,使旧 Token 失效。
## 9. 数据库设计
### 9.1 邀请码批次
~~~sql
CREATE TABLE im_registration_invite_batches (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
quantity INT UNSIGNED NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
expires_at DATETIME(3) NOT NULL,
created_by BIGINT NOT NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX uk_im_registration_invite_batch_tenant_idempotency (tenant_id, idempotency_key)
);
~~~
### 9.2 邀请码
~~~sql
CREATE TABLE im_registration_invite_codes (
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
batch_id BIGINT,
code CHAR(5) CHARACTER SET ascii COLLATE ascii_bin NOT NULL,
status VARCHAR(16) NOT NULL,
expires_at DATETIME(3) NOT NULL,
usage_count INT UNSIGNED NOT NULL DEFAULT 0,
last_used_at DATETIME(3),
created_by BIGINT NOT NULL,
remark VARCHAR(255) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
UNIQUE INDEX uk_im_registration_invite_code_tenant (tenant_id, code),
INDEX idx_im_registration_invite_tenant_status_expires (tenant_id, status, expires_at),
INDEX idx_im_registration_invite_tenant_batch (tenant_id, batch_id)
);
~~~
### 9.3 邀请码使用记录
~~~sql
CREATE TABLE im_registration_invite_uses (
id BIGINT NOT NULL,
invite_code_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
request_id VARCHAR(64) NOT NULL,
used_at DATETIME(3) NOT NULL,
PRIMARY KEY (id),
INDEX idx_im_registration_invite_use_code (invite_code_id),
UNIQUE INDEX uk_im_registration_invite_use_user (user_id)
);
~~~
`im_registration_invite_batches` 和 `im_registration_invite_codes` 位于 HotGo 控制库,使用 `tenant_id` 隔离租户,便于平台统一查询、导出和管理。`usage_count`、`last_used_at` 是租户使用明细的查询投影,不是使用事实源。
`im_registration_invite_uses` 位于对应租户业务库,是邀请码使用事实源。注册成功时,IM 在同一租户库事务中写入使用明细和 `registration.invite_used` outbox 事件;当前由 IM 数据库轮询器按该邀请码的租户明细重新聚合并覆盖控制库统计,重试不会重复计数。后续切换 Kafka 时保持事件类型和 payload 不变。三张表的 BIGINT 主键均使用服务端雪花 ID。
邀请码以明文保存在 HotGo 控制库中。`ascii_bin` 保证查询和唯一索引区分大小写;所有校验必须同时使用 `tenant_id` 和完整 5 位邀请码精确匹配,禁止 `LOWER(code)`、`UPPER(code)` 或模糊查询。
## 10. 主要错误码
| 业务码 | 含义 |
| --- | --- |
| ACCOUNT_REQUIRED | 账号输入为空 |
| ACCOUNT_POLICY_VIOLATION | 账号违反最低技术约束 |
| ACCOUNT_ALREADY_EXISTS | 用户名或手机号命名空间冲突 |
| PHONE_VERIFICATION_REQUIRED | 手机号注册缺少有效验证票据 |
| INVITE_CODE_REQUIRED | 用户名注册缺少邀请码 |
| INVITE_CODE_INVALID | 邀请码不存在 |
| INVITE_CODE_EXPIRED | 邀请码已过期 |
| INVITE_CODE_REVOKED | 邀请码已撤销 |
| INVITE_CODE_RATE_LIMITED | 邀请码校验过于频繁 |
| SMS_DELIVERY_FAILED | 短信服务商发送失败 |
| USERNAME_INVALID_FORMAT | 用户名不是 1~10 位小写英文字母或数字 |
邀请码无效、过期、已撤销可对客户端统一显示“邀请码无效”,详细原因仅写安全审计,降低枚举风险。
## 11. 存量迁移
采用“扩容、回填、切换、收缩”方式:
1. 为 im_users 增加可空 username。
2. 去除 im_auth.uname 中的 basic: 前缀后回填 im_users.username。
3. 将现有 phone 统一规范化为 E.164。
4. 对无 Basic 认证记录的内部、已删除或异常用户生成 reserved_<userid>,并输出迁移报告供人工复核。
5. 检查重复 username、重复 phone 和跨字段冲突。
6. 新版本短期双读旧 im_auth 和新 im_users 账号字段。
7. 切换注册、登录、修改密码、密码重置和 ROOT 工具链。
8. 将 username 改为非空并增加唯一索引。
9. 观察期结束后删除 im_auth.scheme、im_auth.uname 和旧索引。
回滚时在观察期内继续保留旧列和旧读路径,不允许通过重新拼接 basic: 猜测恢复数据。
## 12. 安全与已知风险
1. 5 位邀请码属于短授权码,必须使用密码学安全随机数生成、设置有效期、可撤销并可审计;注册校验必须严格限频以降低枚举风险。邀请码允许多人使用意味着泄露后的影响面更大,应缩短有效期并及时撤销异常邀请码。
2. 邀请码校验按租户、IP、设备和账号输入限频。
3. 邀请码虽按业务要求明文落库和授权查询,但不得写入普通日志、埋点或错误响应;密码、短信验证码和 verification_token 同样禁止记录。
4. 邀请码使用记录、用户创建、票据消费和租户配额扣减应在同一事务完成;邀请码本身不核销、不切换为 used。
5. 短信开关关闭时不能保留固定测试验证码。
6. 批量生成、批量导出和批量撤销必须限制数量并单独授权。
### 12.1 邀请码手机号注册风险
短信关闭时,有效邀请码可以在未完成短信验证的情况下把手机号写入 phone;如果用户同时填写 username,username 保存用户填写值,否则由服务端随机生成 3 位小写字母 + 6 位数字的 username。
为避免把“已填写手机号”和“已验证手机号”混为一谈:
1. im_users.phone 保存手机号方式注册时的号码。
2. 只有成功消费 verification_token 时才建立完成状态的 tel credential。
3. 短信关闭加邀请码注册不建立完成状态的 tel credential。
4. phone 非空账号可以通过 reset_password 短信挑战证明当前持有手机号后自助重置密码。
5. phone=NULL 的账号不能自助找回密码,只能由当前租户管理员重置。
6. 其他要求“已验证手机号”的敏感操作必须检查 tel credential,不能只检查 phone 非空。
7. 后续完成短信验证后,可为同一用户补建 tel credential。
8. 邀请码不证明手机号所有权,允许该路径意味着平台接受手机号被提前占用的业务风险。
## 13. 验收用例
### 13.1 注册
1. 只填写手机号,短信开启且验证成功,不填邀请码可以注册,username 为服务端随机生成的 3 位小写字母 + 6 位数字,phone 为规范化手机号。
2. 只填写手机号,短信开启但没有验证票据,即使邀请码有效也不能注册。
3. 只填写手机号,短信关闭且邀请码有效,无验证码可以注册,username 为服务端随机生成的 3 位小写字母 + 6 位数字,phone 为规范化手机号。
4. 只填写手机号,短信关闭且不填邀请码不能注册。
5. 只填写用户名并提供有效邀请码可以注册,phone=NULL。
6. 只填写用户名但不填邀请码不能注册。
7. 用户名和手机号同时填写且短信开启时,验证码验证成功后可以注册,username 保存用户填写的用户名,phone 保存规范化手机号。
8. 用户名和手机号同时填写且短信关闭时,邀请码有效即可注册,username 保存用户填写的用户名,phone 保存规范化手机号。
9. 手机号非空但格式错误时拒绝,不能回退到用户名方式。
10. 用户名和手机号同时为空时拒绝注册。
11. 过期、已撤销邀请码均不能注册。
12. 同一有效邀请码并发注册时可以有多个不同用户成功,每个用户分别写入一条使用记录,邀请码保持 active。
13. 用户名账号 phone=NULL 时,短信找回接口不能重置密码。
14. 管理员可以重置 phone=NULL 用户的密码,重置后旧 Token 和 Session 全部失效。
15. 普通用户名 `alice01`、`a`、`1234567890` 可以进入注册校验。
16. 普通用户名 `Alice`、`alice_1`、`alice.1`、`中文`、空字符串或超过 10 位时返回 `USERNAME_INVALID_FORMAT`。
17. 用户名格式不合法但手机号合法且非空时仍拒绝,不能忽略用户名输入。
18. 仅手机号注册时,随机 username 必须在租户内唯一;发生唯一索引冲突时服务端重新生成并重试。
19. 注册成功后的账户昵称等于最终 username:填写用户名时昵称等于该用户名;仅手机号注册时昵称等于随机 username。
### 13.2 短信开关
1. 开关关闭后发码接口不调用短信服务商。
2. 开关关闭后发码接口仍返回正常倒计时。
3. 开关从关闭改为开启后,新请求恢复调用短信服务商。
4. 开关修改在规定缓存时间内对 IM 服务生效。
### 13.3 邀请码管理
1. 单个生成可设置有效期。
2. 单个和批量生成的邀请码均匹配 `^[A-Za-z]{5}$`,不包含数字或符号。
3. 批量生成数量正确,同一租户所有邀请码互不重复;并发生成冲突时由唯一索引拦截并重试。
4. `AbCdE` 和 `abcde` 按两个不同邀请码精确查询和校验,不得自动转换大小写。
5. 有权限的管理员可以按完整邀请码查询,并在列表、详情和导出中取得明文。
6. 批量请求重试不会生成第二批数据。
7. 单个和批量撤销立即生效,历史使用用户不受影响。
8. 无权限用户不能查看、查询或导出邀请码明文。
9. 管理员可以查看批次使用统计和单码使用记录。
### 13.4 认证回归
1. 存量 Basic 用户迁移后可使用原账号和密码登录。
2. 登录、注册、修改密码、密码重置和 ROOT 提升不再依赖 im_auth.scheme、im_auth.uname 或 basic: 前缀。
3. 同一 username 或 phone 在不同租户仍可独立注册。
4. im_auth 每个 userid 最多保留一条记录。
## 14. 评审结论
本方案的核心决策为:
1. 注册页的用户名和手机号是两个独立输入框,用户名非空时必须保存,手机号非空时必须保存。
2. username 是所有用户非空的主登录账号。
3. 用户名和手机号同时填写时,username 保存用户名,phone 保存规范化手机号;仅手机号注册时 username 由服务端随机生成 3 位小写字母 + 6 位数字。
4. 手机号非空且短信开关开启时以验证码为注册放行依据;短信开关关闭时以邀请码为注册放行依据。
5. 短信关闭时,手机号输入搭配有效邀请码可注册,但不建立已完成的 tel credential。
6. 邀请码为 5 位大小写英文字母,区分大小写,以明文落库并在租户内唯一;有效期内可供多人使用,支持单个、批量、自定义有效期、明文查询/导出、撤销和使用审计。
7. im_auth 只按 userid 保存 Basic 密码凭据,数据库删除 scheme、uname 和 basic: 前缀。
8. 协议层仍保留 basic/token scheme,不与数据库结构混淆。
9. phone=NULL 的用户名账号不能自助找回密码,只能由租户管理员重置。
10. HotGo 与 IM 当前以共享租户业务库集成,不再使用内部 HMAC 管理 HTTP;跨进程安全事件先经数据库 outbox,后续迁移 Kafka。
评审通过后,实施工作拆分为数据库迁移、IM 注册与认证、HotGo 后台、客户端对接和全量回归五个任务。
@@ -0,0 +1,223 @@
# IM 测试环境接口审核报告(2026-08-20)
## 1. 审核范围
- 对照文档:`docs/IM-前后端对接文档.md`
- 测试环境:`https://api.118im.com`
- WebSocket:`wss://api.118im.com/v0/channels`
- API Key:已使用测试环境普通 Key,报告中不展开。
- 租户:`default`
- 测试账号:本轮通过短信验证码固定码 `123456` 和 WebSocket `{acc}` 临时注册,用户 ID `usrIPhpZJeJDMI`。
- 本轮只新增本报告,不修改原前后端对接文档。
## 2. 总体结论
测试环境已能跑通基础启动、注册、登录、bootstrap、通讯录空列表、建群、工作台、日报/月报草稿、个人资料、隐私、反馈、设备推送 Token 等主链路。整体方向和产品需求基本匹配:Tinode WebSocket 承担消息与 Topic,`/v1/im/*` 承担产品化业务接口。
但当前仍不建议直接按整份对接文档全量联调。主要风险是:部分文档接口没有路由或路由兜底错误,部分配置类接口在测试库空数据时返回 500/502,部分通用响应约定没有统一落到所有列表接口,办公协同和转发/群文件相关接口还存在明显缺口。
## 3. P0/P1 问题
处理备注(2026-08-20):
- 已在提交 `6c6d2f6 fix: stabilize im audit endpoints` 中处理本节前 6 项,以及 `bootstrap.limits` 缺失问题。
- 已用本地 Docker 启动服务并调用本地 API 验证通过;测试环境需发布该提交后再复测公网结果。
- 当前短信供应商未接入时,注册、找回密码和敏感操作验证码均按测试码 `123456` 验证;生产接入短信供应商后应切回真实短信策略。
| 优先级 | 问题 | 测试结果 | 影响 | 建议 |
| --- | --- | --- | --- | --- |
| P0 | 密码重置发码 `scene=reset_password` 返回 502 | `POST /v1/im/auth/verification-codes`,Body `{"scene":"reset_password","phone":"15509271990"}`,带 `Idempotency-Key`,返回 Cloudflare `502` | 忘记密码无法联调,且不是统一 JSON 错误 | 已处理:短信未接入时保留 challenge,并用测试码 `123456` 验证;本地 API 验证返回 200 |
| P0 | 登录后二次身份验证挑战返回 502 | `POST /v1/im/security/challenges`,Body `{"scene":"close_youth_mode","channel":"sms"}`,带 `Idempotency-Key`,返回 Cloudflare `502` | 关闭青少年模式、敏感操作二次验证不可用 | 已处理:短信发送失败不再使 challenge 失败;带 `Idempotency-Key` 创建和 `123456` verify 本地均返回 200 |
| P0 | 总经理信箱配置和提交返回 500 | `GET /v1/im/executive-mailbox/config` 返回 `INTERNAL_ERROR`;按文档提交也返回 500 | 工作台入口开启但页面不可用 | 已处理:修复 `im_mailbox_config`、`im_mailbox_submissions` 的 `SELECT *` 映射问题;本地 config/submit 均返回 200 |
| P1 | 举报原因返回 500 | `GET /v1/im/reports/abuse/reasons?target_type=user` 返回 `INTERNAL_ERROR` | 举报页无法打开 | 已处理:补齐 `AbuseReason` 数据库字段映射;本地返回 200 和 `items` |
| P1 | 转发目标接口不存在 | `GET /v1/im/forward-targets?query=` 返回 404 | 4.17~4.20 转发目标页无法联调 | 已处理:新增 `/v1/im/forward-targets`,返回文件助手和可转发的 P2P/群会话;本地返回 200 |
| P1 | 群文件接口被群详情兜底吞掉 | `GET /v1/im/groups/{topic}/files` 返回群详情 JSON,而不是文件列表或 404 | 前端会误判接口成功,群文件页数据结构错误 | 已处理:未知一级 tail 不再落到群详情;`/files` 未实现时本地返回 404 |
## 4. 章节审核
### 0. 公共对接约定
通过项:
- `POST /v1/im/tenants/resolve` 可未登录调用,返回统一 `code/data/request_id/server_time`。
- `endpoints` 已返回正确公网地址:`https://api.118im.com/v1/im`、`wss://api.118im.com/v0/channels`、`https://api.118im.com/v0/file`。
- 缺 token 访问 `GET /v1/im/bootstrap` 返回统一 JSON:`401 AUTH_REQUIRED`。
问题:
- 公共约定写分页统一返回 `items,next_cursor,has_more`,但很多接口只返回 `items,next_cursor`,例如 `friends`、`friend-requests`、`system-notifications`、`favorites`、`calls`。
- 写请求确实依赖 `Idempotency-Key`,但部分章节没有重复强调;前端实现时必须按公共约定统一加。
### 1. 账号注册、登录与身份安全
通过项:
- 企业解析通过,且不泄漏 `tenant_id`。
- 注册短信发码通过:返回 `challenge_id/expires_in_seconds/resend_after_seconds/phone_mask`。
- 固定验证码 `123456` 验证通过:返回 `verification_token`。
- WebSocket `hi(tenant=default)` 返回 `201`,`acc user:new login:true` 注册成功,返回 `user/token/expires/username`。
- `GET /v1/im/security/realname` 返回 `status=unverified, can_submit=true`。
问题:
- 已处理待发版复测:`reset_password` 发码不再因短信供应商未接入返回 502,本地返回 200。
- 已处理待发版复测:`POST /v1/im/security/challenges` 不再因短信供应商未接入返回 502,本地创建和校验均返回 200。
- 文档目标错误 reason 很完整,但 WS 登录/注册仍主要是 Tinode `ctrl.text/code`;稳定 `params.reason` 是否全链路返回还需要继续核。
### 2. 消息首页、全局导航与系统通知
通过项:
- `GET /v1/im/bootstrap` 已返回 `features/permissions/message_policy/security/modules/system_topic`。
- 最新测试环境已补 `entries` 和 `entry_badges`,包括 `new_friends/groups/file_assistant/system_messages` 入口与未读数字段,能支撑通讯录和首页入口显示。
- `GET /v1/im/system-notifications?limit=5` 返回 `items,next_cursor`。
- `GET /v1/im/search/conversations?q=a` 可返回已创建群会话,包含 `seq/read/unread/public/private`。
问题:
- 已处理待发版复测:`bootstrap` 已补 `limits.max_file_size/max_text_length/max_forward_targets`,本地返回 200。
- 系统通知没有聚合未读接口,当前依赖 `bootstrap.entry_badges.system_unread_count`,需要确认其真实计算来源和实时更新事件。
### 3. 单聊会话、会话建立与消息状态
通过项:
- 路由存在 `GET /v1/im/conversations/{topic}/context`。
- 好友申请接口存在:`POST/GET /v1/im/friend-requests`,列表空数据返回正常。
- 好友列表 `GET /v1/im/friends` 返回正常空列表。
问题:
- 文档出现 `POST /v1/im/conversations/p2p`、`GET /v1/im/conversations/{topic}/messages`、`PATCH /v1/im/conversations/{topic}/settings`,路由清单未看到对应明确 handler,需要进一步实测或补实现。
- 单聊发送约束最终仍要靠 WebSocket `pub` 校验,HTTP 文档里的关系状态和 WS 发布路径需要做端到端用例。
### 4. 消息输入、媒体内容与消息操作
通过项:
- 收藏基础接口存在,`GET /v1/im/favorites?limit=5` 正常返回空列表。
- 消息撤回/编辑/回执类路由挂在 `/v1/im/topics/` 下,代码有统一 handler。
问题:
- 已处理待发版复测:`GET /v1/im/forward-targets?query=` 已新增,本地返回 200。
- 文档里的 `POST /v1/im/messages/forward`、`GET /v1/im/forward-bundles/{bundle_id}` 未在路由表看到。
- 文件上传仍主要使用 `/v0/file`,但业务 `purpose`、附件 ACL、群文件登记接口还没有从测试结果证明可用。
- 已处理待发版复测:`bootstrap.limits.max_forward_targets/max_file_size/max_text_length` 本地已返回。
### 5. 通讯录、好友关系与黑名单
通过项:
- `GET /v1/im/friends`、`GET /v1/im/friend-requests`、`GET /v1/im/blocks` 均返回统一成功空列表。
- `GET /v1/im/users/search` 可调用,空结果返回 `items:[]`。
- `bootstrap.entries.new_friends` 和 `entry_badges.new_friends_pending_count` 已能控制入口和未处理数。
问题:
- 本轮新注册用户搜索自己的 username 返回空列表,可能是“排除自己”的合理策略,也可能是搜索索引/隐私导致,需要用第二个用户互搜确认。
- 新朋友未处理数现在在 `bootstrap.entry_badges` 返回,但需要确认好友申请创建/处理后是否实时准确更新。
### 6. 群聊创建、资料维护与成员治理
通过项:
- `POST /v1/im/groups` 已支持截图里的 `name + description` 创建群聊,返回 `topic/name/description/owner/member_count/version/permissions`。
- `GET /v1/im/groups`、`GET /v1/im/groups/{topic}`、`GET /summary`、`GET /members`、`POST /qrcodes`、`GET /governance`、`GET /role-limits`、`GET /mutes` 均可用。
- 群二维码返回 `qr_token/qr_payload/expires_at/group_snapshot`,符合需求。
问题:
- `GET /v1/im/groups/{topic}/members` 返回的 owner 成员 `user_id` 为空字符串,这不适合前端成员列表和后续管理员/转让操作,应返回 `usr...`。
- 已处理待发版复测:`GET /v1/im/groups/{topic}/files` 不再返回群详情,本地返回 404。
- 已处理待发版复测:`GET /v1/im/groups/{topic}/announcements` 不再返回群详情,未知一级 tail 统一返回 404。
- 文档里的 `POST /v1/im/group-invitations/{invitation_id}/accept` 未在路由表看到;当前更像是走群内 `/invitations` 和扫码 join-request。
### 7. 文件下载、收藏管理与聊天记录检索
通过项:
- `GET /v1/im/favorites` 可用。
- 收藏批量、删除、转发相关 handler 在 `/favorites` 和 `/favorites/` 下存在。
问题:
- 群文件接口不可用,且被错误兜底。
- 文档中的聊天记录搜索、媒体筛选、上下文接口挂在 `/v1/im/topics/{topic}/...`,需要有真实消息后再端到端验证;本轮未创建聊天消息。
- 文件下载鉴权复用 `/v0/file`,本轮未上传真实附件,ACL 未验证。
### 8. 实时语音与视频通话
通过项:
- `GET /v1/im/calls?limit=5` 返回正常空列表。
- 文档对通话主体设计合理:WebSocket 信令为主,HTTP 只做记录查询。
问题:
- 需要真机或至少两个 WS 用户做 call `started/ringing/accept/hang-up` 端到端;本轮只验证 HTTP 通话记录列表。
- Push、好友关系、拉黑、并发通话校验没有通过测试环境验证。
### 9. 企业工作台与办公协同
通过项:
- `GET /v1/im/workbench` 返回模块列表和 `layout_version`。
- `GET /v1/im/organization/departments` 返回正常空列表。
- 日报列表、日报统计、日报草稿创建通过。
- 月报列表、月报统计、月报草稿创建通过。
- 会议创建按文档 Body 通过。
问题:
- `GET /v1/im/meetings?limit=5` 返回 405;文档没明确会议列表接口,但会议通知页面通常需要列表,设计上应补 `GET /v1/im/meetings`。
- `GET /v1/im/organization/search?q=` 空关键词返回 400;文档示例写 `q=`,需明确前端空关键词应不请求还是后端返回空列表。
- `GET /v1/im/executive-mailbox/config` 和提交均返回 500,总经理信箱不可联调。
- `modules[].unread_count` 当前全 0,需要确认是否真实 badge,还是静态占位。
### 10. 个人资料、通用设置、隐私与青少年模式
通过项:
- `GET /v1/im/me`、`GET /v1/im/me/profile`、`GET /v1/im/me/privacy`、`GET /v1/im/settings/youth-mode` 均可用。
- `POST /v1/im/me/qrcodes` 可用,返回 `qr_token/qr_payload/display_profile/expires_at`。
- `PUT /v1/im/devices/{device_id}/push-token` 可用,能登记 APNs 测试 token。
- `GET /v1/im/app/releases/latest?platform=ios&version=1.0.0` 可用。
问题:
- 文档写应用更新参数为 `current_version`,测试使用 `version` 也能返回;实现似乎不消费当前版本参数,建议统一字段名,避免客户端多套逻辑。
- 青少年模式关闭依赖二次验证,而二次验证挑战当前 502。
- Android 系统权限说明多为客户端能力,本轮无法通过服务端验证。
### 11. 反诈安全、举报与意见反馈
通过项:
- `POST /v1/im/security/urls/check` 可用,安全 URL 返回 `decision=allow/risk_level=low`。
- `GET /v1/im/feedback/config` 可用。
- `POST /v1/im/feedback` 可用,返回 `feedback_id/status=received`。
问题:
- `GET /v1/im/reports/abuse/reasons?target_type=user` 返回 500,举报页阻塞。
- 风控 challenge confirm 路由存在,但需要先有业务接口返回 `RISK_CONFIRMATION_REQUIRED` 后才能完整验证。
- 举报提交需要真实目标对象和原因数据,本轮因原因接口 500 未继续提交。
### 12. 联调实施清单
建议把当前验收顺序调整为:
1. 已处理待发版复测:密码重置 502、二次验证 502、总经理信箱 500、举报原因 500。
2. 部分已处理待发版复测:转发目标已补、群文件路径错误兜底已修;消息转发/Forward Bundle 缺路由、群成员 `user_id` 为空仍待处理。
3. 统一公共协议:所有列表是否必须返回 `has_more`;所有写接口是否都必须 `Idempotency-Key`;`bootstrap.limits` 是否需要补。
4. 两用户端到端:好友申请、同意、P2P context、普通消息、已读、拉黑、删除好友。
5. 群端到端:邀请成员、入群审批、公告、群文件、管理员、禁言、群主转让。
6. 真机端到端:文件上传下载、APNs/FCM 推送、WebRTC 通话。
## 5. 代表性实测结果
| 接口 | 方法 | 状态 | 结论 |
| --- | --- | --- | --- |
| `/v1/im/tenants/resolve` | POST | 200 | 企业解析正常,endpoint host 正确 |
| `/v1/im/auth/verification-codes` register | POST | 200 | 注册发码正常 |
| `/v1/im/auth/verification-codes/{id}/verify` | POST | 200 | 固定验证码 `123456` 正常 |
| WebSocket `hi + acc` | WS | 201/200 | 注册并登录成功 |
| `/v1/im/auth/verification-codes` reset_password | POST | 502 | 已处理待发版复测;本地返回 200 |
| `/v1/im/security/challenges` | POST | 502 | 已处理待发版复测;本地创建和 `123456` 校验返回 200 |
| `/v1/im/bootstrap` | GET | 200 | 已包含 `entries/entry_badges`;已补 `limits` 待发版复测 |
| `/v1/im/groups` | POST | 200 | 创建群聊含 `description` 正常 |
| `/v1/im/groups/{topic}/members` | GET | 200 | 可用,但成员 `user_id` 为空 |
| `/v1/im/groups/{topic}/files` | GET | 200 | 已处理待发版复测;未实现时本地返回 404,不再误返回群详情 |
| `/v1/im/forward-targets` | GET | 404 | 已处理待发版复测;本地返回 200 |
| `/v1/im/executive-mailbox/config` | GET | 500 | 已处理待发版复测;本地 config/submit 返回 200 |
| `/v1/im/reports/abuse/reasons` | GET | 500 | 已处理待发版复测;本地返回 200 |
| `/v1/im/feedback` | POST | 200 | 意见反馈提交正常 |
| `/v1/im/devices/{device_id}/push-token` | PUT | 200 | 推送 token 登记正常 |
## 6. 本轮测试产生的数据
- 创建临时用户:`usrIPhpZJeJDMI`
- 创建群:`grpcNdeRoa_VO0`
- 创建会议:`mtg_vN4182ncLyi5wp4A`
- 创建日报草稿:`dr_MdAFCj2fYTkBjiq8`
- 创建月报草稿:`mr_eWjm2hpuzSsqcIcH`
- 创建反馈:`fb_TEJYaStZn9i0JSun`
- 登记设备:`audit-device`
这些数据均在测试租户 `default` 下,可按需要由测试库清理。
+220
View File
@@ -0,0 +1,220 @@
# IM 监控运维对接文档
本文档用于 IM Server 与 IM Manager 发版、健康检查和 Prometheus 监控对接。
## 1. 接入范围
| 服务 | 端口 | 健康检查 | 指标接口 | 说明 |
|---|---:|---|---|---|
| im-server | 6060 | `GET /healthz` | `GET /metrics` | 对外 IM API 与 WebSocket 服务。 |
| im-manager | 8000 | `GET /healthz` | 暂无 | 后台管理服务。 |
两个接口均不需要业务鉴权,不需要 `X-MeetingIM-APIKey`、`Authorization`、租户 Header。
## 2. 应用配置
本轮监控改造没有新增业务配置项,应用配置文件不需要额外增加开关。
需要运维侧配置的是:
| 配置位置 | 配置内容 |
|---|---|
| Docker / K8s 健康探针 | 调用 `/healthz`,不要再用 `nc -z` 只探测端口。 |
| Prometheus | 抓取 `im-server:6060/metrics`。 |
| 发版脚本 | 蓝绿切换前调用新实例 `/healthz`,返回 200 后再切流。 |
| 网关/Nginx | `/healthz`、`/metrics` 建议只允许内网或监控网段访问。 |
## 3. 健康检查
### 3.1 im-server
请求:
```bash
curl -fsS http://im-server:6060/healthz
```
正常响应:
```json
{
"status": "ok",
"checks": {
"mysql": "ok"
}
}
```
异常响应 HTTP 状态码为 `503`,示例:
```json
{
"status": "error",
"checks": {
"mysql": "dial tcp 127.0.0.1:3306: i/o timeout"
}
}
```
检测内容:
| 依赖 | 检测方式 |
|---|---|
| MySQL | `PingContext`,包含租户注册库连接检查。 |
### 3.2 im-manager
请求:
```bash
curl -fsS http://im-manager:8000/healthz
```
正常响应:
```json
{
"status": "ok",
"checks": {
"mysql": "ok",
"redis": "ok",
"kafka": "ok"
}
}
```
如果 Kafka 未启用,响应为:
```json
{
"status": "ok",
"checks": {
"mysql": "ok",
"redis": "ok",
"kafka": "disabled"
}
}
```
检测内容:
| 依赖 | 检测方式 |
|---|---|
| MySQL | `SELECT 1`。 |
| Redis | `PING`。 |
| Kafka | 临时 Sarama client 拉取 broker metadata,可发现网络、认证、broker 不可用问题。 |
## 4. Docker 健康检查
镜像内已改为请求 `/healthz`:
```dockerfile
HEALTHCHECK --interval=1m --timeout=3s --start-period=30s \
CMD wget -q -O /dev/null http://localhost:6060/healthz || exit 1
```
```dockerfile
HEALTHCHECK --interval=1m --timeout=3s --start-period=20s \
CMD wget -q -O /dev/null http://localhost:8000/healthz || exit 1
```
运维如果使用 K8s,建议:
| 探针 | 路径 | 初始延迟 | 周期 | 超时 | 失败次数 |
|---|---|---:|---:|---:|---:|
| im-server readinessProbe | `/healthz` | 15s | 10s | 3s | 3 |
| im-server livenessProbe | `/livez` | 15s | 20s | 3s | 3 |
| im-manager readinessProbe | `/healthz` | 15s | 10s | 3s | 3 |
| im-manager livenessProbe | `/healthz` | 30s | 30s | 3s | 3 |
## 5. Prometheus 指标
目前只在 `im-server` 暴露:
```bash
curl -fsS http://im-server:6060/metrics
```
已支持指标:
| 指标 | 类型 | 说明 |
|---|---|---|
| `ws_connections_active` | Gauge | 当前 WebSocket 连接数。 |
| `http_requests_total{method,path,status}` | Counter | HTTP 请求量,按方法、归一化路径、状态码统计。 |
| `http_request_duration_seconds{method,path,status}` | Histogram | HTTP 请求耗时分布。 |
| `db_query_duration_seconds{driver,operation,status}` | Histogram | MySQL 操作耗时分布。 |
路径中的用户 ID、Topic ID、UUID 等动态片段会归一化为 `{id}`,避免 Prometheus label 过多。
Prometheus 示例配置:
```yaml
scrape_configs:
- job_name: im-server
metrics_path: /metrics
scrape_interval: 15s
scrape_timeout: 5s
static_configs:
- targets:
- im-server:6060
```
## 6. 发版脚本建议
蓝绿部署或滚动发布时,切流前执行:
```bash
curl -fsS --max-time 3 http://127.0.0.1:6060/healthz
```
后台服务:
```bash
curl -fsS --max-time 3 http://127.0.0.1:8000/healthz
```
只有 HTTP 状态码为 `200` 才允许切流。返回 `503` 时应保留旧实例,并输出响应 body 方便定位具体依赖。
## 7. 告警建议
| 告警项 | 建议规则 |
|---|---|
| im-server 健康检查失败 | `/healthz` 连续 3 次非 200。 |
| im-manager 健康检查失败 | `/healthz` 连续 3 次非 200。 |
| WebSocket 连接数异常 | `ws_connections_active` 突降 50% 以上或接近 0。 |
| HTTP 5xx | `rate(http_requests_total{status=~"5.."}[5m]) > 0`。 |
| HTTP 延迟 | P95 超过业务阈值,例如 1s。 |
| DB 延迟 | `db_query_duration_seconds` P95 超过 200ms 或 500ms。 |
## 8. 安全要求
`/healthz` 和 `/metrics` 不带业务鉴权,必须通过网络层控制访问范围。
建议:
1. 只允许内网、K8s 探针、Prometheus、运维跳板机访问。
2. 不要直接暴露到公网。
3. 如必须公网访问,需要网关加 IP 白名单或 Basic Auth。
4. 不在日志中记录 `/metrics` 的完整响应内容。
## 9. 验收命令
im-server:
```bash
curl -i http://im-server:6060/healthz
curl -s http://im-server:6060/metrics | grep -E 'ws_connections_active|http_requests_total|http_request_duration_seconds|db_query_duration_seconds'
```
im-manager:
```bash
curl -i http://im-manager:8000/healthz
```
预期:
1. `/healthz` 正常返回 HTTP 200,`status=ok`。
2. 任一依赖不可用时返回 HTTP 503,并在 `checks` 中标明具体原因。
3. `/metrics` 返回 Prometheus 文本格式。
4. Docker 或 K8s 健康状态不再只依赖端口探测。
@@ -3,6 +3,7 @@
-- 适用目标:已有 Tinode MySQL 库,补齐本项目 1-7 章业务层依赖表与关键字段。
-- 注意:本稿使用 MySQL 8 functional index 处理空 idempotency_key 唯一性。
-- 第一阶段多库方案:本 SQL 的 im_tenant 放 hotgo 公共库;tinode/各租户业务库表暂时保留 tenant_id 字段。
-- 本文件中的非自增 BIGINT 主键由服务端雪花算法生成。
SET NAMES utf8mb4;
@@ -79,13 +80,15 @@ CREATE TABLE IF NOT EXISTS im_verification_codes(
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS im_user_security(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
auth_version INT NOT NULL DEFAULT 1,
password_updated_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(tenant_id,user_id),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_user_security_tenant_user(tenant_id,user_id),
CONSTRAINT fk_im_user_security_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -132,6 +135,7 @@ CREATE TABLE IF NOT EXISTS im_action_tokens(
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS im_youth_mode_settings(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
purpose VARCHAR(64) NOT NULL,
@@ -140,7 +144,8 @@ CREATE TABLE IF NOT EXISTS im_youth_mode_settings(
locked_until DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(tenant_id, user_id, purpose),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_youth_settings_tenant_user_purpose(tenant_id, user_id, purpose),
CONSTRAINT fk_im_youth_settings_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -192,6 +197,7 @@ CREATE UNIQUE INDEX uk_im_friend_requests_idempotency
ON im_friend_requests(tenant_id, applicant_id, (NULLIF(idempotency_key, '')));
CREATE TABLE IF NOT EXISTS im_friendships(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
peer_id BIGINT NOT NULL,
@@ -200,18 +206,21 @@ CREATE TABLE IF NOT EXISTS im_friendships(
topic CHAR(25) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(tenant_id, user_id, peer_id),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_friendships_tenant_pair(tenant_id, user_id, peer_id),
INDEX idx_im_friendships_peer(tenant_id, peer_id, status),
CONSTRAINT fk_im_friendships_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS im_user_blocks(
id BIGINT NOT NULL,
tenant_id BIGINT NOT NULL,
blocker_id BIGINT NOT NULL,
blocked_id BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(tenant_id, blocker_id, blocked_id),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_user_blocks_tenant_pair(tenant_id, blocker_id, blocked_id),
INDEX idx_im_user_blocks_blocked(tenant_id, blocked_id),
CONSTRAINT fk_im_user_blocks_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -312,5 +321,6 @@ CREATE TABLE IF NOT EXISTS im_message_revisions(
-- ALTER TABLE im_filemsglinks ADD COLUMN userid BIGINT AFTER topic;
-- CREATE INDEX im_filemsglinks_tenant_fileid ON im_filemsglinks(tenant_id, fileid);
-- im_usertags/im_topictags/im_devices/im_auth/im_credentials 也已按租户增加 tenant_id;
-- im_usertags 已废弃;im_users.phone 是手机号真源,邮箱存放于 im_user_profiles.email。
-- im_topictags/im_devices/im_auth/im_credentials 也已按租户增加 tenant_id;
-- 若旧库缺字段,需要参照当前 server/db/mysql/schema.sql 逐项补齐。
+23 -3
View File
@@ -205,7 +205,7 @@ HTTP API 通用规则:
| 模块 | 目标表 | 主要职责 |
| --- | --- | --- |
| 好友关系 | `im_friendships`、`im_friend_requests`、`im_user_blocks` | 好友申请、同意、拒绝、删除、拉黑 |
| 用户资料 | `im_user_profiles`、`im_user_privacy_settings` | 昵称、生日、签名、可见性和添加方式 |
| 用户资料 | `im_users`、`im_user_profiles`、`im_user_privacy_settings` | 注册手机号、昵称、邮箱、生日、签名、可见性和添加方式 |
| 群治理 | `im_group_settings`、`im_group_join_requests`、`im_group_announcements`、`im_group_mutes`、`im_group_admin_logs` | 入群审批、公告、禁言、管理员、转让群主 |
| 群文件 | `im_group_files` | 群文件目录、搜索、删除权限 |
| 收藏 | `im_favorites` | 收藏去重、分类、来源、搜索和分享 |
@@ -234,6 +234,12 @@ HTTP API 通用规则:
| 群成员 | `im_group_members` | 群成员业务状态、角色、入群来源和操作者 |
| 消息版本 | `im_message_revisions` | 编辑前内容、版本、操作者和变更原因 |
MySQL 表主键约定:所有 `im_*` 基础表必须具有独立的 `id` 主键。`im_user_security`、`im_kvmeta`、用户资料/隐私、设备 Push Token、好友/黑名单、青少年模式以及群设置/公告已读/禁言等原先使用自然键或复合主键的表,统一改为服务端雪花算法生成的 `BIGINT id` 主键;原业务键保留唯一索引,查询和幂等语义不变。服务启动时会对当前租户库执行幂等迁移,为存量行补齐雪花 ID 后再替换主键。迁移期间不得同时运行不写入 `id` 的旧版本服务。
用户身份字段约定:`im_users.username` 是非空的主登录账号,`im_users.phone` 是手机号方式的注册号码,允许为 `NULL`;是否完成短信验证由已完成的 `tel credential` 判断。用户填写的普通用户名必须匹配 `^[a-z0-9]{1,10}$`,服务端按原始输入校验;手机号方式写入的 E.164 账号和迁移生成的内部保留账号属于系统值例外。注册页面分别展示用户名和手机号,手机号非空时优先走手机号方式并忽略用户名输入:规范化后的 E.164 手机号同时写入 `username` 和 `phone`;只有手机号为空时才使用用户名方式并只写 `username`。短信关闭时,手机号输入搭配有效邀请码仍按手机号方式注册并写两个字段,但不建立已完成的 `tel credential`。`im_user_profiles.email` 仅是未验证的资料字段,不参与登录和密码找回。旧 `im_users.tags` 与 `im_usertags` 已废弃,启动迁移会先从已验证凭证和旧标签回填手机号/邮箱,再删除两者。群组发现仍使用 `im_topics.tags` 与 `im_topictags`,不受本次调整影响。
注册邀请码固定为 5 位 ASCII 大小写英文字母,使用密码学安全随机数生成并区分大小写。邀请码明文存入租户业务库,`code` 字段使用 `ascii_bin` 排序规则和唯一索引;注册时按完整明文精确查询,不允许大小写转换或模糊匹配。邀请码查看和导出必须授权并审计,普通日志不得记录邀请码。
### 7.3 企业码与登录流程
1. 未认证客户端先通过 `hi.tenant` 提交企业码;服务端只按 `im_tenant.code` 查找启用租户并把 `TenantID` 固定到 Session。
@@ -246,6 +252,8 @@ HTTP API 通用规则:
验证码按 `register`、`login`、`reset_password`、`sensitive_action` 用途隔离,相同验证码不能跨用途使用。数据库只保存验证码强哈希和手机号规范化哈希,明文只在发送时短暂存在。
`im_users.phone` 非空的账号可以通过 `reset_password` 短信挑战自助找回密码;成功验证即证明当前持有该号码,不要求注册时已经存在完成状态的 `tel credential`,并可在重置成功后补建凭证。`phone=NULL` 的用户名账号不提供自助找回,只能由当前租户管理员重置。管理员不得跨租户操作,重置行为必须进入安全审计。
密码重置必须在同一事务中完成:
1. 消费有效验证码或二次验证挑战。
@@ -415,6 +423,16 @@ stateDiagram-v2
| 转让群主 | 二次确认;同一事务更新 owner、双方角色、Topic Owner/ACL、审计和 Outbox |
| 解散群聊 | 标记群状态、踢出成员、停用二维码和审批入口 |
### 10.3 群成员列表读取与缓存
群成员列表按需分页读取,不作为进入会话的必拉数据。普通聊天页只依赖群详情、当前用户权限和 `member_count`;成员页、候选人选择器和 `@` 搜索才调用成员列表接口。
服务端成员列表查询接口为 `GET /v1/im/groups/{topic}/members`。参数含义:`query` 用于搜索成员,空值表示读取当前筛选条件下的成员页;`cursor` 必须来自上一页 `next_cursor`;`limit` 默认 `30`,最大 `100`;`mentionable=true` 用于 `@` 选择器,只返回可被提及成员;`role=owner|admin|member` 用于按现有角色筛选;`eligible_role=admin|owner` 用于添加管理员和转让群主候选视图;`mute_eligible=true` 用于禁言候选视图。候选视图的 `eligible/ineligible_reason` 在 HTTP 响应阶段按当前登录人的权限和目标成员状态计算。
成员页按 `im_group_members.id` 游标分页,常规列表走 `(topic, member_status, id)` 索引,角色筛选走 `(topic, role, member_status, id)` 索引,搜索和候选列表必须复用分页查询,不得提供一次返回全群成员的接口。万人群场景中,客户端打开群聊不拉成员列表;打开成员页只拉第一页;搜索、`@`、管理员候选、禁言候选和转让群主候选都带 `query` 缩小结果集。
成员页缓存使用 Redis,缓存维度为 `topic + role + eligible_role + query + cursor + limit + mentionable`,缓存内容为成员页结果和 `next_cursor`。`mute_eligible`、好友备注、当前查看人关系等 viewer-specific 字段不进入公共页缓存,而是在 HTTP 响应阶段按当前登录人补齐。成员加入、退出、角色变更、群昵称变更、成员状态变化、资料投影刷新等写操作必须调用 `invalidateGroupMemberPages(topic)` 清理该群所有成员页缓存。
## 11. 收藏、转发与搜索设计
### 11.1 收藏
@@ -500,12 +518,14 @@ end_reason, created_at, updated_at
### 14.1 个人资料
基础展示资料可以同步到 User Public,但可索引、可审计或敏感字段应进入独立表。
个人资料以 `im_user_profiles` 为唯一业务真源;User Public 与 `im_subscriptions.public` 仅作为 MeetingIM 协议展示投影/缓存,由服务端在资料变更后同步刷新,业务接口不得把这些投影当作可写真源。
| 字段 | 存储建议 |
| --- | --- |
| 头像、昵称、签名 | User Public + `im_user_profiles` |
| 头像、昵称、签名 | `im_user_profiles`;User Public / Subscription Public 仅保留服务端生成的展示投影 |
| 生日、地区、性别 | `im_user_profiles`,按隐私设置返回 |
| 注册手机号 | `im_users.phone`,仅账号安全流程内部使用 |
| 邮箱 | `im_user_profiles.email`,仅作为个人资料,不作为认证凭证 |
| 实名姓名、证件号 | 实名表加密存储,不进入 Public |
| 群昵称 | `im_group_members`,按需投影到 Subscription Private |
@@ -0,0 +1,555 @@
# IM 群消息按月分表改造方案
## 1. 背景与目标
当前每个租户库使用单表 `im_group_messages` 保存全部群消息。随着群数量、消息量和保留时间增长,单表会出现索引膨胀、查询抖动、DDL 风险升高和归档困难等问题。
本方案将群消息正文按自然月拆分为物理表:
```text
im_group_messages_202609
im_group_messages_202610
im_group_messages_202611
```
目标:
1. 每个租户库独立分表,消息表不增加 `tenant_id`。
2. Topic 的 `seqid` 继续由 Redis 原子递增,跨月不重置。
3. Kafka 异步落库稳定路由到首次确定的月表。
4. 客户端继续按 `topic + seq` 拉取,不感知月份。
5. 支持跨月分页、补拉、指定消息、搜索、撤回、编辑和删除。
6. 旧单表平滑迁移,不停服、不丢失、不重复。
7. Redis 保留 24 小时热消息,MySQL 月表仍是最终数据源。
## 2. 核心原则
### 2.1 分表粒度
- 一个租户库每月一张群消息表,不按单个群建表。
- 表名固定为 `im_group_messages_YYYYMM`。
- 月份统一使用 UTC,但最终写入月份由 Redis 全局 `active_month` 决定,避免多节点在月初产生交叠。
- Redis Lua 一次性返回 `seqid + shard_month`;生产端把结果固化到 Kafka,重试、补偿不得重算。
### 2.2 Seq 规则
- `seqid` 在 Topic 内全生命周期单调递增,跨月不归零。
- Redis `topic:last_seq` 是唯一发号源。
- 月表只保存已分配的 Seq,不通过 `MAX(seqid)` 发号。
- 客户端游标继续使用 `before/since/seq`。
### 2.3 分层职责
| 层级 | 职责 |
| --- | --- |
| Redis | 原子 Seq、全局活动月份、长期 Topic 月段路由、短期发送幂等、24 小时热消息、在线状态、实时推送 |
| Kafka | 异步落库、削峰、重试、死信、消费幂等 |
| MySQL 月表 | 群消息正文最终存储 |
| MySQL 路由镜像 | 月表目录、Topic 月段路由持久化镜像;Redis 丢失时用于重建,不进入正常查询热路径 |
| `im_dellog` | 用户视角删除范围,每张月表查询时继续过滤 |
## 3. 表结构
### 3.1 月表模板
第一阶段保持当前字段兼容,降低 Store 层改造范围。
```sql
CREATE TABLE IF NOT EXISTS im_group_messages_202609 (
message_id BIGINT NOT NULL COMMENT 'Snowflake 全局消息 ID',
topic VARCHAR(64) NOT NULL COMMENT '群 Topic',
group_id BIGINT NOT NULL COMMENT '业务群 ID',
seqid BIGINT NOT NULL COMMENT 'Topic 内递增,跨月不重置',
from_user BIGINT NOT NULL,
client_msg_id VARCHAR(64) NULL,
msg_type VARCHAR(32) NOT NULL DEFAULT 'text',
status VARCHAR(32) NOT NULL DEFAULT 'normal',
content JSON NULL,
head JSON NULL,
mentioned_user_ids JSON NULL,
mention_all TINYINT(1) NOT NULL DEFAULT 0,
reply_seqid BIGINT NULL,
root_seqid BIGINT NULL,
file_count INT NOT NULL DEFAULT 0,
content_size INT NOT NULL DEFAULT 0,
has_risk TINYINT(1) NOT NULL DEFAULT 0,
audit_status VARCHAR(32) NOT NULL DEFAULT 'pass',
version INT NOT NULL DEFAULT 1,
recalled_at DATETIME(3) NULL,
recalled_by BIGINT NULL,
deleted_at DATETIME(3) NULL,
expires_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL,
updated_at DATETIME(3) NOT NULL,
PRIMARY KEY (message_id),
UNIQUE KEY uk_topic_seq (topic, seqid),
UNIQUE KEY uk_topic_sender_client_mid (topic, from_user, client_msg_id),
KEY idx_group_seq (group_id, seqid),
KEY idx_topic_created (topic, created_at),
KEY idx_topic_from_seq (topic, from_user, seqid),
KEY idx_audit_created (audit_status, created_at),
KEY idx_expires (expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
说明:
1. `message_id` 使用 Snowflake 全局唯一 ID,作为月表物理主键和跨表引用 ID。
2. 聊天协议定位、补拉和已读水位仍统一使用 `topic + seqid`,不向客户端暴露物理主键。
3. 单表唯一索引不能覆盖跨月重试,跨月幂等由 Redis、消费日志和幂等表共同保证。
4. 文件二进制仍在 OSS,月表仅保存消息和文件元数据。
### 3.2 月表目录
```sql
CREATE TABLE IF NOT EXISTS im_group_message_shards (
shard_month CHAR(6) NOT NULL COMMENT 'UTC 月份 YYYYMM',
table_name VARCHAR(64) NOT NULL,
shard_start DATETIME(3) NOT NULL,
shard_end DATETIME(3) NOT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'prepared'
COMMENT 'prepared/writable/sealing/sealed/archived',
row_count BIGINT NOT NULL DEFAULT 0,
min_created_at DATETIME(3) NULL,
max_created_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (shard_month),
UNIQUE KEY uk_table_name (table_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
表名只允许由服务端根据月份生成,并且必须存在于目录表,禁止使用请求参数直接拼接 SQL。
### 3.3 Redis Topic 月段路由
在线查询使用 Redis 路由,路由 Key 长期不过期:
```text
# 当前可写月份
im:{tenant_id}:groupmsg:active_month
STRING = 202610
# Topic 月份段,score 为该月首条 seq
im:{tenant_id}:groupmsg:route:{topic}
ZSET: 202609 => 1
202610 => 10087
# 月段完整范围,field 为 YYYYMM
im:{tenant_id}:groupmsg:route_meta:{topic}
HASH: 202609 => min_seq=1,max_seq=10086,count=10086
202610 => min_seq=10087,max_seq=12000,count=1914
```
路由规则:
1. `route` 和 `route_meta` 不设置 TTL,只在群物理删除或对应消息保留期结束后清理。
2. 精确 Seq 定位使用 `ZREVRANGEBYSCORE route seq -inf LIMIT 0 1`,找到最大 `min_seq <= seq` 的月份,再校验 Hash 中的 `max_seq`。
3. 倒序分页通过 `ZREVRANGE` 按月段倒序读取,正向补拉通过 `ZRANGE` 读取。
4. 路由 Key 使用 Redis Cluster hash tag,保证 Lua 和 Pipeline 访问同一槽位。
5. Redis 开启 AOF everysec、主从复制和持久化备份;同时保留 MySQL 镜像作为灾备重建来源。
活动月份和 Seq 必须在同一个 Lua 脚本中读取/分配,两个 Key 使用相同的租户 hash tag:
```lua
local shard_month = redis.call('GET', KEYS[1])
if not shard_month or shard_month < ARGV[1] then
redis.call('SET', KEYS[1], ARGV[1])
shard_month = ARGV[1]
end
local seq_id = redis.call('INCR', KEYS[2])
return {seq_id, shard_month}
```
其中 `KEYS[1]` 是租户活动月份,`KEYS[2]` 是 Topic Seq,`ARGV[1]` 来自 Redis `TIME` 转换后的 UTC 月份。业务节点不得根据本机时间计算月份;首次写入可初始化活动月份,之后 Lua 只允许月份单向推进,旧节点不能回拨。
### 3.4 MySQL 路由镜像
```sql
CREATE TABLE IF NOT EXISTS im_group_message_topic_shards (
topic VARCHAR(64) NOT NULL,
shard_month CHAR(6) NOT NULL,
min_seq BIGINT NOT NULL,
max_seq BIGINT NOT NULL,
message_count BIGINT NOT NULL DEFAULT 0,
min_created_at DATETIME(3) NOT NULL,
max_created_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (topic, shard_month),
KEY idx_topic_max_seq (topic, max_seq),
KEY idx_month_topic (shard_month, topic)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
该表不参与正常消息查询,只用于 Kafka 事务持久化、Redis 路由重建、历史迁移、数据核对和归档。Redis 与镜像不一致时,以月表实际数据和 MySQL 镜像修复 Redis。
路由一致性规则:
1. Kafka 消费事务同时提交消息和 MySQL 路由镜像,保证最终数据与镜像一致。
2. MySQL 提交成功后再更新 Redis `route/route_meta`;Redis 更新失败不回滚消息,进入路由修复重试队列。
3. 查询发现 Redis 路由 Key 不存在时,从 MySQL 镜像加载该 Topic 全部有效月段并回填 Redis,然后重试查询。
4. Shard Manager 启动时可按租户增量重建路由,并定时抽样核对 Redis、MySQL 镜像和月表实际 Seq 范围。
5. Redis 路由不设置 TTL;只有群被物理删除,或消息保留策略确认清除对应月份后,才同步删除相应月段。
### 3.5 跨月幂等表
```sql
CREATE TABLE IF NOT EXISTS im_group_message_dedup (
topic VARCHAR(64) NOT NULL,
from_user BIGINT NOT NULL,
client_msg_id VARCHAR(64) NOT NULL,
seqid BIGINT NOT NULL,
shard_month CHAR(6) NOT NULL,
message_id BIGINT NULL,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
expires_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (topic, from_user, client_msg_id),
KEY idx_expires (expires_at),
KEY idx_topic_seq (topic, seqid)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci;
```
- Redis `SET NX` 是入口快速幂等。
- Kafka 消费日志按 `event_id` 防止同一事件重复消费。
- 该表防止月末发送、月初重试时同一 `client_msg_id` 落入两张表。
- 建议保留 7 至 30 天后清理,不作为永久消息索引。
## 4. 建表和月切换
每个租户库运行统一 Shard Manager:
1. 每天巡检上月、当月和未来两个月。
2. 缺表时根据固定模板创建,成功后登记目录。
3. 使用 Redis 分布式锁和 Redis `TIME` 判断月界,确认新月表可写后再原子更新 `active_month`。
4. 当月设置为 `writable`,所有节点通过 Redis 读取同一个活动月份。
5. 上月进入 `sealing`,建议继续接受 7 天延迟 Kafka 消息。
6. 确认没有积压和死信后改为 `sealed`。
7. 建表失败立即告警,不切换 `active_month`,禁止在业务写入事务中临时执行 DDL。
月切换示例:
```text
2026-09-30 23:59:59 UTC seq=10086 -> im_group_messages_202609
2026-10-01 00:00:00 UTC seq=10087 -> im_group_messages_202610
```
Kafka 事件必须固定路由信息:
```json
{
"topic": "grp_xxx",
"seq_id": 10087,
"created_at": "2026-10-01T00:00:00.123Z",
"shard_month": "202610",
"client_msg_id": "client_mid_xxx"
}
```
## 5. 群消息落库
### 5.1 生产端
1. 校验群成员、禁言、群状态和内容策略。
2. 使用 Redis 幂等 Key 抢占 `client_msg_id`。
3. 使用 Redis Lua 原子获取 Topic 下一个 `seqid` 和全局 `active_month`。
4. 生成服务端 UTC `created_at`,并把 Lua 返回的月份作为 `shard_month`。
5. 把 `shard_month` 写入 Kafka;所有重试沿用原值。
6. Kafka 生产成功后按现有策略向客户端返回 Seq。
7. 写入 Redis 热消息并执行实时推送。
### 5.2 Kafka 消费端
消费端按 `tenant + shard_month` 分组批量写入:
1. 校验月份格式和允许范围。
2. 从目录确认目标表已存在。
3. 开启事务。
4. 插入或确认 `im_group_message_dedup`。
5. 插入目标月表。
6. 在同一事务中 UPSERT MySQL 路由镜像的最小/最大 Seq、时间和消息数。
7. 更新消费幂等状态并提交事务。
8. 提交成功后更新 Redis `route/route_meta`,并把 Redis 幂等状态改为 `done`。
路由 UPSERT:
```sql
INSERT INTO im_group_message_topic_shards
(topic, shard_month, min_seq, max_seq, message_count,
min_created_at, max_created_at)
VALUES (?, ?, ?, ?, 1, ?, ?)
ON DUPLICATE KEY UPDATE
min_seq = LEAST(min_seq, VALUES(min_seq)),
max_seq = GREATEST(max_seq, VALUES(max_seq)),
message_count = message_count + 1,
min_created_at = LEAST(min_created_at, VALUES(min_created_at)),
max_created_at = GREATEST(max_created_at, VALUES(max_created_at));
```
### 5.3 异常处理
- 目标月表不存在:消费失败、进入重试并高优告警。
- `sealing` 月表:允许携带原 `shard_month` 的延迟事件写入。
- `sealed` 月表:进入补偿队列,人工确认后临时开放或补写。
- 重复 `event_id`:消费日志直接判定成功。
- 重复 `client_msg_id`:返回原 `seqid/shard_month`,不新增消息。
- 超过最大重试次数:进入死信队列,不能静默丢弃。
- Redis 热缓存失败不回滚 MySQL,客户端按 Seq 回源补拉。
- Redis 路由更新失败:进入独立修复重试;查询侧可从 MySQL 镜像重建,不影响已提交消息。
## 6. 消息查询
### 6.1 总流程
```text
客户端 Topic Seq 请求
-> ACL/成员身份校验
-> Redis 24 小时热消息
-> Redis 是否完整覆盖请求范围
-> Redis Topic 月段路由
-> 月表逐张查询
-> 按 seqid 合并、去重、排序、截断
-> 应用 im_dellog 用户视角过滤
-> 返回客户端
```
Redis 只有完整覆盖整页时才能直接返回;有缺口必须回源 MySQL。
### 6.2 最新消息倒序分页
请求 `topic=grp_xxx&before=10088&limit=50`:
1. 从 Redis Topic 月段路由找出 `min_seq < before` 的月份,按月倒序。
2. 先查最新候选月:
```sql
SELECT ...
FROM im_group_messages_202610 AS m
LEFT JOIN im_dellog AS d
ON d.topic = m.topic
AND m.seqid BETWEEN d.low AND d.hi - 1
AND d.deletedfor = ?
WHERE m.topic = ?
AND m.seqid < ?
AND m.status != 'deleted'
AND d.deletedfor IS NULL
ORDER BY m.seqid DESC
LIMIT ?;
```
3. 数量不足时继续查前一个月。
4. 服务层按 `seqid DESC` 合并并截取 `limit`。
5. 下一页游标仍是本页最小 `seqid`,客户端不感知月份。
禁止对所有历史表执行无上限 `UNION ALL`。
### 6.3 Since 正向补拉
1. 从 Redis Topic 月段路由查找 `max_seq > since_seq` 的月份。
2. 按月升序执行 `seqid > since_seq ORDER BY seqid ASC LIMIT N`。
3. 合并后按 Seq 升序返回。
4. 达到上限后返回最后一条 Seq,客户端继续请求。
### 6.4 指定消息
撤回、回复预览、举报取证按 `topic + seqid` 定位:
```text
ZREVRANGEBYSCORE im:{tenant_id}:groupmsg:route:{topic} {seqid} -inf LIMIT 0 1
HGET im:{tenant_id}:groupmsg:route_meta:{topic} {shard_month}
```
先找到最大 `min_seq <= seqid` 的月份,再校验该月段 `max_seq >= seqid`,随后执行 `WHERE topic=? AND seqid=? LIMIT 1`。Redis 路由 Key 缺失时读取 MySQL 镜像并回填 Redis;镜像也缺失时,仅降级扫描当前月和最近若干月并记录修复任务,不允许扫描全部历史表。
### 6.5 上下文和范围
查询目标消息前后各 20 条时:
1. 定位目标所在月。
2. 在本月分别查询目标前后消息。
3. 任一方向不足时继续查询相邻月份。
4. 合并、去重后按 Seq 正序返回。
### 6.6 搜索和统计
- 短期搜索必须带起止时间,最多覆盖 3 个月,逐月查询后归并。
- 中长期通过 Kafka 建独立搜索索引;结果只返回 `topic + seqid`,服务端回源月表并重新校验 ACL。
- Admin 统计使用按日/月汇总表,不实时扫描所有月表执行 `COUNT(*)`。
## 7. 撤回、编辑和删除
撤回、编辑流程:
1. 根据 `topic + seqid` 定位目标月表。
2. 在目标月表事务中 `SELECT ... FOR UPDATE`。
3. 校验版本、权限和状态。
4. 更新目标月表并写修订记录、业务事件。
5. 提交后更新或删除 Redis 热消息。
修订记录建议增加月份:
```sql
ALTER TABLE im_message_revisions
ADD COLUMN shard_month CHAR(6) NULL AFTER topic;
```
用户侧删除继续使用 `im_dellog` 的 Topic Seq 范围,每张月表查询都应用相同过滤。全局删除或合规处置先定位月表,再更新 `status/deleted_at/content/head`;OSS 附件仍走文件生命周期任务。
## 8. Store 层设计
所有动态表访问统一经过路由组件:
```go
type GroupMessageShardRouter interface {
WriteTable(createdAt time.Time, shardMonth string) (string, error)
CandidateTablesBySeq(topic string, seq int64) ([]string, error)
TablesBefore(topic string, before int64) ([]string, error)
TablesSince(topic string, since int64) ([]string, error)
}
```
改造范围:
- `TenantGroupMessageSave`:按事件 `shard_month` 写入。
- `TenantGroupMessageGetOne`:路由后查询目标月。
- `TenantGroupMessageGetAll`:逐月查询并归并。
- `TenantGroupMessageGetByClientID`:先查 Redis/幂等表,再查询目标月。
- `TenantGroupMessageRecall/Edit`:定位目标月后执行事务。
- `TenantMessageSearch`:按时间选月,或转独立搜索。
- Admin 内部消息查询、举报、收藏、回复和群文件引用同步改造。
动态表名必须满足 `^im_group_messages_[0-9]{6}$` 且月份已登记。SQL 参数不能绑定表名,必须严格白名单。
## 9. 旧表迁移
原 `im_group_messages` 先保留,不删除。
推荐步骤:
1. 上线路由表、幂等表和 Shard Manager。
2. 预建历史月份、当月和未来两个月表。
3. 先发布支持“旧表 + 月表”双读的代码,写入仍走旧表。
4. 记录切换水位 `cutover_seq/cutover_time`。
5. 切换新消息只写月表,Kafka 开始携带 `shard_month`。
6. 按 `created_at` 分批把旧表数据迁入对应月表。
7. 每批使用 `INSERT IGNORE`,校验 `topic + seqid`、数量和内容哈希。
8. 回填 MySQL Topic 月段路由镜像,并由重建任务写入 Redis 路由。
9. 双读期间按 `topic + seqid` 去重。
10. 全量校验通过后停止读取旧表。
11. 旧表改名 `im_group_messages_legacy`,只读保留。
12. 观察至少一个完整保留周期后再单独审批清理。
不建议长期双写旧表和月表,否则两个写目标部分成功会扩大一致性问题。
迁移批次要求:
- 使用主键或 `created_at + id` 游标,禁止 OFFSET 深分页。
- 单批 1,000 至 5,000 条,根据线上延迟调整。
- 每批独立事务,记录开始/结束游标和成功/失败数。
- 支持暂停、续跑和重复执行。
## 10. 归档与保留
状态流转:
```text
prepared -> writable -> sealing -> sealed -> archived
```
- `writable`:当月正常写。
- `sealing`:月末后接受延迟消息,默认 7 天。
- `sealed`:在线 MySQL 只读。
- `archived`:转对象存储或低成本库,在线查询走归档服务。
归档前确认 Kafka 无积压/死信、路由完整、消息数量和 Seq 范围一致,并确认举报、审计、收藏和群文件引用仍可定位。
## 11. 监控与告警
| 指标 | 说明 |
| --- | --- |
| `group_message_persist_total{month,status}` | 各月落库成功/失败 |
| `group_message_persist_latency` | Kafka 到 MySQL 提交耗时 |
| `group_message_shard_missing_total` | 目标月表不存在 |
| `group_message_query_tables` | 单次请求扫描表数 |
| `group_message_query_latency{source}` | Redis、单月、跨月耗时 |
| `group_message_dedup_hit_total` | 幂等命中 |
| `group_message_route_miss_total` | Redis 路由缺失 |
| `group_message_route_rebuild_total{status}` | 从 MySQL 镜像重建 Redis 路由 |
| `group_message_route_repair_backlog` | Redis 路由修复积压 |
| `group_message_dead_letter_total` | 落库死信 |
未来月表未创建、当月不可写、`active_month` 缺失、Kafka 延迟超限、单次扫描超过 3 张表、Redis 路由缺失或修复积压增长时必须告警。
## 12. 验收测试
1. 月末和月初消息落入正确表,Seq 连续。
2. Kafka 次月才消费时仍写事件指定月份。
3. 同一 `client_msg_id` 跨月重试只保留一条。
4. 最新 50 条跨两个月时数量、顺序、游标正确。
5. `since_seq` 跨月补拉无遗漏、无重复。
6. `topic + seq` 可定位历史消息。
7. 回复上下文可跨月读取。
8. 撤回、编辑、举报、收藏、群文件引用可定位旧月。
9. 用户删除范围跨月时过滤正确。
10. Redis 热消息不完整时正确回源。
11. 月表缺失时进入重试/死信并告警。
12. 迁移双读无重复,迁移任务可暂停、续跑。
13. 万人群高并发时 Seq 唯一且落库稳定。
14. 非法月份和非法动态表名被拒绝。
15. 多节点跨月并发时 Lua 返回同一活动月份,Seq 不重号且月段不交叠。
16. 删除 Redis 路由后可从 MySQL 镜像自动重建,查询结果不缺失。
## 13. 实施阶段
### 第一阶段:基础设施
- 建月表模板、目录、MySQL 路由镜像和跨月幂等表。
- 实现 Shard Manager,预建未来两个月。
- 建立长期不过期的 Redis Topic 月段路由和原子 `seqid + active_month` Lua。
- Kafka 群消息事件增加 `shard_month`。
### 第二阶段:写入切换
- 消费者按月落库。
- 完成幂等、重试、死信、MySQL 镜像和 Redis 路由更新。
- Redis 热消息与实时推送保持不变。
### 第三阶段:查询切换
- 完成精确查询、倒序分页、Since 补拉和上下文跨月查询。
- 改造撤回、编辑、删除、举报及 Admin 内部查询。
- 增加跨月查询监控。
### 第四阶段:历史迁移
- 上线旧表/月表双读。
- 回填历史消息与 MySQL 路由镜像,重建 Redis Topic 路由。
- 校验后把旧表改为只读保留。
### 第五阶段:搜索和归档
- 消息搜索切换独立索引或限制时间范围。
- 建立 `sealed/archived` 生命周期和历史恢复机制。
## 14. 结论
群消息按月分表不能只改写入表名,必须一起实现固定 `shard_month`、Redis Topic 月段路由、MySQL 路由镜像、跨月幂等、逐月游标查询、历史消息定位和旧表迁移。
最终链路:
```text
发送:Redis Lua 原子分配 Seq 和 active_month -> Kafka 固定 shard_month -> MySQL 月表
实时:Redis Pub/Sub/Stream -> WebSocket
热读:Redis 24 小时消息
冷读:Redis Topic 月段路由 -> MySQL 月表逐月查询
容灾:MySQL 路由镜像 -> 重建长期 Redis 路由
补偿:客户端按 Topic Seq 拉取
```
客户端协议无需增加月份字段,月表路由完全由服务端处理。
+12 -42
View File
@@ -3,10 +3,12 @@
-- Database: the currently selected tenant database.
-- Important: one tenant owns one database, so these tables intentionally do not contain tenant_id.
-- This SQL is for review first. Do not execute directly in production before confirming indexes and migration order.
-- Non-auto-increment BIGINT primary keys are generated by the server Snowflake generator.
-- 1. Group profile and governance truth.
-- Topic/Subscription remains the communication projection; this table is the product business truth.
CREATE TABLE IF NOT EXISTS im_group_settings (
id BIGINT NOT NULL,
topic VARCHAR(32) NOT NULL,
owner_user_id BIGINT NOT NULL,
name VARCHAR(128) NOT NULL DEFAULT '',
@@ -27,7 +29,8 @@ CREATE TABLE IF NOT EXISTS im_group_settings (
dissolved_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (topic),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_group_settings_topic (topic),
INDEX idx_im_group_settings_owner (owner_user_id, group_status),
INDEX idx_im_group_settings_status_updated (group_status, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -135,19 +138,22 @@ CREATE TABLE IF NOT EXISTS im_group_announcements (
-- 6. Announcement read receipts.
CREATE TABLE IF NOT EXISTS im_group_announcement_reads (
id BIGINT NOT NULL,
announcement_id VARCHAR(64) NOT NULL,
topic VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
read_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (announcement_id, user_id),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_group_announcement_reads_announcement_user (announcement_id, user_id),
INDEX idx_im_group_announcement_reads_topic_user (topic, user_id, read_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 7. Current member mute state.
-- Historical changes are stored in im_group_admin_logs.
CREATE TABLE IF NOT EXISTS im_group_mutes (
id BIGINT NOT NULL,
topic VARCHAR(32) NOT NULL,
user_id BIGINT NOT NULL,
muted_by BIGINT NOT NULL,
@@ -160,7 +166,8 @@ CREATE TABLE IF NOT EXISTS im_group_mutes (
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (topic, user_id),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_group_mutes_topic_user (topic, user_id),
INDEX idx_im_group_mutes_topic_status (topic, status, expires_at),
INDEX idx_im_group_mutes_user_status (user_id, status, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -237,42 +244,5 @@ CREATE TABLE IF NOT EXISTS im_group_admin_logs (
INDEX idx_im_group_admin_logs_object (object_type, object_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 11. Group operation idempotency.
-- This can later be replaced by a global HTTP idempotency table.
CREATE TABLE IF NOT EXISTS im_group_idempotency (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
method VARCHAR(8) NOT NULL,
route VARCHAR(256) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL,
body_hash CHAR(64) NOT NULL,
result_code INT NOT NULL DEFAULT 0,
result_body JSON,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_group_idempotency_request (user_id, method, route, idempotency_key),
INDEX idx_im_group_idempotency_created (created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 12. Group outbox events.
-- Transactional bridge for group.membership.updated, group.join_request.created,
-- group.announcement.updated, group.role.updated, group.mute.updated, group.dissolved, etc.
CREATE TABLE IF NOT EXISTS im_group_events_outbox (
id BIGINT NOT NULL AUTO_INCREMENT,
event_id VARCHAR(64) NOT NULL,
topic VARCHAR(32) NOT NULL,
actor_user_id BIGINT NOT NULL DEFAULT 0,
event_type VARCHAR(64) NOT NULL,
payload JSON,
status VARCHAR(16) NOT NULL DEFAULT 'pending',
attempts INT NOT NULL DEFAULT 0,
next_retry_at DATETIME(3),
delivered_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_group_events_outbox_event (event_id),
INDEX idx_im_group_events_outbox_pending (status, next_retry_at, id),
INDEX idx_im_group_events_outbox_topic (topic, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- 11. Group operation idempotency is handled by Redis-backed HTTP idempotency.
DROP TABLE IF EXISTS im_group_idempotency;
+122
View File
@@ -0,0 +1,122 @@
# IM「联系我们」移动端接口对接文档
> 版本:v1.0
> 日期:2026-09-21
> 状态:**已实现**,可按本文直接联调
> 适用端:iOS / Android / Web(im_webapp)
## 1. 功能说明
移动端「设置 / 关于 / 支持」页面的**联系我们**入口,展示管理员配置的客服邮箱。
数据链路:
```text
管理后台「系统管理 → 字典管理 → 联系我们」
→ IM 管理端数据库(字典类型 im_contact_us,条目即 label/value)
→ im-server 缓存(内存 TTL 1 分钟兜底 + 管理端变更事件主动失效)
→ 移动端 GET /v1/im/contact-us
```
要点:
- 条目内容由运营在管理后台维护,客户端不需要硬编码任何联系方式。
- 管理后台修改后**近实时生效**(事件失效,秒级);事件通道不可用时最长 1 分钟内收敛(缓存 TTL 兜底)。
- 条目数量不固定,客户端必须按列表渲染,不能假设只有一个条目。
- 接口返回为空列表时,客户端应**隐藏联系我们入口**,不要渲染空区块。
## 2. 接口定义
### 2.1 获取联系我们配置
```text
GET {api_path}v1/im/contact-us
```
**请求头**(公共约定同《IM-前后端对接文档.md》§0.2):
| Header | 必填 | 说明 |
| --- | --- | --- |
| `X-MeetingIM-APIKey` | 是 | 应用级接入标识 |
| `X-IM-Schema` | 是 | 固定 `1` |
| `X-Request-ID` | 是 | 请求追踪 ID |
| `Authorization` | 是 | `Bearer <token>`,登录态必填 |
**请求参数**:无
**成功响应**(HTTP 200):
```json
{
"code": 0,
"data": {
"items": [
{ "label": "客服邮箱", "value": "support@example.com" }
]
},
"request_id": "req-contactus-2",
"server_time": "2026-09-21T04:58:19.521Z"
}
```
`data` 字段:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `items` | `array` | 联系方式条目列表,按管理后台配置的排序返回;可能为空数组 `[]` |
| `items[].label` | `string` | 条目名称,如「客服邮箱」,客户端原样展示 |
| `items[].value` | `string` | 条目值,邮箱地址 |
**失败响应**:
| HTTP | code | reason | 场景 |
| --- | --- | --- | --- |
| 403 | 30003 | `API_KEY_REQUIRED` | API Key 缺失或无效 |
| 401 | 30043 | `AUTH_REQUIRED` | 未登录 / token 过期,引导重新登录 |
| 405 | 30002 | `METHOD_NOT_ALLOWED` | 非 GET 请求 |
| 500 | 90000 | `INTERNAL_ERROR` | 服务端内部错误,可重试 |
## 3. 客户端渲染规则
按条目值的形态渲染(客户端本地判断,与服务端无关):
| value 形态 | 判断 | 交互 |
| --- | --- | --- |
| 邮箱 | 包含 `@` 且形如 `x@y.z` | `mailto:{value}`,图标用 `email` |
| 其他 | 不满足邮箱格式 | 纯文本展示,不可点击 |
补充约定:
- 每个条目的展示文案使用 `label`,不要自行翻译或改写。
- 接口失败(网络/500)时静默降级:若客户端保留了旧版本静态联系方式可继续展示,否则隐藏入口,不弹 toast。
- 不需要本地缓存该接口结果;进入页面时请求即可,服务端已有缓存兜底。
## 4. 本地联调示例
```bash
curl -s "http://{host}/{api_path}v1/im/contact-us" \
-H "X-MeetingIM-APIKey: <api_key>" \
-H "X-IM-Schema: 1" \
-H "X-Request-ID: req-contactus-2" \
-H "Authorization: Bearer <token>"
```
实测返回:
```json
{"code":0,"data":{"items":[{"label":"客服邮箱","value":"support@example.com"}]},"request_id":"req-contactus-2","server_time":"2026-09-21T04:58:19.521Z"}
```
## 5. 配置维护(运营侧)
- 入口:管理后台「系统管理 → 字典管理」→ 字典类型 `联系我们`(type=`im_contact_us`)。
- 新增/修改/删除条目即配置生效,`状态=停用` 的条目不会返回。
- 测试环境初始化 SQL:`im_manager/docs/im/IM-联系我们字典配置.sql`(im_manager 仓库)。
- 首次部署后如接口返回空列表,先确认管理后台该字典类型下存在 `状态=启用` 的条目。
## 6. 参考实现
Web 端(im_webapp)已完成对接,可直接参考:
- API 封装:`src/lib/im-api.js` 中 `getContactUs()`
- 页面渲染:`src/views/acc-support-view.jsx`(邮箱 `mailto:`、空列表回退静态链接)
+5 -1
View File
@@ -5,6 +5,7 @@
> 主要依据:`IM-产品原型需求文档.md`、`IM-前后端对接文档.md`
> 多租户依据:`IM-独立库多租户改造方案.md`
> 明确排除:已废弃的 `IM-服务端多租户改造方案.md`
> 数据库更新:2026-08-18 已完成无独立 `id` 表的雪花主键改造和本地库迁移。
## 1. 审核说明
@@ -353,10 +354,13 @@
- 好友关系、认证安全、隐私、收藏、通知、会议、报表等业务表已在 MySQL 适配器中出现对应存储实现。
- 群业务从 Topic/Subscription 扩展为独立业务表,方向符合复杂群管理需求。
- `im_http_idempotency` 和 `im_business_events_outbox` 已纳入应用自动建表与数据库变更汇总脚本。
- MySQL 业务表已统一具备独立 `id` 主键;原先使用自然键或复合主键的 11 张表已改用服务端雪花 `BIGINT id`,原业务键保留唯一索引。
- 应用启动会在当前租户库幂等补列、回填存量雪花 ID 并切换主键;迁移使用数据库级 advisory lock,独立租户库与残留 `tenant_id` 旧结构均可保留原业务唯一性。
- 用户身份字段已结构化:手机号迁入 `im_users.phone`,邮箱迁入 `im_user_profiles.email`;用户 `tags/im_usertags` 在回填后删除,Topic 标签体系继续保留。
### 需要修正
1. 建立唯一 schema 版本和迁移入口,禁止“应用启动建部分表 + 手工 SQL 建另一部分表”长期并存。
1. 继续把其他历史建表/升级逻辑收敛到唯一 schema 版本和迁移入口;本次雪花主键改造已具备统一自动迁移入口。
2. 群业务 SQL 必须参数化目标租户库,禁止硬编码 `tinode`。
3. 新租户建库和存量租户升级必须执行同一套 migration。
4. 为 Topic、Subscription、群业务表之间的复合写操作提供事务边界。
@@ -0,0 +1,386 @@
# IM 青少年模式前后端对接文档
## 1. 基本约定
青少年模式相关接口均为登录后接口。客户端需要携带用户登录 token、普通 API Key 和协议版本头。
公共 Header:
```http
Authorization: Bearer <access_token>
X-MeetingIM-Auth: Bearer <access_token>
X-MeetingIM-APIKey: <api_key>
X-IM-Schema: 1
X-Request-ID: <uuid>
X-Tenant-Code: default
```
所有写接口还需要携带:
```http
Idempotency-Key: <uuid>
```
测试环境验证码固定为 `123456`。`action_token` 为 5 分钟有效、单次消费,只能用于指定动作,不能在多个接口之间重复使用。
当前默认策略项为 `night_quiet`、`content_filter`、`payment_guard` 三条。现阶段后端已完成状态持久化、开启/关闭校验和策略返回;`night_quiet` 已接入音视频呼叫发起限制,`content_filter` 已接入消息发送前内容安全审核,响应中的这两项 `restriction_items[].enforced=true`。`payment_guard` 当前仍为预留项,`enforced=false`。
## 2. 开启青少年模式
### 2.1 查询当前状态
```http
GET /v1/im/settings/youth-mode
```
成功响应:
```json
{
"code": 0,
"data": {
"enabled": false,
"policy_version": 1,
"restrictions": ["night_quiet", "content_filter", "payment_guard"],
"restriction_items": [
{
"code": "night_quiet",
"title": "深夜免打扰",
"description": "夜间减少消息提醒打扰,帮助用户保持健康作息。",
"enforced": true
},
{
"code": "content_filter",
"title": "内容过滤",
"description": "对不适合青少年的内容进行识别和限制展示。",
"enforced": true
},
{
"code": "payment_guard",
"title": "支付守护",
"description": "限制或确认支付、充值等敏感消费行为。",
"enforced": false
}
],
"password_set": false,
"can_enable": false,
"updated_at": "2026-08-20T09:00:00Z"
}
}
```
前端优先使用 `restriction_items` 渲染开启确认页,`restrictions` 保留为兼容旧客户端的 code 数组。最终开启时需要提交用户确认过的 `policy_version`。
### 2.2 发起短信身份验证
```http
POST /v1/im/security/challenges
```
请求体:
```json
{
"scene": "set_independent_password",
"channel": "sms"
}
```
成功响应:
```json
{
"code": 0,
"data": {
"challenge_id": "sec_chg_1",
"phone_mask": "+861****000",
"expires_in_seconds": 300,
"resend_after_seconds": 60
}
}
```
手机号由服务端从当前登录用户读取,客户端不提交目标手机号。
### 2.3 校验短信验证码
```http
POST /v1/im/security/challenges/{challenge_id}/verify
```
请求体:
```json
{
"code": "123456"
}
```
成功响应:
```json
{
"code": 0,
"data": {
"action": "set_independent_password",
"action_token": "set_password_token",
"expires_at": "2026-08-20T09:05:00Z"
}
}
```
这里拿到的 `action_token` 只用于设置独立密码,不能直接用于开启青少年模式。
### 2.4 设置独立密码
```http
POST /v1/im/settings/independent-password
```
请求体:
```json
{
"password": "6439",
"purpose": "youth_mode",
"action_token": "set_password_token"
}
```
成功响应:
```json
{
"code": 0,
"data": {
"status": "ok",
"password_set": true,
"purpose": "youth_mode",
"enable_action_token": "enable_token",
"enable_action_expires_at": "2026-08-20T09:05:00Z",
"next_action": "enable_youth_mode"
}
}
```
服务端会消费 `set_password_token`,并返回新的 `enable_action_token`。前端下一步必须使用 `enable_action_token`,不要复用 `set_password_token`。
独立密码规则:
- 必须是 4 位数字。
- 不能是明显连续或重复组合。
- 服务端只保存强哈希,不返回明文密码。
- 前端离开页面后应清空输入内容,不落普通存储。
### 2.5 开启青少年模式
```http
POST /v1/im/settings/youth-mode/enable
```
请求体:
```json
{
"policy_version": 1,
"action_token": "enable_token"
}
```
成功响应:
```json
{
"code": 0,
"data": {
"enabled": true,
"policy_version": 1,
"password_set": true,
"restrictions": ["night_quiet", "content_filter", "payment_guard"],
"restriction_items": [
{
"code": "night_quiet",
"title": "深夜免打扰",
"description": "夜间减少消息提醒打扰,帮助用户保持健康作息。",
"enforced": true
},
{
"code": "content_filter",
"title": "内容过滤",
"description": "对不适合青少年的内容进行识别和限制展示。",
"enforced": true
},
{
"code": "payment_guard",
"title": "支付守护",
"description": "限制或确认支付、充值等敏感消费行为。",
"enforced": false
}
],
"updated_at": "2026-08-20T09:00:00Z"
}
}
```
开启成功后,客户端应刷新本地设置页和 `bootstrap` 缓存。服务端会持久化状态,前端不能只依赖本地标志位。
## 3. 关闭青少年模式
### 3.1 校验独立密码
```http
POST /v1/im/settings/independent-password/verify
```
请求体:
```json
{
"password": "6439",
"purpose": "youth_mode",
"action": "close_youth_mode"
}
```
成功响应:
```json
{
"code": 0,
"data": {
"action": "close_youth_mode",
"action_token": "close_token",
"expires_at": "2026-08-20T09:05:00Z"
}
}
```
### 3.2 关闭模式
```http
POST /v1/im/settings/youth-mode/disable
X-Action-Token: close_token
```
请求体:
```json
{}
```
成功响应:
```json
{
"code": 0,
"data": {
"enabled": false,
"policy_version": 1,
"password_set": true,
"updated_at": "2026-08-20T09:00:00Z"
}
}
```
关闭接口的 `action_token` 通过 Header `X-Action-Token` 传递,不放在 Body 中。
## 4. Token 使用规则
| 来源 | action | 用途 | 传递方式 | 是否单次消费 |
| --- | --- | --- | --- | --- |
| `security/challenges/{id}/verify` | `set_independent_password` | 设置独立密码 | Body `action_token` | 是 |
| `independent-password` | `enable_youth_mode` | 开启青少年模式 | Body `action_token` | 是 |
| `independent-password/verify` | `close_youth_mode` | 关闭青少年模式 | Header `X-Action-Token` | 是 |
常见错误:
- `ACTION_TOKEN_INVALID`:token 已消费、过期、action 不匹配、target 不匹配或用户不匹配。
- `CODE_INVALID`:短信验证码错误。
- `CHALLENGE_EXPIRED`:短信 challenge 已过期。
- `TOO_MANY_ATTEMPTS`:独立密码或验证码错误次数过多。
- `VERIFICATION_RATE_LIMITED`:发码过于频繁,按 `retry_after_seconds` 倒计时。
## 5. curl 示例
开启流程:
```bash
curl -sS 'https://api.118im.com/v1/im/security/challenges' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access_token>' \
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
-H 'X-MeetingIM-APIKey: <api_key>' \
-H 'X-IM-Schema: 1' \
-H 'X-Tenant-Code: default' \
-H 'X-Request-ID: <uuid>' \
-H 'Idempotency-Key: <uuid>' \
--data '{"scene":"set_independent_password","channel":"sms"}'
```
```bash
curl -sS 'https://api.118im.com/v1/im/security/challenges/<challenge_id>/verify' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access_token>' \
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
-H 'X-MeetingIM-APIKey: <api_key>' \
-H 'X-IM-Schema: 1' \
-H 'X-Tenant-Code: default' \
-H 'X-Request-ID: <uuid>' \
-H 'Idempotency-Key: <uuid>' \
--data '{"code":"123456"}'
```
```bash
curl -sS 'https://api.118im.com/v1/im/settings/independent-password' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access_token>' \
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
-H 'X-MeetingIM-APIKey: <api_key>' \
-H 'X-IM-Schema: 1' \
-H 'X-Tenant-Code: default' \
-H 'X-Request-ID: <uuid>' \
-H 'Idempotency-Key: <uuid>' \
--data '{"password":"6439","purpose":"youth_mode","action_token":"<set_password_token>"}'
```
```bash
curl -sS 'https://api.118im.com/v1/im/settings/youth-mode/enable' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access_token>' \
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
-H 'X-MeetingIM-APIKey: <api_key>' \
-H 'X-IM-Schema: 1' \
-H 'X-Tenant-Code: default' \
-H 'X-Request-ID: <uuid>' \
-H 'Idempotency-Key: <uuid>' \
--data '{"policy_version":1,"action_token":"<enable_action_token>"}'
```
关闭流程:
```bash
curl -sS 'https://api.118im.com/v1/im/settings/independent-password/verify' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access_token>' \
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
-H 'X-MeetingIM-APIKey: <api_key>' \
-H 'X-IM-Schema: 1' \
-H 'X-Tenant-Code: default' \
-H 'X-Request-ID: <uuid>' \
-H 'Idempotency-Key: <uuid>' \
--data '{"password":"6439","purpose":"youth_mode","action":"close_youth_mode"}'
```
```bash
curl -sS 'https://api.118im.com/v1/im/settings/youth-mode/disable' \
-H 'Content-Type: application/json' \
-H 'Authorization: Bearer <access_token>' \
-H 'X-MeetingIM-Auth: Bearer <access_token>' \
-H 'X-MeetingIM-APIKey: <api_key>' \
-H 'X-IM-Schema: 1' \
-H 'X-Tenant-Code: default' \
-H 'X-Request-ID: <uuid>' \
-H 'Idempotency-Key: <uuid>' \
-H 'X-Action-Token: <close_token>' \
--data '{}'
```
+4 -5
View File
@@ -41,17 +41,16 @@ ServerComMessage
2. `sess.bkgTimer` — 后台超时定时器
3. `sess.stop` — 停止信号
4. `sess.detach` — 取消订阅
5. `ticker` — Ping 帧(每 `pongWait * 9/10` 发送一次)
5. `ticker` — Ping 帧(每 15s 发送一次)
### Keep-Alive
| 参数 | 值 | 说明 |
|------|-----|------|
| `writeWait` | 10s | 写超时 |
| `pongWait` | `idleSessionTimeout` | 期望收到 Pong 的时间(默认 180s) |
| `pingPeriod` | `pongWait * 9/10` | Ping 间隔(约 162s) |
| `pingPeriod` | `pongWait * 9/10` | Ping 间隔(约 162s) |
| 心跳探测 | `0x31` / `0x30` | 网络探针(单字节) |
| `pongWait` | `idleSessionTimeout` | 期望收到 Pong 的时间(当前默认 45s) |
| `pingPeriod` | 15s | WebSocket Ping 间隔 |
| 心跳探测 | WebSocket Ping/Pong | 当前 IM 长连接保活以协议层 Ping/Pong 为准;业务 `note kp/kpa/kpv` 不能替代 Pong |
### 缓冲区设置
+6 -6
View File
@@ -174,8 +174,8 @@ Tinode 的心跳配置:
```go
const (
writeWait = 10 * time.Second // 写操作超时
pongWait = idleSessionTimeout // 等待 Pong 超时(默认 60 秒)
pingPeriod = (pongWait * 9) / 10 // 发送 Ping 周期(54 秒)
pongWait = idleSessionTimeout // 等待 Pong 超时(默认 45 秒)
pingPeriod = 15 * time.Second // 发送 Ping 周期(15 秒)
)
```
@@ -190,7 +190,7 @@ const (
│ ┌─────────────────────────────────────────────────────────────────────┐ │
│ │ for { │ │
│ │ select { │ │
│ │ case <-ticker.C: // 每 54 秒 │ │
│ │ case <-ticker.C: // 每 15 秒 │ │
│ │ ws.WriteMessage(PingMessage, nil) // 发送 Ping │ │
│ │ case msg <- sess.send: // 有消息待发送 │ │
│ │ ws.WriteMessage(TextMessage, msg) // 发送消息 │ │
@@ -554,8 +554,8 @@ func (sess *Session) writeLoop() {
| 参数 | 值 | 说明 |
|------|-----|------|
| `writeWait` | 10秒 | 写操作超时 |
| `pongWait` | 配置项 | 等待 Pong 超时(默认 60 秒) |
| `pingPeriod` | `pongWait * 0.9` | 发送 Ping 周期 |
| `pongWait` | `idleSessionTimeout` | 等待 Pong 超时(默认 45 秒) |
| `pingPeriod` | 15 秒 | 发送 Ping 周期 |
### 消息格式
@@ -1290,7 +1290,7 @@ func largeFileRunGarbageCollection(period time.Duration, blockSize int) chan<- b
"listen": ":6060",
"grpc_listen": ":16060",
"max_message_size": 262144,
"idle_session_timeout": 60000,
"idle_session_timeout": 45000,
"ws_compression": true,
"api_key_salt": "your-hmac-salt",
Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 288 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 311 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 95 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

Before

Width:  |  Height:  |  Size: 244 KiB

After

Width:  |  Height:  |  Size: 75 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 244 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 82 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 81 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 79 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 101 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 55 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 254 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 336 KiB

@@ -0,0 +1,226 @@
# IM R11 Traefik 入口故障运维排查手册
## 1. 目标与已知事实
需要定位 2026-10-04 北京时间 `10:39:30–10:42:30`,即 UTC `02:39:30–02:42:30` 的入口整体中断。
已知现象:
- `api.118im.com/metrics` 先后出现超时、HTTP 521 和 HTTP 503。
- 三个源 IP 的 443 在约 10:40:16 同时 `connection refused`。
- 2000 个 WebSocket 几乎同时断开,A/B 两台压测机均受影响。
- Tinode cluster 采样仍为 2/2 live peers。
- 故障后三个源 IP 返回同一张 `TRAEFIK DEFAULT CERT`,证书 `notBefore` 为 `2026-10-04 10:41:32 +08:00`,与入口恢复窗口重合。
- 运维已确认 `216.236.31.189:6443` 按安全策略不对公网开放,只允许集群网络访问;外部调试机 TCP 超时属于预期行为。
排查过程中不要先重启 Traefik、节点或 k3s,也不要清理容器和日志。先完成只读取证。
## 2. 运维进入方式
优先在控制面节点直接执行 `kubectl`。如果控制面节点没有配置 kubeconfig,k3s 通常可使用:
```bash
sudo k3s kubectl get nodes -o wide
```
普通 Kubernetes 可使用:
```bash
kubectl get nodes -o wide
kubectl cluster-info
```
不建议把 6443 直接向公网完全开放。长期方案应采用 VPN/堡垒机或仅允许固定运维出口 IP。短期也可以让运维直接在控制面节点采集下面的结果。
## 3. 第一优先级:确认 Traefik 是否重启
先找到 Traefik 所在 namespace、工作负载和 Pod:
```bash
kubectl get deploy,ds,sts,pod -A -o wide | grep -i traefik
kubectl get pods -A -l app.kubernetes.io/name=traefik -o wide
kubectl get pods -A -l app=traefik -o wide
```
对每个 Traefik Pod 执行:
```bash
kubectl -n kube-system get pod TRAEFIK_POD -o jsonpath='{.metadata.name}{"\nstarted="}{.status.startTime}{"\nrestarts="}{.status.containerStatuses[*].restartCount}{"\nlastReason="}{.status.containerStatuses[*].lastState.terminated.reason}{"\nlastExit="}{.status.containerStatuses[*].lastState.terminated.exitCode}{"\nlastFinished="}{.status.containerStatuses[*].lastState.terminated.finishedAt}{"\n"}'
kubectl -n kube-system describe pod TRAEFIK_POD
kubectl -n kube-system logs TRAEFIK_POD --all-containers --timestamps --since-time=2026-10-04T02:39:30Z
kubectl -n kube-system logs TRAEFIK_POD --all-containers --previous --timestamps --since-time=2026-10-04T02:39:30Z
```
重点检查:
- `OOMKilled`、exit code 137、liveness/readiness failure。
- `Killing`、`Evicted`、`NodeNotReady`、`Preempted`。
- `Configuration loaded`、provider reconnect、TLS store/default certificate 重建。
- 10:41:32 附近是否有容器启动日志。
如果 Traefik 不在 `kube-system`,将命令中的 namespace 替换为实际值。
## 4. 第二优先级:查滚动发布和配置变更
```bash
kubectl -n kube-system get deploy,ds,rs -l app.kubernetes.io/name=traefik -o wide
kubectl -n kube-system rollout history deploy/traefik
kubectl -n kube-system get deploy traefik -o yaml
kubectl -n kube-system get cm,secret | grep -i traefik
```
若由 Helm 管理:
```bash
helm list -A | grep -i traefik
helm history traefik -n kube-system
helm get values traefik -n kube-system -a
```
若由 ArgoCD 管理:
```bash
kubectl get applications.argoproj.io -A
kubectl get applications.argoproj.io -A -o yaml | grep -n -i -C 5 traefik
kubectl get pods -n argocd -o wide
kubectl logs -n argocd -l app.kubernetes.io/name=argocd-application-controller --all-containers --timestamps --since-time=2026-10-04T02:39:30Z
```
同时检查 CI/CD 平台在北京时间 10:39–10:42 是否执行过 Traefik、Ingress、Service、证书或节点部署。需要记录操作者、commit、同步 revision 和开始/结束时间。
## 5. 第三优先级:节点是否重启或资源耗尽
```bash
kubectl get nodes -o wide
kubectl describe nodes
kubectl get events -A --sort-by=.metadata.creationTimestamp
kubectl get events -A --field-selector type=Warning --sort-by=.metadata.creationTimestamp
```
在承载 Traefik 的节点执行:
```bash
uptime -s
last -x | head -30
sudo journalctl --list-boots
sudo journalctl --since '2026-10-04 10:39:30' --until '2026-10-04 10:42:30' -u k3s -u k3s-agent -u kubelet -u containerd --no-pager
sudo journalctl -k --since '2026-10-04 10:39:30' --until '2026-10-04 10:42:30' --no-pager
sudo dmesg -T | grep -Ei 'oom|killed process|memory cgroup|conntrack|nf_conntrack|link.*down|reset|I/O error'
```
重点判断:
- 节点是否在 10:41 左右重启。
- 内核是否 OOM kill Traefik、containerd 或 k3s。
- 磁盘是否满、只读或发生 I/O error。
- 网卡是否 link down、驱动 reset。
- kubelet 是否因 liveness probe 杀掉 Traefik。
## 6. 第四优先级:Service 与 Endpoint 是否短暂清空
```bash
kubectl get svc,endpoints,endpointslices -A | grep -Ei 'traefik|im-server'
kubectl -n kube-system describe svc traefik
kubectl -n kube-system get endpoints traefik -o yaml
kubectl -n kube-system get endpointslices -l kubernetes.io/service-name=traefik -o yaml
kubectl -n im-server-system get pod,svc,endpoints,endpointslices -o wide
kubectl -n im-server-system describe svc im-server
kubectl -n im-server-system get endpoints im-server -o yaml
kubectl -n im-server-system get endpointslices -l kubernetes.io/service-name=im-server -o yaml
```
检查 10:40–10:41 是否发生:
- Traefik Service 没有可用 endpoint。
- `im-server` 三个 Pod 同时 NotReady,导致上游 503。
- selector、port、targetPort 或 Ingress backend 被 ArgoCD/Helm 临时改写。
- StatefulSet 滚动时 readiness 配置错误,所有 endpoint 被同时摘除。
## 7. 第五优先级:连接数和 conntrack
在入口节点执行:
```bash
sudo ss -lntp | grep -E ':443|:6443'
sudo ss -s
sudo sysctl net.netfilter.nf_conntrack_count net.netfilter.nf_conntrack_max
sudo conntrack -S 2>/dev/null || true
sudo nstat -az | grep -Ei 'ListenDrops|ListenOverflows|RetransSegs|InErrors|OutDiscards'
sudo ip -s link
```
如果有节点监控,导出 10:35–10:45 的:
- CPU usage、load、CPU throttling。
- 内存、OOM、working set。
- conntrack 使用率。
- TCP listen overflow、reset、retransmit。
- 网卡吞吐、drop、error。
- Traefik active connection、open connection、5xx、upstream duration。
conntrack 达到 `nf_conntrack_max` 通常更偏向 timeout/drop;本轮观察到短时间 `connection refused`,因此还应优先确认 443 监听进程是否消失或入口规则是否被重建。
## 8. im-server 同期状态
```bash
kubectl -n im-server-system get pods -o wide
kubectl -n im-server-system get sts im-server -o yaml
kubectl -n im-server-system describe sts im-server
for pod in $(kubectl -n im-server-system get pods -l app=im-server -o name); do
kubectl -n im-server-system logs "$pod" --all-containers --timestamps --since-time=2026-10-04T02:39:30Z > "${pod##*/}.log"
kubectl -n im-server-system logs "$pod" --all-containers --previous --timestamps --since-time=2026-10-04T02:39:30Z > "${pod##*/}.previous.log" 2>&1 || true
done
```
如果 `app=im-server` 标签不匹配,先执行 `kubectl -n im-server-system get pods --show-labels`,再替换 selector。
需要确认三个 im-server Pod 的:
- `startTime` 和 `restartCount`。
- readiness/liveness failure。
- 10:40 左右是否仍持续处理 Kafka、cluster RPC 和 WS。
- 是否有 `too many open files`、OOM、accept error、DNS error、MySQL/Kafka timeout。
## 9. Kubernetes API 访问方式
从当前调试机访问三个源 IP 的 443 均正常。运维已确认 `216.236.31.189:6443` 只允许集群网络访问,因此外部连接超时符合既定安全策略,也与 VPN/TUN 无关。
运维应检查控制面节点:
```bash
sudo ss -lntp | grep 6443
sudo nft list ruleset | grep -n -C 3 6443
sudo iptables -S | grep 6443
sudo iptables -t nat -S | grep 6443
```
后续排查采用以下方式之一:
1. 让排查命令直接在控制面节点运行并回传结果包。
2. 如确需远程调试,为固定运维出口 IP 临时增加 6443 allowlist,完成后撤销。
3. 通过堡垒机/VPN 进入控制面网络。
不要为了调试把 6443 对 `0.0.0.0/0` 永久开放。
## 10. 运维需要回传的结果
至少回传以下内容:
1. Traefik Pod YAML、describe、当前日志和 previous 日志。
2. Traefik Deployment/DaemonSet、ReplicaSet 和 rollout history。
3. 10:39:30–10:42:30 的 k3s/kubelet/containerd/kernel journal。
4. 全集群 Warning Event、节点 describe。
5. Traefik 和 im-server 的 Service、EndpointSlice、Ingress YAML。
6. ArgoCD/Helm/CI 在故障窗口的发布历史。
7. 入口节点 CPU、内存、conntrack、TCP overflow/retransmit 和网卡 drop。
拿到这些材料后,按照以下优先级定责:
1. Traefik 容器或节点在 10:41:32 重启。
2. ArgoCD/Helm/CI 触发 Traefik 滚动或配置重建。
3. Traefik/im-server endpoint 短暂清空。
4. OOM、liveness、conntrack、监听队列或节点网络故障。
5. 其他上游负载均衡或云网络事件。
+495
View File
@@ -0,0 +1,495 @@
# IM 后端问题清单
_2026-09-14 全面复核:本次对现有33条逐条重新核实(能实测的实测,能读代码的读代码,均标注核实方式),并把此前散落在各处(`docs/IM-后端问题清单-2026-09-04.md`、历次前端重写工程记录)但从未收录进本清单的问题一并归档,同时确认了几条此前记录、后来查明其实是前端自己的bug(已在下方"已排除"列出,不再作为后端问题)。这次复核期间发现后端正在把数据库从单机MySQL迁移到新k3s集群的Percona XtraDB Cluster(PXC)——`server/meetingim.conf`/`tinode.conf`的MySQL/Redis/Kafka地址已经从旧VM改成新集群地址,代码里也新增了专门处理PXC兼容性问题(命名锁、自增ID越界)的分支——这是今天新发现的几个MySQL相关问题(尤其是周期性的驱动层连接损坏)很可能与之相关的重要背景,供后续排查参考。_
## 目录
**P0 · 安全漏洞 / 核心功能完全不可用**
暂无。
**P1 · 数据完整性 / 权限失效 / 主要功能缺陷**
| 编号 | 接口 | 一句话 |
|---|---|---|
| [问题36](#问题36) | `POST /messages/forward` | 并发转发下响应说"失败"其实已经真实写入——2026-09-14用干净的12并发复测,仍有2/3的"失败"响应对应真实落库的消息,最近一次代码改动(`926b4bb`)改善了部分逻辑但未解决根本问题 |
| [新增80](#新增80) | Bearer Token鉴权(影响所有需要登录态的接口) | 同一个token并发请求,20-45%概率随机返回401 AUTH_REQUIRED,鉴权层把"token真无效"和"Redis/DB瞬时报错"统一处理成同一个错误码且完全不打日志,无法从现有日志区分具体原因 |
| [新增81](#新增81) | `GET /v0/file/s/{id}`(媒体文件服务) | 查询数据库里不存在的列`mtg.recorder_user_id`,25分钟观测窗口内命中这条SQL的请求243次全部失败 |
**P2 · 数据一致性 / 体验缺口 / 校验缺失**
| 编号 | 接口 | 一句话 |
|---|---|---|
| [问题25](#问题25) | `POST /messages/forward`、`/favorites/forward` | 带附言转发时 `head` 声明类型与 `content` 实际结构对不上 |
| [问题27](#问题27) | `GET /reports/daily` | `status` 参数不做取值校验,传任何值都返回200+空列表 |
| [问题33](#问题33) | `GET /reports/daily` | `permissions` 四个字段恒为同一组硬编码值,与用户/参数无关 |
| [问题35](#问题35) | `GET /users/{id}/profile` | 设了备注之后拿不到对方的原昵称(`GET /friends`一侧已修复) |
| [新增3](#新增3) | `PATCH /conversations/{topic}/settings` | 单聊置顶没有批量查询接口,会话列表永远看不出置顶效果 |
| [新增8](#新增8) | `GET /friends` | 完全不感知拉黑关系,与另外两个接口表现不一致 |
| [新增15](#新增15) | `POST /meetings/{id}/cancel` | 完全不校验 `version`,乐观锁摆设 |
| [新增24](#新增24) | `GET /groups/{topic}`、`/settings` | 群成员数存在两套互相独立、可永久失配的计数器 |
| [新增25](#新增25) | 原生WS `{pub}` | link类型消息协议层可构造,但没有真实用户可从UI触达的发送路径 |
| [新增30](#新增30) | `GET /settings/youth-mode` | 开启后不对任何功能做实际限制,`enforced` 恒为硬编码false(前端已移除此功能,仅保留记录完整性) |
| [新增31](#新增31) | `POST /reports/abuse` | 不校验 `requires_description`,可对必填原因提交空描述 |
| [新增34](#新增34) | `POST /tenant-applications` | 统一社会信用代码无去重机制,可无限重复提交 |
| [新增41](#新增41) | `POST /groups/{topic}/mutes` | 无角色层级保护,管理员可禁言群主、任何角色可自我禁言 |
| [新增47](#新增47) | `POST /v0/file/u/` | 真实文件上传上限是32MB而非声明的100MB,且从未用于前端预校验 |
| [新增49](#新增49) | `POST /reports/daily`、`/monthly` | 创建接口对日期只校验格式不校验范围,年份0000/2099均可创建 |
| [新增51](#新增51) | `POST /meetings` | 不校验 `attendee_user_ids` 是否真实存在,可创建任意规模"幽灵参会人" |
| [新增54](#新增54) | 日报/月报SUBMIT | 不校验必填内容,可提交完全空白报告 |
| [新增56](#新增56) | `POST /settings/youth-mode/enable` | `policy_version` 不做版本校验,传任意值即被无条件接受(前端已移除此功能) |
| [新增76](#新增76) | `GET /app/releases/latest?platform=web` | `platform` 只接受ios/android,web端完全不支持检查更新 |
| [新增77](#新增77) | `GET /groups/{topic}/announcements/latest` | 响应没有已读人数字段,"已读N/M"这个设计要求的功能无法实现 |
| [新增78](#新增78) | `GET /topics/{topic}/messages/search`、收藏/通话记录的相关字段 | 发送者/对方user_id被裁掉`usr`前缀,或返回内部hash而非可寻址id |
| [新增82](#新增82) | `cache group settings`(内部缓存,影响群相关读接口的性能与正确性) | 群设置缓存反序列化失败,高频出现(242次/25分钟),覆盖大量不同群 |
| [新增83](#新增83) | `media serve`(文件服务) | 死锁(1213/40001)没有捕获重试,命中的请求直接失败 |
| [新增84](#新增84) | 内部后台任务"media gc" | 周期性(约每50秒一次)触发MySQL驱动层连接损坏("busy buffer"/协议序号错乱),与PXC迁移的时间点吻合,怀疑相关 |
| [新增85](#新增85) | `GET /groups/{topic}/members?role=admin` 等按查询参数区分的缓存 | 写操作(如提升管理员)后,特定查询参数组合的结果有90秒以上的缓存滞后 |
| [新增86](#新增86) | `GET /calls` | 响应字段结构与协议文档完全不同(无`peer`对象、无`incoming`布尔值、时间戳拆成4个字段、`topic`是内部hash) |
| [新增87](#新增87) | 群二维码 `POST /groups/{topic}/qrcodes` vs `POST /qrcodes/resolve` | 铸造接口用`group_snapshot`字段名,解析接口对同样的数据用`group`字段名,两个接口不一致;解析接口另有一个协议文档完全没提过的`status`字段 |
| [新增88](#新增88) | `GET /topics/{topic}/messages/search` | 部分明显应该命中的关键词搜索返回空结果(搜索召回不稳定,2026-09-14复测比更早的记录有改善但未完全解决) |
| [新增89](#新增89) | `im_file_review_tasks` / 养成动态图片审核 | IM 仅创建文件审核任务,没有执行 Worker,所有带图动态都会永久停在 `reviewing/pending` |
| (无编号,功能缺口非bug) | 无 | 整份协议文档没有"移出/踢出群成员"这个操作,`03-group.dc.html`里"移出"按钮因此没有对应接口可用——确认是协议本身没设计这个功能,不是遗漏 |
| (无编号,可能是设计而非缺口) | 无 | 无手机号查询/换绑接口、无登录设备列表接口;`POST /security/challenges`这个"敏感操作二次验证"机制目前只服务青少年模式两个场景,没有覆盖改密码等场景 |
---
## 本次复核确认已修复(不再是问题,供归档参考)
以下条目在2026-09-14用真实账号实测或直接读当前代码确认已经修复,从清单中移除:
- **问题22**(无登出/Token撤销接口):**已修复**。`POST /v1/im/auth/logout`现在是真实可用的接口(之前误判404的路径是没有走对,正确路径是这个)。实测:新token连续3次200 → 调用logout返回200 → 同一token之后5次全部401,确认真实吊销,不是假成功。
- **问题28**(`purpose=user_avatar`被拒):**已修复**。实测提交真实PNG文件,200成功。
- **新增16**(总经理信箱超长内容返回500):**已修复**。代码确认`serveIMExecutiveMailboxSubmissions`现在显式捕获`ErrPolicy`并返回400 VALIDATION_FAILED,不再落到default的500分支。
- **新增17**(`writeIMRelationError`把`ErrNotFound`错误映射成`MALFORMED_REQUEST`):**已修复**。代码确认现在`ErrNotFound`正确映射到404+`RESOURCE_NOT_FOUND`,reason与message一致。
- **新增27**(工作台`unread_count`恒为0):**已修复**(2026-09-10记录"只接入bootstrap,workbench本身还没接",现在确认`serveIMWorkbench`内部也调用了`populateWorkbenchModuleUnreadCounts`)。注:目前只有"会议"模块会计算真实值,其余模块代码上仍是原始SQL的0值,但这正好是原始bug报告实测覆盖的场景,已修复。
- **新增42**(群公告按id查询恒定返回最新公告):**已修复**。代码确认现在正确解析路径里的`announcement_id`并调用`GetAnnouncement`按id查询,不再无视这个参数。
- **新增43**(群文件登记接口无长度/存在性校验):**已修复**。代码确认现在对`file_name`(255)/`mime_type`(128)做长度校验,对`file_size`做负数校验,且会调用`store.Files.Get`验证`file_id`真实存在。
- **新增45**(收藏/搜索/宫格分页off-by-one永久丢失边界记录):**已修复**。实测:24条真实收藏,用limit=10分3页拉取(10+10+4),合并后24条一条不少、无重复。
- **问题30**(系统通知`summary`/`action`结构不一致 + 会议响应事件双写):**已修复/已失效**(2026-10-09复核)。①会议响应双写随上游`0e2f4e7`(2026-09-17把整个workbench/会议模块从im_server移除)消失,现仅剩Kafka单路径写入且按固定event_id幂等upsert,不再适用;②`action`由读路径`normalizeSystemNotificationAction`统一归一为`{type,target_id,payload,...}`;③空`summary`读路径回退`title`;④机器码`summary`(`accepted`/`rejected`/`continue`/`cancel`)已在4处写入点改为人类可读文案(`serveIMFriendRequestAction`/`serveIMRiskChallengeConfirm`/`TenantFriendRequestAccept`/`TenantFriendRequestResolve`)。实测:A对B发好友申请、B拒绝后,A的通知列表返回`summary:"对方已婉拒你的好友申请"`、`action`为归一化结构。
- **新增32**(系统通知重复已读,响应回显请求值但存储不变):**已修复**(2026-10-09复核)。存储层`TenantSystemNotificationMarkRead`在`COALESCE`更新后回读真实行返回,handler用返回值构造响应(单测`TestServeIMSystemNotificationsListAndRead`覆盖)。实测:同一通知先以`read_at=T1`标记已读、再以`T2`重复标记,两次响应均返回持久化的`T1`,不再回显本次提交值。
## 本次复核确认仍是历史误判(从未是真正的后端bug,不应再作为后端问题提出)
以下条目此前曾被记录为"疑似后端bug",后来(多为9月上中旬的复核)证实其实是前端自己调用错了接口/传错了参数,backend行为一直是对的——**不要把这几条再当成后端问题反馈**:
- `GET .../groups/{topic}/members/{user_id}/profile` 查自己恒定400——真相:有专门的`members/me/profile`接口可以正确查询,前端读操作一直在用错误的通用by-id接口,写操作反而一直用对了。已切换到正确接口,问题消失。
- `POST .../groups/{topic}/mutes/batch` 恒定405——真相:真实的批量禁言接口是`/mutes`(不带`/batch`后缀),前端一直在调一个不存在的路径。已改用正确路径。
- `GET /users/{id}/profile` 对好友`usrW9ijv236hHA`恒定403——真相:响应体里其实带着`message:"profile is private"`,是对方自己设置了资料保护,属于设计内行为,不是bug。
- `PATCH .../groups/{topic}/member-settings`(置顶/免打扰)恒定400——真相:这个接口的请求体结构里根本没有`version`字段,后端解码器对未声明字段严格拒绝;前端一直在照抄别的接口的乐观锁写法多传了一个`version`字段。已去掉,问题消失。
- 群设置`governance`字段"被后端写死成默认值"——这是一次静态代码审查的猜测,当场用真实接口交叉验证后证明是错的:`/settings`和`/governance`返回一致,写入后能正确反映在读接口上。从未真正成立过。
---
<a id="问题36"></a>
## 问题 36(P1,2026-09-14复测:仍未修复,最近一次代码改动效果有限):并发转发到同一个会话时,响应说"失败"的条目里相当一部分其实已经成功写入,重试会造成真实内容重复
**证据强度**:✅ 实测确认(多轮独立复现,含2026-09-14最新一轮干净复测)+代码确认
**2026-09-14复测方法与结果**:`git pull`确认`server/kafka_message_persist.go`在9月11日有过一次专门的修复提交(`926b4bb fix im forwarding and message list regressions`),代码上看这次改动新增了`Pending`状态(区分"确定成功"/"确定失败"/"不确定,可能已经发出"三种情况,而不是原来的非成功即失败)。但不能只信代码改动就下结论,实际用真实群(`grpDclMebv3MuI`)、真实消息(seq 135)做了一次干净的12并发转发测试(每个请求用独立的幂等键,模拟"用户看到失败提示后重新点击"的真实场景):
- 12个请求里7个报告"sent"成功(seq 136/137/138/139/140/142/143),3个报告"failed"(`INTERNAL_ERROR`),2个命中已知的401鉴权问题(新增80,未到达转发逻辑)。
- 测试前群里搜索"测试"能查到的最大seq是135;测试后变成130-144连续9个新seq(136-144)。
- **9个真实新消息 vs 7个确认成功——多出来的136-144里的141和144,对应的正是那3条报"failed"里的2条,它们实际上真的写入成功了,只是响应告诉客户端"失败"。**
**结论**:本质问题没有解决——响应仍然不可信,"failed"不能被客户端安全地当作"真的没发生"来处理,重试依然会造成真实重复。9月11日的那次改动可能改善了部分场景(比如更细粒度的pending状态跟踪),但没有堵住这个具体的漏洞。
**根因**(此前已逐行核对,本次未见改变):开启Kafka时,`saveMessageMaybeKafka`先原子预留seq,再异步把消息发布到Kafka——如果produce调用本身报错(例如含糊的ack超时),HTTP handler判定失败并返回给客户端,但已预留的seq和可能已经在网络层提交成功、只是没收到ack的Kafka消息都回滚不了,只要Kafka消费者最终异步写入,"失败"响应背后就会真实落库。
**接口**:`POST /v1/im/messages/forward`
**前端现状**:批量转发的失败提示照原样展示后端返回的`status`/`reason`,无法感知也无法规避"响应可能撒谎"这个问题,需要后端修正响应准确性(例如:明确区分"确定失败可安全重试"和"结果不确定,建议先检查再决定是否重试"两种状态,而不是都归为同一种"failed")。
---
<a id="新增80"></a>
## 新增 80(P1,2026-09-14新发现):Bearer Token鉴权在并发请求下随机出现401,且无法从日志定位具体原因
**证据强度**:✅ 实测确认(自己独立复现,两个不同接口各自独立测试)+代码确认(完整调用链,含具体文件行号)
**现象**:用同一个有效token对`GET /v1/im/groups/{topic}`或`GET /v1/im/me`连续发起20个并发请求,大约15%-65%会随机返回`401 {"code":30043,"reason":"AUTH_REQUIRED"}`,比例不稳定(同一token连续测两轮,一轮5/20失败,几秒后另一轮13/20失败)。对照组:同样并发打`POST /v1/im/tenants/resolve`(只需要API Key,不需要bearer token)20/20全部成功,证明问题只出在bearer token鉴权这一层,不是网络或网关层面的问题。
**排除的可能性**:进入当前所有正在运行的3个`im-server` Pod内部,对`AUTH_TOKEN_KEY`/`MYSQL_DSN`/`REDIS_ADDR`/`REDIS_PASSWORD`/`TENANT_REGISTRY_DB`/`API_KEY_SALT`/`UID_ENCRYPTION_KEY`共7个关键配置项,在每个Pod自己进程内部单独计算哈希值做比对(不在外部解密Secret),3个Pod完全一致——排除"多实例配置不一致"这个最初怀疑的方向;3个Pod是同一批次创建、同一个ReplicaSet,没有游离的旧实例。
**根因(读当前`im_be`源码确认,文件行号)**:`GET /v1/im/groups/{topic}`等接口的鉴权链条是 `authenticateIMHTTPBearer`(`server/im_http.go:7093`) → `resolveIMHTTPBearerToken`(`server/im_http.go:7111`) → `(*authenticator).Authenticate`(`server/auth/token/auth_token.go:103`)。这个函数内部,token签名错误、过期、`serial_num`不匹配、Redis撤销状态查询报错(`IsLoginTokenRevoked`)、按租户路由的DB鉴权版本号查询报错(`GetAuthVersion`/`TenantAdapterFor`)——**所有这些情况的错误全部被统一转换成同一个`types.ErrPermissionDenied`**,最终一律报401,且**这条失败路径上完全没有任何日志输出**,导致即使线上真实发生了这类失败,事后也无法从日志区分具体是哪一种原因触发的。
其中`GetAuthVersion`背后的`TenantAdapterFor`(`server/db/mysql/adapter.go:469`)使用**每个进程自己独立维护的懒加载租户连接缓存**,某个租户如果还没被当前进程缓存过,一波并发请求会各自独立地去开一条新数据库连接并`Ping()`,这种"并发抢着建连接"的场景比"某个pod配置固定错误"更能解释"20个并发里随机几个失败、且失败比例还会波动"这个现象。
**支持证据**:查了同一时间窗口的k8s事件,3个pod中有2个在过去1小时内真实触发过健康检查失败(`Unhealthy: context deadline exceeded`,探测超时只给了1秒);同时机器上2/3节点内存占用在69%-81%(这是个多个服务共享的测试集群)。另外,MySQL的`aborted_connects`计数器非零(13),跟"存在瞬时连接失败"这个方向一致,但不是决定性证据。
**重要背景**:这次复核时发现后端仓库刚经历一次数据库基础设施迁移——`server/meetingim.conf`/`tinode.conf`里的MySQL/Redis/Kafka地址从旧VM换成了新k3s集群的PXC(Percona XtraDB Cluster)地址,且代码里新增了`isPXCStrictModeNamedLockError`/`isIDOutOfRange`两个专门处理PXC兼容性问题的错误分支,说明这个迁移目前还在持续排查兼容性问题的阶段。这个401问题的时间点与这次迁移吻合,虽然鉴权代码本身没有直接调用命名锁,但不排除是同一次基础设施变动带来的连锁不稳定,建议后端一并排查。
**接口**:影响所有需要bearer token鉴权的接口(穷举测试了`GET /v1/im/groups/{topic}`、`GET /v1/im/me`两个,机制上应该影响全部同类接口)
**给后端的建议**:(1)在这条失败路径上补充日志,区分"token确实无效"和"内部依赖(Redis/DB)报错"两种情况,不要两者都报同一个401;(2)审视`TenantAdapterFor`懒加载连接的并发安全性,避免同一租户的并发请求各自重复建连接。
---
<a id="新增81"></a>
## 新增 81(P1,2026-09-14新发现):文件服务查询数据库里不存在的列,几乎每次命中都失败
**证据强度**:✅ 实测确认(真实运行日志,25分钟窗口内3个pod合计243次)
**现象**:`media serve`(文件服务,`/v0/file/s/{id}`)报错`Error 1054 (42S22): Unknown column 'mtg.recorder_user_id' in 'where clause'`,25分钟的观测窗口里3个pod合计出现243次,是这次复核发现的所有问题里频率最高的一个——几乎所有命中这条SQL路径的真实请求都会失败。
**推测**:列名前缀`mtg`大概率是"会议(meeting)"相关表,可能是代码引用了一个数据库里还不存在的列(缺一次迁移),也可能是列名本身写错了。不确定具体原因,需要后端自己确认。
**接口**:`GET /v0/file/s/{id}`(原生Tinode大文件下载,命中会议关联文件时)
**基础设施侧的态度**:这类"代码查询数据库结构对不上"的问题,本身可能是缺一次迁移,但我们不建议自己去手动改数据库列(不清楚正确的类型/默认值/是否有对应的后端代码逻辑依赖这个列的具体语义),必须由后端确认。
---
<a id="问题25"></a>
## 问题 25(P2,最常见路径已修故降级):带附言转发/收藏转发时,`head` 声明的类型与 `content` 的实际结构对不上
**证据强度**:✅ 实测确认(四条路径全部真实构造验证)
**现象**:逐条转发带附言、收藏转发带附言两条路径,`head`仍声明原消息类型(如image/text),但`content`换成`{forwarded,comment,snapshot}`等包装对象,客户端按head类型解析content会得到空白/无效内容。逐条转发不带附言这条最常见路径已经修好(content原样透传)。
**期望**:附言作为独立的一条文本消息发送(对接文档本来就是这么描述的),转发的那条消息仍然原样透传原content。
**接口**:`POST /v1/im/messages/forward`;`POST /v1/im/favorites/forward`
---
<a id="问题27"></a>
## 问题 27(P2):`status` 参数不做取值校验,传任何值都返回200+空列表
**证据强度**:✅ 实测确认(2026-09-14重测,传`status=totally_bogus_value_xyz`依然200+`items:[]`,未变)
**现象**:传完全不存在的status值也返回200,客户端无法区分"确实没数据"和"根本不支持"。
**接口**:`GET /v1/im/reports/daily?status=`(月报同构)
---
<a id="问题33"></a>
## 问题 33(P2):`permissions` 的四个字段在不同参数下恒为同一组值
**证据强度**:✅ 代码确认(2026-09-14重读源码,确认仍是硬编码map字面量,行号已随文件改动漂移到2614-2619行左右)
**现象**:`can_create`/`can_remind_unsubmitted`/`can_view_managed`/`makeup_allowed_days`换任何用户、scope、日期区间组合都恒为同一组值,因为响应体里这部分是Go代码写死的map字面量,完全不引用`principal`/`reports`/任何请求参数。
**接口**:`GET /v1/im/reports/daily`
---
<a id="问题35"></a>
## 问题 35(P2,`GET /friends`一侧已修复,范围收窄到`GET /users/{id}/profile`):设了备注之后,好友主页详情接口拿不到对方的原昵称
**证据强度**:✅ 实测确认(2026-09-14再次复核,问题依然存在)
**现象**:`GET /v1/im/friends`响应里带的嵌套`peer`对象(`peer.display_name`)能正确带对方的真实原始昵称,这一侧的诉求已经满足。但`GET /v1/im/users/{id}/profile`依然没有修:既没有remark字段,也没有独立的原昵称字段——保存备注后本地乐观更新正确显示新备注,但一旦触发这个详情接口的背景刷新重新拉取,会因为接口不带remark字段而把显示"冲"回"未设置",数据没有真的丢,只是这个详情页的显示会"看起来复原了"。
**接口**:`GET /v1/im/users/{id}/profile`(仍缺);对照已修复的`GET /v1/im/friends`
**期望**:`GET /users/{id}/profile`比照`GET /friends`已经落地的`peer`对象模式,补上对方的真实原始昵称字段。
---
<a id="新增3"></a>
## 新增 3(P2):单聊的"置顶聊天"没有任何办法让会话列表知道,因为没有批量查询会话设置的接口
**证据强度**:✅ 实测确认(对照群聊置顶排除法)
**现象**:群聊置顶写入订阅原生private字段能随meta同步下发;单聊置顶写入独立表且不写private字段,只有单条查询接口无批量接口,前端无法为几十上百个会话逐一查询,设置生效但用户看不出效果。
**接口**:`PATCH /v1/im/conversations/{topic}/settings`(写入);缺失批量查询接口
---
<a id="新增8"></a>
## 新增 8(P2):`GET /friends` 完全不感知拉黑关系,与另外两个接口表现不一致
**证据强度**:✅ 实测确认+代码确认
**现象**:拉黑后该好友仍显示`relationship:"friend"`且权限位全开,与`GET /conversations/{topic}/context`(正确显示`blocked_by_me`)、`GET /users/{id}/profile`(裸403)三者互不一致。
**根因**:`friendListItemPayload`及数据源`types.FriendListItem`从结构体到SQL全程未涉及黑名单表。
**接口**:`GET /v1/im/friends`;对照`GET /v1/im/users/{id}/profile`、`GET /v1/im/conversations/{topic}/context`
---
<a id="新增15"></a>
## 新增 15(P2):`POST /meetings/{id}/cancel` 完全不校验 `version`
**证据强度**:✅ 实测确认+代码确认(2026-09-14重读源码,`serveIMMeetingCancel`依然只解析`Version int`但从未在调用`store.Meetings.Cancel`时传入这个字段)
**现象**:故意传`version:999`也直接成功,与同资源my-response接口(版本校验正常409)形成对照——乐观锁形同虚设。
**接口**:`POST /v1/im/meetings/{id}/cancel`
---
<a id="新增24"></a>
## 新增 24(P2):群成员数存在两套互相独立、可永久失配的计数器
**证据强度**:✅ 实测确认(3个真实群交叉对照)+代码确认(2026-09-14重读源码确认未变)
**现象**:同一群同一时刻,`GET /groups/{topic}`与`/settings`显示的`member_count`,跟真实成员列表、qrcodes的`group_snapshot`可能不一致。
**根因**:业务层`im_group_settings.member_count`(只看`im_group_members`)与Tinode原生层`im_topics.subcnt`(只看`im_subscriptions`)是两套独立计数器,`mergeGroupSettingsPayload`(`server/im_http.go:8012`)无条件用业务层值覆盖了本来准确的subcnt值。
**接口**:`GET /v1/im/groups/{topic}`;`/settings`;`POST /groups/{topic}/qrcodes`;`GET /groups/{topic}/members`
---
<a id="新增25"></a>
## 新增 25(P2,产品需求缺口):link类型消息——协议层面可构造,但没有任何真实用户可从webapp UI触达的路径
**证据强度**:✅ 实测确认(正反两方向均构造真实数据验证)
**现象**:WS`{pub}`可直接构造出`content:{url:...}`扁平结构被判定为link类型(可收藏可搜索),但`im_webapp`所有正常发送路径产出的content都不是这个形状,"我的收藏-链接"等tab对真实用户永远是空的。
**接口**:WS `{pub}`(消息发送),无特定REST路径
---
<a id="新增30"></a>
## 新增 30(P2):青少年模式开启后不对任何功能做实际限制,`enforced` 恒为硬编码false
**证据强度**:✅ 实测确认(2026-09-14重新实测,未变)+代码确认
**现象**:restriction_items三项(深夜免打扰/内容过滤/支付守护)的`enforced`恒为硬编码false,全仓库穷举确认没有任何消息发送/好友添加/群聊/支付相关接口会查询用户的青少年模式状态。
**接口**:`GET /v1/im/settings/youth-mode`
**⚠️ 注**:`im_webapp`已明确移除青少年模式这个前端功能(用户产品决策,非技术限制),这条对当前`im_webapp`已无实际影响,仅为完整性保留在清单里。
---
<a id="新增31"></a>
## 新增 31(P2):`POST /reports/abuse` 不校验 `requires_description`,可以对必填原因提交空描述
**证据强度**:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
**现象**:某原因码(如illegal/other)的`requires_description`为true,但提交该reason_code、description为空依然200成功——代码里只校验了`description`长度上限(500),从未检查对应`reason_code`是否要求必填。
**接口**:`POST /v1/im/reports/abuse`
---
<a id="新增34"></a>
## 新增 34(P2):企业注册申请接口对同一个统一社会信用代码没有任何去重/防重复提交机制
**证据强度**:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
**现象**:同一信用代码用三个不同幂等键连续提交三次,全部独立成功建单,与需求文档"应防止重复提交"矛盾。代码上`serveIMTenantApplications`直接调用`CreateTenantApplication`,没有先查询该信用代码是否已有pending申请。
**接口**:`POST /v1/im/tenant-applications`
---
<a id="新增41"></a>
## 新增 41(P2):群禁言接口没有任何角色层级保护——管理员可以禁言群主、任何角色都能自我禁言
**证据强度**:✅ 实测确认(代码逐行核对根因,2026-09-14重读源码确认未变)
**现象**:GET成员列表明确标注群主`eligible:false`/`ineligible_reason:"owner"`,但`POST /groups/{topic}/mutes`对群主本人自我禁言、以及管理员禁言群主均返回200成功——代码只检查了操作者本人是否是管理员,从未检查目标成员的角色。
**接口**:`POST /groups/{topic}/mutes`;对照`GET /groups/{topic}/members?mute_eligible=true`
---
<a id="新增47"></a>
## 新增 47(P2):真实文件上传大小上限是32MB,不是仓库配置声明的100MB,且这个真实上限从未被前端用来做任何提前校验
**证据强度**:✅ 实测确认(2026-09-14重测`GET /bootstrap`,`max_file_size`依然是33554432即32MB,与之前记录一致)
**现象**:`GET /v1/im/bootstrap`声明的`data.limits.max_file_size`是33554432(32MB),但代码仓库`tinode.conf`/`meetingim.conf`里配置的是104857600(100MB)——声明值和真实生效值不一致,且这个字段从未被前端读取用于上传前的预校验。
**接口**:`POST /v0/file/u/`(Tinode原生大文件上传);`GET /v1/im/bootstrap`(`data.limits.max_file_size`)
**⚠️ 说明**:此前记录曾提到仓库配置文件已经改成一致的104857600,但线上实测一直是32MB——这次复核确认线上依然是32MB,怀疑部署环境的实际配置和仓库里能看到的配置文件不是同一份,或者中间有额外的网关/环境变量覆盖,建议后端确认线上部署实际生效的配置来源。
---
<a id="新增49"></a>
## 新增 49(P2):日报/月报创建接口对日期只校验格式不校验范围,年份0000、2099均可创建并成功提交
**证据强度**:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
**现象**:`report_date:"2099-12-31"`、`"0000-01-01"`均能创建、保存草稿、提交成功走完整流程。`parseIMDate`只用`time.ParseInLocation("2006-01-02",...)`校验格式,没有任何年份范围校验。
**接口**:`POST /v1/im/reports/daily`(report_date)、`POST /v1/im/reports/monthly`(month)及对应的`/submit`接口
---
<a id="新增51"></a>
## 新增 51(P2):会议创建接口不校验 `attendee_user_ids` 对应的用户是否真实存在,可创建任意规模的"幽灵参会人"
**证据强度**:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
**现象**:用50个语法合法但真实不存在的user_id+1个真实好友,一次性创建52人会议成功。`parseIMUserIDList`只做格式校验(`types.ParseUserId`)和去重,从未查询数据库确认这些用户真实存在。
**接口**:`POST /v1/im/meetings`
---
<a id="新增54"></a>
## 新增 54(P2):日报/月报SUBMIT接口不校验必填内容,可提交完全空白的报告,与响应自身声明的 `validation.*_required:true` 矛盾
**证据强度**:✅ 实测确认(现象)+代码确认(2026-09-14找到确切根因:`TenantDailyReportSubmit`的UPDATE语句只匹配`report_id`/`author_id`/`version`,完全不检查`work_summary`/`next_plan`等内容字段)
**现象**:`work_summary`/`next_plan`均为空字符串的月报草稿直接调用submit,返回200且`status:"submitted"`,同时响应体自己声明这两个字段`validation.*_required:true`。
**接口**:`POST /v1/im/reports/daily/{id}/submit`、`POST /v1/im/reports/monthly/{id}/submit`
---
<a id="新增56"></a>
## 新增 56(P2):青少年模式启用接口 `policy_version` 不做版本校验,传任意正整数即被无条件接受并永久覆盖
**证据强度**:✅ 实测确认+代码确认(2026-09-14重读源码确认未变)
**现象**:代码只检查`body.PolicyVersion <= 0`(必须是正数),从未跟服务端已知的当前真实policy_version做比对,传任意大的正整数都会被接受并覆盖存储值。
**接口**:`POST /v1/im/settings/youth-mode/enable`
**⚠️ 注**:同新增30,`im_webapp`已移除此功能,仅为完整性保留记录。
---
<a id="新增76"></a>
## 新增 76(P2):`GET /app/releases/latest` 的 `platform` 参数只接受 `ios`/`android`,web端完全不支持检查更新
**证据强度**:✅ 代码确认(2026-09-14重读当前HEAD确认未变)
**现象**:`GET /v1/im/app/releases/latest?platform=web`恒定`400 MALFORMED_REQUEST {"details":{"field":"platform"}}`。
**接口**:`GET /v1/im/app/releases/latest`
**前端现状**:`AboutPage.tsx`捕获这个错误后显示"暂不支持检查更新",不会一直转圈或显示假的"已是最新版本"。
---
<a id="新增77"></a>
## 新增 77(P2):群公告接口响应里没有已读人数字段,设计要求的"已读N/M"功能无法实现
**证据强度**:✅ 实测确认+代码确认(2026-09-14重读`groupAnnouncementPayload`/`groupAnnouncementPayloadWithPermissions`确认未变)
**现象**:`GET /groups/{topic}/announcements/latest`真实响应确认没有任何"已读数"/"已读人数"相关字段,设计稿明确要求的"已读18/28"这类展示前端拿不到数据来源。
**接口**:`GET /v1/im/groups/{topic}/announcements/latest`
**前端现状**:不展示已读人数(没有编造假数据)。
---
<a id="新增78"></a>
## 新增 78(P2):消息搜索/收藏/通话记录里,涉及用户/话题id的字段有不一致的裁剪或哈希化处理
**证据强度**:✅ 实测确认(三处独立复现)
**现象**:
- `GET /topics/{topic}/messages/search`的`items[].from`、收藏`type:"text"`的`snapshot.from`:真实user_id被裁掉`usr`前缀(如`usrH5dvZ3ckWxo`变成`H5dvZ3ckWxo`),直接拿这个值和当前用户完整id比较会永远为false。
- 收藏的`source.topic`(P2P来源)、通话记录`GET /calls`的`topic`字段:用的是服务端内部`p2p...`哈希形式,不是SDK寻址用的`usr...`形式,客户端没有办法把这个哈希反解回对方user_id。
**接口**:`GET /v1/im/topics/{topic}/messages/search`;收藏接口的`snapshot.from`/`source.topic`;`GET /v1/im/calls`的`topic`
**前端现状**:都补了对应的兜底逻辑(`normalizeSender()`补回`usr`前缀;`p2p`前缀的topic降级显示"单聊消息"通用文案),不会崩溃,只是丢失了直接寻址能力。
---
<a id="新增82"></a>
## 新增 82(P2,2026-09-14新发现):群设置缓存反序列化失败,高频出现,覆盖大量不同群
**证据强度**:✅ 实测确认(真实运行日志,25分钟窗口内3个pod合计242次,覆盖十几个不同真实群)
**现象**:`cache group settings[{topic}]: read failed: Uid.UnmarshalJSON: invalid length`,几乎是常态出现,不是个别数据问题——说明这条缓存读取逻辑本身有系统性bug,大概率是并发访问时缓存里的数据被写坏/读到了不完整的中间状态。其中至少一次复现(`grpDclMebv3MuI`)是被我们自己的并发请求触发的,其余多次是某个后台任务在循环访问一批固定的群时触发的。
**接口**:内部缓存逻辑,不直接对应某个REST接口,但会影响所有需要读取群设置的接口的正确性
---
<a id="新增83"></a>
## 新增 83(P2,2026-09-14新发现):`media serve` 死锁没有捕获重试
**证据强度**:✅ 实测确认(真实运行日志,25分钟窗口内3个pod合计82次)
**现象**:`Error 1213 (40001): Deadlock found when trying to get lock; try restarting transaction`——MySQL自己在错误信息里就写明了"try restarting transaction",这是标准的死锁处理方式,但代码里没有捕获这个特定错误码后自动重试,命中死锁的请求直接失败。
**接口**:`GET /v0/file/s/{id}`(文件服务)
---
<a id="新增84"></a>
## 新增 84(P2,2026-09-14新发现):内部周期性后台任务"media gc"约每50秒触发一次MySQL驱动层连接损坏
**证据强度**:✅ 实测确认(真实运行日志,从09:42到10:03连续观察,间隔稳定在46-67秒之间,与是否有外部请求流量无关)
**现象**:`[mysql] connection.go:215 busy buffer`紧接着`packets.go:68 [warn] unexpected sequence nr: expected 0, got 5`,然后`media gc: N driver: bad connection`——这是MySQL驱动协议层面的连接状态错乱,是同一个进程内部对同一条MySQL连接被并发误用导致的典型症状,不是网络问题。
**与PXC迁移的关系**:这个问题的出现时间与本次复核发现的数据库迁移(旧VM单机MySQL → 新k3s集群PXC)时间点吻合,怀疑与迁移相关,但没有直接证据(比如没有看到这个后台任务代码里调用了新增的PXC兼容分支),仅供后端排查参考。
**接口**:内部后台任务,不直接对应外部接口,但可能影响同一时间段内其它数据库操作的稳定性
---
<a id="新增85"></a>
## 新增 85(P2,历史遗留,曾用编号"新增7"记录在`docs/IM-后端问题清单-2026-09-04.md`但未被带入本清单,本次复核重新收录):按查询参数区分的缓存,写操作后特定参数组合的结果有明显滞后
**证据强度**:✅ 实测确认(提升管理员后90秒以上仍查不到);⚠️2026-09-14本次复核未重新实测(判断优先级较低,仅确认历史记录未遗漏)
**现象**:`POST /groups/{topic}/admins/batch`提升成功(version从1到2),但`GET /groups/{topic}/members?role=admin`反复查询超过90秒仍然是`items:[]`——用直连接口的方式排除了前端自己的客户端缓存,是服务端按`role=admin`这个具体参数组合单独缓存了旧的空结果,写操作没有使它失效。
**接口**:`GET /v1/im/groups/{topic}/members?role=admin`(及其它类似的按查询参数缓存的接口)
---
<a id="新增86"></a>
## 新增 86(P2):`GET /calls` 响应字段结构与协议文档完全不同
**证据强度**:✅ 实测确认(真实响应逐字段核对)
**现象**:协议文档描述的字段形状和真实响应完全对不上——没有`peer`对象,只有扁平的`peer_user_id`(真的是`usr...`形式);没有`incoming`布尔值,而是`direction:"incoming"|"outgoing"`字符串;时间戳拆成了`created_at/started_at/finished_at/updated_at`四个字段,没有单一的`ts`;`topic`字段是`p2p...`哈希形式(同新增78的毛病)。
**接口**:`GET /v1/im/calls`
**前端现状**:已按实测字段改正类型声明和渲染逻辑。
---
<a id="新增87"></a>
## 新增 87(P2):群二维码铸造/解析两个接口对同一份群数据用了不同的字段名,解析接口还多了一个文档没提到的字段
**证据强度**:✅ 实测确认(真实铸造+解析双向验证)
**现象**:`POST /groups/{topic}/qrcodes`(铸造)响应把群信息放在`group_snapshot`字段下;`POST /qrcodes/resolve`(解析,`purpose:"join_group"`)响应把同样性质的数据放在`group`字段下——同一份数据两个接口用了不同字段名。另外`resolve`响应还带了一个协议文档完全没提过的`status`字段,扫描"自己已经在的群"时返回`status:"joined"`;"真的还不是成员"这个场景下`status`具体是什么值没有实测到(当时没有第二个测试账号)。
**接口**:`POST /v1/im/groups/{topic}/qrcodes`;`POST /v1/im/qrcodes/resolve`
**前端现状**:做了双重兜底读取(同时尝试`group_snapshot`和`group`两个字段名);`status`按"不是joined就走提交申请"的宽松逻辑兜底处理。
---
<a id="新增88"></a>
## 新增 88(P2,未定论,需要后续继续观察):部分明显应该命中的关键词搜索返回空结果
**证据强度**:⚠️实测确认现象,但成因未定论
**现象**:在真实群里搜索"验证"/"链接"等明显应该命中历史消息的关键词,一度返回`items:[]`,同一时间搜索"测试"能正常命中。2026-09-14复测:搜索"验证"这次能正确命中真实消息了,看起来比之前有改善;但"链接"依然返回空——不过这次没能确认这个群里是否真的存在包含"链接"二字的消息,不能算作确凿的回归证据。
**接口**:`GET /v1/im/topics/{topic}/messages/search`
**建议**:这条目前证据不够充分下定论(可能是搜索索引/召回策略的问题,也可能是测试数据本身的问题),建议后续有更多测试数据时再复核一次。
---
<a id="新增89"></a>
## 新增 89(P1,2026-09-24):文件审核任务无 Worker 消费,带图养成动态永久无法发布
**证据强度**:✅ 测试环境真实日志 + 数据库 + 代码确认
**现象**:用户发布带图动态后,动态保持 `status=reviewing,audit_status=pending`,`scope=friends`列表只查询 `published` 状态,因此列表返回空。实例动态 `1577072518975639552` 的两条图片审核任务(`aliyun_green` 和 `aliyun_sas`)均为 `pending`。
**根因**:文件上传时会无条件向 `im_file_review_tasks` 插入内容审核和恶意文件检测任务,但 IM 当前仅有阿里云提交/轮询客户端,没有实现读取、执行和回写这些任务的 Worker。测试库 `meeting_im.im_file_review_tasks` 共有 174 条任务,全部为 `pending`,`attempts=0`。
**当前临时策略**(2026-09-24 确认):图片审核暂时关闭并默认通过。`cultivate_server` 仍调用 IM 校验文件存在性、归属和上传完成状态,但不再因文件审核的 `pending/risky` 状态阻塞动态发布。文字内容审核保持原有逻辑。
**待正式解决**:
1. 在 IM 中实现文件审核 Worker,完成任务抢占、阿里云提交/轮询、重试、超时、失败降级和结果回写。
2. 区分内容审核与恶意文件检测是否真正启用,未启用的检测不得创建永久 `pending` 的任务。
3. Worker 稳定上线后恢复 `cultivate_server` 的图片审核门禁,并确定现有 174 条历史任务的补偿或废弃策略。
@@ -0,0 +1,239 @@
# IM 后端问题报告:好友申请通过后 `friend_added` 系统提示被投递到旧命名话题 `usr<uid>`,同一会话存在两套 Topic
> 报告日期:2026-09-13
> 客户端:iOS(MeetingIM Swift SDK)
> 涉及账号:`usrv97xq86jw3c`(echarles5,**通过人本人**,本报告取证端)、`usrHnkwn8VSfKc`(charles1,**申请人**)
> 涉及话题:`usrHnkwn8VSfKc`(旧标准命名)与 `p2pv97xq86jw3ceeTCfxVJ8pw`(后端内部命名)
> 优先级建议:**高**(会话列表消息与未读不更新,用户可见)
---
## 1. 问题概述
好友申请通过后,服务端把一条**会话内系统提示** `system_notice / friend_added`("你们已成为好友,现在可以开始聊天了。")下发到了
- ❌ `usrHnkwn8VSfKc`(MeetingIM 的 `usr<对端uid>` 旧标准命名,**本端并未订阅该话题**)
而不是
- ✅ `p2pv97xq86jw3ceeTCfxVJ8pw`(服务端自己在 `me` 的 `meta sub` 里下发、并在 `friend.request.accepted` 的 `payload.topic` 里给出的**真正会话**)。
同一时刻服务端还把**同一条逻辑消息**(同 `ts`、同 `seq=17`)写进了 `p2p...` 话题,形成**双写**。
结果:客户端本地同一好友出现**两条单聊会话**,导致用户可见的两个问题:
1. 会话行的 `usr<uid>` 那条没有服务端下发的 `acs` → `isJoiner == false` → 客户端在会话行右下角显示出 `nosign`(🚫)禁止图标;
2. 客户端会话列表去重逻辑保留 `usr<uid>`、隐藏 `p2p...` → **对方的后续消息与未读数都不更新**(未读还被从总数里扣掉,"消息"Tab 角标偏小)。
---
## 2. 时间线(取证端 = 通过人本人 B,2026-09-13)
| 时间(UTC) | 事件 |
| --- | --- |
| 06:13:39 | B(echarles5)删除好友 A(charles1),双方关系解除(A 端收到 `friend.deleted`) |
| 06:27:09.610 | A 申请、B 通过;服务端下发 `friend_added` 系统提示,**同时写入 `usrHnkwn8VSfKc`(seq 17)与 `p2pv97xq86jw3ceeTCfxVJ8pw`(seq 17)** |
| 06:27:09.594 | `GET /v1/im/friends` 返回 A 的会话 `topic = p2pv97xq86jw3ceeTCfxVJ8pw` |
| 06:27:10.367 | `POST /v1/im/conversations/p2p {"peer_user_id":"usrHnkwn8VSfKc"}` 返回 `topic = p2pv97xq86jw3ceeTCfxVJ8pw` |
| 06:27:11.070 | `me` 的 `meta sub` 只下发 `p2pv97xq86jw3ceeTCfxVJ8pw`(订阅 `acs=JRWPA`、`priv.peer=usrHnkwn8VSfKc`),**没有 `usrHnkwn8VSfKc`** |
| 06:27:32.089 | A 发送消息 `a`,落在 `p2pv97xq86jw3ceeTCfxVJ8pw` seq 18 |
---
## 3. 服务端下行报文(原文)
### 3.1 系统提示被实时投递到旧命名话题(本端未订阅)
```json
{
"data": {
"topic": "usrHnkwn8VSfKc",
"ts": "2026-09-13T06:27:09.61Z",
"seq": 17,
"head": {
"x-im-from": "system",
"x-im-schema": 1,
"x-im-template": "friend_added",
"x-im-type": "system_notice"
},
"content": {
"actors": [
{ "user_id": "usrv97xq86jw3c", "click": { "target_id": "usrv97xq86jw3c", "type": "user_profile" } },
{ "user_id": "usrHnkwn8VSfKc", "click": { "target_id": "usrHnkwn8VSfKc", "type": "user_profile" } }
],
"extra": { "request_id": "frq_OYo72xKkkgU" },
"level": "normal",
"template": "friend_added",
"text": "你们已成为好友,现在可以开始聊天了。"
}
}
}
```
客户端侧随即落库并回执(说明客户端把它当作**合法会话消息**处理):
```text
SqlStore - msgReceived:来自话题 payload usrHnkwn8VSfKc,topicId=9,userId=3,seq=17
出站[回执(note)] {"note":{"what":"recv","topic":"usrHnkwn8VSfKc","seq":17}}
入站[在线状态(pres)] {"pres":{"topic":"me","src":"usrHnkwn8VSfKc","what":"msg","seq":17}}
```
### 3.2 同一条消息也被写进真会话(双写证据)
随后客户端订阅真会话拉历史,拿到**同 `ts`、同 `seq`** 的同一条提示:
```json
{
"data": {
"topic": "p2pv97xq86jw3ceeTCfxVJ8pw",
"ts": "2026-09-13T06:27:09.61Z",
"seq": 17,
"head": { "x-im-from": "system", "x-im-schema": 1, "x-im-template": "friend_added", "x-im-type": "system_notice" },
"content": { "template": "friend_added", "text": "你们已成为好友,现在可以开始聊天了。", "...": "与上一条 content 完全一致" }
}
}
```
```text
SqlStore - msgReceived:来自话题 payload p2pv97xq86jw3ceeTCfxVJ8pw,topicId=11,userId=3,seq=17
```
> 同一 `ts` + 同一 `seq=17` 出现在两个 topic ⇒ 服务端对该提示**写了两份**。
> 本地 `topicId`:`usrHnkwn8VSfKc = 9`、`p2pv97xq86jw3ceeTCfxVJ8pw = 11` —— 旧命名话题**更早存在**,本轮关系重建后并未被撤销或合并。
### 3.3 服务端自己只承认 `p2p...`(`me` 的 `meta sub`)
```json
{
"meta": {
"id": "115975",
"topic": "me",
"ts": "2026-09-13T06:27:32.444Z",
"sub": [
{
"updated": "2026-09-13T06:27:11.07Z",
"online": true,
"acs": { "want": "JRWPA", "given": "JRWPA", "mode": "JRWPA" },
"public": {
"avatar_file_id": "ZrI4Fut4heE.jpg",
"display_name": "charles1",
"fn": "charles1",
"name": "charles1",
"nickname": "charles1",
"public_account": "usrHnkwn8VSfKc"
},
"private": { "created_by": "http", "peer": "usrHnkwn8VSfKc" },
"topic": "p2pv97xq86jw3ceeTCfxVJ8pw",
"touched": "2026-09-13T06:27:32.089Z",
"seq": 18
}
]
}
}
```
> `sub` 数组里**只有** `p2pv97xq86jw3ceeTCfxVJ8pw`。服务端却把 `{data}` 投到了 `usrHnkwn8VSfKc` —— 这是一个**本端从未订阅**的话题。
### 3.4 业务事件与 HTTP 接口都指向 `p2p...`
```json
{"info":{"topic":"usrv97xq86jw3c","from":"usrHnkwn8VSfKc","what":"im","event":"friend.request.accepted",
"payload":{"topic":"p2pv97xq86jw3ceeTCfxVJ8pw","user_id":"usrHnkwn8VSfKc","request_id":"frq_OYo72xKkkgU"}}}
```
```json
{"user_id":"usrHnkwn8VSfKc","display_name":"charles1","relationship":"friend",
"topic":"p2pv97xq86jw3ceeTCfxVJ8pw","avatar_file_id":"ZrI4Fut4heE.jpg"}
```
> `friend.request.accepted` 的 `payload.topic`、`GET /v1/im/friends` 的 `topic`、`POST /v1/im/conversations/p2p` 的响应都明确是 `p2pv97xq86jw3ceeTCfxVJ8pw`。
> 说明服务端**知道**该会话的真实 topic,但系统提示仍写到了 `usr<uid>`。
---
## 4. 与接口文档契约的冲突
| 文档位置 | 约定 | 实际 |
| --- | --- | --- |
| 《IM-前后端对接文档》0.5.4.3.1(约 330 行) | "会话内系统消息**必须进入对应 P2P 或群 Topic 的消息流**,占用 `seq`" | `friend_added` 同时进入 `usr<uid>`(非对应会话) |
| 同章 `system_notice` 下行示例(约 337 行) | `{"data":{"topic":"p2p...", ... "x-im-template":"friend_added" ...}}` | 实际 `"topic":"usrHnkwn8VSfKc"` |
| 同文档 3.5(约 1108 行) | 好友申请通过后服务端"在同一事务内建立双方关系和 **P2P 订阅**" | 建立了 `p2p...` 订阅,但旧 `usr<uid>` topic/订阅仍在,且系统提示写到旧 topic |
| 同文档 `GET /v1/im/friends`(约 1564 行) | 好友的会话 `topic` 为 `p2p...` | 一致 ✅(正因如此客户端更无法理解 `usr<uid>` 上的消息) |
---
## 5. 对客户端的影响(为什么客户端只能兜、不能治)
1. **会话列表出现两条同一好友的会话**,去重只能靠启发式规则(按对端 uid 分组打分)择一保留;
2. **保留错的那条就丢消息/未读**:旧命名话题 `acs` 为空(`isJoiner == false`)但本地已落库消息,客户端历史上"标准命名优先"的规则会保留它、隐藏真会话 → 用户看到会话不更新;
3. **误显示禁止图标**:`acs` 为空时客户端无法区分"未同步"与"已不是成员",会在会话行右下角画 `nosign`(🚫);
4. **历史数据永久分裂**:已有一批用户的本地历史落在 `usr<uid>`,服务端新会话在 `p2p...`,客户端无法自动合并;
5. 客户端对**未订阅**话题收到的 `{data}` 只能被动落库(已回执 `recv`),无法判断该不该收。
---
## 6. 需要后端确认 / 处理
1. **`friend_added` 等会话内系统提示为什么写到 `usr<uid>`?**
请改为只写**会话 topic**(当前为 `p2p...`),与 `payload.topic` / `GET /friends` 的 `topic` 保持一致。
2. **同一对好友为什么存在两套 Topic + 两份订阅?**
重装/删好友/重新加好友后,旧的 `usr<uid>` topic 与订阅应当被撤销或合并;
当前 `usrHnkwn8VSfKc`(本端 uid 维度)与 `p2pv97xq86jw3ceeTCfxVJ8pw` 同时存在,且系统提示双写。
3. **`{data}` 投递到未在 `me` 的 `meta sub` 里的话题是否预期?**
若不是,请修投递;若是(设计如此),请在文档里明确 `usr<uid>` 仍是合法会话名,并同步在 `meta sub` 里下发它的 `acs`/`seq`/`pub`,否则客户端无法正确渲染。
4. **是否可以把单聊统一收敛到 `p2p...`**,并提供旧 `usr<uid>` 历史消息的迁移/合并方案(客户端愿意配合按话题重定向)。
5. **(附带,需确认口径)** `friend.request.accepted` 的 `payload.user_id` 与文档不一致:
文档写 `payload:{"user_id":"usr_actor"}`(通过人),实测该帧在**通过人本人**设备上 `from` 和 `payload.user_id` 都是**申请人** `usrHnkwn8VSfKc`。客户端目前已按"`payload.user_id` = 对方"消费(用于刷新会话 context、反查会话),若后端要改回文档口径请提前同步。
---
## 7. 复现步骤
### 前置
- 账号 A:`usrHnkwn8VSfKc`(charles1),账号 B:`usrv97xq86jw3c`(echarles5),两台设备/两个模拟器分别登录;
- 两端都开启 Debug 日志(会打印 `入站[消息数据(data)]` 与 `SqlStore - msgReceived`)。
### 步骤
1. **先制造"旧命名话题"残留**:B 删除 A 好友(B 端:聊天 → A 的会话 → 右上角设置 → 「删除好友」,确认删除)。
- 预期:A 端收到 `{"info":{"...","event":"friend.deleted","payload":{"user_id":"usrv97xq86jw3c"}}}`。
- 此时 B 端本地已存在旧命名话题 `usr<A的uid>`(本次取证中为 `usrHnkwn8VSfKc`)。
2. A 向 B 发起好友申请(A 端:添加好友 → 搜索 `echarles5` → 发送申请)。
3. B 通过好友申请(B 端:通讯录 → 新的朋友 → 通过)。
4. **观察 B 端日志**,应能复现:
```text
入站[消息数据(data)] {"data":{"topic":"usrHnkwn8VSfKc","seq":17,"head":{"x-im-template":"friend_added","x-im-type":"system_notice"},...}}
SqlStore - msgReceived:来自话题 payload usrHnkwn8VSfKc,topicId=9,userId=3,seq=17
入站[在线状态(pres)] {"pres":{"topic":"me","src":"usrHnkwn8VSfKc","what":"msg","seq":17}}
```
随后拉真会话历史时,同 `ts`/`seq` 的提示又出现在 `p2pv97xq86jw3ceeTCfxVJ8pw`;
并且 `me` 的 `{meta sub}` 里**只有** `p2pv97xq86jw3ceeTCfxVJ8pw`。
5. **观察 B 端界面**:会话列表里 A 的那一行副标题是"你们已成为好友,现在可以开始聊天了。",右下角带 🚫 图标。
6. A 再发一条消息(如 `a`)→ B 端该会话行**副标题与未读数都不更新**(消息实际落在 `p2pv97xq86jw3ceeTCfxVJ8pw`,被去重隐藏)。
7. **服务端核对**:`topics` 表应能查到同一对好友的两行
(`usrHnkwn8VSfKc` 与 `p2pv97xq86jw3ceeTCfxVJ8pw`),以及双方的订阅记录;`messages` 表里 `friend_added` 在两个话题下各有一条(同 `ts`、同 `seq`)。
### 最小验证(不依赖"先删好友"这段历史)
让 A 申请、B 通过,直接核对服务端把 `friend_added` 写到了哪个 topic:若仍是 `usr<A的uid>`,即复现本问题(第 1 步只是让残留话题必然存在,便于稳定复现)。
---
## 8. 验收标准
1. 好友申请通过后,`friend_added` **只**下发到会话 topic(当前为 `p2p...`),且该 topic 出现在 `me` 的 `meta sub` 中;`usr<uid>` 上不再出现任何消息;
2. `me` 的 `meta sub` 中同一对好友**只有一个** P2P topic、一份订阅;历史遗留的 `usr<uid>` topic/订阅被撤销或合并;
3. 对不存在的会话不再有"投递到未订阅话题"的 `{data}`(或按文档补齐该话题的 `meta sub`);
4. 客户端表现(不依赖本轮客户端兜底):B 端会话列表只出现**一行** A 的会话,副标题为最新消息、未读角标正确、**无 🚫 图标**;A 端同理。
---
## 9. 客户端已做的兜底(不依赖后端修复,但不能替代后端修)
1. `Utils.duplicateP2PTopicNames` 改为**按对端 uid 分组 + 打分择一保留**
(本地有消息 8 > 仍是成员 `isJoiner` 4 > 标准命名 2 > 有 `priv.peer` 1),
让"有消息且在关系内"的 `p2p...` 真会话优先,同时保留"有历史优先"以免去重丢消息;
2. `ChatListViewCell` 的禁止图标改为**仅在确认服务端下发过 `acs.mode` 且确非成员**时显示
(`acs` 未同步不再误画 🚫);
3. 会话列表过滤对"本地已有消息但 `acs` 未下发"的单聊放宽(`isLocalKnownP2PConversation`),
保证"有消息就一定有会话行"。
> 以上兜底只解决"显示哪一条、图标对不对"。**系统提示被写进隐藏话题、历史分裂在两个 topic** 这两点必须后端收敛后才能根治。
+524
View File
@@ -0,0 +1,524 @@
{\rtf1\ansi\ansicpg936\cocoartf2821
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\froman\fcharset0 Times-Roman;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0
\deftab720
\pard\pardeftab720\partightenfactor0
\f0\fs24 \cf0 \expnd0\expndtw0\kerning0
\outl0\strokewidth0 \strokec2 \uc0\u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u26381 \u21153 \u21327 \u35758 \
\uc0\u21069 \u35328 \
\
\uc0\u27426 \u36814 \u24744 \u20351 \u29992 \u12304 \u28023 \u21475 \u24066 \u26539 \u32676 \u32593 \u32476 \u31185 \u25216 \u26377 \u38480 \u20844 \u21496 \u12305 \u20844 \u21496 \u65288 \u19979 \u31216 \'93\u25105 \u20844 \u21496 \'94\u25110 \'93\u25105 \u20204 \'94\u65289 \u20026 \u24744 \u25552 \u20379 \u30340 \u31508 \u22696 \u35759 \u20351 \u30340 \u26381 \u21153 \u65281 \
\
\uc0\u29305 \u21035 \u25552 \u31034 \u65306 \
\
\uc0\u20026 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u65288 \u20197 \u19979 \u31616 \u31216 \'93\u26412 \u36719 \u20214 \'94\u65289 \u21450 \u26381 \u21153 \u65292 \u24744 \u24212 \u24403 \u38405 \u35835 \u24182 \u36981 \u23432 \u12298 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u26381 \u21153 \u21327 \u35758 \u12299 \u65288 \u20197 \u19979 \u31616 \u31216 \'93\u26412 \u21327 \u35758 \'94\u65289 \u12290 \u35831 \u24744 \u21153 \u24517 \u20180 \u32454 \u38405 \u35835 \u12289 \u20805 \u20998 \u29702 \u35299 \u26412 \u21327 \u35758 \u21508 \u26465 \u27454 \u20869 \u23481 \u65292 \u29305 \u21035 \u26159 \u35905 \u20813 \u25110 \u32773 \u38480 \u21046 \u25105 \u20844 \u21496 \u36131 \u20219 \u30340 \u26465 \u27454 \u12289 \u23545 \u29992 \u25143 \u26435 \u21033 \u36827 \u34892 \u38480 \u21046 \u30340 \u26465 \u27454 \u12289 \u32422 \u23450 \u31649 \u36758 \u19982 \u27861 \u24459 \u36866 \u29992 \u26465 \u27454 \u65292 \u20197 \u21450 \u24320 \u36890 \u25110 \u20351 \u29992 \u26576 \u39033 \u26381 \u21153 \u30340 \u21333 \u29420 \u21327 \u35758 \u12290 \u38480 \u21046 \u12289 \u20813 \u36131 \u26465 \u27454 \u25110 \u32773 \u20854 \u20182 \u28041 \u21450 \u24744 \u37325 \u22823 \u26435 \u30410 \u30340 \u26465 \u27454 \u21487 \u33021 \u20197 \u21152 \u31895 \u31561 \u24418 \u24335 \u25552 \u31034 \u24744 \u37325 \u28857 \u27880 \u24847 \u12290 \u22914 \u24744 \u23545 \u26412 \u21327 \u35758 \u26377 \u20219 \u20309 \u30097 \u38382 \u65292 \u21487 \u36890 \u36807 \u25991 \u26411 \u32852 \u31995 \u26041 \u24335 \u32852 \u31995 \u25105 \u20204 \u12290 \
\
\uc0\u38500 \u38750 \u24744 \u24050 \u38405 \u35835 \u24182 \u25509 \u21463 \u26412 \u21327 \u35758 \u25152 \u26377 \u26465 \u27454 \u65292 \u21542 \u21017 \u24744 \u26080 \u26435 \u27880 \u20876 \u25110 \u30331 \u24405 \u20351 \u29992 \u26412 \u36719 \u20214 \u21450 \u30456 \u20851 \u26381 \u21153 \u12290 \u24744 \u28857 \u20987 \'93\u21516 \u24847 \'94\u65292 \u25110 \u32773 \u24744 \u27880 \u20876 \u25110 \u30331 \u24405 \u26412 \u36719 \u20214 \u30340 \u34892 \u20026 \u65292 \u25110 \u32773 \u20854 \u20182 \u26126 \u31034 \u25110 \u32773 \u40664 \u31034 \u26041 \u24335 \u34920 \u31034 \u25509 \u21463 \u26412 \u21327 \u35758 \u30340 \u34892 \u20026 \u21363 \u35270 \u20026 \u24744 \u24050 \u38405 \u35835 \u24182 \u21516 \u24847 \u25509 \u21463 \u26412 \u21327 \u35758 \u30340 \u32422 \u26463 \u12290 \u26412 \u21327 \u35758 \u21363 \u22312 \u24744 \u19982 \u25105 \u20844 \u21496 \u20043 \u38388 \u20135 \u29983 \u27861 \u24459 \u25928 \u21147 \u65292 \u25104 \u20026 \u23545 \u21452 \u26041 \u22343 \u20855 \u26377 \u32422 \u26463 \u21147 \u30340 \u27861 \u24459 \u25991 \u20214 \u12290 \
\
\uc0\u22914 \u26524 \u24744 \u26410 \u28385 18\u21608 \u23681 \u65292 \u25110 \u23384 \u22312 \u20854 \u20182 \u19981 \u20855 \u22791 \u19982 \u29992 \u25143 \u34892 \u20026 \u30456 \u36866 \u24212 \u30340 \u27665 \u20107 \u34892 \u20026 \u33021 \u21147 \u30340 \u24773 \u24418 \u65292 \u35831 \u22312 \u27861 \u23450 \u30417 \u25252 \u20154 \u30340 \u38506 \u21516 \u12289 \u25351 \u23548 \u19979 \u38405 \u35835 \u26412 \u21327 \u35758 \u65292 \u24182 \u29305 \u21035 \u27880 \u24847 \u26410 \u25104 \u24180 \u20154 \u20351 \u29992 \u26465 \u27454 \u65292 \u22312 \u30830 \u20445 \u30417 \u25252 \u20154 \u21516 \u24847 \u26412 \u21327 \u35758 \u20869 \u23481 \u21518 \u20351 \u29992 \u26412 \u36719 \u20214 \u12290 \u24744 \u21450 \u24744 \u30340 \u30417 \u25252 \u20154 \u24212 \u20381 \u29031 \u27861 \u24459 \u27861 \u35268 \u35268 \u23450 \u65292 \u25215 \u25285 \u21516 \u24847 \u26412 \u21327 \u35758 \u21450 \u20351 \u29992 \u26412 \u36719 \u20214 \u32780 \u23548 \u33268 \u30340 \u30456 \u24212 \u21518 \u26524 \u12290 \
\uc0\u19968 \u12289 \u21327 \u35758 \u30340 \u33539 \u22260 \
\
1.1 \uc0\u21327 \u35758 \u36866 \u29992 \u20027 \u20307 \u33539 \u22260 \
\uc0\u26412 \u21327 \u35758 \u26159 \u24744 \u19982 \u25105 \u20204 \u20043 \u38388 \u20851 \u20110 \u24744 \u19979 \u36733 \u12289 \u23433 \u35013 \u12289 \u27880 \u20876 \u12289 \u30331 \u24405 \u12289 \u20351 \u29992 \u26412 \u36719 \u20214 \u20197 \u21450 \u20351 \u29992 \u25105 \u20204 \u25552 \u20379 \u30340 \u30456 \u20851 \u26381 \u21153 \u25152 \u35746 \u31435 \u30340 \u21327 \u35758 \u12290 \u21253 \u25324 \u26412 \u21327 \u35758 \u27491 \u25991 \u12289 \u12298 \u38544 \u31169 \u21327 \u35758 \u12299 \u21450 \u25105 \u20844 \u21496 \u24050 \u32463 \u21457 \u24067 \u30340 \u25110 \u23558 \u26469 \u21487 \u33021 \u21457 \u24067 /\u26356 \u26032 \u30340 \u24182 \u37319 \u21462 \u21512 \u29702 \u36884 \u24452 \u36890 \u30693 \u30340 \u21508 \u31867 \u35268 \u21017 \u12289 \u35268 \u33539 \u12289 \u36890 \u30693 \u31561 \u12290 \
\
1.2 \uc0\u21327 \u35758 \u20851 \u31995 \
\uc0\u26412 \u21327 \u35758 \u20869 \u23481 \u21516 \u26102 \u21253 \u25324 \u25105 \u20204 \u21487 \u33021 \u19981 \u26029 \u21457 \u24067 \u30340 \u20851 \u20110 \u26412 \u26381 \u21153 \u30340 \u30456 \u20851 \u21327 \u35758 \u12289 \u19994 \u21153 \u35268 \u21017 \u12289 \u36890 \u30693 \u31561 \u20869 \u23481 \u12290 \u19978 \u36848 \u20869 \u23481 \u19968 \u32463 \u27491 \u24335 \u21457 \u24067 \u65292 \u21363 \u20026 \u26412 \u21327 \u35758 \u19981 \u21487 \u20998 \u21106 \u30340 \u32452 \u25104 \u37096 \u20998 \u65292 \u24744 \u21516 \u26679 \u24212 \u24403 \u36981 \u23432 \u12290 \
\
1.3 \uc0\u30456 \u20851 \u23450 \u20041 \
\'93\uc0\u25105 \u20844 \u21496 \u25110 \u25105 \u20204 \'94\u65292 \u26159 \u25351 \u31508 \u22696 \u35759 \u20351 \u21450 \u30456 \u20851 \u26381 \u21153 \u21487 \u33021 \u23384 \u22312 \u30340 \u36816 \u33829 \u20851 \u32852 \u21333 \u20301 \u65292 \u21363 \u12304 \u28023 \u21475 \u24066 \u26539 \u32676 \u32593 \u32476 \u31185 \u25216 \u26377 \u38480 \u20844 \u21496 \u12305 \u20844 \u21496 \u21450 \u20854 \u20851 \u32852 \u20844 \u21496 \u12290 \
\'93\uc0\u29992 \u25143 \'94\u65292 \u26159 \u25351 \u20351 \u29992 \u26412 \u36719 \u20214 \u21450 \u30456 \u20851 \u26381 \u21153 \u30340 \u20351 \u29992 \u32773 \u65292 \u22312 \u26412 \u21327 \u35758 \u20013 \u26356 \u22810 \u22320 \u31216 \u20026 \'93\u24744 \'94\u12290 \
\'93\uc0\u26381 \u21153 \'94\u65292 \u26159 \u25351 \u25105 \u20204 \u21521 \u24744 \u25552 \u20379 \u30340 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u30456 \u20851 \u26381 \u21153 \u65292 \u20855 \u20307 \u20197 \u25105 \u20204 \u23454 \u38469 \u25552 \u20379 \u30340 \u20026 \u20934 \u12290 \u21516 \u26102 \u65292 \u25105 \u20204 \u26377 \u26435 \u23545 \u25552 \u20379 \u30340 \u36719 \u20214 \u26381 \u21153 \u36827 \u34892 \u22686 \u21152 \u12289 \u20851 \u20572 \u25110 \u20854 \u20182 \u35843 \u25972 \u12290 \
\'93\uc0\u36134 \u21495 \'94\u65292 \u25351 \u29992 \u25143 \u22312 \u20351 \u29992 \u26381 \u21153 \u26102 \u21487 \u33021 \u38656 \u35201 \u27880 \u20876 \u30340 \u36134 \u21495 \u12290 \u29992 \u25143 \u21487 \u22312 \u26412 \u36719 \u20214 \u27880 \u20876 \u24182 \u33719 \u24471 \u30340 \u36134 \u21495 \u65292 \u20316 \u20026 \u30331 \u24405 \u24182 \u20351 \u29992 \u26412 \u36719 \u20214 \u30340 \u20973 \u35777 \u12290 \
\uc0\u20108 \u12289 \u20851 \u20110 \u26412 \u26381 \u21153 \
\
2.1 \uc0\u26412 \u26381 \u21153 \u30340 \u20869 \u23481 \
\uc0\u26412 \u26381 \u21153 \u20869 \u23481 \u26159 \u25351 \u25105 \u20204 \u21521 \u29992 \u25143 \u25552 \u20379 \u30340 \u36328 \u24179 \u21488 \u30340 \u36890 \u35759 \u24037 \u20855 \u65288 \u20197 \u19979 \u31616 \u31216 \'93\u31508 \u22696 \u35759 \u20351 \'94\u65289 \u65292 \u25903 \u25345 \u21333 \u20154 \u12289 \u22810 \u20154 \u21442 \u19982 \u65292 \u22312 \u21457 \u36865 \u35821 \u38899 \u30701 \u20449 \u12289 \u35270 \u39057 \u12289 \u22270 \u29255 \u21644 \u25991 \u23383 \u31561 \u21363 \u26102 \u36890 \u35759 \u26381 \u21153 \u30340 \u22522 \u30784 \u19978 \u65292 \u21516 \u26102 \u20026 \u29992 \u25143 \u25552 \u20379 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u32593 \u32476 \u23186 \u20307 \u12289 \u20114 \u32852 \u32593 \u22686 \u20540 \u12289 \u20114 \u21160 \u23089 \u20048 \u12289 \u30005 \u23376 \u21830 \u21153 \u12289 \u30005 \u23376 \u38065 \u21253 \u12289 \u24320 \u25918 \u24179 \u21488 \u12289 \u19982 \u20854 \u20182 \u36719 \u20214 \u25110 \u30828 \u20214 \u20449 \u24687 \u20114 \u36890 \u31561 \u21151 \u33021 \u25110 \u20869 \u23481 \u30340 \u36719 \u20214 \u35768 \u21487 \u21450 \u26381 \u21153 \u65288 \u20197 \u19979 \u31616 \u31216 \'93\u26412 \u26381 \u21153 \'94\u65289 \u21644 \u24191 \u21578 \u31561 \u26381 \u21153 \u65288 \u20197 \u19979 \u31616 \u31216 \'93\u26412 \u26381 \u21153 \'94\u65289 \u12290 \
\
2.2 \uc0\u26412 \u26381 \u21153 \u30340 \u24418 \u24335 \
\
2.2.1 \uc0\u24744 \u20351 \u29992 \u26412 \u26381 \u21153 \u38656 \u35201 \u19979 \u36733 \u31508 \u22696 \u35759 \u20351 \u23458 \u25143 \u31471 \u36719 \u20214 \u65292 \u23545 \u20110 \u36825 \u20123 \u36719 \u20214 \u65292 \u25105 \u20204 \u32473 \u20104 \u24744 \u19968 \u39033 \u20010 \u20154 \u30340 \u12289 \u19981 \u21487 \u36716 \u35753 \u21450 \u38750 \u25490 \u20182 \u24615 \u30340 \u35768 \u21487 \u12290 \u24744 \u20165 \u21487 \u20026 \u35775 \u38382 \u25110 \u20351 \u29992 \u26412 \u26381 \u21153 \u30340 \u30446 \u30340 \u32780 \u20351 \u29992 \u36825 \u20123 \u36719 \u20214 \u21450 \u26381 \u21153 \u12290 \
\
2.2.2 \uc0\u26412 \u26381 \u21153 \u20013 \u31508 \u22696 \u35759 \u20351 \u23458 \u25143 \u31471 \u36719 \u20214 \u21487 \u33021 \u20250 \u25552 \u20379 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 iOS\u12289 Android\u12289 Windows\u12289 Mac\u31561 \u22810 \u20010 \u24212 \u29992 \u29256 \u26412 \u65292 \u29992 \u25143 \u24517 \u39035 \u36873 \u25321 \u19982 \u25152 \u23433 \u35013 \u32456 \u31471 \u35774 \u22791 \u30456 \u21305 \u37197 \u30340 \u36719 \u20214 \u29256 \u26412 \u12290 \
\
2.3 \uc0\u26412 \u26381 \u21153 \u35768 \u21487 \u30340 \u33539 \u22260 \
\
2.3.1 \uc0\u25105 \u20204 \u32473 \u20104 \u24744 \u19968 \u39033 \u20010 \u20154 \u30340 \u12289 \u19981 \u21487 \u36716 \u35753 \u21450 \u38750 \u25490 \u20182 \u24615 \u30340 \u35768 \u21487 \u65292 \u20197 \u20351 \u29992 \u26412 \u36719 \u20214 \u12290 \u24744 \u21487 \u20197 \u20026 \u38750 \u21830 \u19994 \u30446 \u30340 \u22312 \u21333 \u19968 \u21488 \u32456 \u31471 \u35774 \u22791 \u19978 \u23433 \u35013 \u12289 \u20351 \u29992 \u12289 \u26174 \u31034 \u12289 \u36816 \u34892 \u26412 \u36719 \u20214 \u12290 \
\
2.3.2 \uc0\u26412 \u26465 \u21450 \u26412 \u21327 \u35758 \u20854 \u20182 \u26465 \u27454 \u26410 \u26126 \u30830 \u25480 \u26435 \u30340 \u20854 \u20182 \u19968 \u20999 \u26435 \u21033 \u20173 \u30001 \u25105 \u20204 \u20445 \u30041 \u65292 \u24744 \u22312 \u34892 \u20351 \u36825 \u20123 \u26435 \u21033 \u26102 \u39035 \u21478 \u22806 \u21462 \u24471 \u25105 \u20204 \u30340 \u20070 \u38754 \u35768 \u21487 \u12290 \u25105 \u20204 \u22914 \u26524 \u26410 \u34892 \u20351 \u21069 \u36848 \u20219 \u20309 \u26435 \u21033 \u65292 \u24182 \u19981 \u26500 \u25104 \u23545 \u35813 \u26435 \u21033 \u30340 \u25918 \u24323 \u12290 \
\
2.4 \uc0\u25353 \u29616 \u29366 \u25552 \u20379 \u26381 \u21153 \
\uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u31508 \u22696 \u35759 \u20351 \u30340 \u26381 \u21153 \u26159 \u25353 \u29031 \u29616 \u26377 \u25216 \u26415 \u21644 \u26465 \u20214 \u25152 \u33021 \u36798 \u21040 \u30340 \u29616 \u29366 \u25552 \u20379 \u30340 \u12290 \u31508 \u22696 \u35759 \u20351 \u20250 \u23613 \u26368 \u22823 \u21162 \u21147 \u21521 \u24744 \u25552 \u20379 \u26381 \u21153 \u65292 \u30830 \u20445 \u26381 \u21153 \u30340 \u36830 \u32493 \u24615 \u21644 \u23433 \u20840 \u24615 \u65307 \u20294 \u31508 \u22696 \u35759 \u20351 \u19981 \u33021 \u38543 \u26102 \u39044 \u35265 \u21644 \u38450 \u33539 \u27861 \u24459 \u12289 \u25216 \u26415 \u20197 \u21450 \u20854 \u20182 \u39118 \u38505 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u19981 \u21487 \u25239 \u21147 \u12289 \u30149 \u27602 \u12289 \u26408 \u39532 \u12289 \u40657 \u23458 \u25915 \u20987 \u12289 \u31995 \u32479 \u19981 \u31283 \u23450 \u12289 \u31532 \u19977 \u26041 \u26381 \u21153 \u32570 \u38519 \u12289 \u25919 \u24220 \u34892 \u20026 \u31561 \u21407 \u22240 \u21487 \u33021 \u23548 \u33268 \u30340 \u26381 \u21153 \u20013 \u26029 \u12289 \u25968 \u25454 \u20002 \u22833 \u20197 \u21450 \u20854 \u20182 \u30340 \u25439 \u22833 \u21644 \u39118 \u38505 \u12290 \
\
\uc0\u19977 \u12289 \u36719 \u20214 \u30340 \u33719 \u21462 \u19982 \u20351 \u29992 \
\
3.1 \uc0\u24744 \u21487 \u20197 \u30452 \u25509 \u20174 \u25105 \u20204 \u30340 \u32593 \u31449 \u19978 \u33719 \u21462 \u26412 \u36719 \u20214 \u65292 \u20063 \u21487 \u20197 \u20174 \u24471 \u21040 \u25105 \u20204 \u25480 \u26435 \u30340 \u31532 \u19977 \u26041 \u33719 \u21462 \u12290 \
\
3.2 \uc0\u22914 \u26524 \u24744 \u20174 \u26410 \u32463 \u25105 \u20204 \u25480 \u26435 \u30340 \u31532 \u19977 \u26041 \u33719 \u21462 \u26412 \u36719 \u20214 \u25110 \u19982 \u26412 \u36719 \u20214 \u21517 \u31216 \u30456 \u21516 \u30340 \u23433 \u35013 \u31243 \u24207 \u65292 \u25105 \u20204 \u26080 \u27861 \u20445 \u35777 \u35813 \u36719 \u20214 \u33021 \u22815 \u27491 \u24120 \u20351 \u29992 \u65292 \u24182 \u23545 \u22240 \u27492 \u32473 \u24744 \u36896 \u25104 \u30340 \u25439 \u22833 \u19981 \u20104 \u36127 \u36131 \u12290 \
\
3.3 \uc0\u25105 \u20204 \u21487 \u33021 \u20026 \u19981 \u21516 \u30340 \u32456 \u31471 \u35774 \u22791 \u24320 \u21457 \u20102 \u19981 \u21516 \u30340 \u36719 \u20214 \u29256 \u26412 \u65292 \u24744 \u24212 \u24403 \u26681 \u25454 \u23454 \u38469 \u24773 \u20917 \u36873 \u25321 \u19979 \u36733 \u21512 \u36866 \u30340 \u29256 \u26412 \u36827 \u34892 \u23433 \u35013 \u12290 \
\
3.4 \uc0\u19979 \u36733 \u23433 \u35013 \u31243 \u24207 \u21518 \u65292 \u24744 \u38656 \u35201 \u25353 \u29031 \u35813 \u31243 \u24207 \u25552 \u31034 \u30340 \u27493 \u39588 \u27491 \u30830 \u23433 \u35013 \u12290 \
\
3.5 \uc0\u20026 \u25552 \u20379 \u26356 \u21152 \u20248 \u36136 \u12289 \u23433 \u20840 \u30340 \u26381 \u21153 \u65292 \u22312 \u26412 \u36719 \u20214 \u23433 \u35013 \u26102 \u25105 \u20204 \u21487 \u33021 \u20250 \u25512 \u33616 \u24744 \u23433 \u35013 \u20854 \u20182 \u36719 \u20214 \u65292 \u24744 \u21487 \u20197 \u36873 \u25321 \u23433 \u35013 \u25110 \u19981 \u23433 \u35013 \u12290 \
\
3.6 \uc0\u20026 \u20102 \u25913 \u21892 \u29992 \u25143 \u20307 \u39564 \u12289 \u20445 \u35777 \u26381 \u21153 \u30340 \u23433 \u20840 \u24615 \u21450 \u20135 \u21697 \u21151 \u33021 \u30340 \u19968 \u33268 \u24615 \u65292 \u25105 \u20204 \u21487 \u33021 \u20250 \u19981 \u26102 \u23545 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u36827 \u34892 \u26356 \u26032 \u12290 \u25105 \u20204 \u26377 \u26435 \u19981 \u32463 \u36807 \u21521 \u24744 \u29305 \u21035 \u36890 \u30693 \u32780 \u23545 \u36719 \u20214 \u36827 \u34892 \u26356 \u26032 \u65292 \u25110 \u32773 \u23545 \u36719 \u20214 \u30340 \u37096 \u20998 \u21151 \u33021 \u25928 \u26524 \u36827 \u34892 \u25913 \u21464 \u25110 \u38480 \u21046 \u12290 \u24744 \u24212 \u24403 \u23558 \u30456 \u20851 \u36719 \u20214 \u26356 \u26032 \u21040 \u26368 \u26032 \u29256 \u26412 \u65292 \u21542 \u21017 \u25105 \u20204 \u24182 \u19981 \u20445 \u35777 \u20854 \u33021 \u27491 \u24120 \u20351 \u29992 \u12290 \
\
3.7 \uc0\u22914 \u26524 \u24744 \u19981 \u20877 \u38656 \u35201 \u20351 \u29992 \u26412 \u36719 \u20214 \u25110 \u32773 \u38656 \u35201 \u23433 \u35013 \u26032 \u29256 \u36719 \u20214 \u65292 \u21487 \u20197 \u33258 \u34892 \u21368 \u36733 \u12290 \u22914 \u26524 \u24744 \u24895 \u24847 \u24110 \u21161 \u25105 \u20204 \u25913 \u36827 \u20135 \u21697 \u26381 \u21153 \u65292 \u35831 \u21578 \u30693 \u21368 \u36733 \u30340 \u21407 \u22240 \u12290 \
\
\uc0\u22235 \u12289 \u29992 \u25143 \u20010 \u20154 \u20449 \u24687 \u20445 \u25252 \
\
4.1 \uc0\u20445 \u25252 \u29992 \u25143 \u20010 \u20154 \u20449 \u24687 \u26159 \u25105 \u20204 \u30340 \u19968 \u39033 \u22522 \u26412 \u21407 \u21017 \u65292 \u25105 \u20204 \u23558 \u25353 \u29031 \u26412 \u21327 \u35758 \u30340 \u35268 \u23450 \u25910 \u38598 \u12289 \u20351 \u29992 \u12289 \u23384 \u20648 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u24182 \u20250 \u37319 \u21462 \u21512 \u29702 \u30340 \u25514 \u26045 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
4.2 \uc0\u24744 \u22312 \u27880 \u20876 \u36134 \u21495 \u25110 \u20351 \u29992 \u26412 \u26381 \u21153 \u30340 \u36807 \u31243 \u20013 \u65292 \u38656 \u35201 \u25552 \u20379 \u19968 \u20123 \u24517 \u35201 \u30340 \u20449 \u24687 \u65292 \u20363 \u22914 \u65306 \u20026 \u21521 \u24744 \u25552 \u20379 \u36134 \u21495 \u27880 \u20876 \u26381 \u21153 \u25110 \u36827 \u34892 \u29992 \u25143 \u36523 \u20221 \u35782 \u21035 \u65292 \u38656 \u35201 \u24744 \u22635 \u20889 \u25163 \u26426 \u21495 \u30721 \u12289 \u36523 \u20221 \u35777 \u21495 \u30721 \u31561 \u65307 \u31508 \u22696 \u35759 \u20351 \u26576 \u20123 \u21151 \u33021 \u21487 \u33021 \u38656 \u35201 \u25910 \u38598 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u21253 \u25324 \u65306 \u26165 \u31216 \u12289 \u22836 \u20687 \u12289 \u25163 \u26426 \u21495 \u12289 \u36523 \u20221 \u35777 \u21495 \u12289 \u21152 \u20837 \u32676 \u32452 \u12289 \u22909 \u21451 \u20851 \u31995 \u31561 \u12290 \u33509 \u22269 \u23478 \u27861 \u24459 \u27861 \u35268 \u25110 \u25919 \u31574 \u26377 \u29305 \u27530 \u35268 \u23450 \u30340 \u65292 \u24744 \u38656 \u35201 \u25552 \u20379 \u30495 \u23454 \u30340 \u36523 \u20221 \u20449 \u24687 \u12290 \u33509 \u24744 \u25552 \u20379 \u30340 \u20449 \u24687 \u19981 \u23436 \u25972 \u65292 \u21017 \u26080 \u27861 \u20351 \u29992 \u26412 \u26381 \u21153 \u25110 \u22312 \u20351 \u29992 \u36807 \u31243 \u20013 \u21463 \u21040 \u38480 \u21046 \u12290 \u24744 \u20102 \u35299 \u24182 \u21516 \u24847 \u65292 \u24744 \u26377 \u20041 \u21153 \u20445 \u25345 \u24744 \u25552 \u20379 \u20449 \u24687 \u30340 \u30495 \u23454 \u24615 \u12289 \u26377 \u25928 \u24615 \u21450 \u23436 \u25972 \u24615 \u12290 \
\
4.3 \uc0\u19968 \u33324 \u24773 \u20917 \u19979 \u65292 \u24744 \u21487 \u38543 \u26102 \u26597 \u30475 \u12289 \u20462 \u25913 \u33258 \u24049 \u25552 \u20132 \u30340 \u20449 \u24687 \u65292 \u20294 \u20986 \u20110 \u23433 \u20840 \u24615 \u21644 \u36523 \u20221 \u35782 \u21035 \u65288 \u22914 \u21495 \u30721 \u30003 \u35785 \u26381 \u21153 \u65289 \u30340 \u32771 \u34385 \u65292 \u24744 \u21487 \u33021 \u26080 \u27861 \u20462 \u25913 \u27880 \u20876 \u26102 \u25552 \u20379 \u30340 \u21021 \u22987 \u27880 \u20876 \u20449 \u24687 \u21450 \u20854 \u20182 \u39564 \u35777 \u20449 \u24687 \u12290 \
\
4.4 \uc0\u25105 \u20204 \u23558 \u36816 \u29992 \u21508 \u31181 \u23433 \u20840 \u25216 \u26415 \u21644 \u31243 \u24207 \u24314 \u31435 \u23436 \u21892 \u30340 \u31649 \u29702 \u21046 \u24230 \u26469 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u20197 \u20813 \u36973 \u21463 \u26410 \u32463 \u25480 \u26435 \u30340 \u35775 \u38382 \u12289 \u20351 \u29992 \u25110 \u27844 \u38706 \u12290 \
\
4.5 \uc0\u26410 \u32463 \u24744 \u30340 \u21516 \u24847 \u65292 \u25105 \u20204 \u19981 \u20250 \u23558 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u36716 \u31227 \u25110 \u25259 \u38706 \u32473 \u20219 \u20309 \u38750 \u20851 \u32852 \u30340 \u31532 \u19977 \u26041 \u65292 \u38500 \u38750 \u65306 \
\uc0\u65288 1\u65289 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u25110 \u27861 \u38498 \u12289 \u25919 \u24220 \u26426 \u20851 \u35201 \u27714 \u65307 \
\uc0\u65288 2\u65289 \u20026 \u23436 \u25104 \u21512 \u24182 \u12289 \u20998 \u31435 \u12289 \u25910 \u36141 \u25110 \u36164 \u20135 \u36716 \u35753 \u32780 \u36716 \u31227 \u65307 \
\uc0\u65288 3\u65289 \u20026 \u25552 \u20379 \u24744 \u35201 \u27714 \u30340 \u26381 \u21153 \u25152 \u24517 \u38656 \u65307 \
\uc0\u65288 4\u65289 \u20381 \u25454 \u20854 \u20182 \u30456 \u20851 \u21327 \u35758 \u35268 \u21017 \u21487 \u20197 \u36716 \u31227 \u25110 \u25259 \u38706 \u32473 \u20219 \u20309 \u31532 \u19977 \u26041 \u30340 \u24773 \u24418 \u12290 \
\
4.6 \uc0\u25105 \u20204 \u38750 \u24120 \u37325 \u35270 \u23545 \u26410 \u25104 \u24180 \u20154 \u20010 \u20154 \u20449 \u24687 \u30340 \u20445 \u25252 \u12290 \u33509 \u24744 \u26159 18\u21608 \u23681 \u20197 \u19979 \u30340 \u26410 \u25104 \u24180 \u20154 \u65292 \u22312 \u20351 \u29992 \u25105 \u20204 \u30340 \u26381 \u21153 \u21069 \u65292 \u24212 \u20107 \u20808 \u21462 \u24471 \u24744 \u23478 \u38271 \u25110 \u27861 \u23450 \u30417 \u25252 \u20154 \u30340 \u20070 \u38754 \u21516 \u24847 \u12290 \
\
4.7 \uc0\u25105 \u20204 \u21487 \u33021 \u20250 \u22312 \u21521 \u24744 \u25552 \u20379 \u26381 \u21153 \u30340 \u36807 \u31243 \u20043 \u20013 \u25152 \u25910 \u38598 \u30340 \u20449 \u24687 \u29992 \u20316 \u19979 \u21015 \u29992 \u36884 \u65306 \
(1) \uc0\u21521 \u24744 \u25552 \u20379 \u26381 \u21153 \u65307 \
(2) \uc0\u22312 \u25105 \u20204 \u25552 \u20379 \u26381 \u21153 \u26102 \u65292 \u29992 \u20110 \u36523 \u20221 \u39564 \u35777 \u12289 \u23458 \u25143 \u26381 \u21153 \u12289 \u23433 \u20840 \u38450 \u33539 \u12289 \u35784 \u39575 \u30417 \u27979 \u12289 \u23384 \u26723 \u21644 \u22791 \u20221 \u29992 \u36884 \u65292 \u30830 \u20445 \u25105 \u20204 \u21521 \u24744 \u25552 \u20379 \u30340 \u20135 \u21697 \u21644 \u26381 \u21153 \u30340 \u23433 \u20840 \u24615 ;\
(3) \uc0\u24110 \u21161 \u25105 \u20204 \u35774 \u35745 \u26032 \u26381 \u21153 \u65292 \u25913 \u36827 \u25105 \u20204 \u29616 \u26377 \u26381 \u21153 \u65307 \
(4) \uc0\u20351 \u25105 \u20204 \u26356 \u21152 \u20102 \u35299 \u24744 \u22914 \u20309 \u25509 \u20837 \u21644 \u20351 \u29992 \u25105 \u20204 \u30340 \u26381 \u21153 \u65292 \u20174 \u32780 \u38024 \u23545 \u24615 \u22320 \u22238 \u24212 \u24744 \u30340 \u20010 \u24615 \u21270 \u38656 \u27714 \u65292 \u20363 \u22914 \u35821 \u35328 \u35774 \u32622 \u12289 \u20301 \u32622 \u35774 \u32622 \u12289 \u20010 \u24615 \u21270 \u30340 \u24110 \u21161 \u26381 \u21153 \u21644 \u25351 \u23548 \u65292 \u25110 \u23545 \u24744 \u21644 \u20854 \u20182 \u29992 \u25143 \u20316 \u20986 \u20854 \u20182 \u26041 \u38754 \u30340 \u22238 \u24212 \u65307 \
(5) \uc0\u21521 \u24744 \u25552 \u20379 \u19982 \u24744 \u26356 \u21152 \u30456 \u20851 \u30340 \u24191 \u21578 \u20197 \u26367 \u20195 \u26222 \u36941 \u25237 \u25918 \u30340 \u24191 \u21578 \u65307 \u35780 \u20272 \u25105 \u20204 \u26381 \u21153 \u20013 \u30340 \u24191 \u21578 \u21644 \u20854 \u20182 \u20419 \u38144 \u21450 \u25512 \u24191 \u27963 \u21160 \u30340 \u25928 \u26524 \u65292 \u24182 \u21152 \u20197 \u25913 \u36827 \u65307 \
(6) \uc0\u36719 \u20214 \u35748 \u35777 \u25110 \u31649 \u29702 \u36719 \u20214 \u21319 \u32423 \u65307 \
(7) \uc0\u35753 \u24744 \u21442 \u19982 \u26377 \u20851 \u25105 \u20204 \u20135 \u21697 \u21644 \u26381 \u21153 \u30340 \u35843 \u26597 \u12290 \
\
## \uc0\u20116 \u12289 \u20027 \u26435 \u21033 \u20041 \u21153 \u26465 \u27454 \
\
5.1 \uc0\u36134 \u21495 \u20351 \u29992 \u35268 \u33539 \
\
5.1.1 \uc0\u24744 \u22312 \u20351 \u29992 \u26412 \u26381 \u21153 \u21069 \u38656 \u35201 \u27880 \u20876 \u19968 \u20010 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u12290 \u26412 \u36719 \u20214 \u20026 \u24744 \u25552 \u20379 \u27880 \u20876 \u21450 \u30331 \u24405 \u36890 \u36947 \u65292 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u36890 \u36807 \u25163 \u26426 \u21495 \u30721 \u36827 \u34892 \u27880 \u20876 \u65292 \u24744 \u21487 \u25353 \u29031 \u39029 \u38754 \u25552 \u31034 \u22635 \u20889 \u20449 \u24687 \u12289 \u38405 \u35835 \u24182 \u21516 \u24847 \u26412 \u21327 \u35758 \u65292 \u22312 \u23436 \u25104 \u20840 \u37096 \u27880 \u20876 \u30331 \u24405 \u31243 \u24207 \u21518 \u65292 \u25104 \u20026 \u26412 \u36719 \u20214 \u29992 \u25143 \u12290 \u21478 \u22806 \u25105 \u20204 \u26377 \u26435 \u26681 \u25454 \u29992 \u25143 \u38656 \u27714 \u25110 \u20135 \u21697 \u38656 \u35201 \u23545 \u36134 \u21495 \u27880 \u20876 \u30340 \u26041 \u24335 \u36827 \u34892 \u21464 \u26356 \u65292 \u20851 \u20110 \u24744 \u20351 \u29992 \u36134 \u21495 \u30340 \u20855 \u20307 \u35268 \u21017 \u65292 \u35831 \u36981 \u23432 \u30456 \u20851 \u36134 \u21495 \u20351 \u29992 \u21327 \u35758 \u20197 \u21450 \u25105 \u20204 \u20026 \u27492 \u21457 \u24067 \u30340 \u19987 \u39033 \u35268 \u21017 \u12290 \
\
5.1.2 \uc0\u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u30340 \u25152 \u26377 \u26435 \u24402 \u25105 \u20204 \u20844 \u21496 \u25152 \u26377 \u65292 \u29992 \u25143 \u23436 \u25104 \u30003 \u35831 \u27880 \u20876 \u25163 \u32493 \u21518 \u65292 \u20165 \u33719 \u24471 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u30340 \u20351 \u29992 \u26435 \u65292 \u19988 \u35813 \u20351 \u29992 \u26435 \u20165 \u23646 \u20110 \u21021 \u22987 \u30003 \u35831 \u27880 \u20876 \u20154 \u12290 \u21516 \u26102 \u65292 \u21021 \u22987 \u30003 \u35831 \u27880 \u20876 \u20154 \u19981 \u24471 \u36192 \u19982 \u12289 \u20511 \u29992 \u12289 \u20986 \u31199 \u12289 \u36716 \u35753 \u25110 \u21806 \u21334 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u25110 \u32773 \u20197 \u20854 \u20182 \u26041 \u24335 \u35768 \u21487 \u38750 \u21021 \u22987 \u30003 \u35831 \u27880 \u20876 \u20154 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u12290 \u38750 \u21021 \u22987 \u30003 \u35831 \u27880 \u20876 \u20154 \u19981 \u24471 \u36890 \u36807 \u21463 \u36192 \u12289 \u32487 \u25215 \u12289 \u25215 \u31199 \u12289 \u21463 \u35753 \u25110 \u32773 \u20854 \u20182 \u20219 \u20309 \u26041 \u24335 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u12290 \u22914 \u25105 \u20204 \u21028 \u26029 \u24744 \u36134 \u21495 \u30340 \u20351 \u29992 \u21487 \u33021 \u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u12289 \u21361 \u21450 \u24744 \u30340 \u36134 \u21495 \u23433 \u20840 \u21450 /\u25110 \u26412 \u36719 \u20214 \u20449 \u24687 \u23433 \u20840 \u30340 \u65292 \u25105 \u20204 \u21487 \u25298 \u32477 \u25552 \u20379 \u30456 \u24212 \u26381 \u21153 \u25110 \u32456 \u27490 \u26412 \u21327 \u35758 \u12290 \u25105 \u20204 \u26377 \u26435 \u35270 \u24773 \u20917 \u23553 \u31105 \u25110 \u27880 \u38144 \u12289 \u22238 \u25910 \u35813 \u36134 \u21495 \u65292 \u30001 \u27492 \u24102 \u26469 \u30340 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u20869 \u23481 \u12289 \u34394 \u25311 \u36130 \u20135 \u28165 \u31354 \u31561 \u25439 \u22833 \u30001 \u24744 \u26412 \u20154 \u21450 \u35813 \u36134 \u21495 \u20351 \u29992 \u32773 \u33258 \u34892 \u25215 \u25285 \u12290 \
\
5.1.3 \uc0\u24744 \u27880 \u20876 \u33719 \u24471 \u30340 \u36134 \u21495 \u12289 \u35774 \u32622 \u30340 \u23494 \u30721 \u26159 \u24744 \u30331 \u24405 \u24182 \u20197 \u29992 \u25143 \u36523 \u20221 \u20351 \u29992 \u26412 \u36719 \u20214 \u30340 \u20973 \u35777 \u65292 \u24744 \u30340 \u36134 \u25143 \u20165 \u38480 \u24744 \u26412 \u20154 \u20351 \u29992 \u65292 \u24744 \u26377 \u36131 \u20219 \u22949 \u21892 \u20445 \u31649 \u24744 \u30340 \u27880 \u20876 \u29992 \u25143 \u20449 \u24687 \u21450 \u29992 \u25143 \u23494 \u30721 \u30340 \u23433 \u20840 \u65292 \u24744 \u38656 \u35201 \u23545 \u24744 \u30340 \u27880 \u20876 \u29992 \u25143 \u20197 \u21450 \u23494 \u30721 \u19979 \u30340 \u34892 \u20026 \u25215 \u25285 \u27861 \u24459 \u36131 \u20219 \u12290 \u24744 \u21516 \u24847 \u22312 \u20219 \u20309 \u24773 \u20917 \u19979 \u19981 \u21521 \u20182 \u20154 \u27844 \u38706 \u29992 \u25143 \u21450 \u23494 \u30721 \u20449 \u24687 \u12290 \u24403 \u22312 \u24744 \u24576 \u30097 \u20182 \u20154 \u22312 \u20351 \u29992 \u24744 \u30340 \u36134 \u21495 \u26102 \u65292 \u24744 \u24212 \u31435 \u21363 \u36890 \u30693 \u25105 \u20204 \u12290 \
\
5.1.4 \uc0\u29992 \u25143 \u27880 \u20876 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u21518 \u22914 \u26524 \u38271 \u26399 \u65288 \u36229 \u36807 \u12304 6\u12305 \u20010 \u26376 \u65289 \u19981 \u30331 \u24405 \u35813 \u36134 \u21495 \u65292 \u25105 \u20204 \u26377 \u26435 \u22238 \u25910 \u35813 \u36134 \u21495 \u65292 \u20197 \u20813 \u36896 \u25104 \u36164 \u28304 \u28010 \u36153 \u65292 \u30001 \u27492 \u24102 \u26469 \u30340 \u20219 \u20309 \u25439 \u22833 \u22343 \u30001 \u29992 \u25143 \u33258 \u34892 \u25215 \u25285 \u12290 \
\
5.1.5 \uc0\u25105 \u20204 \u29305 \u21035 \u25552 \u37266 \u65292 \u24744 \u24212 \u22949 \u21892 \u20445 \u31649 \u24744 \u30340 \u36134 \u21495 \u21644 \u23494 \u30721 \u12290 \u24403 \u24744 \u20351 \u29992 \u23436 \u26412 \u26381 \u21153 \u21518 \u65292 \u24212 \u23433 \u20840 \u36864 \u20986 \u12290 \u24744 \u39035 \u23545 \u36134 \u21495 \u36827 \u34892 \u30340 \u25152 \u26377 \u27963 \u21160 \u21644 \u34892 \u20026 \u36127 \u36131 \u65292 \u22240 \u24744 \u20445 \u31649 \u19981 \u21892 \u23548 \u33268 \u36973 \u21463 \u36134 \u21495 \u25110 \u23494 \u30721 \u22833 \u31363 \u12289 \u36134 \u21495 \u34987 \u38750 \u27861 \u20351 \u29992 \u65292 \u30456 \u20851 \u36131 \u20219 \u30001 \u24744 \u33258 \u34892 \u25215 \u25285 \u65292 \u24744 \u21487 \u36890 \u36807 \u21496 \u27861 \u12289 \u34892 \u25919 \u31561 \u25937 \u27982 \u36884 \u24452 \u21521 \u20405 \u26435 \u34892 \u20026 \u20154 \u36861 \u20607 \u12290 \
5.1.6\uc0\u24744 \u19981 \u24471 \u24694 \u24847 \u27880 \u20876 \u36134 \u21495 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u39057 \u32321 \u27880 \u20876 \u12289 \u25209 \u37327 \u27880 \u20876 \u12289 \u20351 \u29992 \u20182 \u20154 \u36523 \u20221 \u27880 \u20876 \u25110 \u20854 \u20182 \u19981 \u20197 \u27491 \u24120 \u20351 \u29992 \u26412 \u36719 \u20214 \u26381 \u21153 \u20026 \u30446 \u30340 \u30340 \u36134 \u21495 \u27880 \u20876 \u34892 \u20026 \u12290 \u38500 \u27492 \u22806 \u65292 \u24744 \u30340 \u36134 \u25143 \u19981 \u24471 \u20197 \u20219 \u20309 \u26041 \u24335 \u36716 \u35753 \u65292 \u21542 \u21017 \u25105 \u20204 \u26377 \u26435 \u36861 \u31350 \u24744 \u30340 \u36829 \u32422 \u36131 \u20219 \u65292 \u19988 \u30001 \u27492 \u20135 \u29983 \u30340 \u36131 \u20219 \u21450 \u21518 \u26524 \u22343 \u30001 \u24744 \u33258 \u34892 \u25215 \u25285 \u12290 \
\
5.2 \uc0\u29992 \u25143 \u27880 \u24847 \u20107 \u39033 \
\
5.2.1 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65306 \u20026 \u20102 \u21521 \u24744 \u25552 \u20379 \u26377 \u25928 \u30340 \u26381 \u21153 \u65292 \u26412 \u36719 \u20214 \u20250 \u21033 \u29992 \u24744 \u32456 \u31471 \u35774 \u22791 \u30340 \u22788 \u29702 \u22120 \u21644 \u24102 \u23485 \u31561 \u36164 \u28304 \u12290 \u26412 \u36719 \u20214 \u20351 \u29992 \u36807 \u31243 \u20013 \u21487 \u33021 \u20135 \u29983 \u25968 \u25454 \u27969 \u37327 \u30340 \u36153 \u29992 \u65292 \u29992 \u25143 \u38656 \u33258 \u34892 \u21521 \u36816 \u33829 \u21830 \u20102 \u35299 \u30456 \u20851 \u36164 \u36153 \u20449 \u24687 \u65292 \u24182 \u33258 \u34892 \u25215 \u25285 \u30456 \u20851 \u36153 \u29992 \u12290 \
\
5.2.2 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65306 \u26412 \u36719 \u20214 \u30340 \u26576 \u20123 \u21151 \u33021 \u21487 \u33021 \u20250 \u35753 \u31532 \u19977 \u26041 \u30693 \u26195 \u29992 \u25143 \u30340 \u20449 \u24687 \u65292 \u20363 \u22914 \u65306 \u29992 \u25143 \u36873 \u25321 \u31508 \u22696 \u35759 \u20351 \u19982 \u20854 \u20182 \u36719 \u20214 \u25110 \u30828 \u20214 \u20449 \u24687 \u20114 \u36890 \u21518 \u65292 \u20854 \u20182 \u36719 \u20214 \u25110 \u30828 \u20214 \u30340 \u25552 \u20379 \u26041 \u21487 \u20197 \u33719 \u21462 \u29992 \u25143 \u22312 \u31508 \u22696 \u35759 \u20351 \u20027 \u21160 \u20844 \u24320 \u25110 \u20256 \u36755 \u30340 \u30456 \u20851 \u20449 \u24687 \u65292 \u29992 \u25143 \u22312 \u36873 \u25321 \u21069 \u24212 \u20805 \u20998 \u20102 \u35299 \u20854 \u20182 \u36719 \u20214 \u25110 \u30828 \u20214 \u30340 \u20135 \u21697 \u21151 \u33021 \u21450 \u20449 \u24687 \u20445 \u25252 \u31574 \u30053 \u12290 \
\
5.2.3 \uc0\u24744 \u22312 \u20351 \u29992 \u26412 \u36719 \u20214 \u26576 \u19968 \u29305 \u23450 \u26381 \u21153 \u26102 \u65292 \u35813 \u26381 \u21153 \u21487 \u33021 \u20250 \u21478 \u26377 \u21333 \u29420 \u30340 \u21327 \u35758 \u12289 \u30456 \u20851 \u19994 \u21153 \u35268 \u21017 \u31561 \u65288 \u20197 \u19979 \u32479 \u31216 \u20026 \'93\u21333 \u29420 \u21327 \u35758 \'94\u65289 \u65292 \u24744 \u22312 \u20351 \u29992 \u35813 \u39033 \u26381 \u21153 \u21069 \u35831 \u38405 \u35835 \u24182 \u21516 \u24847 \u30456 \u20851 \u30340 \u21333 \u29420 \u21327 \u35758 \u65307 \u24744 \u20351 \u29992 \u21069 \u36848 \u29305 \u23450 \u26381 \u21153 \u65292 \u21363 \u35270 \u20026 \u24744 \u25509 \u21463 \u30456 \u20851 \u21333 \u29420 \u21327 \u35758 \u12290 \u20363 \u22914 \u65306 \u22914 \u26524 \u24744 \u38656 \u35201 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u25903 \u20184 \u30456 \u20851 \u26381 \u21153 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u31508 \u22696 \u35759 \u20351 \u32418 \u21253 \u12289 \u36716 \u36134 \u12289 \u25910 \u27454 \u12289 \u20805 \u20540 \u12289 \u25552 \u29616 \u31561 \u25903 \u20184 \u26381 \u21153 \u65289 \u26102 \u65292 \u38656 \u38405 \u35835 \u24182 \u36981 \u23432 \u12298 \u31508 \u22696 \u35759 \u20351 \u25903 \u20184 \u26381 \u21153 \u21327 \u35758 \u12299 \u65307 \u24744 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u25903 \u20184 \u30456 \u20851 \u26381 \u21153 \u65292 \u21363 \u35270 \u20026 \u24744 \u25509 \u21463 \u12298 \u31508 \u22696 \u35759 \u20351 \u25903 \u20184 \u26381 \u21153 \u21327 \u35758 \u12299 \u12290 \
\
5.2.4 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u25105 \u20204 \u23558 \u23613 \u20854 \u21830 \u19994 \u19978 \u30340 \u21512 \u29702 \u21162 \u21147 \u20445 \u38556 \u24744 \u22312 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u20013 \u30340 \u25968 \u25454 \u23384 \u20648 \u23433 \u20840 \u65292 \u20294 \u26159 \u65292 \u25105 \u20204 \u24182 \u19981 \u33021 \u23601 \u27492 \u25552 \u20379 \u23436 \u20840 \u20445 \u35777 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u20197 \u19979 \u24773 \u24418 \u65306 \
\
5.2.4.1 \uc0\u25105 \u20204 \u19981 \u23545 \u24744 \u22312 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u20013 \u30456 \u20851 \u25968 \u25454 \u30340 \u21024 \u38500 \u25110 \u23384 \u20648 \u22833 \u36133 \u36127 \u36131 \u65307 \
\
5.2.4.2 \uc0\u25105 \u20204 \u26377 \u26435 \u26681 \u25454 \u23454 \u38469 \u24773 \u20917 \u33258 \u34892 \u20915 \u23450 \u21333 \u20010 \u29992 \u25143 \u22312 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u20013 \u25968 \u25454 \u30340 \u26368 \u38271 \u23384 \u20648 \u26399 \u38480 \u65292 \u24182 \u22312 \u26381 \u21153 \u22120 \u19978 \u20026 \u20854 \u20998 \u37197 \u25968 \u25454 \u26368 \u22823 \u23384 \u20648 \u31354 \u38388 \u31561 \u12290 \u24744 \u21487 \u26681 \u25454 \u33258 \u24049 \u30340 \u38656 \u35201 \u33258 \u34892 \u22791 \u20221 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u20013 \u30340 \u30456 \u20851 \u25968 \u25454 \u65307 \
\
5.2.4.3 \uc0\u22914 \u26524 \u24744 \u20572 \u27490 \u20351 \u29992 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u25110 \u26381 \u21153 \u34987 \u32456 \u27490 \u25110 \u21462 \u28040 \u65292 \u25105 \u20204 \u21487 \u20197 \u20174 \u26381 \u21153 \u22120 \u19978 \u27704 \u20037 \u22320 \u21024 \u38500 \u24744 \u30340 \u25968 \u25454 \u12290 \u26381 \u21153 \u20572 \u27490 \u12289 \u32456 \u27490 \u25110 \u21462 \u28040 \u21518 \u65292 \u25105 \u20204 \u27809 \u26377 \u20041 \u21153 \u21521 \u24744 \u36820 \u36824 \u20219 \u20309 \u25968 \u25454 \u12290 \
\
5.2.5 \uc0\u29992 \u25143 \u22312 \u20351 \u29992 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u26102 \u65292 \u38656 \u33258 \u34892 \u25215 \u25285 \u22914 \u19979 \u26469 \u33258 \u25105 \u20204 \u19981 \u21487 \u25511 \u21046 \u30340 \u39118 \u38505 \u20869 \u23481 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \
\
5.2.5.1 \uc0\u30001 \u20110 \u19981 \u21487 \u25239 \u21147 \u22240 \u32032 \u21487 \u33021 \u23548 \u33268 \u30340 \u20010 \u20154 \u20449 \u24687 \u20002 \u22833 \u12289 \u27844 \u38706 \u31561 \u39118 \u38505 \u65307 \
\
5.2.5.2 \uc0\u29992 \u25143 \u24517 \u39035 \u36873 \u25321 \u19982 \u25152 \u23433 \u35013 \u32456 \u31471 \u35774 \u22791 \u30456 \u21305 \u37197 \u30340 \u36719 \u20214 \u29256 \u26412 \u65292 \u21542 \u21017 \u65292 \u30001 \u20110 \u36719 \u20214 \u19982 \u32456 \u31471 \u35774 \u22791 \u22411 \u21495 \u19981 \u21305 \u37197 \u25152 \u23548 \u33268 \u30340 \u20219 \u20309 \u38382 \u39064 \u25110 \u25439 \u23475 \u65292 \u22343 \u30001 \u29992 \u25143 \u33258 \u34892 \u25215 \u25285 \u65307 \
\
5.2.5.3 \uc0\u29992 \u25143 \u22312 \u20351 \u29992 \u26412 \u36719 \u20214 \u35775 \u38382 \u31532 \u19977 \u26041 \u32593 \u31449 \u26102 \u65292 \u22240 \u31532 \u19977 \u26041 \u32593 \u31449 \u21450 \u30456 \u20851 \u20869 \u23481 \u25152 \u21487 \u33021 \u23548 \u33268 \u30340 \u39118 \u38505 \u65292 \u30001 \u29992 \u25143 \u33258 \u34892 \u25215 \u25285 \u65307 \
\
5.2.5.4 \uc0\u29992 \u25143 \u21457 \u24067 \u30340 \u20869 \u23481 \u34987 \u20182 \u20154 \u36716 \u21457 \u12289 \u20998 \u20139 \u65292 \u22240 \u27492 \u31867 \u20256 \u25773 \u21487 \u33021 \u24102 \u26469 \u30340 \u39118 \u38505 \u21644 \u36131 \u20219 \u65307 \
\
5.2.5.5 \uc0\u30001 \u20110 \u26080 \u32447 \u32593 \u32476 \u20449 \u21495 \u19981 \u31283 \u23450 \u12289 \u26080 \u32447 \u32593 \u32476 \u24102 \u23485 \u23567 \u31561 \u21407 \u22240 \u65292 \u25152 \u24341 \u36215 \u30340 \u31508 \u22696 \u35759 \u20351 \u30331 \u24405 \u22833 \u36133 \u12289 \u36164 \u26009 \u21516 \u27493 \u19981 \u23436 \u25972 \u12289 \u39029 \u38754 \u25171 \u24320 \u36895 \u24230 \u24930 \u31561 \u39118 \u38505 \u12290 \
\
5.2.6 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u31508 \u22696 \u35759 \u20351 \u21487 \u33021 \u20250 \u19981 \u26102 \u21457 \u24067 \u24182 \u25191 \u34892 \u19968 \u20123 \u29992 \u25143 \u22870 \u21169 \u27963 \u21160 \u65292 \u30456 \u20851 \u27963 \u21160 \u30340 \u20855 \u20307 \u35268 \u21017 \u30340 \u21046 \u23450 \u26435 \u21644 \u26368 \u32456 \u35299 \u37322 \u26435 \u24402 \u25105 \u20204 \u25152 \u26377 \u65292 \u25105 \u20204 \u26377 \u26435 \u26681 \u25454 \u33258 \u24049 \u30340 \u21028 \u26029 \u20915 \u23450 \u25552 \u21069 \u32456 \u27490 \u27963 \u21160 \u25110 \u24310 \u38271 \u27963 \u21160 \u26102 \u38388 \u12290 \u21516 \u19968 \u31508 \u22696 \u35759 \u20351 \u29992 \u25143 \u21517 \u12289 \u21516 \u19968 \u25163 \u26426 \u21495 \u12289 \u21516 \u19968 \u36523 \u20221 \u35777 \u21495 \u30721 \u12289 \u21516 \u19968 \u25163 \u26426 \u35774 \u22791 \u22343 \u35270 \u20026 \u21516 \u19968 \u29992 \u25143 \u12290 \u22914 \u26524 \u25105 \u20204 \u21457 \u29616 \u26377 \u29992 \u25143 \u21033 \u29992 \u19981 \u27491 \u24403 \u25163 \u27573 \u21442 \u19982 \u27963 \u21160 \u65292 \u25110 \u32773 \u22914 \u26524 \u29992 \u25143 \u23384 \u22312 \u25110 \u28041 \u23244 \u36829 \u27861 \u36829 \u35268 \u34892 \u20026 \u24773 \u20917 \u65288 \u21253 \u25324 \u19981 \u38480 \u20110 \u27927 \u38065 \u12289 \u22806 \u25346 \u12289 \u20316 \u24330 \u12289 \u22871 \u29616 \u12289 \u36172 \u21338 \u12289 \u21047 \u22870 \u21169 \u31867 \u30340 \u27963 \u21160 \u12289 \u20854 \u20182 \u39118 \u38505 \u34892 \u20026 \u65289 \u65292 \u25105 \u20204 \u26377 \u26435 \u21462 \u28040 \u24744 \u21442 \u19982 \u27963 \u21160 \u30340 \u36164 \u26684 \u65292 \u24182 \u26377 \u26435 \u36861 \u22238 \u24744 \u24050 \u32463 \u33719 \u24471 \u30340 \u22870 \u21169 \u25110 \u20195 \u24065 \u25110 \u20854 \u20182 \u22870 \u21697 \u12290 \u22914 \u26524 \u22240 \u38750 \u23454 \u21517 \u35748 \u35777 \u23548 \u33268 \u24744 \u26080 \u27861 \u21442 \u19982 \u27963 \u21160 \u30340 \u65292 \u35270 \u20026 \u24744 \u33258 \u21160 \u25918 \u24323 \u27963 \u21160 \u36164 \u26684 \u12290 \
\
5.3 \uc0\u31532 \u19977 \u26041 \u20135 \u21697 \u21644 \u26381 \u21153 \
\
5.3.1 \uc0\u24744 \u22312 \u20351 \u29992 \u26412 \u36719 \u20214 \u31532 \u19977 \u26041 \u25552 \u20379 \u30340 \u20135 \u21697 \u25110 \u26381 \u21153 \u26102 \u65292 \u38500 \u36981 \u23432 \u26412 \u21327 \u35758 \u32422 \u23450 \u22806 \u65292 \u36824 \u24212 \u36981 \u23432 \u31532 \u19977 \u26041 \u30340 \u29992 \u25143 \u21327 \u35758 \u12290 \u25105 \u20204 \u21644 \u31532 \u19977 \u26041 \u23545 \u21487 \u33021 \u20986 \u29616 \u30340 \u32416 \u32439 \u22312 \u27861 \u24459 \u35268 \u23450 \u21644 \u32422 \u23450 \u30340 \u33539 \u22260 \u20869 \u21508 \u33258 \u25215 \u25285 \u36131 \u20219 \u12290 \
\
5.3.2 \uc0\u22240 \u29992 \u25143 \u20351 \u29992 \u26412 \u36719 \u20214 \u25110 \u35201 \u27714 \u25105 \u20204 \u25552 \u20379 \u29305 \u23450 \u26381 \u21153 \u26102 \u65292 \u26412 \u36719 \u20214 \u21487 \u33021 \u20250 \u35843 \u29992 \u31532 \u19977 \u26041 \u31995 \u32479 \u25110 \u32773 \u36890 \u36807 \u31532 \u19977 \u26041 \u25903 \u25345 \u29992 \u25143 \u30340 \u20351 \u29992 \u25110 \u35775 \u38382 \u65292 \u20351 \u29992 \u25110 \u35775 \u38382 \u30340 \u32467 \u26524 \u30001 \u35813 \u31532 \u19977 \u26041 \u25552 \u20379 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u31532 \u19977 \u26041 \u36890 \u36807 \u31508 \u22696 \u35759 \u20351 \u24320 \u25918 \u24179 \u21488 \u25509 \u20837 \u30340 \u20869 \u23481 \u31561 \u65289 \u65292 \u25105 \u20204 \u19981 \u20445 \u35777 \u36890 \u36807 \u31532 \u19977 \u26041 \u25552 \u20379 \u26381 \u21153 \u21450 \u20869 \u23481 \u30340 \u23433 \u20840 \u24615 \u12289 \u20934 \u30830 \u24615 \u12289 \u26377 \u25928 \u24615 \u21450 \u20854 \u23427 \u19981 \u30830 \u23450 \u30340 \u39118 \u38505 \u65292 \u24744 \u24212 \u33258 \u34892 \u21028 \u26029 \u27492 \u31867 \u31532 \u19977 \u26041 \u32593 \u31449 \u25110 \u32773 \u26381 \u21153 \u30340 \u23433 \u20840 \u24615 \u21644 \u21487 \u29992 \u24615 \u65292 \u24182 \u33258 \u34892 \u25215 \u25285 \u30456 \u20851 \u39118 \u38505 \u21644 \u36131 \u20219 \u65292 \u30001 \u27492 \u33509 \u24341 \u21457 \u30340 \u20219 \u20309 \u20105 \u35758 \u21450 \u25439 \u23475 \u65292 \u19982 \u25105 \u20204 \u26080 \u20851 \u65292 \u25105 \u20204 \u19981 \u25215 \u25285 \u20219 \u20309 \u36131 \u20219 \u12290 \
\
5.3.3 \uc0\u31532 \u19977 \u26041 \u26381 \u21153 \u25552 \u20379 \u32773 \u21487 \u20197 \u36890 \u36807 \u26412 \u36719 \u20214 \u21521 \u24744 \u25552 \u20379 \u20135 \u21697 \u25110 \u32773 \u26381 \u21153 \u65292 \u27604 \u22914 \u24744 \u21487 \u20197 \u36141 \u20080 \u24182 \u20351 \u29992 \u31532 \u19977 \u26041 \u24320 \u21457 \u30340 \u24212 \u29992 \u31243 \u24207 \u12289 \u25509 \u21463 \u31532 \u19977 \u26041 \u25552 \u20379 \u30340 \u26381 \u21153 \u21450 \u20135 \u21697 \u65292 \u25110 \u32773 \u36890 \u36807 \u31508 \u22696 \u35759 \u20351 \u25480 \u26435 \u30331 \u24405 \u33021 \u21147 \u30331 \u24405 \u24182 \u20351 \u29992 \u20854 \u20182 \u20114 \u32852 \u32593 \u26381 \u21153 \u12290 \u22312 \u27492 \u36807 \u31243 \u20013 \u65292 \u31532 \u19977 \u26041 \u26381 \u21153 \u25552 \u20379 \u32773 \u21487 \u33021 \u20250 \u25910 \u38598 \u12289 \u20351 \u29992 \u21644 \u23384 \u20648 \u24744 \u30340 \u30456 \u20851 \u25968 \u25454 \u25110 \u20449 \u24687 \u65292 \u25105 \u20204 \u36890 \u36807 \u19982 \u31532 \u19977 \u26041 \u26381 \u21153 \u25552 \u20379 \u32773 \u31614 \u35746 \u30340 \u26377 \u20851 \u21327 \u35758 \u12289 \u31508 \u22696 \u35759 \u20351 \u30456 \u20851 \u35268 \u33539 \u20197 \u21450 \u21512 \u29702 \u30340 \u20135 \u21697 \u27969 \u31243 \u35774 \u35745 \u65292 \u20005 \u26684 \u35201 \u27714 \u31532 \u19977 \u26041 \u26381 \u21153 \u25552 \u20379 \u32773 \u33719 \u21462 \u24744 \u30340 \u20219 \u20309 \u25968 \u25454 \u22343 \u24212 \u36981 \u23432 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u30340 \u35268 \u23450 \u65292 \u24517 \u39035 \u20107 \u20808 \u33719 \u24471 \u24744 \u30340 \u26126 \u30830 \u21516 \u24847 \u65292 \u37319 \u21462 \u24517 \u35201 \u30340 \u25968 \u25454 \u20445 \u25252 \u25514 \u26045 \u65292 \u19988 \u20165 \u20026 \u20135 \u21697 \u25110 \u26381 \u21153 \u20043 \u30446 \u30340 \u21512 \u29702 \u20351 \u29992 \u24744 \u30340 \u30456 \u20851 \u25968 \u25454 \u65292 \u21521 \u24744 \u25552 \u20379 \u20462 \u25913 \u25110 \u21024 \u38500 \u33258 \u24049 \u25968 \u25454 \u30340 \u26041 \u24335 \u65292 \u24182 \u22312 \u20572 \u27490 \u26381 \u21153 \u26102 \u24517 \u39035 \u21024 \u38500 \u20840 \u37096 \u25968 \u25454 \u31561 \u65292 \u23613 \u26368 \u22823 \u21487 \u33021 \u20445 \u38556 \u24744 \u30340 \u25968 \u25454 \u21644 \u38544 \u31169 \u19981 \u21463 \u20405 \u29359 \u12290 \u24744 \u22312 \u25509 \u21463 \u25110 \u20351 \u29992 \u31532 \u19977 \u26041 \u20135 \u21697 \u25110 \u26381 \u21153 \u21069 \u24212 \u20805 \u20998 \u20102 \u35299 \u31532 \u19977 \u26041 \u20135 \u21697 \u25110 \u26381 \u21153 \u30340 \u26465 \u27454 \u21450 \u25919 \u31574 \u12290 \u22914 \u26524 \u24744 \u21457 \u29616 \u26377 \u31532 \u19977 \u26041 \u26381 \u21153 \u25552 \u20379 \u32773 \u23384 \u22312 \u30456 \u20851 \u36829 \u27861 \u36829 \u35268 \u34892 \u20026 \u65292 \u21487 \u20197 \u21521 \u25105 \u20204 \u25237 \u35785 \u65292 \u25105 \u20204 \u23558 \u26597 \u23454 \u21518 \u20104 \u20197 \u22788 \u29702 \u12290 \
\
5.3.4 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u25105 \u20204 \u26377 \u26435 \u20915 \u23450 \u23558 \u26412 \u36719 \u20214 \u20316 \u21830 \u19994 \u29992 \u36884 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u24320 \u21457 \u12289 \u20351 \u29992 \u26412 \u36719 \u20214 \u30340 \u37096 \u20998 \u26381 \u21153 \u20026 \u31532 \u19977 \u26041 \u20316 \u25512 \u24191 \u31561 \u65292 \u25105 \u20204 \u25215 \u35834 \u22312 \u25512 \u24191 \u36807 \u31243 \u20013 \u20005 \u26684 \u25353 \u29031 \u26412 \u21327 \u35758 \u32422 \u23450 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u21516 \u26102 \u24744 \u20063 \u21487 \u20197 \u26681 \u25454 \u31995 \u32479 \u35774 \u32622 \u36873 \u25321 \u23631 \u34109 \u12289 \u25298 \u32477 \u25509 \u25910 \u30456 \u20851 \u25512 \u24191 \u20449 \u24687 \u12290 \
\
5.4\uc0\u36134 \u21495 \u20449 \u24687 \u35774 \u32622 \u21450 \u32500 \u25252 \
\
5.4.1\uc0\u24744 \u22312 \u27880 \u20876 \u25110 \u20351 \u29992 \u26412 \u36719 \u20214 \u26102 \u35774 \u32622 \u30340 \u21517 \u31216 \u12289 \u22836 \u20687 \u21644 \u31616 \u20171 \u31561 \u36134 \u21495 \u20449 \u24687 \u36164 \u26009 \u24212 \u36981 \u23432 \u27861 \u24459 \u27861 \u35268 \u12289 \u31038 \u20250 \u36947 \u24503 \u39118 \u23578 \u21644 \u20449 \u24687 \u30495 \u23454 \u24615 \u31561 \u21407 \u21017 \u65292 \u19981 \u24471 \u20986 \u29616 \u19981 \u33391 \u20449 \u24687 \u65292 \u19981 \u24471 \u20882 \u29992 \u20182 \u20154 \u22995 \u21517 \u12289 \u21517 \u31216 \u12289 \u23383 \u21495 \u12289 \u22836 \u20687 \u31561 \u25110 \u37319 \u21462 \u20854 \u20182 \u36275 \u20197 \u24341 \u36215 \u28151 \u28102 \u30340 \u26041 \u24335 \u35774 \u32622 \u36134 \u21495 \u65292 \u19981 \u24471 \u20405 \u23475 \u31532 \u19977 \u26041 \u30340 \u21512 \u27861 \u26435 \u30410 \u12290 \
\
5.4.2\uc0\u22312 \u20351 \u29992 \u26412 \u36719 \u20214 \u26381 \u21153 \u26102 \u65292 \u24744 \u24212 \u24403 \u25353 \u26412 \u36719 \u20214 \u39029 \u38754 \u30340 \u25552 \u31034 \u20934 \u30830 \u23436 \u25972 \u22320 \u25552 \u20379 \u24744 \u30340 \u20449 \u24687 \u65288 \u21253 \u25324 \u24744 \u30340 \u22995 \u21517 \u21450 \u30005 \u23376 \u37038 \u20214 \u22320 \u22336 \u12289 \u32852 \u31995 \u30005 \u35805 \u12289 \u32852 \u31995 \u22320 \u22336 \u31561 \u65289 \u65292 \u20197 \u20415 \u26412 \u36719 \u20214 \u25110 \u20854 \u20182 \u29992 \u25143 \u19982 \u24744 \u32852 \u31995 \u12290 \
\
5.4.3\uc0\u24744 \u24212 \u24403 \u21450 \u26102 \u26356 \u26032 \u24744 \u25552 \u20379 \u30340 \u20449 \u24687 \u65292 \u22312 \u27861 \u24459 \u26377 \u26126 \u30830 \u35268 \u23450 \u35201 \u27714 \u26412 \u36719 \u20214 \u24517 \u39035 \u23545 \u29992 \u25143 \u30340 \u20449 \u24687 \u36827 \u34892 \u26680 \u23454 \u30340 \u24773 \u20917 \u19979 \u65292 \u26412 \u36719 \u20214 \u23558 \u20381 \u27861 \u19981 \u26102 \u22320 \u23545 \u24744 \u30340 \u20449 \u24687 \u36827 \u34892 \u26816 \u26597 \u26680 \u23454 \u65292 \u24744 \u24212 \u24403 \u37197 \u21512 \u25552 \u20379 \u26368 \u26032 \u12289 \u30495 \u23454 \u12289 \u23436 \u25972 \u12289 \u26377 \u25928 \u30340 \u20449 \u24687 \u12290 \
\
5.4.4\uc0\u22914 \u26412 \u36719 \u20214 \u25353 \u24744 \u26368 \u21518 \u19968 \u27425 \u25552 \u20379 \u30340 \u20449 \u24687 \u19982 \u24744 \u32852 \u31995 \u26410 \u26524 \u12289 \u24744 \u26410 \u25353 \u26412 \u36719 \u20214 \u30340 \u35201 \u27714 \u21450 \u26102 \u25552 \u20379 \u20449 \u24687 \u12289 \u24744 \u25552 \u20379 \u30340 \u20449 \u24687 \u23384 \u22312 \u26126 \u26174 \u19981 \u23454 \u25110 \u34892 \u25919 \u21496 \u27861 \u26426 \u20851 \u26680 \u23454 \u24744 \u25552 \u20379 \u30340 \u20449 \u24687 \u26080 \u25928 \u30340 \u65292 \u24744 \u23558 \u25215 \u25285 \u22240 \u27492 \u23545 \u24744 \u33258 \u36523 \u12289 \u20182 \u20154 \u21450 \u26412 \u36719 \u20214 \u36896 \u25104 \u30340 \u20840 \u37096 \u25439 \u22833 \u19982 \u19981 \u21033 \u21518 \u26524 \u12290 \u26412 \u36719 \u20214 \u21487 \u21521 \u24744 \u21457 \u20986 \u35810 \u38382 \u25110 \u35201 \u27714 \u25972 \u25913 \u30340 \u36890 \u30693 \u65292 \u24182 \u35201 \u27714 \u24744 \u36827 \u34892 \u37325 \u26032 \u35748 \u35777 \u65292 \u30452 \u33267 \u20013 \u27490 \u12289 \u32456 \u27490 \u23545 \u24744 \u25552 \u20379 \u37096 \u20998 \u25110 \u20840 \u37096 \u36719 \u20214 \u20135 \u21697 \u25110 \u26381 \u21153 \u65292 \u26412 \u36719 \u20214 \u23545 \u27492 \u19981 \u25215 \u25285 \u36131 \u20219 \u12290 \
\
5.5\uc0\u36134 \u21495 \u27880 \u38144 \u21450 \u22238 \u25910 \
\
5.5.1\uc0\u24744 \u21487 \u20197 \u20381 \u29031 \u26412 \u36719 \u20214 \u30340 \u27969 \u31243 \u30003 \u35831 \u27880 \u38144 \u24744 \u30340 \u36134 \u21495 \u65292 \u20294 \u24744 \u20173 \u24212 \u23545 \u24744 \u22312 \u27880 \u38144 \u36134 \u21495 \u21069 \u25110 \u20351 \u29992 \u26412 \u36719 \u20214 \u26381 \u21153 \u26399 \u38388 \u30340 \u34892 \u20026 \u25215 \u25285 \u30456 \u24212 \u36131 \u20219 \u12290 \u27880 \u38144 \u25104 \u21151 \u21518 \u65292 \u38500 \u38750 \u27861 \u24459 \u27861 \u35268 \u21478 \u26377 \u35268 \u23450 \u65292 \u25105 \u20204 \u19981 \u20250 \u20026 \u24744 \u25552 \u20379 \u36134 \u21495 \u35760 \u24405 \u12289 \u20869 \u23481 \u12289 \u34394 \u25311 \u36130 \u20135 \u31561 \u24674 \u22797 \u25110 \u25552 \u20379 \u26381 \u21153 \u65292 \u35831 \u24744 \u35880 \u24910 \u25805 \u20316 \u12290 \
\
5.5.2\uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u20026 \u20102 \u20805 \u20998 \u20351 \u29992 \u36134 \u21495 \u36164 \u28304 \u65292 \u22914 \u24744 \u22312 \u27880 \u20876 \u21518 \u26410 \u21450 \u26102 \u36827 \u34892 \u21021 \u27425 \u30331 \u24405 \u20351 \u29992 \u25110 \u36830 \u32493 \u36229 \u36807 \u12304 6\u12305 \u20010 \u26376 \u26410 \u30331 \u24405 \u36134 \u21495 \u24182 \u20351 \u29992 \u65292 \u19988 \u19981 \u23384 \u22312 \u26410 \u21040 \u26399 \u25110 \u26410 \u23653 \u34892 \u23436 \u27605 \u30340 \u25345 \u32493 \u24615 \u26412 \u36719 \u20214 \u26381 \u21153 \u30340 \u65292 \u25105 \u20204 \u26377 \u26435 \u25910 \u22238 \u24744 \u30340 \u36134 \u21495 \u12290 \u22914 \u24744 \u30340 \u36134 \u21495 \u34987 \u25910 \u22238 \u65292 \u24744 \u26080 \u27861 \u36890 \u36807 \u24744 \u27492 \u21069 \u25345 \u26377 \u30340 \u36134 \u21495 \u30331 \u24405 \u24182 \u20351 \u29992 \u26412 \u36719 \u20214 \u65292 \u24744 \u36134 \u21495 \u19979 \u20445 \u23384 \u30340 \u20010 \u24615 \u21270 \u35774 \u32622 \u21644 \u20351 \u29992 \u35760 \u24405 \u20063 \u23558 \u26080 \u27861 \u24674 \u22797 \u12290 \
\
5.6\uc0\u36134 \u21495 \u36829 \u35268 \u22788 \u32622 \
\uc0\u33509 \u24744 \u22312 \u27880 \u20876 \u25110 \u20351 \u29992 \u26412 \u36719 \u20214 \u36134 \u21495 \u26102 \u65292 \u23384 \u22312 \u20219 \u20309 \u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u25110 \u19981 \u31526 \u21512 \u26412 \u21327 \u35758 \u32422 \u23450 \u30340 \u34892 \u20026 \u65292 \u25105 \u20204 \u26377 \u26435 \u19981 \u20104 \u27880 \u20876 \u65307 \u24050 \u32463 \u27880 \u20876 \u30340 \u65292 \u25105 \u20204 \u26377 \u26435 \u35270 \u24773 \u20917 \u35201 \u27714 \u29992 \u25143 \u38480 \u26399 \u25913 \u27491 \u12289 \u25110 \u21333 \u26041 \u37319 \u21462 \u30701 \u26399 \u23553 \u31105 \u12289 \u27704 \u20037 \u23553 \u31105 \u12289 \u27880 \u38144 \u36134 \u21495 \u31561 \u25514 \u26045 \u12290 \
\
\uc0\u20845 \u12289 \u29992 \u25143 \u34892 \u20026 \u35268 \u33539 \
\
6.1 \uc0\u20449 \u24687 \u20869 \u23481 \u35268 \u33539 \
\
6.1.1 \uc0\u26412 \u26465 \u25152 \u36848 \u20449 \u24687 \u20869 \u23481 \u26159 \u25351 \u29992 \u25143 \u20351 \u29992 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u36807 \u31243 \u20013 \u25152 \u21046 \u20316 \u12289 \u22797 \u21046 \u12289 \u21457 \u24067 \u12289 \u20256 \u25773 \u30340 \u20219 \u20309 \u20869 \u23481 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u12289 \u26165 \u31216 \u12289 \u22836 \u20687 \u12289 \u22995 \u21517 \u12289 \u29992 \u25143 \u35828 \u26126 \u31561 \u27880 \u20876 \u20449 \u24687 \u65292 \u25110 \u25991 \u23383 \u12289 \u35821 \u38899 \u12289 \u22270 \u29255 \u31561 \u21457 \u36865 \u12289 \u22238 \u22797 \u12289 \u22270 \u25991 \u21644 \u30456 \u20851 \u38142 \u25509 \u39029 \u38754 \u65292 \u20197 \u21450 \u20854 \u20182 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u25110 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u25152 \u20135 \u29983 \u30340 \u20869 \u23481 \u12290 \
\
6.1.2 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u31508 \u22696 \u35759 \u20351 \u19968 \u30452 \u33268 \u21147 \u20110 \u20026 \u29992 \u25143 \u25552 \u20379 \u25991 \u26126 \u20581 \u24247 \u12289 \u35268 \u33539 \u26377 \u24207 \u30340 \u32593 \u32476 \u29615 \u22659 \u65292 \u24744 \u19981 \u24471 \u21033 \u29992 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u25110 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u21046 \u20316 \u12289 \u22797 \u21046 \u12289 \u21457 \u24067 \u12289 \u20256 \u25773 \u22914 \u19979 \u24178 \u25200 \u31508 \u22696 \u35759 \u20351 \u27491 \u24120 \u36816 \u34892 \u65292 \u20197 \u21450 \u20405 \u29359 \u20854 \u20182 \u29992 \u25143 \u25110 \u31532 \u19977 \u26041 \u21512 \u27861 \u26435 \u30410 \u30340 \u20869 \u23481 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \
\
6.1.2.1 \uc0\u21457 \u24067 \u12289 \u20256 \u36865 \u12289 \u20256 \u25773 \u12289 \u23384 \u20648 \u36829 \u21453 \u22269 \u23478 \u27861 \u24459 \u27861 \u35268 \u31105 \u27490 \u30340 \u20869 \u23481 \u65306 \
\uc0\u65288 1\u65289 \u36829 \u21453 \u23466 \u27861 \u35268 \u23450 \u30340 \u22522 \u26412 \u21407 \u21017 \u65292 \u29053 \u21160 \u25239 \u25298 \u25110 \u32773 \u30772 \u22351 \u23466 \u27861 \u31561 \u27861 \u24459 \u27861 \u35268 \u23454 \u26045 \u30340 \u65307 \
\uc0\u65288 2\u65289 \u21361 \u23475 \u22269 \u23478 \u23433 \u20840 \u65292 \u27844 \u38706 \u22269 \u23478 \u31192 \u23494 \u65292 \u39072 \u35206 \u22269 \u23478 \u25919 \u26435 \u65292 \u30772 \u22351 \u22269 \u23478 \u32479 \u19968 \u12289 \u20027 \u26435 \u21644 \u39046 \u22303 \u23436 \u25972 \u30340 \u65307 \
\uc0\u65288 3\u65289 \u25439 \u23475 \u22269 \u23478 \u33635 \u35465 \u21644 \u21033 \u30410 \u12290 \u23459 \u25196 \u24656 \u24598 \u20027 \u20041 \u12289 \u26497 \u31471 \u20027 \u20041 \u30340 \u65307 \
\uc0\u65288 4\u65289 \u35787 \u27585 \u27665 \u26063 \u20248 \u31168 \u25991 \u21270 \u20256 \u32479 \u65292 \u29053 \u21160 \u27665 \u26063 \u20167 \u24680 \u12289 \u27665 \u26063 \u27495 \u35270 \u65292 \u20405 \u23475 \u27665 \u26063 \u39118 \u20439 \u20064 \u24815 \u65292 \u27498 \u26354 \u27665 \u26063 \u21382 \u21490 \u21644 \u27665 \u26063 \u21382 \u21490 \u20154 \u29289 \u65292 \u20260 \u23475 \u27665 \u26063 \u24863 \u24773 \u65292 \u30772 \u22351 \u27665 \u26063 \u22242 \u32467 \u30340 \u65307 \
\uc0\u65288 5\u65289 \u29053 \u21160 \u30772 \u22351 \u22269 \u23478 \u23447 \u25945 \u25919 \u31574 \u65292 \u23459 \u25196 \u23447 \u25945 \u29378 \u28909 \u65292 \u21361 \u23475 \u23447 \u25945 \u21644 \u30566 \u65292 \u20260 \u23475 \u20449 \u25945 \u20844 \u27665 \u23447 \u25945 \u24863 \u24773 \u65292 \u30772 \u22351 \u20449 \u25945 \u20844 \u27665 \u21644 \u19981 \u20449 \u25945 \u20844 \u27665 \u22242 \u32467 \u65292 \u23459 \u25196 \u37034 \u25945 \u21644 \u23553 \u24314 \u36855 \u20449 \u30340 \u65307 \
\uc0\u65288 6\u65289 \u25955 \u24067 \u35875 \u35328 \u65292 \u21361 \u23475 \u31038 \u20250 \u20844 \u24503 \u65292 \u25200 \u20081 \u31038 \u20250 \u31209 \u24207 \u65292 \u30772 \u22351 \u31038 \u20250 \u31283 \u23450 \u30340 \u65307 \
\uc0\u65288 7\u65289 \u25955 \u24067 \u28139 \u31229 \u12289 \u33394 \u24773 \u12289 \u36172 \u21338 \u12289 \u26292 \u21147 \u12289 \u24656 \u24598 \u65292 \u20256 \u25480 \u29359 \u32618 \u26041 \u27861 \u25110 \u32773 \u25945 \u21766 \u29359 \u32618 \u30340 \u65307 \
\uc0\u65288 8\u65289 \u35837 \u35876 \u25110 \u32773 \u20398 \u36785 \u20182 \u20154 \u65292 \u20405 \u23475 \u20182 \u20154 \u21512 \u27861 \u26435 \u30410 \u30340 \u65307 \
\uc0\u65288 9\u65289 \u29053 \u21160 \u38750 \u27861 \u38598 \u20250 \u12289 \u32467 \u31038 \u12289 \u28216 \u34892 \u12289 \u31034 \u23041 \u12289 \u32858 \u20247 \u25200 \u20081 \u31038 \u20250 \u31209 \u24207 \u65307 \
\uc0\u65288 10\u65289 \u20197 \u38750 \u27861 \u27665 \u38388 \u32452 \u32455 \u21517 \u20041 \u27963 \u21160 \u30340 \u65307 \
\uc0\u65288 11\u65289 \u19981 \u31526 \u21512 \u12298 \u21363 \u26102 \u36890 \u35759 \u24037 \u20855 \u20844 \u20247 \u20449 \u24687 \u26381 \u21153 \u21457 \u23637 \u31649 \u29702 \u26242 \u34892 \u35268 \u23450 \u12299 \u21450 \u36981 \u23432 \u27861 \u24459 \u27861 \u35268 \u12289 \u31038 \u20250 \u20027 \u20041 \u21046 \u24230 \u12289 \u22269 \u23478 \u21033 \u30410 \u12289 \u20844 \u27665 \u21512 \u27861 \u21033 \u30410 \u12289 \u20844 \u20849 \u31209 \u24207 \u12289 \u31038 \u20250 \u36947 \u24503 \u39118 \u23578 \u21644 \u20449 \u24687 \u30495 \u23454 \u24615 \u31561 \'93\u19971 \u26465 \u24213 \u32447 \'94\u35201 \u27714 \u30340 \u65307 \
\uc0\u65288 12\u65289 \u20405 \u23475 \u26410 \u25104 \u24180 \u20154 \u21512 \u27861 \u26435 \u30410 \u25110 \u32773 \u25439 \u23475 \u26410 \u25104 \u24180 \u20154 \u36523 \u24515 \u20581 \u24247 \u30340 \u65307 \
\uc0\u65288 13\u65289 \u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u24213 \u32447 \u12289 \u31038 \u20250 \u20027 \u20041 \u21046 \u24230 \u24213 \u32447 \u12289 \u22269 \u23478 \u21033 \u30410 \u24213 \u32447 \u12289 \u20844 \u27665 \u21512 \u27861 \u26435 \u30410 \u24213 \u32447 \u12289 \u31038 \u20250 \u20844 \u20849 \u31209 \u24207 \u24213 \u32447 \u12289 \u36947 \u24503 \u39118 \u23578 \u24213 \u32447 \u21644 \u20449 \u24687 \u30495 \u23454 \u24615 \u24213 \u32447 \u30340 \'93\u19971 \u26465 \u24213 \u32447 \'94\u35201 \u27714 \u30340 \u65307 \
\uc0\u65288 14\u65289 \u21547 \u26377 \u27861 \u24459 \u12289 \u34892 \u25919 \u27861 \u35268 \u31105 \u27490 \u30340 \u20854 \u20182 \u20869 \u23481 \u30340 \u12290 \
\
6.1.2.2 \uc0\u21457 \u24067 \u12289 \u20256 \u36865 \u12289 \u20256 \u25773 \u12289 \u23384 \u20648 \u20405 \u29359 \u20182 \u20154 \u21517 \u35465 \u26435 \u12289 \u32918 \u20687 \u26435 \u12289 \u30693 \u35782 \u20135 \u26435 \u12289 \u21830 \u19994 \u31192 \u23494 \u31561 \u21512 \u27861 \u26435 \u21033 \u30340 \u20869 \u23481 \u65307 \
\
6.1.2.3 \uc0\u28041 \u21450 \u20182 \u20154 \u38544 \u31169 \u12289 \u20010 \u20154 \u20449 \u24687 \u25110 \u36164 \u26009 \u30340 \u65307 \
\
6.1.2.4 \uc0\u21457 \u34920 \u12289 \u20256 \u36865 \u12289 \u20256 \u25773 \u39578 \u25200 \u12289 \u24191 \u21578 \u20449 \u24687 \u12289 \u36807 \u24230 \u33829 \u38144 \u20449 \u24687 \u21450 \u22403 \u22334 \u20449 \u24687 \u25110 \u21547 \u26377 \u20219 \u20309 \u24615 \u25110 \u24615 \u26263 \u31034 \u30340 \u65307 \
\
6.1.2.5 \uc0\u20854 \u20182 \u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u12289 \u25919 \u31574 \u21450 \u20844 \u24207 \u33391 \u20439 \u12289 \u31038 \u20250 \u20844 \u24503 \u25110 \u24178 \u25200 \u31508 \u22696 \u35759 \u20351 \u27491 \u24120 \u36816 \u34892 \u21644 \u20405 \u29359 \u20854 \u20182 \u29992 \u25143 \u25110 \u31532 \u19977 \u26041 \u21512 \u27861 \u26435 \u30410 \u20869 \u23481 \u30340 \u20449 \u24687 \u12290 \
\
6.1.2.6 \uc0\u24744 \u36890 \u36807 \u26412 \u26381 \u21153 \u21457 \u36865 \u25110 \u20256 \u25773 \u30340 \u20869 \u23481 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u32593 \u39029 \u12289 \u25991 \u23383 \u12289 \u22270 \u29255 \u12289 \u38899 \u39057 \u12289 \u35270 \u39057 \u12289 \u22270 \u34920 \u31561 \u65289 \u22343 \u30001 \u24744 \u33258 \u34892 \u25215 \u25285 \u36131 \u20219 \u12290 \
\
6.1.2.6.1 \uc0\u24744 \u21457 \u36865 \u25110 \u20256 \u25773 \u30340 \u20869 \u23481 \u24212 \u26377 \u21512 \u27861 \u26469 \u28304 \u65292 \u30456 \u20851 \u20869 \u23481 \u20026 \u24744 \u25152 \u26377 \u25110 \u24744 \u24050 \u33719 \u24471 \u26435 \u21033 \u20154 \u30340 \u25480 \u26435 \u12290 \
\
6.1.2.6.2 \uc0\u24744 \u21516 \u24847 \u31508 \u22696 \u35759 \u20351 \u21487 \u20026 \u23653 \u34892 \u26412 \u21327 \u35758 \u25110 \u25552 \u20379 \u26412 \u26381 \u21153 \u30340 \u30446 \u30340 \u32780 \u20351 \u29992 \u24744 \u21457 \u36865 \u25110 \u20256 \u25773 \u30340 \u20869 \u23481 \u12290 \
\
6.1.2.6.3 \uc0\u22914 \u26524 \u31508 \u22696 \u35759 \u20351 \u25910 \u21040 \u26435 \u21033 \u20154 \u36890 \u30693 \u65292 \u20027 \u24352 \u24744 \u21457 \u36865 \u25110 \u20256 \u25773 \u30340 \u20869 \u23481 \u20405 \u29359 \u20854 \u30456 \u20851 \u26435 \u21033 \u30340 \u65292 \u24744 \u21516 \u24847 \u31508 \u22696 \u35759 \u20351 \u26377 \u26435 \u36827 \u34892 \u29420 \u31435 \u21028 \u26029 \u24182 \u37319 \u21462 \u21024 \u38500 \u12289 \u23631 \u34109 \u25110 \u26029 \u24320 \u38142 \u25509 \u31561 \u25514 \u26045 \u12290 \
\
6.1.2.7 \uc0\u24744 \u20351 \u29992 \u26412 \u26381 \u21153 \u26102 \u19981 \u24471 \u36829 \u21453 \u22269 \u23478 \u27861 \u24459 \u27861 \u35268 \u12289 \u20405 \u29359 \u20182 \u20154 \u21512 \u27861 \u26435 \u30410 \u12290 \
\uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u22914 \u26524 \u24744 \u34987 \u20182 \u20154 \u25237 \u35785 \u20405 \u26435 \u25110 \u24744 \u25237 \u35785 \u20182 \u20154 \u20405 \u26435 \u65292 \u31508 \u22696 \u35759 \u20351 \u26377 \u26435 \u23558 \u20105 \u35758 \u20013 \u30456 \u20851 \u26041 \u30340 \u20027 \u20307 \u12289 \u32852 \u31995 \u26041 \u24335 \u12289 \u25237 \u35785 \u30456 \u20851 \u20869 \u23481 \u31561 \u24517 \u35201 \u20449 \u24687 \u25552 \u20379 \u32473 \u20854 \u20182 \u20105 \u35758 \u26041 \u25110 \u30456 \u20851 \u37096 \u38376 \u65292 \u20197 \u20415 \u21450 \u26102 \u35299 \u20915 \u25237 \u35785 \u32416 \u32439 \u65292 \u20445 \u25252 \u20182 \u20154 \u21512 \u27861 \u26435 \u30410 \u12290 \
\
6.2 \uc0\u36719 \u20214 \u20351 \u29992 \u35268 \u33539 \
\
6.2.1 \uc0\u38500 \u38750 \u27861 \u24459 \u20801 \u35768 \u25110 \u25105 \u20204 \u20070 \u38754 \u35768 \u21487 \u65292 \u24744 \u20351 \u29992 \u26412 \u36719 \u20214 \u36807 \u31243 \u20013 \u19981 \u24471 \u20174 \u20107 \u19979 \u21015 \u34892 \u20026 \u65306 \
\
6.2.1.1 \uc0\u21024 \u38500 \u26412 \u36719 \u20214 \u21450 \u20854 \u21103 \u26412 \u19978 \u20851 \u20110 \u33879 \u20316 \u26435 \u30340 \u20449 \u24687 \u65307 \
\
6.2.1.2 \uc0\u23545 \u26412 \u36719 \u20214 \u36827 \u34892 \u21453 \u21521 \u24037 \u31243 \u12289 \u21453 \u21521 \u27719 \u32534 \u12289 \u21453 \u21521 \u32534 \u35793 \u65292 \u25110 \u32773 \u20197 \u20854 \u20182 \u26041 \u24335 \u23581 \u35797 \u21457 \u29616 \u26412 \u36719 \u20214 \u30340 \u28304 \u20195 \u30721 \u65307 \
\
6.2.1.3 \uc0\u23545 \u25105 \u20204 \u20139 \u26377 \u30693 \u35782 \u20135 \u26435 \u30340 \u20869 \u23481 \u36827 \u34892 \u20351 \u29992 \u12289 \u20986 \u31199 \u12289 \u20986 \u20511 \u12289 \u22797 \u21046 \u12289 \u20462 \u25913 \u12289 \u38142 \u25509 \u12289 \u36716 \u36733 \u12289 \u27719 \u32534 \u12289 \u21457 \u34920 \u12289 \u20986 \u29256 \u12289 \u24314 \u31435 \u38236 \u20687 \u31449 \u28857 \u31561 \u65307 \
\
6.2.1.4 \uc0\u23545 \u26412 \u36719 \u20214 \u25110 \u32773 \u26412 \u36719 \u20214 \u36816 \u34892 \u36807 \u31243 \u20013 \u37322 \u25918 \u21040 \u20219 \u20309 \u32456 \u31471 \u20869 \u23384 \u20013 \u30340 \u25968 \u25454 \u12289 \u36719 \u20214 \u36816 \u34892 \u36807 \u31243 \u20013 \u23458 \u25143 \u31471 \u19982 \u26381 \u21153 \u22120 \u31471 \u30340 \u20132 \u20114 \u25968 \u25454 \u65292 \u20197 \u21450 \u26412 \u36719 \u20214 \u36816 \u34892 \u25152 \u24517 \u38656 \u30340 \u31995 \u32479 \u25968 \u25454 \u65292 \u36827 \u34892 \u22797 \u21046 \u12289 \u20462 \u25913 \u12289 \u22686 \u21152 \u12289 \u21024 \u38500 \u12289 \u25346 \u36733 \u36816 \u34892 \u25110 \u21019 \u24314 \u20219 \u20309 \u34893 \u29983 \u20316 \u21697 \u65292 \u24418 \u24335 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u20351 \u29992 \u25554 \u20214 \u12289 \u22806 \u25346 \u25110 \u38750 \u25105 \u20204 \u32463 \u25480 \u26435 \u30340 \u31532 \u19977 \u26041 \u24037 \u20855 /\u26381 \u21153 \u25509 \u20837 \u26412 \u36719 \u20214 \u21644 \u30456 \u20851 \u31995 \u32479 \u65307 \
\
6.2.1.5 \uc0\u36890 \u36807 \u20462 \u25913 \u25110 \u20266 \u36896 \u36719 \u20214 \u36816 \u34892 \u20013 \u30340 \u25351 \u20196 \u12289 \u25968 \u25454 \u65292 \u22686 \u21152 \u12289 \u21024 \u38500 \u12289 \u25913 \u21464 \u36719 \u20214 \u30340 \u21151 \u33021 \u25110 \u36816 \u34892 \u25928 \u26524 \u65292 \u25110 \u32773 \u23558 \u29992 \u20110 \u19978 \u36848 \u30446 \u30340 \u30340 \u36719 \u20214 \u12289 \u26041 \u27861 \u36827 \u34892 \u36816 \u33829 \u25110 \u21521 \u20844 \u20247 \u20256 \u25773 \u65292 \u26080 \u35770 \u36825 \u20123 \u34892 \u20026 \u26159 \u21542 \u20026 \u21830 \u19994 \u30446 \u30340 \u65307 \
\
6.2.1.6 \uc0\u36890 \u36807 \u38750 \u25105 \u20204 \u24320 \u21457 \u12289 \u25480 \u26435 \u12289 \u35768 \u21487 \u30340 \u31532 \u19977 \u26041 \u36719 \u20214 \u12289 \u25554 \u20214 \u12289 \u22806 \u25346 \u12289 \u31995 \u32479 \u65292 \u30331 \u24405 \u25110 \u20351 \u29992 \u25105 \u20204 \u30340 \u36719 \u20214 \u21450 \u26381 \u21153 \u65292 \u25110 \u21046 \u20316 \u12289 \u21457 \u24067 \u12289 \u20256 \u25773 \u19978 \u36848 \u24037 \u20855 \u65292 \u25110 \u23545 \u26412 \u36719 \u20214 \u30340 \u27491 \u24120 \u36816 \u34892 \u36827 \u34892 \u24178 \u25200 \u12289 \u30772 \u22351 \u12289 \u20462 \u25913 \u25110 \u26045 \u21152 \u20854 \u20182 \u24433 \u21709 \u65307 \
\
6.2.1.7 \uc0\u33258 \u34892 \u25110 \u32773 \u25480 \u26435 \u20182 \u20154 \u12289 \u31532 \u19977 \u26041 \u36719 \u20214 \u23545 \u26412 \u36719 \u20214 \u21450 \u20854 \u32452 \u20214 \u12289 \u27169 \u22359 \u12289 \u25968 \u25454 \u36827 \u34892 \u24178 \u25200 \u65307 \
\
6.2.1.8 \uc0\u20197 \u20219 \u20309 \u26041 \u24335 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u30423 \u38142 \u12289 \u20887 \u20313 \u30423 \u21462 \u12289 \u38750 \u27861 \u25235 \u21462 \u12289 \u27169 \u25311 \u19979 \u36733 \u12289 \u28145 \u24230 \u38142 \u25509 \u12289 \u20551 \u20882 \u27880 \u20876 \u31561 \u65289 \u30452 \u25509 \u25110 \u38388 \u25509 \u30423 \u21462 \u26412 \u36719 \u20214 \u30340 \u35270 \u39057 \u12289 \u22270 \u25991 \u12289 \u29992 \u25143 \u20449 \u24687 \u31561 \u20449 \u24687 \u20869 \u23481 \u65307 \
\
6.2.1.9 \uc0\u21033 \u29992 \u25110 \u38024 \u23545 \u26412 \u36719 \u20214 \u36827 \u34892 \u20219 \u20309 \u21361 \u23475 \u35745 \u31639 \u26426 \u32593 \u32476 \u23433 \u20840 \u30340 \u34892 \u20026 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \u38750 \u27861 \u20405 \u20837 \u32593 \u32476 \u12289 \u24178 \u25200 \u32593 \u32476 \u27491 \u24120 \u21151 \u33021 \u12289 \u31363 \u21462 \u32593 \u32476 \u25968 \u25454 \u31561 \u21361 \u23475 \u32593 \u32476 \u23433 \u20840 \u30340 \u27963 \u21160 \u65292 \u25110 \u20026 \u19978 \u36848 \u27963 \u21160 \u25552 \u20379 \u31243 \u24207 \u12289 \u24037 \u20855 \u31561 \u25903 \u25345 \u19982 \u24110 \u21161 \u65307 \u20225 \u22270 \u24178 \u28041 \u12289 \u30772 \u22351 \u26412 \u36719 \u20214 \u31995 \u32479 \u25110 \u32593 \u31449 \u30340 \u27491 \u24120 \u36816 \u34892 \u65292 \u25925 \u24847 \u20256 \u25773 \u24694 \u24847 \u31243 \u24207 \u25110 \u30149 \u27602 \u20197 \u21450 \u20854 \u20182 \u30772 \u22351 \u24178 \u25200 \u27491 \u24120 \u32593 \u32476 \u20449 \u24687 \u26381 \u21153 \u30340 \u34892 \u20026 \u31561 \u65307 \
\
6.2.1.10 \uc0\u20854 \u20182 \u20197 \u20219 \u20309 \u19981 \u21512 \u27861 \u30340 \u26041 \u24335 \u12289 \u20026 \u20219 \u20309 \u19981 \u21512 \u27861 \u30340 \u30446 \u30340 \u12289 \u25110 \u19982 \u25105 \u20204 \u20026 \u27492 \u21046 \u23450 \u30340 \u20854 \u20182 \u35268 \u33539 \u21644 \u26631 \u20934 \u19981 \u19968 \u33268 \u30340 \u26041 \u24335 \u20351 \u29992 \u26412 \u36719 \u20214 \u21450 \u26410 \u32463 \u25105 \u20204 \u26126 \u30830 \u25480 \u26435 \u30340 \u34892 \u20026 \u12290 \
\
6.2.2 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u22522 \u20110 \u29992 \u25143 \u20307 \u39564 \u12289 \u31508 \u22696 \u35759 \u20351 \u25110 \u30456 \u20851 \u26381 \u21153 \u24179 \u21488 \u36816 \u33829 \u23433 \u20840 \u12289 \u24179 \u21488 \u35268 \u21017 \u35201 \u27714 \u21450 \u20581 \u24247 \u21457 \u23637 \u31561 \u32508 \u21512 \u22240 \u32032 \u65292 \u25105 \u20204 \u26377 \u26435 \u36873 \u25321 \u25552 \u20379 \u26381 \u21153 \u30340 \u23545 \u35937 \u65292 \u26377 \u26435 \u20915 \u23450 \u21151 \u33021 \u35774 \u32622 \u65292 \u26377 \u26435 \u20915 \u23450 \u21151 \u33021 \u24320 \u25918 \u12289 \u25968 \u25454 \u25509 \u21475 \u21644 \u30456 \u20851 \u25968 \u25454 \u25259 \u38706 \u30340 \u23545 \u35937 \u21644 \u33539 \u22260 \u12290 \u38024 \u23545 \u20197 \u19979 \u24773 \u24418 \u65292 \u26377 \u26435 \u35270 \u20855 \u20307 \u24773 \u20917 \u20013 \u27490 \u25110 \u32456 \u27490 \u25552 \u20379 \u26412 \u26381 \u21153 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \
\
6.2.2.1 \uc0\u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u25110 \u26412 \u21327 \u35758 \u35268 \u23450 \u30340 \u65307 \
\
6.2.2.2 \uc0\u24433 \u21709 \u26381 \u21153 \u20307 \u39564 \u30340 \u65307 \
\
6.2.2.3 \uc0\u23384 \u22312 \u23433 \u20840 \u38544 \u24739 \u30340 \u65307 \
\
6.2.2.4 \uc0\u19982 \u31508 \u22696 \u35759 \u20351 \u25110 \u20854 \u26381 \u21153 \u24179 \u21488 \u24050 \u26377 \u20027 \u35201 \u21151 \u33021 \u25110 \u21151 \u33021 \u32452 \u20214 \u30456 \u20284 \u12289 \u30456 \u21516 \u65292 \u25110 \u21487 \u23454 \u29616 \u19978 \u36848 \u21151 \u33021 \u25110 \u21151 \u33021 \u32452 \u20214 \u30340 \u20027 \u35201 \u25928 \u29992 \u30340 \u65307 \
\
6.2.2.5 \uc0\u30028 \u38754 \u12289 \u39118 \u26684 \u12289 \u21151 \u33021 \u12289 \u25551 \u36848 \u25110 \u20351 \u29992 \u32773 \u20307 \u39564 \u19982 \u31508 \u22696 \u35759 \u20351 \u25110 \u20854 \u26381 \u21153 \u24179 \u21488 \u30456 \u20284 \u65292 \u21487 \u33021 \u36896 \u25104 \u31508 \u22696 \u35759 \u20351 \u29992 \u25143 \u35748 \u20026 \u20854 \u25152 \u20351 \u29992 \u30340 \u21151 \u33021 \u25110 \u26381 \u21153 \u26469 \u28304 \u20110 \u25105 \u20204 \u25110 \u32463 \u25105 \u20204 \u25480 \u26435 \u30340 \u65307 \
\
6.2.2.6 \uc0\u36829 \u21453 \u31508 \u22696 \u35759 \u20351 \u25110 \u20854 \u26381 \u21153 \u24179 \u21488 \u36816 \u33829 \u21407 \u21017 \u65292 \u25110 \u19981 \u31526 \u21512 \u25105 \u20204 \u20854 \u20182 \u31649 \u29702 \u35201 \u27714 \u30340 \u12290 \
\
6.3 \uc0\u26381 \u21153 \u36816 \u33829 \u35268 \u33539 \
\
6.3.1 \uc0\u38500 \u38750 \u24744 \u19982 \u31508 \u22696 \u35759 \u20351 \u21478 \u26377 \u32422 \u23450 \u65292 \u24744 \u21516 \u24847 \u26412 \u26381 \u21153 \u20165 \u20026 \u24744 \u20010 \u20154 \u38750 \u21830 \u19994 \u24615 \u36136 \u30340 \u20351 \u29992 \u12290 \
\
6.3.2 \uc0\u24744 \u24212 \u24403 \u36890 \u36807 \u31508 \u22696 \u35759 \u20351 \u25552 \u20379 \u25110 \u35748 \u21487 \u30340 \u26041 \u24335 \u20351 \u29992 \u26412 \u26381 \u21153 \u12290 \u24744 \u20381 \u26412 \u21327 \u35758 \u26465 \u27454 \u25152 \u33719 \u24471 \u30340 \u26435 \u21033 \u19981 \u21487 \u36716 \u35753 \u12290 \
\
6.3.3 \uc0\u24744 \u19981 \u24471 \u20351 \u29992 \u26410 \u32463 \u31508 \u22696 \u35759 \u20351 \u25480 \u26435 \u30340 \u25554 \u20214 \u12289 \u22806 \u25346 \u25110 \u31532 \u19977 \u26041 \u24037 \u20855 \u23545 \u26412 \u21327 \u35758 \u39033 \u19979 \u30340 \u26381 \u21153 \u36827 \u34892 \u24178 \u25200 \u12289 \u30772 \u22351 \u12289 \u20462 \u25913 \u25110 \u26045 \u21152 \u20854 \u20182 \u24433 \u21709 \u12290 \
\
6.3.4 \uc0\u38500 \u38750 \u27861 \u24459 \u20801 \u35768 \u25110 \u25105 \u20204 \u20070 \u38754 \u35768 \u21487 \u65292 \u24744 \u20351 \u29992 \u26412 \u26381 \u21153 \u36807 \u31243 \u20013 \u19981 \u24471 \u20174 \u20107 \u19979 \u21015 \u34892 \u20026 \u65306 \
\
1) \uc0\u25552 \u20132 \u12289 \u21457 \u24067 \u34394 \u20551 \u20449 \u24687 \u65292 \u25110 \u20882 \u20805 \u12289 \u21033 \u29992 \u20182 \u20154 \u21517 \u20041 \u30340 \u65307 \
\
2) \uc0\u35825 \u23548 \u20854 \u20182 \u29992 \u25143 \u28857 \u20987 \u38142 \u25509 \u39029 \u38754 \u25110 \u20998 \u20139 \u20449 \u24687 \u30340 \u65307 \
\
3) \uc0\u34394 \u26500 \u20107 \u23454 \u12289 \u38544 \u30610 \u30495 \u30456 \u20197 \u35823 \u23548 \u12289 \u27450 \u39575 \u20182 \u20154 \u30340 \u65307 \
\
4) \uc0\u20405 \u29359 \u20182 \u20154 \u21517 \u35465 \u26435 \u12289 \u32918 \u20687 \u26435 \u12289 \u30693 \u35782 \u20135 \u26435 \u12289 \u21830 \u19994 \u31192 \u23494 \u31561 \u21512 \u27861 \u26435 \u21033 \u30340 \u65307 \
\
5) \uc0\u26410 \u32463 \u25105 \u20204 \u20070 \u38754 \u35768 \u21487 \u21033 \u29992 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u21644 \u20219 \u20309 \u21151 \u33021 \u65292 \u20197 \u21450 \u31532 \u19977 \u26041 \u36816 \u33829 \u24179 \u21488 \u36827 \u34892 \u25512 \u24191 \u25110 \u30456 \u20114 \u25512 \u24191 \u30340 \u65307 \
\
6) \uc0\u21033 \u29992 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u25110 \u26412 \u36719 \u20214 \u21450 \u26381 \u21153 \u20174 \u20107 \u20219 \u20309 \u36829 \u27861 \u29359 \u32618 \u27963 \u21160 \u30340 \u65307 \
\
8) \uc0\u20854 \u20182 \u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u35268 \u23450 \u12289 \u20405 \u29359 \u20854 \u20182 \u29992 \u25143 \u21512 \u27861 \u26435 \u30410 \u12289 \u24178 \u25200 \u20135 \u21697 \u27491 \u24120 \u36816 \u33829 \u25110 \u25105 \u20204 \u26410 \u26126 \u30830 \u25480 \u26435 \u30340 \u34892 \u20026 \u12290 \
\
6.3.5 \uc0\u24744 \u22312 \u20351 \u29992 \u26412 \u26381 \u21153 \u36807 \u31243 \u20013 \u24212 \u24403 \u36981 \u23432 \u24403 \u22320 \u30456 \u20851 \u30340 \u27861 \u24459 \u27861 \u35268 \u65292 \u24182 \u23562 \u37325 \u24403 \u22320 \u30340 \u36947 \u24503 \u21644 \u39118 \u20439 \u20064 \u24815 \u12290 \u22914 \u26524 \u24744 \u30340 \u34892 \u20026 \u36829 \u21453 \u20102 \u24403 \u22320 \u27861 \u24459 \u27861 \u35268 \u25110 \u36947 \u24503 \u39118 \u20439 \u65292 \u24744 \u24212 \u24403 \u20026 \u27492 \u21333 \u29420 \u25215 \u25285 \u36131 \u20219 \u12290 \
\
6.3.6 \uc0\u24744 \u24212 \u36991 \u20813 \u22240 \u20351 \u29992 \u26412 \u26381 \u21153 \u32780 \u20351 \u31508 \u22696 \u35759 \u20351 \u21367 \u20837 \u25919 \u27835 \u21644 \u20844 \u20849 \u20107 \u20214 \u65292 \u21542 \u21017 \u31508 \u22696 \u35759 \u20351 \u26377 \u26435 \u26242 \u20572 \u25110 \u32456 \u27490 \u23545 \u24744 \u30340 \u26381 \u21153 \u12290 \
\
6.4 \uc0\u23545 \u33258 \u24049 \u34892 \u20026 \u36127 \u36131 \
\uc0\u24744 \u20805 \u20998 \u20102 \u35299 \u24182 \u21516 \u24847 \u65292 \u24744 \u24517 \u39035 \u20026 \u33258 \u24049 \u27880 \u20876 \u36134 \u21495 \u19979 \u30340 \u19968 \u20999 \u34892 \u20026 \u36127 \u36131 \u65292 \u21253 \u25324 \u24744 \u25152 \u21457 \u34920 \u30340 \u20219 \u20309 \u20869 \u23481 \u20197 \u21450 \u30001 \u27492 \u20135 \u29983 \u30340 \u20219 \u20309 \u21518 \u26524 \u12290 \u24744 \u24212 \u23545 \u26412 \u26381 \u21153 \u20013 \u30340 \u20869 \u23481 \u33258 \u34892 \u21152 \u20197 \u21028 \u26029 \u65292 \u24182 \u25215 \u25285 \u22240 \u20351 \u29992 \u20869 \u23481 \u32780 \u24341 \u36215 \u30340 \u25152 \u26377 \u39118 \u38505 \u65292 \u21253 \u25324 \u22240 \u23545 \u20869 \u23481 \u30340 \u27491 \u30830 \u24615 \u12289 \u23436 \u25972 \u24615 \u25110 \u23454 \u29992 \u24615 \u30340 \u20381 \u36182 \u32780 \u20135 \u29983 \u30340 \u39118 \u38505 \u12290 \u25105 \u20204 \u26080 \u27861 \u19988 \u19981 \u20250 \u23545 \u22240 \u21069 \u36848 \u39118 \u38505 \u32780 \u23548 \u33268 \u30340 \u20219 \u20309 \u25439 \u22833 \u25110 \u25439 \u23475 \u25215 \u25285 \u36131 \u20219 \u12290 \
\
6.5 \uc0\u36829 \u32422 \u22788 \u29702 \
\
6.5.1 \uc0\u22914 \u26524 \u25105 \u20204 \u21457 \u29616 \u25110 \u25910 \u21040 \u20182 \u20154 \u20030 \u25253 \u25110 \u25237 \u35785 \u29992 \u25143 \u36829 \u21453 \u26412 \u21327 \u35758 \u32422 \u23450 \u30340 \u65292 \u25105 \u20204 \u26377 \u26435 \u19981 \u32463 \u36890 \u30693 \u38543 \u26102 \u23545 \u30456 \u20851 \u20869 \u23481 \u36827 \u34892 \u21024 \u38500 \u12289 \u23631 \u34109 \u65292 \u24182 \u35270 \u34892 \u20026 \u24773 \u33410 \u23545 \u36829 \u35268 \u36134 \u21495 \u22788 \u20197 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u35686 \u21578 \u12289 \u38480 \u21046 \u25110 \u31105 \u27490 \u20351 \u29992 \u37096 \u20998 \u25110 \u20840 \u37096 \u21151 \u33021 \u12289 \u36134 \u21495 \u23553 \u31105 \u30452 \u33267 \u27880 \u38144 \u30340 \u22788 \u32602 \u65292 \u24182 \u20844 \u21578 \u22788 \u29702 \u32467 \u26524 \u12290 \
\
6.5.2 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u25105 \u20204 \u26377 \u26435 \u20381 \u25454 \u21512 \u29702 \u21028 \u26029 \u23545 \u36829 \u21453 \u26377 \u20851 \u27861 \u24459 \u27861 \u35268 \u25110 \u26412 \u21327 \u35758 \u35268 \u23450 \u30340 \u34892 \u20026 \u36827 \u34892 \u22788 \u32602 \u65292 \u23545 \u36829 \u27861 \u36829 \u35268 \u30340 \u20219 \u20309 \u29992 \u25143 \u37319 \u21462 \u36866 \u24403 \u30340 \u27861 \u24459 \u34892 \u21160 \u65292 \u24182 \u20381 \u25454 \u27861 \u24459 \u27861 \u35268 \u20445 \u23384 \u26377 \u20851 \u20449 \u24687 \u21521 \u26377 \u20851 \u37096 \u38376 \u25253 \u21578 \u31561 \u65292 \u29992 \u25143 \u24212 \u21333 \u29420 \u25215 \u25285 \u30001 \u27492 \u32780 \u20135 \u29983 \u30340 \u19968 \u20999 \u27861 \u24459 \u36131 \u20219 \u12290 \
\
6.5.3 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u22240 \u24744 \u36829 \u21453 \u26412 \u21327 \u35758 \u25110 \u30456 \u20851 \u26381 \u21153 \u26465 \u27454 \u30340 \u35268 \u23450 \u65292 \u23548 \u33268 \u25110 \u20135 \u29983 \u31532 \u19977 \u26041 \u20027 \u24352 \u30340 \u20219 \u20309 \u32034 \u36180 \u12289 \u35201 \u27714 \u25110 \u25439 \u22833 \u65292 \u24744 \u24212 \u24403 \u29420 \u31435 \u25215 \u25285 \u36131 \u20219 \u65307 \u25105 \u20204 \u22240 \u27492 \u36973 \u21463 \u25439 \u22833 \u30340 \u65292 \u24744 \u20063 \u24212 \u24403 \u19968 \u24182 \u36180 \u20607 \u12290 \
\
6.5.4\uc0\u22914 \u24744 \u36829 \u21453 \u26412 \u21327 \u35758 \u32422 \u23450 \u65292 \u25105 \u20204 \u26377 \u26435 \u20316 \u20986 \u29420 \u31435 \u21028 \u26029 \u65292 \u31435 \u21363 \u26242 \u20572 \u25110 \u32456 \u27490 \u21521 \u24744 \u25552 \u20379 \u37096 \u20998 \u25110 \u20840 \u37096 \u26412 \u36719 \u20214 \u26381 \u21153 \u65292 \u21253 \u25324 \u31105 \u35328 \u12289 \u23631 \u34109 \u20449 \u24687 \u12289 \u21024 \u38500 \u21457 \u24067 \u20869 \u23481 \u12289 \u23553 \u31105 \u36134 \u21495 \u12289 \u27880 \u38144 \u36134 \u21495 \u31561 \u25514 \u26045 \u12290 \u24744 \u24212 \u33258 \u34892 \u25215 \u25285 \u33258 \u36523 \u25439 \u22833 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u34394 \u25311 \u36130 \u20135 \u28165 \u31354 \u12289 \u26080 \u27861 \u27491 \u24120 \u20351 \u29992 \u36134 \u21495 \u21450 \u30456 \u20851 \u26412 \u36719 \u20214 \u26381 \u21153 \u12289 \u26080 \u27861 \u27491 \u24120 \u33719 \u21462 \u24744 \u36134 \u21495 \u20869 \u36164 \u20135 \u25110 \u20854 \u20182 \u26435 \u30410 \u31561 \u65289 \u65292 \u20197 \u21450 \u30456 \u24212 \u27861 \u24459 \u36131 \u20219 \u12290 \
\
6.5.5\uc0\u24744 \u25215 \u25285 \u27861 \u24459 \u36131 \u20219 \u30340 \u24418 \u24335 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \u23545 \u21463 \u21040 \u20405 \u23475 \u32773 \u36180 \u20607 \u25439 \u22833 \u12289 \u36180 \u31036 \u36947 \u27465 \u12289 \u28040 \u38500 \u24433 \u21709 \u12289 \u36820 \u36824 \u36130 \u20135 \u31561 \u12290 \u22914 \u24744 \u30340 \u34892 \u20026 \u32473 \u25105 \u20204 \u36896 \u25104 \u25439 \u22833 \u30340 \u65292 \u24744 \u24212 \u25215 \u25285 \u20840 \u37096 \u36180 \u20607 \u36131 \u20219 \u65292 \u21253 \u25324 \u34892 \u25919 \u22788 \u32602 \u25110 \u25439 \u23475 \u36180 \u20607 \u27454 \u31561 \u30452 \u25509 \u25439 \u22833 \u20197 \u21450 \u21830 \u35465 \u25439 \u22833 \u12289 \u21644 \u35299 \u27454 \u12289 \u24459 \u24072 \u36153 \u12289 \u35785 \u35772 \u36153 \u31561 \u38388 \u25509 \u25439 \u22833 \u12290 \
\
6.5.6\uc0\u24744 \u20805 \u20998 \u20102 \u35299 \u65292 \u20219 \u20309 \u21521 \u25105 \u20844 \u21496 \u30340 \u38599 \u21592 \u25110 \u39038 \u38382 \u31561 \u25552 \u20379 \u23454 \u29289 \u12289 \u29616 \u37329 \u12289 \u29616 \u37329 \u31561 \u20215 \u29289 \u12289 \u21171 \u21153 \u12289 \u26053 \u28216 \u31561 \u20215 \u20540 \u26126 \u26174 \u36229 \u20986 \u27491 \u24120 \u21830 \u21153 \u27965 \u35848 \u33539 \u30068 \u30340 \u21033 \u30410 \u65292 \u22343 \u35270 \u20026 \u21830 \u19994 \u36159 \u36162 \u34892 \u20026 \u12290 \u21457 \u29983 \u21069 \u36848 \u24773 \u24418 \u30340 \u65292 \u25105 \u20844 \u21496 \u21487 \u31435 \u21363 \u32456 \u27490 \u19982 \u24744 \u30340 \u25152 \u26377 \u21512 \u20316 \u24182 \u36861 \u31350 \u24744 \u30340 \u27861 \u24459 \u36131 \u20219 \u12290 \
\
6.5.7\uc0\u24744 \u22240 \u36829 \u32422 \u23548 \u33268 \u25105 \u20204 \u32456 \u27490 \u26412 \u21327 \u35758 \u26102 \u65292 \u20986 \u20110 \u32500 \u25252 \u36719 \u20214 \u31209 \u24207 \u30340 \u30446 \u30340 \u65292 \u25105 \u20204 \u21487 \u26681 \u25454 \u19982 \u24744 \u22312 \u20854 \u20182 \u21327 \u35758 \u39033 \u19979 \u36798 \u25104 \u30340 \u32422 \u23450 \u32456 \u27490 \u19982 \u24744 \u22312 \u20854 \u20182 \u21327 \u35758 \u39033 \u19979 \u30340 \u21512 \u20316 \u12290 \u22914 \u24744 \u36829 \u21453 \u25105 \u20204 \u19982 \u24744 \u22312 \u20854 \u20182 \u21512 \u20316 \u39033 \u19979 \u35746 \u31435 \u30340 \u21327 \u35758 \u65292 \u25105 \u20204 \u20134 \u26377 \u26435 \u32456 \u27490 \u26412 \u21327 \u35758 \u12290 \u38500 \u21069 \u36848 \u21327 \u35758 \u32456 \u27490 \u22806 \u65292 \u25105 \u20204 \u20134 \u21487 \u20851 \u32852 \u36861 \u31350 \u24744 \u30340 \u36829 \u32422 \u36131 \u20219 \u12290 \
\
6.5.8\uc0\u25105 \u20204 \u21487 \u23558 \u23545 \u24744 \u19978 \u36848 \u36829 \u32422 \u34892 \u20026 \u30340 \u22788 \u32622 \u20449 \u24687 \u65292 \u20197 \u21450 \u20854 \u20182 \u32463 \u22269 \u23478 \u34892 \u25919 \u25110 \u21496 \u27861 \u26426 \u20851 \u29983 \u25928 \u27861 \u24459 \u25991 \u20070 \u30830 \u35748 \u30340 \u36829 \u27861 \u20449 \u24687 \u65292 \u22312 \u26412 \u36719 \u20214 \u19978 \u20104 \u20197 \u20844 \u31034 \u12290 \u23545 \u28041 \u23244 \u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u30340 \u34892 \u20026 \u65292 \u25105 \u20204 \u26377 \u20041 \u21153 \u20445 \u23384 \u26377 \u20851 \u35760 \u24405 \u65292 \u21521 \u22269 \u23478 \u26377 \u20851 \u26426 \u20851 \u25253 \u21578 \u24182 \u37197 \u21512 \u35843 \u26597 \u12290 \
\
\uc0\u19971 \u12289 \u30693 \u35782 \u20135 \u26435 \u22768 \u26126 \
\
7.1 \uc0\u31508 \u22696 \u35759 \u20351 \u22312 \u26412 \u26381 \u21153 \u20013 \u25552 \u20379 \u30340 \u20869 \u23481 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u32593 \u39029 \u12289 \u25991 \u23383 \u12289 \u22270 \u29255 \u12289 \u38899 \u39057 \u12289 \u35270 \u39057 \u12289 \u22270 \u34920 \u12289 \u25991 \u23383 \u34920 \u36848 \u21450 \u20854 \u32452 \u21512 \u12289 \u30028 \u38754 \u35774 \u35745 \u12289 \u29256 \u38754 \u26694 \u26550 \u12289 \u22270 \u26631 \u12289 \u21830 \u26631 \u31561 \u65289 \u30340 \u30693 \u35782 \u20135 \u26435 \u24402 \u31508 \u22696 \u35759 \u20351 \u25152 \u26377 \u65292 \u21463 \u20013 \u21326 \u20154 \u27665 \u20849 \u21644 \u22269 \u33879 \u20316 \u26435 \u27861 \u12289 \u21830 \u26631 \u27861 \u12289 \u19987 \u21033 \u27861 \u12289 \u21453 \u19981 \u27491 \u24403 \u31454 \u20105 \u27861 \u21450 \u22269 \u38469 \u26465 \u32422 \u31561 \u27861 \u24459 \u27861 \u35268 \u30340 \u20445 \u25252 \u12290 \u26410 \u32463 \u25105 \u20204 \u20070 \u38754 \u20801 \u35768 \u65292 \u29992 \u25143 \u19981 \u24471 \u20026 \u20219 \u20309 \u30446 \u30340 \u25797 \u33258 \u20351 \u29992 \u12289 \u22797 \u21046 \u12289 \u20877 \u36896 \u36825 \u20123 \u20869 \u23481 \u12289 \u25110 \u21019 \u36896 \u19982 \u20869 \u23481 \u26377 \u20851 \u30340 \u27966 \u29983 \u20135 \u21697 \u12290 \u29992 \u25143 \u22312 \u20351 \u29992 \u26412 \u26381 \u21153 \u20013 \u25152 \u20135 \u29983 \u30340 \u20869 \u23481 \u30340 \u30693 \u35782 \u20135 \u26435 \u24402 \u29992 \u25143 \u25110 \u30456 \u20851 \u26435 \u21033 \u20154 \u25152 \u26377 \u12290 \
\
7.2 \uc0\u38500 \u21478 \u26377 \u29305 \u21035 \u22768 \u26126 \u22806 \u65292 \u31508 \u22696 \u35759 \u20351 \u25552 \u20379 \u26412 \u26381 \u21153 \u26102 \u25152 \u20381 \u25454 \u36719 \u20214 \u30340 \u33879 \u20316 \u26435 \u12289 \u19987 \u21033 \u26435 \u21450 \u20854 \u20182 \u30693 \u35782 \u20135 \u26435 \u22343 \u24402 \u31508 \u22696 \u35759 \u20351 \u25152 \u26377 \u12290 \
\
7.3 \uc0\u31508 \u22696 \u35759 \u20351 \u22312 \u26412 \u26381 \u21153 \u20013 \u25152 \u20351 \u29992 \u30340 \'93\u31508 \u22696 \u35759 \u20351 \'94\u31561 \u24418 \u35937 \u31561 \u21830 \u19994 \u26631 \u35782 \u65292 \u20854 \u33879 \u20316 \u26435 \u25110 \u21830 \u26631 \u26435 \u24402 \u31508 \u22696 \u35759 \u20351 \u25152 \u26377 \u12290 \
\
7.4 \uc0\u19978 \u36848 \u21450 \u20854 \u20182 \u20219 \u20309 \u26412 \u26381 \u21153 \u21253 \u21547 \u30340 \u20869 \u23481 \u30340 \u30693 \u35782 \u20135 \u26435 \u22343 \u21463 \u21040 \u27861 \u24459 \u20445 \u25252 \u65292 \u26410 \u32463 \u31508 \u22696 \u35759 \u20351 \u12289 \u29992 \u25143 \u25110 \u30456 \u20851 \u26435 \u21033 \u20154 \u20070 \u38754 \u35768 \u21487 \u65292 \u20219 \u20309 \u20154 \u19981 \u24471 \u20197 \u20219 \u20309 \u24418 \u24335 \u36827 \u34892 \u20351 \u29992 \u25110 \u21019 \u20316 \u30456 \u20851 \u34893 \u29983 \u20316 \u21697 \u12290 \
\
\uc0\u20843 \u12289 \u32456 \u31471 \u23433 \u20840 \u36131 \u20219 \
\
8.1 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u26412 \u36719 \u20214 \u21516 \u22823 \u22810 \u25968 \u20114 \u32852 \u32593 \u36719 \u20214 \u19968 \u26679 \u65292 \u21487 \u33021 \u20250 \u21463 \u22810 \u31181 \u22240 \u32032 \u24433 \u21709 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u29992 \u25143 \u21407 \u22240 \u12289 \u32593 \u32476 \u26381 \u21153 \u36136 \u37327 \u12289 \u31038 \u20250 \u29615 \u22659 \u31561 \u65307 \u20063 \u21487 \u33021 \u20250 \u21463 \u21508 \u31181 \u23433 \u20840 \u38382 \u39064 \u30340 \u20405 \u25200 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u20182 \u20154 \u38750 \u27861 \u21033 \u29992 \u29992 \u25143 \u36164 \u26009 \u65292 \u36827 \u34892 \u29616 \u23454 \u20013 \u30340 \u39578 \u25200 \u65307 \u29992 \u25143 \u19979 \u36733 \u23433 \u35013 \u30340 \u20854 \u20182 \u36719 \u20214 \u25110 \u35775 \u38382 \u30340 \u20854 \u20182 \u32593 \u31449 \u20013 \u21487 \u33021 \u21547 \u26377 \u30149 \u27602 \u12289 \u26408 \u39532 \u31243 \u24207 \u25110 \u20854 \u20182 \u24694 \u24847 \u31243 \u24207 \u65292 \u23041 \u32961 \u24744 \u30340 \u32456 \u31471 \u35774 \u22791 \u20449 \u24687 \u21644 \u25968 \u25454 \u23433 \u20840 \u65292 \u32487 \u32780 \u24433 \u21709 \u26412 \u36719 \u20214 \u30340 \u27491 \u24120 \u20351 \u29992 \u31561 \u12290 \u22240 \u27492 \u65292 \u24744 \u24212 \u21152 \u24378 \u20449 \u24687 \u23433 \u20840 \u21450 \u20010 \u20154 \u20449 \u24687 \u30340 \u20445 \u25252 \u24847 \u35782 \u65292 \u27880 \u24847 \u23494 \u30721 \u20445 \u25252 \u65292 \u20197 \u20813 \u36973 \u21463 \u25439 \u22833 \u12290 \
\
8.2 \uc0\u24744 \u19981 \u24471 \u21046 \u20316 \u12289 \u21457 \u24067 \u12289 \u20351 \u29992 \u12289 \u20256 \u25773 \u29992 \u20110 \u31363 \u21462 \u31508 \u22696 \u35759 \u20351 \u36134 \u21495 \u21450 \u20182 \u20154 \u20010 \u20154 \u20449 \u24687 \u12289 \u36130 \u20135 \u30340 \u24694 \u24847 \u31243 \u24207 \u12290 \
\
8.3 \uc0\u32500 \u25252 \u36719 \u20214 \u23433 \u20840 \u19982 \u27491 \u24120 \u20351 \u29992 \u26159 \u25105 \u20204 \u21644 \u24744 \u30340 \u20849 \u21516 \u36131 \u20219 \u65292 \u25105 \u20204 \u23558 \u25353 \u29031 \u34892 \u19994 \u26631 \u20934 \u21512 \u29702 \u23457 \u24910 \u22320 \u37319 \u21462 \u24517 \u35201 \u25216 \u26415 \u25514 \u26045 \u20445 \u25252 \u24744 \u30340 \u32456 \u31471 \u35774 \u22791 \u20449 \u24687 \u21644 \u25968 \u25454 \u23433 \u20840 \u65292 \u20294 \u26159 \u24744 \u25215 \u35748 \u21644 \u21516 \u24847 \u25105 \u20204 \u24182 \u19981 \u33021 \u23601 \u27492 \u25552 \u20379 \u23436 \u20840 \u20445 \u35777 \u12290 \
\
8.4 \uc0\u22312 \u20219 \u20309 \u24773 \u20917 \u19979 \u65292 \u24744 \u19981 \u24212 \u36731 \u20449 \u20511 \u27454 \u12289 \u32034 \u35201 \u23494 \u30721 \u25110 \u20854 \u20182 \u28041 \u21450 \u36130 \u20135 \u30340 \u32593 \u32476 \u20449 \u24687 \u12290 \u28041 \u21450 \u36130 \u20135 \u25805 \u20316 \u30340 \u65292 \u35831 \u19968 \u23450 \u20808 \u26680 \u23454 \u23545 \u26041 \u36523 \u20221 \u65292 \u24182 \u35831 \u32463 \u24120 \u30041 \u24847 \u25105 \u20204 \u26377 \u20851 \u38450 \u33539 \u35784 \u39575 \u29359 \u32618 \u30340 \u25552 \u31034 \u12290 \
\
\uc0\u20061 \u12289 \u31532 \u19977 \u26041 \u36719 \u20214 \u25110 \u25216 \u26415 \
\
9.1 \uc0\u26412 \u36719 \u20214 \u21487 \u33021 \u20250 \u20351 \u29992 \u31532 \u19977 \u26041 \u36719 \u20214 \u25110 \u25216 \u26415 \u65288 \u21253 \u25324 \u26412 \u36719 \u20214 \u21487 \u33021 \u20351 \u29992 \u30340 \u24320 \u25918 \u28304 \u20195 \u30721 \u21644 \u20844 \u20849 \u39046 \u22495 \u20195 \u30721 \u31561 \u65292 \u19979 \u21516 \u65289 \u65292 \u36825 \u31181 \u20351 \u29992 \u24050 \u32463 \u33719 \u24471 \u21512 \u27861 \u25480 \u26435 \u12290 \
\
9.2 \uc0\u26412 \u36719 \u20214 \u22914 \u26524 \u20351 \u29992 \u20102 \u31532 \u19977 \u26041 \u30340 \u36719 \u20214 \u25110 \u25216 \u26415 \u65292 \u25105 \u20204 \u23558 \u25353 \u29031 \u30456 \u20851 \u27861 \u35268 \u25110 \u32422 \u23450 \u65292 \u23545 \u30456 \u20851 \u30340 \u21327 \u35758 \u25110 \u20854 \u20182 \u25991 \u20214 \u65292 \u21487 \u33021 \u36890 \u36807 \u26412 \u21327 \u35758 \u38468 \u20214 \u12289 \u22312 \u26412 \u36719 \u20214 \u23433 \u35013 \u21253 \u29305 \u23450 \u25991 \u20214 \u22841 \u20013 \u25171 \u21253 \u12289 \u25110 \u36890 \u36807 \u24320 \u28304 \u36719 \u20214 \u39029 \u38754 \u31561 \u24418 \u24335 \u36827 \u34892 \u23637 \u31034 \u65292 \u23427 \u20204 \u21487 \u33021 \u20250 \u20197 \'93\u36719 \u20214 \u20351 \u29992 \u35768 \u21487 \u21327 \u35758 \'94\u12289 \'93\u25480 \u26435 \u21327 \u35758 \'94\u12289 \'93\u24320 \u25918 \u28304 \u20195 \u30721 \u35768 \u21487 \u35777 \'94\u25110 \u20854 \u20182 \u24418 \u24335 \u26469 \u34920 \u36798 \u12290 \u21069 \u36848 \u36890 \u36807 \u21508 \u31181 \u24418 \u24335 \u23637 \u31034 \u30340 \u30456 \u20851 \u21327 \u35758 \u12289 \u20854 \u20182 \u25991 \u20214 \u21450 \u32593 \u39029 \u65292 \u22343 \u26159 \u26412 \u21327 \u35758 \u19981 \u21487 \u20998 \u21106 \u30340 \u32452 \u25104 \u37096 \u20998 \u65292 \u19982 \u26412 \u21327 \u35758 \u20855 \u26377 \u21516 \u31561 \u30340 \u27861 \u24459 \u25928 \u21147 \u65292 \u24744 \u24212 \u24403 \u36981 \u23432 \u36825 \u20123 \u35201 \u27714 \u12290 \u22914 \u26524 \u24744 \u27809 \u26377 \u36981 \u23432 \u36825 \u20123 \u35201 \u27714 \u65292 \u35813 \u31532 \u19977 \u26041 \u25110 \u32773 \u22269 \u23478 \u26426 \u20851 \u21487 \u33021 \u20250 \u23545 \u24744 \u25552 \u36215 \u35785 \u35772 \u12289 \u32602 \u27454 \u25110 \u37319 \u21462 \u20854 \u20182 \u21046 \u35009 \u25514 \u26045 \u65292 \u24182 \u35201 \u27714 \u25105 \u20204 \u32473 \u20104 \u37197 \u21512 \u65292 \u24744 \u24212 \u24403 \u33258 \u34892 \u25215 \u25285 \u27861 \u24459 \u36131 \u20219 \u12290 \
\
9.3 \uc0\u22914 \u22240 \u26412 \u36719 \u20214 \u20351 \u29992 \u30340 \u31532 \u19977 \u26041 \u36719 \u20214 \u25110 \u25216 \u26415 \u24341 \u21457 \u30340 \u20219 \u20309 \u32416 \u32439 \u65292 \u24212 \u30001 \u35813 \u31532 \u19977 \u26041 \u36127 \u36131 \u35299 \u20915 \u65292 \u25105 \u20204 \u19981 \u25215 \u25285 \u20219 \u20309 \u36131 \u20219 \u12290 \u25105 \u20204 \u19981 \u23545 \u31532 \u19977 \u26041 \u36719 \u20214 \u25110 \u25216 \u26415 \u25552 \u20379 \u23458 \u26381 \u25903 \u25345 \u65292 \u33509 \u24744 \u38656 \u35201 \u33719 \u21462 \u25903 \u25345 \u65292 \u35831 \u19982 \u31532 \u19977 \u26041 \u32852 \u31995 \u12290 \
\
APP\uc0\u28041 \u21450 \u20869 \u23884 \u31532 \u19977 \u26041 \u20195 \u30721 \u12289 \u25554 \u20214 \u20256 \u36755 \u20010 \u20154 \u20449 \u24687 \u30340 \u24773 \u24418 \u36880 \u39033 \u21015 \u20030 \u22914 \u19979 \u65306 \
\
**Bugly SDK**\
\uc0\u20027 \u20307 \u65306 \u28145 \u22323 \u24066 \u33150 \u35759 \u35745 \u31639 \u26426 \u31995 \u32479 \u26377 \u38480 \u20844 \u21496 \
\uc0\u29992 \u36884 \u65306 \u24212 \u29992 \u23849 \u28291 \u38382 \u39064 \u25429 \u33719 \
\uc0\u25910 \u38598 \u30340 \u20010 \u20154 \u20449 \u24687 \u31867 \u22411 \u65306 \u30828 \u20214 \u22411 \u21495 \u12289 \u35774 \u22791 MAC\u22320 \u22336 \u12289 \u21807 \u19968 \u35774 \u22791 \u35782 \u21035 \u30721 \u65288 \u22914 IMEI/MEID/ ANDROID ID /SN/OPENUDID/GUID\u12289 SIM\u21345 IMSI\u20449 \u24687 \u22312 \u20869 \u30340 \u25551 \u36848 \u20010 \u20154 \u24120 \u29992 \u35774 \u22791 \u22522 \u26412 \u24773 \u20917 \u30340 \u20449 \u24687 \u65289 \u12289 \u35774 \u22791 \u22411 \u21495 \u12289 \u35774 \u22791 \u21046 \u36896 \u21830 \u12289 \u25805 \u20316 \u31995 \u32479 \u21450 \u20854 \u29256 \u26412 \u65307 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 \
https://privacy.qq.com/document/preview/fc748b3d96224fdb825ea79e132c1a56\
\
**OPPO PUSH SDK**\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u26631 \u35782 \u31526 \u65288 \u22914 IMEI\u12289 OAID\u12289 \u30828 \u20214 \u24207 \u21015 \u21495 /Serial\u12289 IMSI\u12289 Android ID\u12289 GAID/AAID\u65289 \u12289 \u24212 \u29992 \u20449 \u24687 \u12289 \u32593 \u32476 \u20449 \u24687 \u12289 IP\u22320 \u22336 \u12289 \u35774 \u22791 \u20449 \u24687 \
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 OPPO\u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u24191 \u19996 \u27431 \u29632 \u31227 \u21160 \u36890 \u20449 \u26377 \u38480 \u20844 \u21496 \
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://open.oppomobile.com/wiki/doc#id=10194\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://open.oppomobile.com/wiki/doc#id=10288\
\
**VIVO PUSH SDK**\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u20449 \u24687 \u12289 IMEI\u12289 OAID\u12289 GAID/AAID\u12289 Android ID\u12289 \u32593 \u32476 \u20449 \u24687 \
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 VIVO\u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u32500 \u27779 \u31227 \u21160 \u36890 \u20449 \u26377 \u38480 \u20844 \u21496 \
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://dev.vivo.com.cn/documentCenter/doc/366\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://dev.vivo.com.cn/documentCenter/doc/652\
\
**\uc0\u21326 \u20026 PUSH SDK**\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u24212 \u29992 \u22522 \u26412 \u20449 \u24687 \u12289 \u24212 \u29992 \u20869 \u35774 \u22791 \u26631 \u35782 \u31526 \u12289 \u35774 \u22791 \u30340 \u30828 \u20214 \u20449 \u24687 \u12289 \u36719 \u20214 \u21015 \u34920 \u12289 \u31995 \u32479 \u22522 \u26412 \u20449 \u24687 \u21644 \u31995 \u32479 \u35774 \u32622 \u20449 \u24687 \
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u21326 \u20026 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u21326 \u20026 \u36719 \u20214 \u25216 \u26415 \u26377 \u38480 \u20844 \u21496 \
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://developer.huawei.com/consumer/cn/doc/app/20213\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/sdk-data-security-0000001050042177\
\
**hmscore-base-sdk**\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u24212 \u29992 \u22522 \u26412 \u20449 \u24687 \u12289 \u24212 \u29992 \u20869 \u35774 \u22791 \u26631 \u35782 \u31526 \u12289 \u35774 \u22791 \u30340 \u30828 \u20214 \u20449 \u24687 \u12289 \u36719 \u20214 \u21015 \u34920 \u12289 \u31995 \u32479 \u22522 \u26412 \u20449 \u24687 \u21644 \u31995 \u32479 \u35774 \u32622 \u20449 \u24687 \
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u21326 \u20026 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u21326 \u20026 \u36719 \u20214 \u25216 \u26415 \u26377 \u38480 \u20844 \u21496 \
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://developer.huawei.com/consumer/cn/doc/app/20213\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/sdk-data-security-0000001050042177\
\
**\uc0\u23567 \u31859 PUSH SDK**\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u26631 \u35782 \u31526 \u65288 \u22914 Android ID\u12289 OAID\u65289 \u12289 \u24212 \u29992 \u20449 \u24687 \u12289 \u35774 \u22791 \u20449 \u24687 \u12289 \u32593 \u32476 \u20449 \u24687 \u65288 \u26412 \u22320 \u22788 \u29702 \u65289 \u12289 SSID\
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u23567 \u31859 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u21271 \u20140 \u23567 \u31859 \u31227 \u21160 \u36719 \u20214 \u26377 \u38480 \u20844 \u21496 \
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://dev.mi.com/console/appservice/push.html\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://dev.mi.com/console/doc/detail?pId=1822\
\
**\uc0\u39749 \u26063 PUSH SDK**\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u26631 \u35782 \u31526 \u65288 \u22914 IMEI\u12289 MEID\u12289 \u30828 \u20214 \u24207 \u21015 \u21495 /Serial\u65289 \u12289 \u35774 \u22791 MAC\u22320 \u22336 \u12289 \u20301 \u32622 \u20449 \u24687 \u65288 \u22914 GPS\u65289 \u12289 WLAN\u25509 \u20837 \u28857 \u65288 \u22914 SSID\u12289 BSSID\u65289 \u12289 \u35774 \u22791 \u20449 \u24687 \
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u39749 \u26063 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u29664 \u28023 \u24066 \u39749 \u26063 \u36890 \u35759 \u35774 \u22791 \u26377 \u38480 \u20844 \u21496 \u21450 \u20854 \u20851 \u32852 \u26041 \u65288 \u31616 \u31216 \u39749 \u26063 \u65289 \u12289 \u19982 \u39749 \u26063 \u24314 \u31435 \u21512 \u20316 \u30340 \u21512 \u20316 \u26041 \u20844 \u21496 \
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://open.flyme.cn/\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://open.flyme.cn/docs?id=202\
\
**\uc0\u39640 \u24503 PUSH SDK**\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u26631 \u35782 \u31526 (GAID/AAID)\u65292 \u24212 \u29992 Token\u12289 \u25512 \u36865 \u28040 \u24687 \u20869 \u23481 \
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u39640 \u24503 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u39640 \u24503 \u32456 \u31471 \u26377 \u38480 \u20844 \u21496 \
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://lbs.amap.com/\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://lbs.amap.com/pages/privacy/\
\
\uc0\u21313 \u12289 \u19981 \u21487 \u25239 \u21147 \u21450 \u20854 \u20182 \u20813 \u36131 \u20107 \u30001 \
\
10.1 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u22312 \u20351 \u29992 \u26412 \u26381 \u21153 \u30340 \u36807 \u31243 \u20013 \u65292 \u21487 \u33021 \u20250 \u36935 \u21040 \u19981 \u21487 \u25239 \u21147 \u31561 \u39118 \u38505 \u22240 \u32032 \u65292 \u20351 \u26412 \u26381 \u21153 \u21457 \u29983 \u20013 \u26029 \u12290 \u19981 \u21487 \u25239 \u21147 \u26159 \u25351 \u19981 \u33021 \u39044 \u35265 \u12289 \u19981 \u33021 \u20811 \u26381 \u24182 \u19981 \u33021 \u36991 \u20813 \u19988 \u23545 \u19968 \u26041 \u25110 \u21452 \u26041 \u36896 \u25104 \u37325 \u22823 \u24433 \u21709 \u30340 \u23458 \u35266 \u20107 \u20214 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u33258 \u28982 \u28798 \u23475 \u22914 \u27700 \u28798 \u12289 \u22320 \u38663 \u12289 \u30239 \u30123 \u27969 \u34892 \u21644 \u39118 \u26292 \u31561 \u20197 \u21450 \u31038 \u20250 \u20107 \u20214 \u22914 \u25112 \u20105 \u12289 \u21160 \u20081 \u12289 \u25919 \u24220 \u34892 \u20026 \u31561 \u12290 \u20986 \u29616 \u19978 \u36848 \u24773 \u20917 \u26102 \u65292 \u31508 \u22696 \u35759 \u20351 \u23558 \u21162 \u21147 \u22312 \u31532 \u19968 \u26102 \u38388 \u19982 \u30456 \u20851 \u21333 \u20301 \u37197 \u21512 \u65292 \u21450 \u26102 \u36827 \u34892 \u20462 \u22797 \u65292 \u20294 \u30001 \u27492 \u32473 \u24744 \u36896 \u25104 \u30340 \u25439 \u22833 \u31508 \u22696 \u35759 \u20351 \u22312 \u27861 \u24459 \u20801 \u35768 \u30340 \u33539 \u22260 \u20869 \u20813 \u36131 \u12290 \
\
10.2 \uc0\u22312 \u27861 \u24459 \u20801 \u35768 \u30340 \u33539 \u22260 \u20869 \u65292 \u31508 \u22696 \u35759 \u20351 \u23545 \u20197 \u19979 \u24773 \u24418 \u23548 \u33268 \u30340 \u26381 \u21153 \u20013 \u26029 \u25110 \u21463 \u38459 \u19981 \u25215 \u25285 \u36131 \u20219 \u65306 \
\
\uc0\u65288 1\u65289 \u21463 \u21040 \u35745 \u31639 \u26426 \u30149 \u27602 \u12289 \u26408 \u39532 \u25110 \u20854 \u20182 \u24694 \u24847 \u31243 \u24207 \u12289 \u40657 \u23458 \u25915 \u20987 \u30340 \u30772 \u22351 \u65307 \
\
\uc0\u65288 2\u65289 \u29992 \u25143 \u25110 \u31508 \u22696 \u35759 \u20351 \u30340 \u30005 \u33041 \u36719 \u20214 \u12289 \u31995 \u32479 \u12289 \u30828 \u20214 \u21644 \u36890 \u20449 \u32447 \u36335 \u20986 \u29616 \u25925 \u38556 \u65307 \
\
\uc0\u65288 3\u65289 \u29992 \u25143 \u25805 \u20316 \u19981 \u24403 \u65307 \
\
\uc0\u65288 4\u65289 \u29992 \u25143 \u36890 \u36807 \u38750 \u31508 \u22696 \u35759 \u20351 \u25480 \u26435 \u30340 \u26041 \u24335 \u20351 \u29992 \u26412 \u26381 \u21153 \u65307 \
\
\uc0\u65288 5\u65289 \u20854 \u20182 \u31508 \u22696 \u35759 \u20351 \u26080 \u27861 \u25511 \u21046 \u25110 \u21512 \u29702 \u39044 \u35265 \u30340 \u24773 \u24418 \u12290 \
\
10.3 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u22312 \u20351 \u29992 \u26412 \u26381 \u21153 \u30340 \u36807 \u31243 \u20013 \u65292 \u21487 \u33021 \u20250 \u36935 \u21040 \u32593 \u32476 \u20449 \u24687 \u25110 \u20854 \u20182 \u29992 \u25143 \u34892 \u20026 \u24102 \u26469 \u30340 \u39118 \u38505 \u65292 \u31508 \u22696 \u35759 \u20351 \u19981 \u23545 \u20219 \u20309 \u20449 \u24687 \u30340 \u30495 \u23454 \u24615 \u12289 \u36866 \u29992 \u24615 \u12289 \u21512 \u27861 \u24615 \u25215 \u25285 \u36131 \u20219 \u65292 \u20063 \u19981 \u23545 \u22240 \u20405 \u26435 \u34892 \u20026 \u32473 \u24744 \u36896 \u25104 \u30340 \u25439 \u23475 \u36127 \u36131 \u12290 \u36825 \u20123 \u39118 \u38505 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \
\
\uc0\u65288 1\u65289 \u26469 \u33258 \u20182 \u20154 \u21311 \u21517 \u25110 \u20882 \u21517 \u30340 \u21547 \u26377 \u23041 \u32961 \u12289 \u35837 \u35876 \u12289 \u20196 \u20154 \u21453 \u24863 \u25110 \u38750 \u27861 \u20869 \u23481 \u30340 \u20449 \u24687 \u65307 \
\
\uc0\u65288 2\u65289 \u22240 \u20351 \u29992 \u26412 \u21327 \u35758 \u39033 \u19979 \u30340 \u26381 \u21153 \u65292 \u36973 \u21463 \u20182 \u20154 \u35823 \u23548 \u12289 \u27450 \u35784 \u25110 \u20854 \u20182 \u23548 \u33268 \u25110 \u21487 \u33021 \u23548 \u33268 \u30340 \u20219 \u20309 \u24515 \u29702 \u12289 \u29983 \u29702 \u19978 \u30340 \u20260 \u23475 \u20197 \u21450 \u32463 \u27982 \u19978 \u30340 \u25439 \u22833 \u65307 \
\
\uc0\u65288 3\u65289 \u20854 \u20182 \u22240 \u32593 \u32476 \u20449 \u24687 \u25110 \u29992 \u25143 \u34892 \u20026 \u24341 \u36215 \u30340 \u39118 \u38505 \u12290 \
\
10.4 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u26412 \u26381 \u21153 \u24182 \u38750 \u20026 \u26576 \u20123 \u29305 \u23450 \u30446 \u30340 \u32780 \u35774 \u35745 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u26680 \u35774 \u26045 \u12289 \u20891 \u20107 \u29992 \u36884 \u12289 \u21307 \u30103 \u35774 \u26045 \u12289 \u20132 \u36890 \u36890 \u20449 \u31561 \u37325 \u35201 \u39046 \u22495 \u12290 \u22914 \u26524 \u22240 \u20026 \u36719 \u20214 \u25110 \u26381 \u21153 \u30340 \u21407 \u22240 \u23548 \u33268 \u19978 \u36848 \u25805 \u20316 \u22833 \u36133 \u32780 \u24102 \u26469 \u30340 \u20154 \u21592 \u20260 \u20129 \u12289 \u36130 \u20135 \u25439 \u22833 \u21644 \u29615 \u22659 \u27745 \u26579 \u31561 \u65292 \u31508 \u22696 \u35759 \u20351 \u19981 \u25215 \u25285 \u27861 \u24459 \u36131 \u20219 \u12290 \
\
10.5 \uc0\u31508 \u22696 \u35759 \u20351 \u26681 \u25454 \u26412 \u21327 \u35758 \u32422 \u23450 \u33719 \u24471 \u22788 \u29702 \u36829 \u27861 \u36829 \u35268 \u20869 \u23481 \u30340 \u26435 \u21033 \u65292 \u35813 \u26435 \u21033 \u19981 \u26500 \u25104 \u31508 \u22696 \u35759 \u20351 \u30340 \u20041 \u21153 \u25110 \u25215 \u35834 \u65292 \u31508 \u22696 \u35759 \u20351 \u19981 \u33021 \u20445 \u35777 \u21450 \u26102 \u21457 \u29616 \u36829 \u27861 \u34892 \u20026 \u25110 \u36827 \u34892 \u30456 \u24212 \u22788 \u29702 \u12290 \
\
10.6 \uc0\u22312 \u20219 \u20309 \u24773 \u20917 \u19979 \u65292 \u24744 \u19981 \u24212 \u36731 \u20449 \u20511 \u27454 \u12289 \u32034 \u35201 \u23494 \u30721 \u25110 \u20854 \u20182 \u28041 \u21450 \u36130 \u20135 \u30340 \u32593 \u32476 \u20449 \u24687 \u12290 \u28041 \u21450 \u36130 \u20135 \u25805 \u20316 \u30340 \u65292 \u35831 \u19968 \u23450 \u20808 \u26680 \u23454 \u23545 \u26041 \u36523 \u20221 \u65292 \u24182 \u35831 \u32463 \u24120 \u30041 \u24847 \u31508 \u22696 \u35759 \u20351 \u26377 \u20851 \u38450 \u33539 \u35784 \u39575 \u29359 \u32618 \u30340 \u25552 \u31034 \u12290 \
\
\uc0\u21313 \u19968 \u12289 \u26381 \u21153 \u30340 \u21464 \u26356 \u12289 \u20013 \u26029 \u12289 \u32456 \u27490 \
\
11.1 \uc0\u31508 \u22696 \u35759 \u20351 \u21487 \u33021 \u20250 \u23545 \u26381 \u21153 \u20869 \u23481 \u36827 \u34892 \u21464 \u26356 \u65292 \u20063 \u21487 \u33021 \u20250 \u20013 \u26029 \u12289 \u20013 \u27490 \u25110 \u32456 \u27490 \u26381 \u21153 \u12290 \
\
11.2 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u31508 \u22696 \u35759 \u20351 \u26377 \u26435 \u33258 \u20027 \u20915 \u23450 \u32463 \u33829 \u31574 \u30053 \u12290 \u22312 \u31508 \u22696 \u35759 \u20351 \u21457 \u29983 \u21512 \u24182 \u12289 \u20998 \u31435 \u12289 \u25910 \u36141 \u12289 \u36164 \u20135 \u36716 \u35753 \u26102 \u65292 \u31508 \u22696 \u35759 \u20351 \u21487 \u21521 \u31532 \u19977 \u26041 \u36716 \u35753 \u26412 \u26381 \u21153 \u19979 \u30456 \u20851 \u36164 \u20135 \u65307 \u31508 \u22696 \u35759 \u20351 \u20063 \u21487 \u22312 \u21333 \u26041 \u36890 \u30693 \u24744 \u21518 \u65292 \u23558 \u26412 \u21327 \u35758 \u19979 \u37096 \u20998 \u25110 \u20840 \u37096 \u26381 \u21153 \u36716 \u20132 \u30001 \u31532 \u19977 \u26041 \u32463 \u33829 \u25110 \u23653 \u34892 \u12290 \u20855 \u20307 \u21463 \u35753 \u20027 \u20307 \u20197 \u31508 \u22696 \u35759 \u20351 \u36890 \u30693 \u30340 \u20026 \u20934 \u12290 \
\
11.3 \uc0\u22914 \u21457 \u29983 \u19979 \u21015 \u20219 \u20309 \u19968 \u31181 \u24773 \u24418 \u65292 \u31508 \u22696 \u35759 \u20351 \u26377 \u26435 \u19981 \u32463 \u36890 \u30693 \u32780 \u20013 \u26029 \u25110 \u32456 \u27490 \u21521 \u24744 \u25552 \u20379 \u30340 \u26381 \u21153 \u65306 \
\
\uc0\u65288 1\u65289 \u26681 \u25454 \u27861 \u24459 \u35268 \u23450 \u24744 \u24212 \u25552 \u20132 \u30495 \u23454 \u20449 \u24687 \u65292 \u32780 \u24744 \u25552 \u20379 \u30340 \u20010 \u20154 \u36164 \u26009 \u19981 \u30495 \u23454 \u12289 \u25110 \u19982 \u27880 \u20876 \u26102 \u20449 \u24687 \u19981 \u19968 \u33268 \u21448 \u26410 \u33021 \u25552 \u20379 \u21512 \u29702 \u35777 \u26126 \u65307 \
\
\uc0\u65288 2\u65289 \u24744 \u36829 \u21453 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u25110 \u26412 \u21327 \u35758 \u30340 \u32422 \u23450 \u65307 \
\
\uc0\u65288 3\u65289 \u25353 \u29031 \u27861 \u24459 \u35268 \u23450 \u25110 \u20027 \u31649 \u37096 \u38376 \u30340 \u35201 \u27714 \u65307 \
\
\uc0\u65288 4\u65289 \u20986 \u20110 \u23433 \u20840 \u30340 \u21407 \u22240 \u25110 \u20854 \u20182 \u24517 \u35201 \u30340 \u24773 \u24418 \u12290 \
\
11.4 \uc0\u31508 \u22696 \u35759 \u20351 \u26377 \u26435 \u23545 \u37096 \u20998 \u26381 \u21153 \u36827 \u34892 \u25910 \u36153 \u12290 \u33509 \u24744 \u26410 \u25353 \u26102 \u36275 \u39069 \u20184 \u36153 \u65292 \u31508 \u22696 \u35759 \u20351 \u26377 \u26435 \u20013 \u26029 \u12289 \u20013 \u27490 \u25110 \u32456 \u27490 \u25552 \u20379 \u26381 \u21153 \u12290 \
\
11.5 \uc0\u25105 \u20204 \u22987 \u32456 \u22312 \u19981 \u26029 \u21464 \u26356 \u21644 \u25913 \u36827 \u26412 \u36719 \u20214 \u26381 \u21153 \u65292 \u25105 \u20204 \u20250 \u23613 \u26368 \u22823 \u21162 \u21147 \u21521 \u24744 \u25552 \u20379 \u26381 \u21153 \u65292 \u30830 \u20445 \u26381 \u21153 \u30340 \u36830 \u36143 \u24615 \u21644 \u23433 \u20840 \u24615 \u12290 \u22522 \u20110 \u27492 \u65292 \u25105 \u20204 \u21487 \u33021 \u20250 \u23545 \u26412 \u36719 \u20214 \u36827 \u34892 \u26356 \u26032 \u65292 \u24744 \u24212 \u23558 \u36719 \u20214 \u26356 \u26032 \u21040 \u26368 \u26032 \u29256 \u26412 \u65292 \u21542 \u21017 \u25105 \u20204 \u19981 \u20445 \u35777 \u24744 \u21487 \u27491 \u24120 \u20351 \u29992 \u26412 \u36719 \u20214 \u12290 \
\
11.6 \uc0\u24744 \u29702 \u35299 \u24182 \u21516 \u24847 \u65292 \u25105 \u20844 \u21496 \u26681 \u25454 \u33258 \u36523 \u21830 \u19994 \u20915 \u31574 \u12289 \u25919 \u24220 \u34892 \u20026 \u12289 \u19981 \u21487 \u25239 \u21147 \u31561 \u21407 \u22240 \u21487 \u33021 \u20250 \u36873 \u25321 \u20013 \u27490 \u12289 \u20013 \u26029 \u21450 \u32456 \u27490 \u26412 \u36719 \u20214 \u30340 \u37096 \u20998 \u25110 \u20840 \u37096 \u26381 \u21153 \u12290 \u22914 \u26377 \u27492 \u31561 \u24773 \u24418 \u21457 \u29983 \u65292 \u25105 \u20204 \u20250 \u37319 \u21462 \u21512 \u29702 \u30340 \u26041 \u24335 \u24182 \u20197 \u21512 \u29702 \u30340 \u26399 \u38480 \u25552 \u21069 \u36890 \u30693 \u24744 \u65292 \u38500 \u27861 \u24459 \u27861 \u35268 \u21478 \u26377 \u35268 \u23450 \u22806 \u65292 \u25105 \u20204 \u19981 \u25215 \u25285 \u30001 \u27492 \u23545 \u24744 \u36896 \u25104 \u30340 \u25439 \u22833 \u12290 \
\
11.7 \uc0\u24744 \u26377 \u36131 \u20219 \u33258 \u34892 \u22791 \u20221 \u23384 \u20648 \u22312 \u26412 \u26381 \u21153 \u20013 \u30340 \u25968 \u25454 \u12290 \u22914 \u26524 \u24744 \u30340 \u26381 \u21153 \u34987 \u32456 \u27490 \u65292 \u31508 \u22696 \u35759 \u20351 \u21487 \u20197 \u20174 \u26381 \u21153 \u22120 \u19978 \u27704 \u20037 \u22320 \u21024 \u38500 \u24744 \u30340 \u25968 \u25454 \u65292 \u20294 \u27861 \u24459 \u27861 \u35268 \u21478 \u26377 \u35268 \u23450 \u30340 \u38500 \u22806 \u12290 \u26381 \u21153 \u32456 \u27490 \u21518 \u65292 \u31508 \u22696 \u35759 \u20351 \u27809 \u26377 \u20041 \u21153 \u21521 \u24744 \u36820 \u36824 \u25968 \u25454 \u12290 \
\
\uc0\u21313 \u20108 \u12289 \u26410 \u25104 \u24180 \u20154 \u20351 \u29992 \u26465 \u27454 \
\
12.1 \uc0\u33509 \u29992 \u25143 \u26410 \u28385 18\u21608 \u23681 \u65292 \u21017 \u20026 \u26410 \u25104 \u24180 \u20154 \u65292 \u24212 \u22312 \u30417 \u25252 \u20154 \u30417 \u25252 \u12289 \u25351 \u23548 \u19979 \u38405 \u35835 \u26412 \u21327 \u35758 \u21644 \u20351 \u29992 \u26412 \u26381 \u21153 \u12290 \
\
12.2 \uc0\u26410 \u25104 \u24180 \u20154 \u29992 \u25143 \u28041 \u19990 \u26410 \u28145 \u65292 \u23481 \u26131 \u34987 \u32593 \u32476 \u34394 \u25311 \u36855 \u24785 \u65292 \u19988 \u22909 \u22855 \u24515 \u24378 \u65292 \u36935 \u20107 \u32570 \u20047 \u38543 \u26426 \u24212 \u21464 \u30340 \u22788 \u29702 \u33021 \u21147 \u65292 \u24456 \u23481 \u26131 \u34987 \u21035 \u26377 \u29992 \u24515 \u30340 \u20154 \u21033 \u29992 \u32780 \u21448 \u32570 \u20047 \u33258 \u25105 \u20445 \u25252 \u33021 \u21147 \u12290 \u22240 \u27492 \u65292 \u26410 \u25104 \u24180 \u20154 \u29992 \u25143 \u22312 \u20351 \u29992 \u26412 \u26381 \u21153 \u26102 \u24212 \u27880 \u24847 \u20197 \u19979 \u20107 \u39033 \u65292 \u25552 \u39640 \u23433 \u20840 \u24847 \u35782 \u65292 \u21152 \u24378 \u33258 \u25105 \u20445 \u25252 \u65306 \
\
\uc0\u65288 1\u65289 \u35748 \u28165 \u32593 \u32476 \u19990 \u30028 \u19982 \u29616 \u23454 \u19990 \u30028 \u30340 \u21306 \u21035 \u65292 \u36991 \u20813 \u27785 \u36855 \u20110 \u32593 \u32476 \u65292 \u24433 \u21709 \u26085 \u24120 \u30340 \u23398 \u20064 \u29983 \u27963 \u65307 \
\
\uc0\u65288 2\u65289 \u22635 \u20889 \u20010 \u20154 \u36164 \u26009 \u26102 \u65292 \u21152 \u24378 \u20010 \u20154 \u20445 \u25252 \u24847 \u35782 \u65292 \u20197 \u20813 \u19981 \u33391 \u20998 \u23376 \u23545 \u20010 \u20154 \u29983 \u27963 \u36896 \u25104 \u39578 \u25200 \u65307 \
\
\uc0\u65288 3\u65289 \u22312 \u30417 \u25252 \u20154 \u25110 \u32769 \u24072 \u30340 \u25351 \u23548 \u19979 \u65292 \u23398 \u20064 \u27491 \u30830 \u20351 \u29992 \u32593 \u32476 \u65307 \
\
\uc0\u65288 4\u65289 \u36991 \u20813 \u19982 \u38476 \u29983 \u32593 \u21451 \u38543 \u24847 \u35265 \u38754 \u25110 \u21442 \u21152 \u32852 \u35850 \u27963 \u21160 \u65292 \u20197 \u20813 \u19981 \u27861 \u20998 \u23376 \u26377 \u26426 \u21487 \u20056 \u65292 \u21361 \u21450 \u33258 \u36523 \u23433 \u20840 \u12290 \
\
12.3 \uc0\u30417 \u25252 \u20154 \u12289 \u23398 \u26657 \u22343 \u24212 \u23545 \u26410 \u25104 \u24180 \u20154 \u20351 \u29992 \u26412 \u26381 \u21153 \u26102 \u22810 \u20570 \u24341 \u23548 \u12290 \u29305 \u21035 \u26159 \u23478 \u38271 \u24212 \u20851 \u24515 \u23376 \u22899 \u30340 \u25104 \u38271 \u65292 \u27880 \u24847 \u19982 \u23376 \u22899 \u30340 \u27807 \u36890 \u65292 \u25351 \u23548 \u23376 \u22899 \u19978 \u32593 \u24212 \u35813 \u27880 \u24847 \u30340 \u23433 \u20840 \u38382 \u39064 \u65292 \u38450 \u24739 \u20110 \u26410 \u28982 \u12290 \
\
\uc0\u21313 \u19977 \u12289 \u21327 \u35758 \u21464 \u26356 \u19982 \u32456 \u27490 \
13.1\uc0\u21327 \u35758 \u21464 \u26356 \u36890 \u30693 \
\uc0\u25105 \u20204 \u26377 \u26435 \u22312 \u24517 \u35201 \u26102 \u20462 \u25913 \u26412 \u21327 \u35758 \u20869 \u23481 \u65292 \u24182 \u23558 \u36890 \u36807 \u21512 \u29702 \u30340 \u26041 \u24335 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u26412 \u36719 \u20214 \u20844 \u21578 \u12289 \u31995 \u32479 \u28040 \u24687 \u12289 \u31449 \u20869 \u20449 \u12289 \u25163 \u26426 \u30701 \u20449 \u12289 \u30005 \u23376 \u37038 \u20214 \u31561 \u65289 \u21450 \u21512 \u29702 \u30340 \u26399 \u38480 \u25552 \u21069 \u36890 \u30693 \u24744 \u65292 \u35813 \u31561 \u36890 \u30693 \u20110 \u21457 \u36865 \u20043 \u26085 \u35270 \u20026 \u24050 \u36865 \u36798 \u24744 \u12290 \u24744 \u21487 \u20197 \u36890 \u36807 \u36719 \u20214 \u30456 \u20851 \u39029 \u38754 \u26597 \u38405 \u26368 \u26032 \u29256 \u26412 \u30340 \u21327 \u35758 \u20869 \u23481 \u12290 \
\
13.2\uc0\u21464 \u26356 \u21327 \u35758 \u29983 \u25928 \
\uc0\u26412 \u21327 \u35758 \u21464 \u26356 \u29983 \u25928 \u21518 \u65292 \u22914 \u24744 \u32487 \u32493 \u20351 \u29992 \u26412 \u36719 \u20214 \u65292 \u21363 \u35270 \u20026 \u24744 \u24050 \u25509 \u21463 \u20462 \u25913 \u21518 \u30340 \u21327 \u35758 \u12290 \u22914 \u24744 \u19981 \u25509 \u21463 \u20462 \u25913 \u21518 \u30340 \u21327 \u35758 \u65292 \u24212 \u24403 \u20572 \u27490 \u20351 \u29992 \u26412 \u36719 \u20214 \u65292 \u25105 \u20204 \u23558 \u26681 \u25454 \u20844 \u24179 \u21407 \u21017 \u19982 \u24744 \u32456 \u27490 \u21327 \u35758 \u12290 \
\
13.3\uc0\u21327 \u35758 \u32456 \u27490 \
\uc0\u24744 \u27880 \u38144 \u24744 \u30340 \u36134 \u21495 \u25110 \u25105 \u20204 \u36890 \u30693 \u24744 \u32456 \u27490 \u20026 \u24744 \u25552 \u20379 \u26412 \u36719 \u20214 \u26381 \u21153 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u23553 \u31105 \u36134 \u21495 \u12289 \u27880 \u38144 \u36134 \u21495 \u31561 \u65289 \u26102 \u65292 \u26412 \u21327 \u35758 \u23558 \u21516 \u26102 \u32456 \u27490 \u65292 \u25105 \u20204 \u23558 \u19981 \u20877 \u21478 \u34892 \u36890 \u30693 \u24744 \u32456 \u27490 \u26412 \u21327 \u35758 \u12290 \
\
\uc0\u21313 \u22235 \u12289 \u36890 \u30693 \
14.1\uc0\u32852 \u31995 \u26041 \u24335 \
14.1.1\uc0\u24744 \u22312 \u27880 \u20876 \u25104 \u20026 \u26412 \u36719 \u20214 \u29992 \u25143 \u65292 \u24182 \u25509 \u21463 \u26412 \u36719 \u20214 \u26381 \u21153 \u26102 \u65292 \u24744 \u24212 \u35813 \u21521 \u26412 \u36719 \u20214 \u25552 \u20379 \u30495 \u23454 \u26377 \u25928 \u30340 \u32852 \u31995 \u26041 \u24335 \u65288 \u21253 \u25324 \u24744 \u30340 \u30005 \u23376 \u37038 \u20214 \u22320 \u22336 \u12289 \u32852 \u31995 \u30005 \u35805 \u12289 \u32852 \u31995 \u22320 \u22336 \u31561 \u65289 \u65292 \u23545 \u20110 \u32852 \u31995 \u26041 \u24335 \u21457 \u29983 \u21464 \u26356 \u30340 \u65292 \u24744 \u26377 \u20041 \u21153 \u21450 \u26102 \u26356 \u26032 \u26377 \u20851 \u20449 \u24687 \u65292 \u24182 \u20445 \u25345 \u21487 \u34987 \u32852 \u31995 \u30340 \u29366 \u24577 \u12290 \
\
14.1.2\uc0\u24744 \u22312 \u27880 \u20876 \u26412 \u36719 \u20214 \u29992 \u25143 \u26102 \u29983 \u25104 \u30340 \u29992 \u20110 \u30331 \u38470 \u26412 \u36719 \u20214 \u25509 \u25910 \u31449 \u20869 \u20449 \u12289 \u31995 \u32479 \u28040 \u24687 \u30340 \u36134 \u21495 \u65292 \u20063 \u20316 \u20026 \u24744 \u30340 \u26377 \u25928 \u32852 \u31995 \u26041 \u24335 \u12290 \
\
14.1.3\uc0\u26412 \u36719 \u20214 \u23558 \u21521 \u24744 \u30340 \u19978 \u36848 \u32852 \u31995 \u26041 \u24335 \u30340 \u20854 \u20013 \u20043 \u19968 \u25110 \u20854 \u20013 \u33509 \u24178 \u21521 \u24744 \u36865 \u36798 \u21508 \u31867 \u36890 \u30693 \u65292 \u32780 \u27492 \u31867 \u36890 \u30693 \u30340 \u20869 \u23481 \u21487 \u33021 \u23545 \u24744 \u30340 \u26435 \u21033 \u20041 \u21153 \u20135 \u29983 \u37325 \u22823 \u30340 \u26377 \u21033 \u25110 \u19981 \u21033 \u24433 \u21709 \u65292 \u35831 \u24744 \u21153 \u24517 \u21450 \u26102 \u20851 \u27880 \u12290 \
\uc0\u24744 \u26377 \u26435 \u36890 \u36807 \u24744 \u27880 \u20876 \u26102 \u22635 \u20889 \u30340 \u25163 \u26426 \u21495 \u30721 \u25110 \u32773 \u30005 \u23376 \u37038 \u31665 \u33719 \u21462 \u24744 \u24863 \u20852 \u36259 \u30340 \u21830 \u21697 \u24191 \u21578 \u20449 \u24687 \u12289 \u20419 \u38144 \u20248 \u24800 \u31561 \u21830 \u19994 \u24615 \u20449 \u24687 \u65307 \u24744 \u22914 \u26524 \u19981 \u24895 \u24847 \u25509 \u25910 \u27492 \u31867 \u20449 \u24687 \u65292 \u24744 \u26377 \u26435 \u36890 \u36807 \u26412 \u36719 \u20214 \u25552 \u20379 \u30340 \u30456 \u24212 \u30340 \u36864 \u35746 \u21151 \u33021 \u36827 \u34892 \u36864 \u35746 \u12290 \
\
14.2\uc0\u36890 \u30693 \u36865 \u36798 \
14.2.1\uc0\u26412 \u36719 \u20214 \u36890 \u36807 \u19978 \u36848 \u32852 \u31995 \u26041 \u24335 \u21521 \u24744 \u21457 \u20986 \u36890 \u30693 \u65292 \u20854 \u20013 \u20197 \u30005 \u23376 \u30340 \u26041 \u24335 \u21457 \u20986 \u30340 \u20070 \u38754 \u36890 \u30693 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u22312 \u26412 \u36719 \u20214 \u20844 \u21578 \u65292 \u21521 \u24744 \u25552 \u20379 \u30340 \u32852 \u31995 \u30005 \u35805 \u21457 \u36865 \u25163 \u26426 \u30701 \u20449 \u65292 \u21521 \u24744 \u25552 \u20379 \u30340 \u30005 \u23376 \u37038 \u20214 \u22320 \u22336 \u21457 \u36865 \u30005 \u23376 \u37038 \u20214 \u65292 \u21521 \u24744 \u30340 \u36134 \u21495 \u21457 \u36865 \u20449 \u24687 \u12289 \u31995 \u32479 \u28040 \u24687 \u20197 \u21450 \u31449 \u20869 \u20449 \u20449 \u24687 \u65292 \u22312 \u21457 \u36865 \u25104 \u21151 \u21518 \u21363 \u35270 \u20026 \u36865 \u36798 \u65307 \u20197 \u32440 \u36136 \u36733 \u20307 \u21457 \u20986 \u30340 \u20070 \u38754 \u36890 \u30693 \u65292 \u25353 \u29031 \u25552 \u20379 \u32852 \u31995 \u22320 \u22336 \u20132 \u37038 \u21518 \u30340 \u31532 \u12304 7\u12305 \u20010 \u33258 \u28982 \u26085 \u21363 \u35270 \u20026 \u36865 \u36798 \u12290 \
\
14.2.2\uc0\u23545 \u20110 \u22312 \u26412 \u36719 \u20214 \u19978 \u22240 \u20132 \u26131 \u27963 \u21160 \u24341 \u36215 \u30340 \u20219 \u20309 \u32416 \u32439 \u65292 \u24744 \u21516 \u24847 \u21496 \u27861 \u26426 \u20851 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u20154 \u27665 \u27861 \u38498 \u65289 \u21487 \u20197 \u36890 \u36807 \u25163 \u26426 \u30701 \u20449 \u12289 \u30005 \u23376 \u37038 \u20214 \u31561 \u29616 \u20195 \u36890 \u35759 \u26041 \u24335 \u25110 \u37038 \u23492 \u26041 \u24335 \u21521 \u24744 \u36865 \u36798 \u27861 \u24459 \u25991 \u20070 \u65288 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u35785 \u35772 \u25991 \u20070 \u65289 \u12290 \u24744 \u25351 \u23450 \u25509 \u25910 \u27861 \u24459 \u25991 \u20070 \u30340 \u25163 \u26426 \u21495 \u30721 \u12289 \u30005 \u23376 \u37038 \u31665 \u25110 \u36719 \u20214 \u36134 \u21495 \u31561 \u32852 \u31995 \u26041 \u24335 \u20026 \u24744 \u22312 \u26412 \u36719 \u20214 \u27880 \u20876 \u12289 \u26356 \u26032 \u26102 \u25552 \u20379 \u30340 \u25163 \u26426 \u21495 \u30721 \u12289 \u30005 \u23376 \u37038 \u31665 \u32852 \u31995 \u26041 \u24335 \u20197 \u21450 \u22312 \u27880 \u20876 \u26412 \u36719 \u20214 \u29992 \u25143 \u26102 \u29983 \u25104 \u30340 \u36134 \u21495 \u65292 \u21496 \u27861 \u26426 \u20851 \u21521 \u19978 \u36848 \u32852 \u31995 \u26041 \u24335 \u21457 \u20986 \u27861 \u24459 \u25991 \u20070 \u21363 \u35270 \u20026 \u36865 \u36798 \u12290 \u24744 \u25351 \u23450 \u30340 \u37038 \u23492 \u22320 \u22336 \u20026 \u24744 \u30340 \u27861 \u23450 \u32852 \u31995 \u22320 \u22336 \u25110 \u24744 \u25552 \u20379 \u30340 \u26377 \u25928 \u32852 \u31995 \u22320 \u22336 \u12290 \
\
14.2.3\uc0\u24744 \u21516 \u24847 \u21496 \u27861 \u26426 \u20851 \u21487 \u37319 \u21462 \u20197 \u19978 \u19968 \u31181 \u25110 \u22810 \u31181 \u36865 \u36798 \u26041 \u24335 \u21521 \u24744 \u36865 \u36798 \u27861 \u24459 \u25991 \u20070 \u65292 \u21496 \u27861 \u26426 \u20851 \u37319 \u21462 \u22810 \u31181 \u26041 \u24335 \u21521 \u24744 \u36865 \u36798 \u27861 \u24459 \u25991 \u20070 \u65292 \u36865 \u36798 \u26102 \u38388 \u20197 \u19978 \u36848 \u36865 \u36798 \u26041 \u24335 \u20013 \u26368 \u20808 \u36865 \u36798 \u30340 \u20026 \u20934 \u12290 \
\uc0\u24744 \u21516 \u24847 \u19978 \u36848 \u36865 \u36798 \u26041 \u24335 \u36866 \u29992 \u20110 \u21508 \u20010 \u21496 \u27861 \u31243 \u24207 \u38454 \u27573 \u12290 \u22914 \u36827 \u20837 \u35785 \u35772 \u31243 \u24207 \u30340 \u65292 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u19968 \u23457 \u12289 \u20108 \u23457 \u12289 \u20877 \u23457 \u12289 \u25191 \u34892 \u20197 \u21450 \u30563 \u20419 \u31243 \u24207 \u31561 \u12290 \
\
14.2.4\uc0\u20320 \u24212 \u24403 \u20445 \u35777 \u25152 \u25552 \u20379 \u30340 \u32852 \u31995 \u26041 \u24335 \u26159 \u20934 \u30830 \u12289 \u26377 \u25928 \u30340 \u65292 \u24182 \u36827 \u34892 \u23454 \u26102 \u26356 \u26032 \u12290 \u22914 \u26524 \u22240 \u25552 \u20379 \u30340 \u32852 \u31995 \u26041 \u24335 \u19981 \u30830 \u20999 \u65292 \u25110 \u19981 \u21450 \u26102 \u21578 \u30693 \u21464 \u26356 \u21518 \u30340 \u32852 \u31995 \u26041 \u24335 \u65292 \u20351 \u27861 \u24459 \u25991 \u20070 \u26080 \u27861 \u36865 \u36798 \u25110 \u26410 \u21450 \u26102 \u36865 \u36798 \u65292 \u30001 \u24744 \u33258 \u34892 \u25215 \u25285 \u30001 \u27492 \u21487 \u33021 \u20135 \u29983 \u30340 \u27861 \u24459 \u21518 \u26524 \u12290 \
\
\uc0\u21313 \u20116 \u12289 \u20854 \u20182 \
15.1 \uc0\u24744 \u20351 \u29992 \u26412 \u36719 \u20214 \u21363 \u35270 \u20026 \u24744 \u24050 \u38405 \u35835 \u24182 \u21516 \u24847 \u21463 \u26412 \u21327 \u35758 \u30340 \u32422 \u26463 \u12290 \u25105 \u20204 \u26377 \u26435 \u22312 \u24517 \u35201 \u26102 \u20462 \u25913 \u26412 \u21327 \u35758 \u26465 \u27454 \u12290 \u24744 \u21487 \u20197 \u22312 \u26412 \u36719 \u20214 \u30340 \u26368 \u26032 \u29256 \u26412 \u20013 \u26597 \u38405 \u30456 \u20851 \u21327 \u35758 \u26465 \u27454 \u12290 \u26412 \u21327 \u35758 \u26465 \u27454 \u21464 \u26356 \u21518 \u65292 \u22914 \u26524 \u24744 \u32487 \u32493 \u20351 \u29992 \u26412 \u36719 \u20214 \u65292 \u21363 \u35270 \u20026 \u24744 \u24050 \u25509 \u21463 \u20462 \u25913 \u21518 \u30340 \u21327 \u35758 \u12290 \u22914 \u26524 \u24744 \u19981 \u25509 \u21463 \u20462 \u25913 \u21518 \u30340 \u21327 \u35758 \u65292 \u24212 \u24403 \u20572 \u27490 \u20351 \u29992 \u26412 \u36719 \u20214 \u12290 \
\
15.2 \uc0\u26412 \u21327 \u35758 \u30340 \u25104 \u31435 \u12289 \u29983 \u25928 \u12289 \u23653 \u34892 \u12289 \u35299 \u37322 \u21450 \u20105 \u35758 \u35299 \u20915 \u65292 \u36866 \u29992 \u20013 \u21326 \u20154 \u27665 \u20849 \u21644 \u22269 \u22823 \u38470 \u22320 \u21306 \u27861 \u24459 \u27861 \u35268 \u65307 \u22914 \u27861 \u24459 \u26080 \u30456 \u20851 \u35268 \u23450 \u30340 \u65292 \u21442 \u29031 \u21830 \u19994 \u24815 \u20363 \u21450 /\u25110 \u34892 \u19994 \u24815 \u20363 \u12290 \
\
15.3 \uc0\u33509 \u24744 \u21644 \u25105 \u20204 \u20043 \u38388 \u21457 \u29983 \u20219 \u20309 \u32416 \u32439 \u25110 \u20105 \u35758 \u65292 \u39318 \u20808 \u24212 \u21451 \u22909 \u21327 \u21830 \u35299 \u20915 \u65307 \u21327 \u21830 \u19981 \u25104 \u30340 \u65292 \u24744 \u21516 \u24847 \u23558 \u32416 \u32439 \u25110 \u20105 \u35758 \u25552 \u20132 \u25105 \u20844 \u21496 \u25152 \u22312 \u22320 \u20154 \u27665 \u27861 \u38498 \u31649 \u36758 \u12290 \
\
15.4 \uc0\u26412 \u21327 \u35758 \u25152 \u26377 \u26465 \u27454 \u30340 \u26631 \u39064 \u20165 \u20026 \u38405 \u35835 \u26041 \u20415 \u65292 \u26412 \u36523 \u24182 \u26080 \u23454 \u38469 \u21547 \u20041 \u65292 \u19981 \u33021 \u20316 \u20026 \u26412 \u21327 \u35758 \u21547 \u20041 \u35299 \u37322 \u30340 \u20381 \u25454 \u12290 \
\
15.5 \uc0\u26412 \u21327 \u35758 \u21487 \u33021 \u23384 \u22312 \u21253 \u25324 \u20013 \u25991 \u12289 \u33521 \u25991 \u22312 \u20869 \u30340 \u22810 \u31181 \u35821 \u35328 \u30340 \u29256 \u26412 \u65292 \u33509 \u23384 \u22312 \u20013 \u25991 \u29256 \u26412 \u19982 \u20854 \u20182 \u35821 \u35328 \u30340 \u29256 \u26412 \u30456 \u20914 \u31361 \u30340 \u22320 \u26041 \u65292 \u20197 \u20013 \u25991 \u29256 \u26412 \u20026 \u20934 \u12290 \
\
15.6 \uc0\u26412 \u21327 \u35758 \u26465 \u27454 \u26080 \u35770 \u22240 \u20309 \u31181 \u21407 \u22240 \u37096 \u20998 \u26080 \u25928 \u25110 \u19981 \u21487 \u25191 \u34892 \u65292 \u20854 \u20313 \u26465 \u27454 \u20173 \u26377 \u25928 \u65292 \u23545 \u21452 \u26041 \u20855 \u26377 \u32422 \u26463 \u21147 \u12290 \
\
15.7 \uc0\u22914 \u24744 \u23545 \u26412 \u21327 \u35758 \u26377 \u20219 \u20309 \u24847 \u35265 \u25110 \u24314 \u35758 \u65292 \u24744 \u21487 \u36890 \u36807 \u26412 \u24179 \u21488 \u32852 \u31995 \u25105 \u20204 \u65292 \u32852 \u31995 \u26041 \u24335 \u20026 \u12304 fengqunwangluo2025@163.com\u12305 \u65292 \u25105 \u20204 \u20250 \u32473 \u20104 \u24744 \u24517 \u35201 \u30340 \u24110 \u21161 \u12290 \
\
\uc0\u33258 \u21551 \u21160 \u25110 \u20851 \u32852 \u21551 \u21160 \u35828 \u26126 \
\
\uc0\u25512 \u36865 \u30456 \u20851 :\u20026 \u30830 \u20445 \u26412 \u24212 \u29992 \u22788 \u20110 \u21518 \u21488 \u25110 \u21518 \u21488 \u36816 \u34892 \u29366 \u24577 \u19979 \u21487 \u27491 \u24120 \u25509 \u25910 \u21040 \u23458 \u25143 \u31471 \u25512 \u36865 \u30340 \u24191 \u25773 \u20449 \u24687 \u65292 \u26412 \u24212 \u29992 \u39035 \u20351 \u29992 (\u33258 \u21551 \u21160 )\u33021 \u21147 \u65292 \u23558 \u23384 \u22312 \u19968 \u23450 \u39057 \u29575 \u36890 \u36807 \u31995 \u32479 \u21457 \u36865 \u24191 \u25773 \u21796 \u37266 \u26412 \u24212 \u29992 \u33258 \u21551 \u21160 \u25110 \u20851 \u32852 \u21551 \u21160 \u34892 \u20026 \u65292 \u26159 \u22240 \u23454 \u29616 \u21151 \u33021 \u21450 \u26381 \u21153 \u25152 \u24517 \u35201 \u30340 ;\u24403 \u24744 \u25171 \u24320 \u20869 \u23481 \u31867 \u25512 \u36865 \u28040 \u24687 \u65292 \u22312 \u24449 \u24471 \u24744 \u30340 \u26126 \u30830 \u21516 \u24847 \u21518 \u65292 \u20250 \u36339 \u36716 \u25171 \u24320 \u30456 \u20851 \u20869 \u23481 \u12290 \u22312 \u26410 \u24449 \u24471 \u24744 \u21516 \u24847 \u30340 \u24773 \u20917 \u19979 \u65292 \u21017 \u19981 \u20250 \u26377 \u20851 \u32852 \u21551 \u21160 \u12290 \
\
\uc0\u35835 \u21462 \u21098 \u36148 \u26495 \u20449 \u24687 \u65306 \
\
1\uc0\u12289 \u29992 \u20110 \u22797 \u21046 \u65288 \u25163 \u26426 \u21495 \u12289 id\u65289 \u26041 \u20415 \u29992 \u29992 \u25143 \u25628 \u32034 \u28155 \u21152 \u22909 \u21451 \
\
2\uc0\u12289 \u29992 \u20110 \u22797 \u21046 \u65288 \u24179 \u21488 \u32852 \u31995 \u26041 \u24335 \u65289 \u26041 \u20415 \u29992 \u25143 \u25552 \u20132 \u24847 \u35265 \u21453 \u39304 \u12289 \u20030 \u25253 \u31561 }
+35
View File
@@ -0,0 +1,35 @@
**1. `/healthz`接口**
现在的健康检查(`nc -z localhost 6060`/`8000`)只能证明端口开着,测不出"连接池耗尽、死锁"这种进程还活着但已经不干活的情况。
要求:`GET /healthz`,不需要鉴权,正常200/异常503,body统一格式:
```json
{"status": "ok", "checks": {"mysql": "ok", "redis": "ok", "kafka": "ok"}}
```
异常时把出问题的依赖写清楚原因,其他正常的照样标ok:
```json
{"status": "error", "checks": {"mysql": "ok", "kafka": "timeout: dial tcp kafka:9092: i/o timeout"}}
```
各服务测什么:
- **im-server**:MySQL(唯一确认在用的外部依赖)。Redis代码里还没真正接入,先不用测
- **im-manager**:MySQL、Redis(`REDIS_DB=2`在用)、Kafka(崩溃日志能看到它真的在跑queue consumer,是真实依赖)
检测建议用真实操作确认能用(MySQL `SELECT 1`/`Ping()`,Redis `PING`,Kafka查broker元数据),不要只判断连接对象是不是nil。
顺手把两个服务Dockerfile里的`HEALTHCHECK`换成打这个接口,`deploy.sh`的探测逻辑也建议一起换。
---
**2. `/metrics`业务指标接口**(不急,可以往后放)
`im-server`加`GET /metrics`,标准Prometheus文本格式,Go生态直接用官方库`prometheus/client_golang/promhttp`,几行代码接完。
按价值排序,可以只做第一个:
| 指标 | 类型 | 说明 |
|---|---|---|
| `ws_connections_active` | Gauge | 当前WS连接数,**优先级最高**——直接对应之前"WS总掉线"那次排查,有曲线就不用再靠人工连测 |
| `http_requests_total{method,path,status}` | Counter | 按路径/状态码分的请求量,看5xx占比 |
| `http_request_duration_seconds` | Histogram | 接口延迟分布 |
| `db_query_duration_seconds` | Histogram | 数据库查询延迟 |
+442
View File
@@ -0,0 +1,442 @@
{\rtf1\ansi\ansicpg936\cocoartf2821
\cocoatextscaling0\cocoaplatform0{\fonttbl\f0\froman\fcharset0 Times-Roman;}
{\colortbl;\red255\green255\blue255;\red0\green0\blue0;}
{\*\expandedcolortbl;;\cssrgb\c0\c0\c0;}
\paperw11900\paperh16840\margl1440\margr1440\vieww11520\viewh8400\viewkind0
\deftab720
\pard\pardeftab720\partightenfactor0
\f0\fs24 \cf0 \expnd0\expndtw0\kerning0
\outl0\strokewidth0 \strokec2 \
\uc0\u23562 \u25964 \u30340 \u31508 \u22696 \u35759 \u20351 \u29992 \u25143 \u65306 \
\
\uc0\u27426 \u36814 \u24744 \u20351 \u29992 \u28023 \u21475 \u24066 \u26539 \u32676 \u32593 \u32476 \u31185 \u25216 \u26377 \u38480 \u20844 \u21496 \u25552 \u20379 \u30340 \'93\u31508 \u22696 \u35759 \u20351 \'94\u36719 \u20214 \u21450 \u30456 \u20851 \u26381 \u21153 \u65281 \u20316 \u20026 \'93\u31508 \u22696 \u35759 \u20351 \'94\u30340 \u24320 \u21457 \u32773 \u65288 \u20197 \u19979 \u31616 \u31216 \'93\u25105 \u20204 \'94\u65289 \u65292 \u25105 \u20204 \u28145 \u30693 \u20010 \u20154 \u20449 \u24687 \u23545 \u24744 \u30340 \u37325 \u35201 \u24615 \u65292 \u24182 \u20250 \u23613 \u21147 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u23433 \u20840 \u21487 \u38752 \u12290 \u25105 \u20204 \u33268 \u21147 \u20110 \u32500 \u25345 \u24744 \u23545 \u25105 \u20204 \u30340 \u20449 \u20219 \u65292 \u24682 \u23432 \u20197 \u19979 \u21407 \u21017 \u65292 \u20445 \u25252 \u21644 \u22788 \u29702 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65306 \u26435 \u36131 \u19968 \u33268 \u21407 \u21017 \u65292 \u30446 \u30340 \u26126 \u30830 \u21407 \u21017 \u65292 \u36873 \u25321 \u21516 \u24847 \u21407 \u21017 \u65292 \u26368 \u23569 \u24517 \u35201 \u21407 \u21017 \u65292 \u30830 \u20445 \u23433 \u20840 \u21407 \u21017 \u65292 \u20027 \u20307 \u21442 \u19982 \u21407 \u21017 \u65292 \u20844 \u24320 \u36879 \u26126 \u21407 \u21017 \u12290 \u21516 \u26102 \u25105 \u20204 \u25215 \u35834 \u65292 \u25105 \u20204 \u23558 \u25353 \u19994 \u30028 \u25104 \u29087 \u30340 \u23433 \u20840 \u35299 \u20915 \u26041 \u26696 \u65292 \u37319 \u21462 \u30456 \u24212 \u30340 \u23433 \u20840 \u20445 \u25252 \u25514 \u26045 \u26469 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
\uc0\u29256 \u26412 \u29983 \u25928 \u26085 \u26399 \u65306 2025\u24180 8\u26376 1\u26085 \
\
\uc0\u22914 \u24744 \u26377 \u20219 \u20309 \u30097 \u38382 \u12289 \u24847 \u35265 \u25110 \u24314 \u35758 \u65292 \u24744 \u21487 \u36890 \u36807 \u20197 \u19979 \u32852 \u31995 \u26041 \u24335 \u19982 \u25105 \u20204 \u32852 \u31995 \u65306 \
\
\uc0\u32852 \u31995 \u37038 \u31665 \u65306 fengqunwangluo2025@163.com\
\
\uc0\u32852 \u31995 \u22320 \u22336 \u65306 \u28023 \u21335 \u30465 \u28023 \u21475 \u24066 \u32654 \u20848 \u21306 \u28023 \u24220 \u34903 \u36947 \u28023 \u24220 \u36335 4\u21495 \u28023 \u21475 \u23486 \u39302 \u38468 \u27004 \u31532 4\u23618 C\u21306 \u31532 7\u38388 \
\
\uc0\u12304 \u29305 \u21035 \u25552 \u31034 \u12305 \
\
\uc0\u35831 \u24744 \u20180 \u32454 \u38405 \u35835 \u12298 \u38544 \u31169 \u25919 \u31574 \u12299 \u35814 \u32454 \u20102 \u35299 \u25105 \u20204 \u23545 \u20449 \u24687 \u30340 \u25910 \u38598 \u12289 \u20351 \u29992 \u12289 \u20445 \u23384 \u26041 \u24335 \u31561 \u65292 \u24182 \u30830 \u23450 \u20102 \u35299 \u25105 \u20204 \u23545 \u24744 \u20010 \u20154 \u20449 \u24687 \u30340 \u22788 \u29702 \u35268 \u21017 \u12290 \u23545 \u26412 \u25919 \u31574 \u20013 \u25105 \u20204 \u35748 \u20026 \u19982 \u24744 \u30340 \u26435 \u30410 \u23384 \u22312 \u37325 \u22823 \u20851 \u31995 \u30340 \u26465 \u27454 \u65292 \u25105 \u20204 \u37319 \u29992 \u23383 \u20307 \u21152 \u31895 \u24418 \u24335 \u25552 \u31034 \u24744 \u29305 \u21035 \u27880 \u24847 \u12290 \u25105 \u20204 \u24314 \u35758 \u24744 \u20180 \u32454 \u38405 \u35835 \u24182 \u29702 \u35299 \u26412 \u25919 \u31574 \u20840 \u37096 \u20869 \u23481 \u65292 \u23588 \u20854 \u26159 \u30830 \u20445 \u24050 \u20840 \u37096 \u30693 \u26195 \u24182 \u20805 \u20998 \u29702 \u35299 \u21152 \u31895 \u23383 \u20307 \u20869 \u23481 \u30340 \u21547 \u20041 \u21450 \u30456 \u24212 \u30340 \u27861 \u24459 \u21518 \u26524 \u65292 \u20197 \u20415 \u24744 \u23545 \u25105 \u20204 \u30340 \u26381 \u21153 \u20316 \u20986 \u36866 \u24403 \u30340 \u36873 \u25321 \u12290 \u26412 \u38544 \u31169 \u25919 \u31574 \u20013 \u28041 \u21450 \u30340 \u30456 \u20851 \u26415 \u35821 \u65292 \u25105 \u20204 \u23613 \u37327 \u20197 \u31616 \u26126 \u25212 \u35201 \u30340 \u34920 \u36848 \u65292 \u20197 \u20415 \u24744 \u26356 \u22909 \u22320 \u29702 \u35299 \u12290 \u28041 \u21450 \u20855 \u20307 \u30340 \u20135 \u21697 \u65288 \u25110 \u26381 \u21153 \u65289 \u65292 \u22914 \u27861 \u24459 \u12289 \u34892 \u25919 \u27861 \u35268 \u35268 \u23450 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u24212 \u24403 \u21462 \u24471 \u20010 \u20154 \u21333 \u29420 \u21516 \u24847 \u30340 \u65292 \u25105 \u20204 \u23558 \u26681 \u25454 \u24744 \u30340 \u23454 \u38469 \u20351 \u29992 \u24773 \u20917 \u21333 \u29420 \u24449 \u27714 \u24744 \u30340 \u21516 \u24847 \u12290 \u24744 \u20351 \u29992 \u25110 \u22312 \u25105 \u20204 \u26356 \u26032 \u26412 \u38544 \u31169 \u25919 \u31574 \u21518 \u65288 \u25105 \u20204 \u20250 \u21450 \u26102 \u25552 \u31034 \u24744 \u26356 \u26032 \u30340 \u24773 \u20917 \u65289 \u32487 \u32493 \u20351 \u29992 \u25105 \u20204 \u30340 \u20135 \u21697 \u19982 /\u25110 \u26381 \u21153 \u65292 \u21363 \u34920 \u31034 \u24744 \u21516 \u24847 \u26412 \u38544 \u31169 \u25919 \u31574 \u65288 \u21547 \u26356 \u26032 \u29256 \u26412 \u65289 \u20869 \u23481 \u65292 \u24182 \u19988 \u21516 \u24847 \u25105 \u20204 \u25353 \u29031 \u26412 \u38544 \u31169 \u25919 \u31574 \u25910 \u38598 \u12289 \u20351 \u29992 \u12289 \u20445 \u23384 \u21644 \u20849 \u20139 \u24744 \u30340 \u30456 \u20851 \u20449 \u24687 \u12290 \
\uc0\u22312 \u20351 \u29992 \u25105 \u20204 \u25552 \u20379 \u30340 \u21508 \u39033 \u20135 \u21697 \u25110 \u26381 \u21153 \u21069 \u65292 \u35831 \u24744 \u21153 \u24517 \u20180 \u32454 \u38405 \u35835 \u24182 \u36879 \u24443 \u29702 \u35299 \u26412 \u25919 \u31574 \u65292 \u22312 \u30830 \u35748 \u20805 \u20998 \u29702 \u35299 \u24182 \u21516 \u24847 \u21518 \u20351 \u29992 \u30456 \u20851 \u20135 \u21697 \u25110 \u26381 \u21153 \u12290 \u19968 \u26086 \u24744 \u24320 \u22987 \u20351 \u29992 \u25105 \u20204 \u21508 \u39033 \u20135 \u21697 \u25110 \u26381 \u21153 \u65292 \u21363 \u34920 \u31034 \u24744 \u24050 \u20805 \u20998 \u29702 \u35299 \u24182 \u21516 \u24847 \u20351 \u29992 \u25105 \u20204 \u30340 \u30456 \u20851 \u20135 \u21697 \u25110 \u26381 \u21153 \u65292 \u20197 \u21450 \u22312 \u20351 \u29992 \u30456 \u20851 \u20135 \u21697 \u25110 \u26381 \u21153 \u36807 \u31243 \u20013 \u25152 \u38656 \u30340 \u24517 \u35201 \u20010 \u20154 \u20449 \u24687 \u65292 \u24182 \u32473 \u20104 \u30456 \u24212 \u30340 \u25910 \u38598 \u20351 \u29992 \u25480 \u26435 \u12290 \u22914 \u23545 \u26412 \u25919 \u31574 \u20869 \u23481 \u26377 \u20219 \u20309 \u30097 \u38382 \u12289 \u24847 \u35265 \u25110 \u24314 \u35758 \u65292 \u24744 \u21487 \u20197 \u36890 \u36807 \u25105 \u20204 \u25552 \u20379 \u30340 \u32852 \u31995 \u26041 \u24335 \u21521 \u25105 \u20204 \u21672 \u35810 \u12290 \
\uc0\u20026 \u21327 \u21161 \u24744 \u20102 \u35299 \u25105 \u20204 \u38544 \u31169 \u20445 \u25252 \u25919 \u31574 \u65292 \u20197 \u19979 \u26159 \u26412 \u25919 \u31574 \u30340 \u35201 \u28857 \u35828 \u26126 \u65306 \
1\uc0\u12289 \u25105 \u20204 \u20250 \u36981 \u24490 \u38544 \u31169 \u20445 \u25252 \u25919 \u31574 \u25910 \u38598 \u12289 \u20351 \u29992 \u24744 \u30340 \u20449 \u24687 \u65292 \u20294 \u19981 \u20250 \u20165 \u22240 \u24744 \u21516 \u24847 \u26412 \u25919 \u31574 \u32780 \u37319 \u29992 \u24378 \u21046 \u25414 \u32465 \u30340 \u26041 \u24335 \u25910 \u38598 \u20840 \u37096 \u20010 \u20154 \u20449 \u24687 \u12290 \
2\uc0\u12289 \u25105 \u20204 \u20250 \u36880 \u19968 \u35828 \u26126 \u25152 \u25910 \u38598 \u30340 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u31867 \u22411 \u21450 \u20854 \u23545 \u24212 \u30340 \u29992 \u36884 \u65292 \u20197 \u20415 \u24744 \u20102 \u35299 \u25105 \u20204 \u38024 \u23545 \u26576 \u19968 \u29305 \u23450 \u21151 \u33021 \u25910 \u38598 \u30340 \u20855 \u20307 \u20010 \u20154 \u20449 \u24687 \u31867 \u21035 \u12289 \u20351 \u29992 \u29702 \u30001 \u21450 \u25910 \u38598 \u26041 \u24335 \u12290 \
3\uc0\u12289 \u24744 \u21487 \u20197 \u35775 \u38382 \u12289 \u26356 \u27491 \u12289 \u21024 \u38500 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12289 \u25913 \u21464 \u24744 \u25480 \u26435 \u21516 \u24847 \u30340 \u33539 \u22260 \u20197 \u21450 \u35831 \u27714 \u27880 \u38144 \u36134 \u25143 \u65292 \u25105 \u20204 \u20026 \u24744 \u25552 \u20379 \u20102 \u34892 \u20351 \u36825 \u20123 \u26435 \u21033 \u30340 \u36884 \u24452 \u12290 \
4\uc0\u12289 \u38500 \u38750 \u20877 \u27425 \u24449 \u24471 \u24744 \u30340 \u21516 \u24847 \u65292 \u25105 \u20204 \u19981 \u20250 \u23558 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u29992 \u20110 \u26412 \u25919 \u31574 \u26410 \u36733 \u26126 \u30340 \u20854 \u20182 \u30446 \u30340 \u12290 \
\
\uc0\u24341 \u35328 \
\
\uc0\u31508 \u22696 \u35759 \u20351 \u37325 \u35270 \u29992 \u25143 \u20449 \u24687 \u23433 \u20840 \u19982 \u38544 \u31169 \u20445 \u25252 \u65292 \u31469 \u23613 \u20840 \u21147 \u36890 \u36807 \u21512 \u29702 \u26377 \u25928 \u30340 \u20449 \u24687 \u23433 \u20840 \u25216 \u26415 \u21450 \u31649 \u29702 \u27969 \u31243 \u65292 \u38450 \u27490 \u24744 \u30340 \u20449 \u24687 \u27844 \u38706 \u12289 \u25439 \u27585 \u12289 \u20002 \u22833 \u65307 \u20026 \u24744 \u25552 \u20379 \u20415 \u21033 \u30340 \u20449 \u24687 \u31649 \u29702 \u36873 \u39033 \u65292 \u22686 \u24378 \u24744 \u23545 \u20449 \u24687 \u31649 \u29702 \u30340 \u20415 \u25463 \u24615 \u65307 \u20445 \u25252 \u24744 \u30340 \u36890 \u20449 \u31192 \u23494 \u65292 \u20445 \u38556 \u24744 \u30340 \u20449 \u24687 \u21450 \u36890 \u20449 \u23433 \u20840 \u12290 \u31508 \u22696 \u35759 \u20351 \u20005 \u26684 \u36981 \u23432 \u27861 \u24459 \u27861 \u35268 \u65292 \u20165 \u25910 \u38598 \u24517 \u35201 \u30340 \u20449 \u24687 \u65292 \u24182 \u25552 \u21319 \u20449 \u24687 \u22788 \u29702 \u36879 \u26126 \u24230 \u65292 \u22312 \u20135 \u21697 \u25110 \u26381 \u21153 \u24320 \u21457 \u30340 \u21508 \u20010 \u29615 \u33410 \u65292 \u32508 \u21512 \u27861 \u24459 \u12289 \u20135 \u21697 \u12289 \u35774 \u35745 \u31561 \u22810 \u26041 \u22240 \u32032 \u65292 \u34701 \u20837 \u38544 \u31169 \u20445 \u25252 \u30340 \u29702 \u24565 \u65292 \u20026 \u24744 \u25552 \u20379 \u26356 \u21152 \u23433 \u20840 \u12289 \u21487 \u38752 \u30340 \u26381 \u21153 \u12290 \
\
\uc0\u25105 \u20204 \u21046 \u23450 \u24182 \u36981 \u24490 \u26412 \u12298 \u38544 \u31169 \u25919 \u31574 \u12299 \u65288 \u20197 \u19979 \u31616 \u31216 \'93\u26412 \u25919 \u31574 \'94\u65289 \u65292 \u20027 \u35201 \u21521 \u24744 \u35828 \u26126 \u65306 \
\
\'95 \uc0\u25105 \u20204 \u25910 \u38598 \u21738 \u20123 \u20449 \u24687 \u65307 \
\'95 \uc0\u25105 \u20204 \u25910 \u38598 \u20449 \u24687 \u30340 \u29992 \u36884 \u65307 \
\'95 \uc0\u25105 \u20204 \u22914 \u20309 \u20849 \u20139 \u12289 \u36716 \u35753 \u12289 \u20844 \u24320 \u25259 \u38706 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65307 \
\'95 \uc0\u25105 \u20204 \u22914 \u20309 \u20445 \u23384 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65307 \
\'95 \uc0\u25105 \u20204 \u22914 \u20309 \u20351 \u29992 Cookie\u21644 \u21516 \u31867 \u25216 \u26415 \u65307 \
\'95 \uc0\u24744 \u22914 \u20309 \u31649 \u29702 \u20010 \u20154 \u20449 \u24687 \u65307 \
\'95 \uc0\u26410 \u25104 \u24180 \u20154 \u20010 \u20154 \u20449 \u24687 \u20445 \u25252 \u65307 \
\'95 \uc0\u36890 \u30693 \u21644 \u20462 \u35746 \u65307 \
\'95 \uc0\u22914 \u20309 \u32852 \u31995 \u25105 \u20204 \u65307 \
\
\uc0\u19968 \u12289 \u20010 \u20154 \u20449 \u24687 \u30340 \u23450 \u20041 \u19982 \u33539 \u22260 \
\
\uc0\u38500 \u21478 \u26377 \u32422 \u23450 \u22806 \u65292 \u26412 \u25919 \u31574 \u25152 \u29992 \u26415 \u35821 \u20855 \u26377 \u20197 \u19979 \u23450 \u20041 \u12290 \
\
1.1 \uc0\u20010 \u20154 \u20449 \u24687 \u65306 \u26159 \u25351 \u20197 \u30005 \u23376 \u25110 \u32773 \u20854 \u20182 \u26041 \u24335 \u35760 \u24405 \u30340 \u33021 \u22815 \u21333 \u29420 \u25110 \u32773 \u19982 \u20854 \u20182 \u20449 \u24687 \u32467 \u21512 \u35782 \u21035 \u33258 \u28982 \u20154 \u20010 \u20154 \u36523 \u20221 \u25110 \u32773 \u21453 \u26144 \u29305 \u23450 \u33258 \u28982 \u20154 \u27963 \u21160 \u24773 \u20917 \u30340 \u21508 \u31181 \u20449 \u24687 \u12290 \u26412 \u38544 \u31169 \u25919 \u31574 \u20013 \u28041 \u21450 \u30340 \u20010 \u20154 \u20449 \u24687 \u21253 \u25324 \u33258 \u28982 \u20154 \u30340 \u22995 \u21517 \u12289 \u26165 \u31216 \u12289 \u20986 \u29983 \u26085 \u26399 \u12289 \u22269 \u31821 \u12289 \u36523 \u20221 \u35777 \u20214 \u21495 \u30721 \u21644 \u26377 \u25928 \u26399 \u12289 \u24615 \u21035 \u12289 \u24180 \u40836 \u12289 \u20010 \u20154 \u29983 \u29289 \u35782 \u21035 \u20449 \u24687 \u12289 \u20303 \u22336 \u22320 \u25110 \u24037 \u20316 \u21333 \u20301 \u22320 \u22336 \u12289 \u30005 \u35805 \u21495 \u30721 \u12289 \u20844 \u21496 \u12289 \u26426 \u26500 \u12289 \u32844 \u19994 \u12290 \
\
1.2 \uc0\u20010 \u20154 \u25935 \u24863 \u20449 \u24687 \u65306 \u26159 \u25351 \u19968 \u26086 \u27844 \u38706 \u12289 \u38750 \u27861 \u25552 \u20379 \u25110 \u28389 \u29992 \u21487 \u33021 \u21361 \u23475 \u20154 \u36523 \u21644 \u36130 \u20135 \u23433 \u20840 \u65292 \u26497 \u26131 \u23548 \u33268 \u20010 \u20154 \u21517 \u35465 \u12289 \u36523 \u24515 \u20581 \u24247 \u21463 \u21040 \u25439 \u23475 \u25110 \u27495 \u35270 \u24615 \u24453 \u36935 \u31561 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \u20363 \u22914 \u65292 \u20010 \u20154 \u25935 \u24863 \u20449 \u24687 \u21253 \u25324 \u36523 \u20221 \u35777 \u20214 \u21495 \u30721 \u12289 \u20010 \u20154 \u29983 \u29289 \u35782 \u21035 \u20449 \u24687 \u12289 \u38134 \u34892 \u36134 \u21495 \u12289 \u36890 \u20449 \u20869 \u23481 \u12289 \u20581 \u24247 \u29983 \u29702 \u20449 \u24687 \u31561 \u12290 \u25925 \u24744 \u24212 \u33258 \u34892 \u35880 \u24910 \u21028 \u26029 \u30830 \u23450 \u30456 \u20851 \u20449 \u24687 \u30340 \u30495 \u23454 \u24615 \u12289 \u21512 \u27861 \u24615 \u21644 \u26377 \u25928 \u24615 \u65292 \u24182 \u33258 \u34892 \u25215 \u25285 \u22240 \u27492 \u20135 \u29983 \u30340 \u36131 \u20219 \u19982 \u25439 \u22833 \u12290 \
\
1.3 \uc0\u21311 \u21517 \u21270 \u22788 \u29702 \u21518 \u30340 \u20449 \u24687 \u65292 \u21363 \u36890 \u36807 \u23545 \u20010 \u20154 \u20449 \u24687 \u30340 \u25216 \u26415 \u22788 \u29702 \u65292 \u20351 \u24471 \u20010 \u20154 \u20449 \u24687 \u20027 \u20307 \u26080 \u27861 \u34987 \u35782 \u21035 \u65292 \u19988 \u22788 \u29702 \u21518 \u19981 \u33021 \u34987 \u22797 \u21407 \u30340 \u20449 \u24687 \u65292 \u21311 \u21517 \u21270 \u22788 \u29702 \u21518 \u30340 \u20449 \u24687 \u19981 \u23646 \u20110 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
1.4 \uc0\u20854 \u20182 \u26080 \u27861 \u35782 \u21035 \u29305 \u23450 \u33258 \u28982 \u20154 \u36523 \u20221 \u25110 \u32773 \u21453 \u26144 \u29305 \u23450 \u33258 \u28982 \u20154 \u27963 \u21160 \u24773 \u20917 \u30340 \u20449 \u24687 \u19981 \u23646 \u20110 \u20010 \u20154 \u20449 \u24687 \u12290 \u38500 \u19978 \u36848 \u23450 \u20041 \u22806 \u65292 \u26412 \u25919 \u31574 \u25152 \u29992 \u20854 \u20182 \u26415 \u35821 \u19982 \u12298 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u26381 \u21153 \u21327 \u35758 \u12299 \u20013 \u30340 \u26415 \u35821 \u20855 \u26377 \u30456 \u21516 \u30340 \u28085 \u20041 \u12290 \u22914 \u24744 \u26377 \u38382 \u39064 \u65292 \u35831 \u32852 \u31995 \u25105 \u20204 \u12290 \
\
\uc0\u20108 \u12289 \u25105 \u20204 \u25910 \u38598 \u30340 \u20449 \u24687 \
\
2.1 \uc0\u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u21151 \u33021 \u20998 \u20026 \u26680 \u24515 \u21151 \u33021 \u21644 \u38468 \u21152 \u21151 \u33021 \u65292 \u25105 \u20204 \u23558 \u35814 \u32454 \u21521 \u24744 \u35828 \u26126 \u21508 \u19994 \u21153 \u21151 \u33021 \u25152 \u25910 \u38598 \u30340 \u20010 \u20154 \u20449 \u24687 \u20197 \u21450 \u25910 \u38598 \u26041 \u27861 \u12289 \u30446 \u30340 \u65292 \u25298 \u32477 \u25552 \u20379 \u30340 \u24433 \u21709 \u31561 \u12290 \u19994 \u21153 \u21151 \u33021 \u20855 \u20307 \u21253 \u25324 \u65306 \
\
2.1.1 \uc0\u26680 \u24515 \u21151 \u33021 \u65292 \u21253 \u25324 \u21019 \u24314 \u36134 \u21495 \u24182 \u23436 \u21892 \u36164 \u26009 \u12289 \u20250 \u35805 \u21151 \u33021 \u12289 \u21457 \u29616 \u21151 \u33021 \u12290 \
\
2.1.2 \uc0\u38468 \u21152 \u21151 \u33021 \u65292 \u21253 \u25324 \u20010 \u20154 \u20027 \u39029 \u12289 \u36523 \u20221 \u35748 \u35777 \u12290 \
\
2.2 \uc0\u24744 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u26680 \u24515 \u21151 \u33021 \u21069 \u65292 \u38656 \u35201 \u21019 \u24314 \u36134 \u21495 \u24182 \u23436 \u21892 \u20010 \u20154 \u36164 \u26009 \u12290 \u36890 \u36807 \u29992 \u25143 \u20027 \u21160 \u25552 \u20379 \u30340 \u26041 \u24335 \u65292 \u25105 \u20204 \u20250 \u25910 \u38598 \u24744 \u30340 \u20197 \u19979 \u20449 \u24687 \u65306 \
\
2.2.1 \uc0\u24744 \u38656 \u35201 \u21521 \u25105 \u20204 \u25552 \u20379 \u24744 \u30340 \u25163 \u26426 \u21495 \u30721 \u65292 \u24182 \u35774 \u32622 \u24744 \u22312 \u31508 \u22696 \u35759 \u20351 \u30340 \u29992 \u25143 \u23494 \u30721 \u12290 \u25163 \u26426 \u21495 \u30721 \u30830 \u23450 \u21518 \u65292 \u26242 \u26102 \u26080 \u27861 \u20462 \u25913 \u12290 \u22914 \u26524 \u24744 \u25298 \u32477 \u25552 \u20379 \u25163 \u26426 \u21495 \u30721 \u31508 \u22696 \u35759 \u20351 \u23558 \u26080 \u27861 \u20026 \u24744 \u21019 \u24314 \u36134 \u21495 \u24182 \u25552 \u20379 \u26381 \u21153 \u12290 \u21516 \u26102 \u65292 \u20026 \u20102 \u20445 \u35777 \u36719 \u20214 \u27491 \u24120 \u36816 \u34892 \u65292 \u25105 \u20204 \u23558 \u20250 \u37319 \u38598 \u24744 \u30340 \u35774 \u22791 \u20449 \u24687 \u65292 \u20855 \u20307 \u21253 \u25324 \u65306 iOS\u31995 \u32479 \u65306 SDK\u29256 \u26412 \u12289 \u35774 \u22791 \u29992 \u25143 \u21517 \u12289 \u25163 \u26426 \u22411 \u21495 \u12289 \u31995 \u32479 \u22411 \u21495 \u12289 \u38142 \u25509 \u30340 \u26080 \u32447 MAC\u22320 \u22336 \u12289 \u35774 \u22791 \u38543 \u26426 \u24207 \u21015 \u21495 \u12289 IMSI\u12289 SIM\u21345 \u22269 \u23478 \u12289 \u30913 \u30424 \u24635 \u31354 \u38388 \u12289 \u36816 \u34892 \u20869 \u23384 \u12289 \u30005 \u27744 \u30005 \u37327 \u12289 \u32593 \u32476 \u29615 \u22659 \u12289 \u36816 \u33829 \u21830 \u12289 \u31995 \u32479 \u29256 \u26412 \u12289 CPU\u22411 \u21495 \u12289 \u23631 \u24149 \u20998 \u36776 \u29575 \u12289 IMEI\u12289 \u25163 \u26426 \u21697 \u29260 \u12289 \u24179 \u21488 \u31867 \u22411 \u12289 \u35774 \u22791 \u24207 \u21015 \u21495 \u31561 \u12290 \
\
2.2.2 \uc0\u24744 \u20351 \u29992 \u20250 \u35805 \u21151 \u33021 \u36827 \u34892 \u35821 \u38899 \u36890 \u35805 \u12289 \u21457 \u36865 \u35821 \u38899 \u20449 \u24687 \u12289 \u25293 \u25668 \u21450 \u30456 \u20876 \u26102 \u65307 \u25105 \u20204 \u20250 \u35843 \u29992 \u24744 \u20197 \u19979 \u35774 \u22791 \u26435 \u38480 \u65306 \u35774 \u22791 \u40614 \u20811 \u39118 \u26435 \u38480 \u12289 \u31995 \u32479 \u30456 \u20876 \u26435 \u38480 \u12289 \u31995 \u32479 \u25668 \u20687 \u22836 \u26435 \u38480 \u12290 \u19978 \u36848 \u21151 \u33021 \u21487 \u33021 \u38656 \u35201 \u24744 \u22312 \u24744 \u30340 \u35774 \u22791 \u20013 \u21521 \u25105 \u20204 \u24320 \u21551 \u24744 \u30340 \u30456 \u26426 \u65288 \u25668 \u20687 \u22836 \u65289 \u12289 \u30456 \u20876 \u65288 \u22270 \u29255 \u24211 \u65289 \u12289 \u40614 \u20811 \u39118 \u20197 \u21450 \u36890 \u35759 \u24405 \u30340 \u35775 \u38382 \u26435 \u38480 \u65292 \u20197 \u23454 \u29616 \u36825 \u20123 \u21151 \u33021 \u25152 \u28041 \u21450 \u30340 \u20449 \u24687 \u30340 \u25910 \u38598 \u21644 \u20351 \u29992 \u12290 \u35831 \u24744 \u27880 \u24847 \u65292 \u24744 \u24320 \u21551 \u36825 \u20123 \u26435 \u38480 \u21363 \u20195 \u34920 \u24744 \u25480 \u26435 \u25105 \u20204 \u21487 \u20197 \u25910 \u38598 \u21644 \u20351 \u29992 \u36825 \u20123 \u20010 \u20154 \u20449 \u24687 \u26469 \u23454 \u29616 \u19978 \u36848 \u30340 \u21151 \u33021 \u65292 \u24744 \u20851 \u38381 \u26435 \u38480 \u21363 \u20195 \u34920 \u24744 \u21462 \u28040 \u20102 \u36825 \u20123 \u25480 \u26435 \u65292 \u21017 \u25105 \u20204 \u23558 \u19981 \u20877 \u32487 \u32493 \u25910 \u38598 \u21644 \u20351 \u29992 \u24744 \u30340 \u36825 \u20123 \u20010 \u20154 \u20449 \u24687 \u65292 \u20063 \u26080 \u27861 \u20026 \u24744 \u25552 \u20379 \u19978 \u36848 \u19982 \u36825 \u20123 \u25480 \u26435 \u25152 \u23545 \u24212 \u30340 \u21151 \u33021 \u12290 \u24744 \u20851 \u38381 \u26435 \u38480 \u30340 \u20915 \u23450 \u19981 \u20250 \u24433 \u21709 \u27492 \u21069 \u22522 \u20110 \u24744 \u30340 \u25480 \u26435 \u25152 \u36827 \u34892 \u30340 \u20010 \u20154 \u20449 \u24687 \u30340 \u22788 \u29702 \u12290 \
\
\
2.3 \uc0\u20026 \u20102 \u25552 \u39640 \u24744 \u30340 \u31038 \u20132 \u23646 \u24615 \u65292 \u22312 \u31038 \u32676 \u20013 \u32467 \u23454 \u26356 \u22810 \u24535 \u21516 \u36947 \u21512 \u30340 \u26379 \u21451 \u65292 \u25105 \u20204 \u22312 \u20197 \u19979 \u38468 \u21152 \u21151 \u33021 \u20013 \u21487 \u33021 \u20250 \u25910 \u38598 \u21644 \u20351 \u29992 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \u22914 \u26524 \u24744 \u19981 \u25552 \u20379 \u36825 \u20123 \u20010 \u20154 \u20449 \u24687 \u65292 \u24744 \u20381 \u28982 \u21487 \u20197 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u30340 \u26680 \u24515 \u21151 \u33021 \u12290 \
\
2.3.1 \uc0\u24744 \u21487 \u20197 \u36873 \u25321 \u21521 \u25105 \u20204 \u25552 \u20379 \u26165 \u31216 \u12289 \u22995 \u21517 \u12289 \u24615 \u21035 \u12289 \u24180 \u40836 \u12289 \u25152 \u22312 \u22320 \u12289 \u34892 \u19994 \u12289 \u20844 \u21496 /\u26426 \u26500 \u12289 \u32844 \u19994 \u12290 \u24744 \u21487 \u20197 \u22312 \u20010 \u20154 \u20027 \u39029 \u36164 \u26009 \u20013 \u36873 \u25321 \u26159 \u21542 \u22635 \u20889 \u19978 \u36848 \u20449 \u24687 \u12290 \u22914 \u24744 \u25552 \u20379 \u19978 \u36848 \u20449 \u24687 \u65292 \u21487 \u20197 \u20351 \u20854 \u20182 \u29992 \u25143 \u23545 \u24744 \u26377 \u26356 \u20840 \u38754 \u30340 \u20102 \u35299 \u12290 \u24744 \u20063 \u21487 \u20197 \u22312 \u24744 \u30340 \u20010 \u20154 \u20027 \u39029 \u20013 \u23558 \u19978 \u36848 \u38544 \u31169 \u20107 \u39033 \u35774 \u32622 \u20026 \u25152 \u26377 \u20154 /\u20165 \u22909 \u21451 /\u20165 \u33258 \u24049 \u21487 \u35265 \u12290 \
\
2.4 \uc0\u24744 \u21487 \u20197 \u36873 \u25321 \u30003 \u35831 \u20010 \u20154 \u21152 V\u35748 \u35777 \u65292 \u30003 \u35831 \u21152 V\u35748 \u35777 \u24744 \u38656 \u35201 \u25552 \u20379 \u24744 \u30340 \u20010 \u20154 \u31616 \u20171 \u12289 \u26165 \u31216 \u12289 \u24182 \u19978 \u20256 \u32844 \u20301 \u12289 \u25216 \u33021 \u35777 \u26126 \u26448 \u26009 \u65292 \u21253 \u21547 \u22995 \u21517 \u12289 \u20844 \u21496 /\u32452 \u32455 \u20449 \u24687 \u12289 \u32844 \u20301 \u20449 \u24687 \u30340 \u21152 \u30422 \u20844 \u31456 \u30340 \u25130 \u22270 \u26448 \u26009 \u12289 \u20844 \u24320 \u21487 \u26597 \u30340 \u23448 \u26041 \u20010 \u20154 \u20449 \u24687 \u39029 \u25130 \u22270 \u26448 \u26009 \u12290 \u25104 \u20026 \u31508 \u22696 \u35759 \u20351 \u35748 \u35777 \u20250 \u21592 \u21487 \u20197 \u25552 \u21319 \u22312 \u31038 \u32676 \u30340 \u20010 \u20154 \u31038 \u20132 \u23646 \u24615 \u12290 \
\
2.5 \uc0\u20026 \u20102 \u24110 \u21161 \u24744 \u26356 \u22909 \u22320 \u20351 \u29992 \u25105 \u20204 \u30340 \u20135 \u21697 \u25110 \u26381 \u21153 \u65292 \u25105 \u20204 \u36824 \u20250 \u25910 \u38598 \u21253 \u25324 \u65306 \u24744 \u30340 \u22909 \u21451 \u21015 \u34920 \u12289 \u32676 \u21015 \u34920 \u20449 \u24687 \u12289 \u22768 \u32441 \u29305 \u24449 \u20540 \u20449 \u24687 \u12290 \u20026 \u30830 \u20445 \u24744 \u20351 \u29992 \u25105 \u20204 \u26381 \u21153 \u26102 \u33021 \u19982 \u24744 \u35748 \u35782 \u30340 \u20154 \u36827 \u34892 \u32852 \u31995 \u65292 \u22914 \u24744 \u36873 \u25321 \u24320 \u21551 \u23548 \u20837 \u36890 \u35759 \u24405 \u21151 \u33021 \u65292 \u25105 \u20204 \u21487 \u33021 \u23545 \u24744 \u32852 \u31995 \u20154 \u30340 \u22995 \u21517 \u21644 \u30005 \u35805 \u21495 \u30721 \u36827 \u34892 \u21152 \u23494 \u65292 \u24182 \u20165 \u25910 \u38598 \u21152 \u23494 \u21518 \u30340 \u20449 \u24687 \u12290 \
\
2.6 \uc0\u25105 \u20204 \u21487 \u33021 \u20250 \u33719 \u24471 \u24744 \u22312 \u20351 \u29992 \u31532 \u19977 \u26041 \u21512 \u20316 \u20249 \u20276 \u26381 \u21153 \u26102 \u25152 \u20135 \u29983 \u25110 \u20998 \u20139 \u30340 \u20449 \u24687 \u12290 \u21253 \u25324 \u65306 \u24744 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u24080 \u25143 \u30331 \u24405 \u31532 \u19977 \u26041 \u21512 \u20316 \u20249 \u20276 \u26381 \u21153 \u26102 \u65292 \u25110 \u32773 \u24744 \u20351 \u29992 \u31532 \u19977 \u26041 \u21512 \u20316 \u20249 \u20276 \u26381 \u21153 \u36134 \u25143 \u30331 \u24405 \u31508 \u22696 \u35759 \u20351 \u26102 \u65292 \u25105 \u20204 \u20250 \u33719 \u24471 \u24744 \u30331 \u24405 \u31532 \u19977 \u26041 \u21512 \u20316 \u20249 \u20276 \u26381 \u21153 \u30340 \u21517 \u31216 \u12289 \u30331 \u24405 \u26102 \u38388 \u25110 \u30331 \u24405 \u31508 \u22696 \u35759 \u20351 \u30340 \u30331 \u24405 \u26102 \u38388 \u65292 \u26041 \u20415 \u24744 \u36827 \u34892 \u25480 \u26435 \u31649 \u29702 \u12290 \u35831 \u24744 \u20180 \u32454 \u38405 \u35835 \u31532 \u19977 \u26041 \u21512 \u20316 \u20249 \u20276 \u26381 \u21153 \u30340 \u29992 \u25143 \u21327 \u35758 \u25110 \u38544 \u31169 \u25919 \u31574 \u12290 \
\
\uc0\u25105 \u20204 \u30340 \u20135 \u21697 \u22312 \u24212 \u29992 \u36816 \u34892 \u26399 \u38388 \u38656 \u35201 \u25910 \u38598 \u24744 \u30340 \u35774 \u22791 \u21807 \u19968 \u35782 \u21035 \u30721 \u65288 MAC\u22320 \u22336 \u21450 IMEI/IDFA\u12289 SIM \u21345 IMSI \u20449 \u24687 \u12289 OAID\u12289 ANDROID ID\u65289 \u20197 \u25552 \u20379 \u32479 \u35745 \u20998 \u26512 \u31561 \u26381 \u21153 \u65292 \u24182 \u36890 \u36807 \u24212 \u29992 \u21551 \u21160 \u25968 \u25454 \u21450 \u24322 \u24120 \u38169 \u35823 \u26085 \u24535 \u20998 \u26512 \u25913 \u36827 \u24615 \u33021 \u21644 \u29992 \u25143 \u20307 \u39564 \u65292 \u20026 \u29992 \u25143 \u25552 \u20379 \u26356 \u22909 \u30340 \u26381 \u21153 \u12290 \
\
APP\uc0\u28041 \u21450 \u23884 \u20837 \u31532 \u19977 \u26041 \u20195 \u30721 \u12289 \u25554 \u20214 \u20256 \u36755 \u20010 \u20154 \u20449 \u24687 \u30340 \u24773 \u24418 \u36880 \u39033 \u21015 \u20030 \u22914 \u19979 \u65306 \
\
\uc0\u33719 \u21462 \u35774 \u22791 \u30340 \u20256 \u24863 \u22120 \
\
\uc0\u29992 \u36884 \u65306 \u33719 \u21462 \u35774 \u22791 \u30340 \u20256 \u24863 \u22120 \u21015 \u34920 \u20026 \u20102 \u26816 \u27979 \u12289 \u38450 \u27490 \u23433 \u21331 \u27169 \u25311 \u22120 \u31561 \u19977 \u26041 \u27169 \u25311 \u22120 \u30340 \u20351 \u29992 \
\
Bugly SDK\
\
\uc0\u20027 \u20307 \u65306 \u28145 \u22323 \u24066 \u33150 \u35759 \u35745 \u31639 \u26426 \u31995 \u32479 \u26377 \u38480 \u20844 \u21496 \
\
\uc0\u29992 \u36884 \u65306 \u24212 \u29992 \u23849 \u28291 \u38382 \u39064 \u25429 \u33719 \
\
\uc0\u25910 \u38598 \u30340 \u20010 \u20154 \u20449 \u24687 \u31867 \u22411 \u65306 \u30828 \u20214 \u22411 \u21495 \u12289 \u35774 \u22791 MAC\u22320 \u22336 \u12289 \u21807 \u19968 \u35774 \u22791 \u35782 \u21035 \u30721 \u65288 \u22914 IMEI/MEID/ ANDROID ID /SN/OPENUDID/GUID\u12289 SIM\u21345 IMSI\u20449 \u24687 \u22312 \u20869 \u30340 \u25551 \u36848 \u20010 \u20154 \u24120 \u29992 \u35774 \u22791 \u22522 \u26412 \u24773 \u20917 \u30340 \u20449 \u24687 \u65289 \u12289 \u35774 \u22791 \u22411 \u21495 \u12289 \u35774 \u22791 \u21046 \u36896 \u21830 \u12289 \u25805 \u20316 \u31995 \u32479 \u21450 \u20854 \u29256 \u26412 \u65307 \
\
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 \
\
https://privacy.qq.com/document/preview/fc748b3d96224fdb825ea79e132c1a56\
\
OPPO PUSH SDK\
\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u26631 \u35782 \u31526 \u65288 \u22914 IMEI\u12289 OAID\u12289 \u30828 \u20214 \u24207 \u21015 \u21495 /Serial\u12289 IMSI\u12289 Android ID\u12289 GAID/AAID\u65289 \u12289 \u24212 \u29992 \u20449 \u24687 \u12289 \u32593 \u32476 \u20449 \u24687 \u12289 IP\u22320 \u22336 \u12289 \u35774 \u22791 \u20449 \u24687 \
\
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 OPPO\u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u24191 \u19996 \u27426 \u22826 \u31185 \u25216 \u26377 \u38480 \u20844 \u21496 \
\
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://open.oppomobile.com/wiki/doc#id=10194\
\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://open.oppomobile.com/new/developmentDoc/info?id=11228\
\
VIVO PUSH SDK\
\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u20449 \u24687 \u12289 IMEI\u12289 OAID\u12289 GAID/AAID\u12289 Android ID\u12289 \u32593 \u32476 \u20449 \u24687 \
\
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 VIVO\u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u32500 \u27779 \u31227 \u21160 \u36890 \u20449 \u26377 \u38480 \u20844 \u21496 \
\
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://dev.vivo.com.cn/documentCenter/doc/366\
\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://dev.vivo.com.cn/documentCenter/doc/652\
\
\uc0\u21326 \u20026 PUSH SDK\
\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u24212 \u29992 \u22522 \u26412 \u20449 \u24687 \u12289 \u24212 \u29992 \u20869 \u35774 \u22791 \u26631 \u35782 \u31526 \u12289 \u35774 \u22791 \u30340 \u30828 \u20214 \u20449 \u24687 \u12289 \u36719 \u20214 \u21015 \u34920 \u12289 \u31995 \u32479 \u22522 \u26412 \u20449 \u24687 \u21644 \u31995 \u32479 \u35774 \u32622 \u20449 \u24687 \
\
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u21326 \u20026 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u21326 \u20026 \u36719 \u20214 \u25216 \u26415 \u26377 \u38480 \u20844 \u21496 \
\
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://developer.huawei.com/consumer/cn/doc/app/20213\
\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/sdk-data-security-0000001050042177\
\
hmscore-base-sdk\
\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u24212 \u29992 \u22522 \u26412 \u20449 \u24687 \u12289 \u24212 \u29992 \u20869 \u35774 \u22791 \u26631 \u35782 \u31526 \u12289 \u35774 \u22791 \u30340 \u30828 \u20214 \u20449 \u24687 \u12289 \u36719 \u20214 \u21015 \u34920 \u12289 \u31995 \u32479 \u22522 \u26412 \u20449 \u24687 \u21644 \u31995 \u32479 \u35774 \u32622 \u20449 \u24687 \
\
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u21326 \u20026 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u21326 \u20026 \u36719 \u20214 \u25216 \u26415 \u26377 \u38480 \u20844 \u21496 \
\
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://developer.huawei.com/consumer/cn/doc/app/20213\
\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://developer.huawei.com/consumer/cn/doc/development/HMSCore-Guides/sdk-data-security-0000001050042177\
\
\uc0\u23567 \u31859 PUSH SDK\
\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u26631 \u35782 \u31526 \u65288 \u22914 Android ID\u12289 OAID\u65289 \u12289 \u24212 \u29992 \u20449 \u24687 \u12289 \u35774 \u22791 \u20449 \u24687 \u12289 \u32593 \u32476 \u20449 \u24687 \u65288 \u26412 \u22320 \u22788 \u29702 \u65289 \u12289 SSID\
\
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u23567 \u31859 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u21271 \u20140 \u23567 \u31859 \u31227 \u21160 \u36719 \u20214 \u26377 \u38480 \u20844 \u21496 \
\
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://dev.mi.com/console/appservice/push.html\
\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://dev.mi.com/console/doc/detail?pId=1822\
\
\uc0\u33635 \u32768 PUSH SDK\
\
\uc0\u28041 \u21450 \u20010 \u20154 \u20449 \u24687 \u65306 \u35774 \u22791 \u26631 \u35782 \u31526 (GAID/AAID)\u65292 \u24212 \u29992 Token\u12289 \u25512 \u36865 \u28040 \u24687 \u20869 \u23481 \
\
\uc0\u20351 \u29992 \u30446 \u30340 \u65306 \u25512 \u36865 \u28040 \u24687 \
\
\uc0\u20351 \u29992 \u22330 \u26223 \u65306 \u22312 \u33635 \u32768 \u25163 \u26426 \u32456 \u31471 \u25512 \u36865 \u28040 \u24687 \u26102 \u20351 \u29992 \
\
\uc0\u31532 \u19977 \u26041 \u20027 \u20307 \u65306 \u33635 \u32768 \u32456 \u31471 \u26377 \u38480 \u20844 \u21496 \
\
\uc0\u25910 \u38598 \u26041 \u24335 \u65306 SDK\u33258 \u34892 \u37319 \u38598 \
\
\uc0\u23448 \u32593 \u38142 \u25509 \u65306 https://lbs.amap.com/\
\
\uc0\u38544 \u31169 \u25919 \u31574 \u65306 https://lbs.amap.com/pages/privacy/\
\
\uc0\u24744 \u25552 \u20379 \u30340 \u19978 \u36848 \u20449 \u24687 \u23558 \u22312 \u24744 \u20351 \u29992 \u26412 \u26381 \u21153 \u26399 \u38388 \u25345 \u32493 \u25480 \u26435 \u25105 \u20204 \u20351 \u29992 \u65292 \u20026 \u26041 \u20415 \u24744 \u31649 \u29702 \u36719 \u20214 \u36134 \u25143 \u65292 \u25105 \u20204 \u20250 \u22312 \u36719 \u20214 \u36134 \u25143 \u26381 \u21153 \u20013 \u21516 \u27493 \u24182 \u26174 \u31034 \u24744 \u30340 \u27880 \u20876 \u20449 \u24687 \u65292 \u21253 \u25324 \u36890 \u36807 \u36719 \u20214 \u36134 \u25143 \u38598 \u20013 \u23637 \u31034 \u24744 \u30340 \u20010 \u20154 \u36164 \u26009 \u12289 \u20139 \u21463 \u26435 \u30410 \u31561 \u12290 \u22312 \u24744 \u20027 \u21160 \u27880 \u38144 \u36134 \u21495 \u26102 \u65292 \u25105 \u20204 \u23558 \u26681 \u25454 \u36866 \u29992 \u27861 \u24459 \u27861 \u35268 \u30340 \u35201 \u27714 \u31435 \u21363 \u20351 \u20854 \u21311 \u21517 \u25110 \u21024 \u38500 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
\uc0\u19977 \u12289 \u25105 \u20204 \u22914 \u20309 \u20351 \u29992 \u25910 \u38598 \u30340 \u20449 \u24687 \
\
\uc0\u25105 \u20204 \u20005 \u26684 \u36981 \u23432 \u27861 \u24459 \u27861 \u35268 \u30340 \u35268 \u23450 \u21450 \u19982 \u29992 \u25143 \u30340 \u32422 \u23450 \u65292 \u23558 \u25910 \u38598 \u30340 \u20449 \u24687 \u29992 \u20110 \u20197 \u19979 \u29992 \u36884 \u12290 \u33509 \u25105 \u20204 \u36229 \u20986 \u20197 \u19979 \u29992 \u36884 \u20351 \u29992 \u24744 \u30340 \u20449 \u24687 \u65292 \u25105 \u20204 \u23558 \u20877 \u27425 \u21521 \u24744 \u36827 \u34892 \u35828 \u26126 \u65292 \u24182 \u24449 \u24471 \u24744 \u30340 \u21516 \u24847 \u12290 \
\
3.1 \uc0\u21521 \u24744 \u25552 \u20379 \u26381 \u21153 \u12290 \
\
3.2 \uc0\u28385 \u36275 \u24744 \u30340 \u38656 \u27714 \u12290 \u20363 \u22914 \u65292 \u35821 \u35328 \u35774 \u23450 \u12289 \u20301 \u32622 \u35774 \u23450 \u12289 \u24110 \u21161 \u26381 \u21153 \u12290 \
\
3.3 \uc0\u20135 \u21697 \u24320 \u21457 \u21644 \u26381 \u21153 \u20248 \u21270 \u12290 \u20363 \u22914 \u65292 \u24403 \u25105 \u20204 \u30340 \u31995 \u32479 \u21457 \u29983 \u25925 \u38556 \u26102 \u65292 \u25105 \u20204 \u20250 \u35760 \u24405 \u21644 \u20998 \u26512 \u31995 \u32479 \u25925 \u38556 \u26102 \u20135 \u29983 \u30340 \u20449 \u24687 \u65292 \u20248 \u21270 \u25105 \u20204 \u30340 \u26381 \u21153 \u12290 \
\
3.4 \uc0\u23433 \u20840 \u20445 \u38556 \u12290 \u20363 \u22914 \u65292 \u25105 \u20204 \u20250 \u23558 \u24744 \u30340 \u20449 \u24687 \u29992 \u20110 \u36523 \u20221 \u39564 \u35777 \u12289 \u23433 \u20840 \u38450 \u33539 \u12289 \u21453 \u35784 \u39575 \u30417 \u27979 \u12289 \u23384 \u26723 \u22791 \u20221 \u12289 \u23458 \u25143 \u30340 \u23433 \u20840 \u26381 \u21153 \u31561 \u29992 \u36884 \u12290 \u20363 \u22914 \u65292 \u24744 \u19979 \u36733 \u25110 \u23433 \u35013 \u30340 \u23433 \u20840 \u36719 \u20214 \u20250 \u23545 \u24694 \u24847 \u31243 \u24207 \u25110 \u30149 \u27602 \u36827 \u34892 \u26816 \u27979 \u65292 \u25110 \u20026 \u24744 \u35782 \u21035 \u35784 \u39575 \u20449 \u24687 \u12290 \
\
3.5 \uc0\u35780 \u20272 \u12289 \u25913 \u21892 \u25105 \u20204 \u30340 \u24191 \u21578 \u25237 \u25918 \u21644 \u20854 \u20182 \u20419 \u38144 \u21450 \u25512 \u24191 \u27963 \u21160 \u30340 \u25928 \u26524 \u12290 \
\
3.6 \uc0\u31649 \u29702 \u36719 \u20214 \u12290 \u20363 \u22914 \u65292 \u36827 \u34892 \u36719 \u20214 \u35748 \u35777 \u12289 \u36719 \u20214 \u21319 \u32423 \u31561 \u12290 \
\
3.7 \uc0\u36992 \u35831 \u24744 \u21442 \u19982 \u26377 \u20851 \u25105 \u20204 \u26381 \u21153 \u30340 \u35843 \u26597 \u12290 \u20026 \u20102 \u35753 \u24744 \u26377 \u26356 \u22909 \u30340 \u20307 \u39564 \u12289 \u25913 \u21892 \u25105 \u20204 \u30340 \u26381 \u21153 \u25110 \u32463 \u24744 \u21516 \u24847 \u30340 \u20854 \u20182 \u29992 \u36884 \u65292 \u22312 \u31526 \u21512 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u30340 \u21069 \u25552 \u19979 \u65292 \u25105 \u20204 \u21487 \u33021 \u23558 \u36890 \u36807 \u26576 \u20123 \u26381 \u21153 \u25152 \u25910 \u38598 \u30340 \u20449 \u24687 \u29992 \u20110 \u25105 \u20204 \u30340 \u20854 \u20182 \u26381 \u21153 \u12290 \u20363 \u22914 \u65292 \u23558 \u24744 \u22312 \u20351 \u29992 \u25105 \u20204 \u26576 \u39033 \u26381 \u21153 \u26102 \u30340 \u20449 \u24687 \u65292 \u29992 \u20110 \u29992 \u25143 \u30740 \u31350 \u20998 \u26512 \u19982 \u32479 \u35745 \u31561 \u26381 \u21153 \u12290 \u20026 \u20102 \u30830 \u20445 \u26381 \u21153 \u30340 \u23433 \u20840 \u65292 \u24110 \u21161 \u25105 \u20204 \u26356 \u22909 \u22320 \u20102 \u35299 \u25105 \u20204 \u24212 \u29992 \u31243 \u24207 \u30340 \u36816 \u34892 \u24773 \u20917 \u65292 \u25105 \u20204 \u21487 \u33021 \u35760 \u24405 \u30456 \u20851 \u20449 \u24687 \u65292 \u20363 \u22914 \u65292 \u24744 \u20351 \u29992 \u24212 \u29992 \u31243 \u24207 \u30340 \u39057 \u29575 \u12289 \u25925 \u38556 \u20449 \u24687 \u12289 \u24635 \u20307 \u20351 \u29992 \u24773 \u20917 \u12289 \u24615 \u33021 \u25968 \u25454 \u20197 \u21450 \u24212 \u29992 \u31243 \u24207 \u30340 \u26469 \u28304 \u12290 \u25105 \u20204 \u19981 \u20250 \u23558 \u25105 \u20204 \u23384 \u20648 \u22312 \u20998 \u26512 \u36719 \u20214 \u20013 \u30340 \u20449 \u24687 \u19982 \u24744 \u22312 \u24212 \u29992 \u31243 \u24207 \u20013 \u25552 \u20379 \u30340 \u20010 \u20154 \u36523 \u20221 \u20449 \u24687 \u30456 \u32467 \u21512 \u12290 \
\
\uc0\u22235 \u12289 \u25105 \u20204 \u22914 \u20309 \u20849 \u20139 \u12289 \u36716 \u35753 \u12289 \u20844 \u24320 \u25259 \u38706 \u24744 \u30340 \u20449 \u24687 \
4.1 \uc0\u25105 \u20204 \u23558 \u36981 \u23432 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u65292 \u23545 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u20104 \u20197 \u20445 \u23494 \u12290 \u38500 \u20197 \u19979 \u24773 \u20917 \u22806 \u65292 \u25105 \u20204 \u19981 \u20250 \u21521 \u20854 \u20182 \u20154 \u20849 \u20139 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65306 \
A. \uc0\u21482 \u26377 \u20849 \u20139 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u25165 \u33021 \u25552 \u20379 \u24744 \u25152 \u38656 \u35201 \u30340 \u26381 \u21153 \u12290 \
\
B. \uc0\u20026 \u20102 \u25552 \u21319 \u25105 \u20204 \u30340 \u20135 \u21697 \u21450 \u26381 \u21153 \u36136 \u37327 \u25110 \u21521 \u24744 \u25552 \u20379 \u20840 \u26032 \u30340 \u20135 \u21697 \u21450 \u26381 \u21153 \u65292 \u25105 \u20204 \u20250 \u22312 \u20851 \u32852 \u20844 \u21496 \u20869 \u37096 \u20849 \u20139 \u24744 \u30340 \u30456 \u20851 \u20449 \u24687 \u12290 \
\
C. \uc0\u24744 \u36890 \u36807 \u31508 \u22696 \u35759 \u20351 \u36719 \u20214 \u20351 \u29992 \u31532 \u19977 \u26041 \u26381 \u21153 \u26102 \u65292 \u31508 \u22696 \u35759 \u20351 \u20250 \u25353 \u29031 \'93\u20010 \u20154 \u20449 \u24687 \u30340 \u25910 \u38598 \'94\u26465 \u27454 \u21015 \u26126 \u30340 \u20849 \u20139 \u20869 \u23481 \u19982 \u25552 \u20379 \u26381 \u21153 \u30340 \u31532 \u19977 \u26041 \u25110 \u21512 \u20316 \u20249 \u20276 \u65288 \u24191 \u21578 \u12289 \u20998 \u26512 \u26381 \u21153 \u31867 \u30340 \u25480 \u26435 \u21512 \u20316 \u20249 \u20276 \u12289 \u20379 \u24212 \u21830 \u12289 \u26381 \u21153 \u25552 \u20379 \u21830 \u21644 \u20854 \u20182 \u21512 \u20316 \u20249 \u20276 \u65289 \u20849 \u20139 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
D. \uc0\u20026 \u32500 \u25252 \u29992 \u25143 \u21512 \u27861 \u26435 \u30410 \u65292 \u22312 \u21327 \u21161 \u22788 \u29702 \u19982 \u24744 \u26377 \u20851 \u30340 \u32416 \u32439 \u25110 \u20105 \u35758 \u26102 \u65292 \u25105 \u20204 \u21487 \u33021 \u21521 \u19982 \u24744 \u23384 \u22312 \u21033 \u23475 \u20851 \u31995 \u30340 \u31532 \u19977 \u26041 \u25552 \u20379 \u35299 \u20915 \u32416 \u32439 \u25110 \u20105 \u35758 \u25152 \u24517 \u38656 \u30340 \u20449 \u24687 \u12290 \
\
E. \uc0\u26681 \u25454 \u27861 \u24459 \u35268 \u23450 \u65292 \u21009 \u20107 \u20390 \u26597 \u26426 \u20851 \u20026 \u35843 \u26597 \u29359 \u32618 \u65292 \u20381 \u27861 \u23450 \u31243 \u24207 \u35843 \u21462 \u30340 \u24517 \u35201 \u20010 \u20154 \u20449 \u24687 \u65292 \u25110 \u34892 \u25919 \u26426 \u20851 \u12289 \u21496 \u27861 \u26426 \u26500 \u65292 \u20381 \u27861 \u23450 \u31243 \u24207 \u24182 \u32463 \u24744 \u25480 \u26435 \u21516 \u24847 \u35843 \u21462 \u30340 \u24517 \u35201 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
F. \uc0\u22312 \u27861 \u24459 \u35201 \u27714 \u25110 \u20801 \u35768 \u30340 \u33539 \u22260 \u20869 \u65292 \u20026 \u20102 \u20445 \u25252 \u31038 \u20250 \u20844 \u20849 \u21033 \u30410 \u12289 \u36130 \u20135 \u25110 \u23433 \u20840 \u32780 \u25552 \u20379 \u24517 \u35201 \u30340 \u20449 \u24687 \u12290 \
\
G. \uc0\u22312 \u27861 \u23450 \u24773 \u24418 \u19979 \u30340 \u20849 \u20139 \u65306 \u25105 \u20204 \u21487 \u33021 \u20250 \u26681 \u25454 \u27861 \u24459 \u27861 \u35268 \u35268 \u23450 \u12289 \u35785 \u35772 \u20105 \u35758 \u35299 \u20915 \u38656 \u35201 \u65292 \u25110 \u25353 \u34892 \u25919 \u12289 \u21496 \u27861 \u26426 \u20851 \u20381 \u27861 \u25552 \u20986 \u30340 \u35201 \u27714 \u65292 \u23545 \u22806 \u20849 \u20139 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
H. \uc0\u21478 \u34892 \u33719 \u24471 \u24744 \u30340 \u26126 \u30830 \u21516 \u24847 \u25110 \u25480 \u26435 \u12290 \
\
4.2 \uc0\u38750 \u32463 \u27861 \u23450 \u31243 \u24207 \u25110 \u33719 \u24471 \u24744 \u30340 \u21516 \u24847 \u65292 \u25105 \u20204 \u19981 \u20250 \u23558 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u25552 \u20379 \u32473 \u20219 \u20309 \u31532 \u19977 \u26041 \u26426 \u26500 \u25110 \u20010 \u20154 \u12290 \u23545 \u25105 \u20204 \u19982 \u20043 \u20849 \u20139 \u20010 \u20154 \u20449 \u24687 \u30340 \u20844 \u21496 \u12289 \u32452 \u32455 \u21644 \u20010 \u20154 \u65292 \u25105 \u20204 \u20250 \u19982 \u20043 \u31614 \u32626 \u20005 \u26684 \u30340 \u20445 \u23494 \u21327 \u35758 \u65292 \u35201 \u27714 \u20182 \u20204 \u25353 \u29031 \u25105 \u20204 \u30340 \u35828 \u26126 \u12289 \u26412 \u38544 \u31169 \u25919 \u31574 \u21450 \u20854 \u20182 \u20219 \u20309 \u30456 \u20851 \u30340 \u20445 \u23494 \u21644 \u23433 \u20840 \u25514 \u26045 \u26469 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
4.3 \uc0\u25105 \u20204 \u23558 \u36981 \u23432 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u65292 \u23545 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u20104 \u20197 \u20445 \u23494 \u12290 \u38500 \u38750 \u20107 \u20808 \u33719 \u24471 \u24744 \u30340 \u26126 \u30830 \u21516 \u24847 \u25110 \u25480 \u26435 \u65292 \u25110 \u20381 \u29031 \u27861 \u24459 \u35268 \u23450 \u25152 \u24517 \u39035 \u65292 \u25105 \u20204 \u19981 \u20250 \u20844 \u24320 \u25259 \u38706 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
4.4 \uc0\u21478 \u22806 \u65292 \u26681 \u25454 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u21450 \u22269 \u23478 \u26631 \u20934 \u65292 \u20197 \u19979 \u24773 \u24418 \u20013 \u65292 \u25105 \u20204 \u21487 \u33021 \u20250 \u20849 \u20139 \u12289 \u36716 \u35753 \u12289 \u20844 \u24320 \u25259 \u38706 \u20010 \u20154 \u20449 \u24687 \u26080 \u38656 \u20107 \u20808 \u24449 \u24471 \u20010 \u20154 \u20449 \u24687 \u20027 \u20307 \u30340 \u25480 \u26435 \u21516 \u24847 \u65306 \
A. \uc0\u19982 \u22269 \u23478 \u23433 \u20840 \u12289 \u22269 \u38450 \u23433 \u20840 \u30452 \u25509 \u30456 \u20851 \u30340 \u65307 \
\
B. \uc0\u19982 \u20844 \u20849 \u23433 \u20840 \u12289 \u20844 \u20849 \u21355 \u29983 \u12289 \u37325 \u22823 \u20844 \u20849 \u21033 \u30410 \u30452 \u25509 \u30456 \u20851 \u30340 \u65307 \
\
C. \uc0\u19982 \u29359 \u32618 \u20390 \u26597 \u12289 \u36215 \u35785 \u12289 \u23457 \u21028 \u21644 \u21028 \u20915 \u25191 \u34892 \u31561 \u30452 \u25509 \u30456 \u20851 \u30340 \u65307 \
\
D. \uc0\u20986 \u20110 \u32500 \u25252 \u20010 \u20154 \u20449 \u24687 \u20027 \u20307 \u25110 \u20854 \u20182 \u20010 \u20154 \u30340 \u29983 \u21629 \u12289 \u36130 \u20135 \u31561 \u37325 \u22823 \u21512 \u27861 \u26435 \u30410 \u20294 \u21448 \u24456 \u38590 \u24471 \u21040 \u26412 \u20154 \u21516 \u24847 \u30340 \u65307 \
\
E. \uc0\u20010 \u20154 \u20449 \u24687 \u20027 \u20307 \u33258 \u34892 \u21521 \u31038 \u20250 \u20844 \u20247 \u20844 \u24320 \u30340 \u20010 \u20154 \u20449 \u24687 \u65307 \
\
F. \uc0\u20174 \u21512 \u27861 \u20844 \u24320 \u25259 \u38706 \u30340 \u20449 \u24687 \u20013 \u25910 \u38598 \u20010 \u20154 \u20449 \u24687 \u30340 \u65292 \u22914 \u21512 \u27861 \u30340 \u26032 \u38395 \u25253 \u36947 \u12289 \u25919 \u24220 \u20449 \u24687 \u20844 \u24320 \u31561 \u28192 \u36947 \u12290 \
\
4.5 \uc0\u22914 \u21457 \u29983 \u25910 \u36141 \u12289 \u20860 \u24182 \u12289 \u37325 \u32452 \u31561 \u21464 \u26356 \u65292 \u25105 \u20204 \u20250 \u35201 \u27714 \u21464 \u26356 \u21518 \u30340 \u20027 \u20307 \u20381 \u28982 \u36981 \u23432 \u26412 \u38544 \u31169 \u25919 \u31574 \u32422 \u23450 \u65292 \u23653 \u34892 \u21407 \u26377 \u36131 \u20219 \u21450 \u20041 \u21153 \u12290 \u22914 \u21464 \u26356 \u21518 \u30340 \u20027 \u20307 \u38656 \u21464 \u26356 \u20010 \u20154 \u20449 \u24687 \u20351 \u29992 \u30446 \u30340 \u65292 \u25105 \u20204 \u20250 \u35201 \u27714 \u20854 \u20107 \u20808 \u33719 \u24471 \u24744 \u30340 \u26126 \u31034 \u21516 \u24847 \u12290 \u23545 \u20869 \u32780 \u35328 \u65292 \u22312 \u20043 \u21069 \u30340 \u25910 \u36141 \u12289 \u20860 \u24182 \u27963 \u21160 \u20013 \u65292 \u23545 \u20110 \u34987 \u25910 \u36141 \u12289 \u34987 \u20860 \u24182 \u23545 \u35937 \u31649 \u29702 \u30340 \u25968 \u25454 (\u21253 \u25324 \u29992 \u25143 \u25968 \u25454 )\u22343 \u37319 \u29992 \u19982 \u31508 \u22696 \u35759 \u20351 \u33258 \u26377 \u25968 \u25454 \u30456 \u21516 \u30340 \u26631 \u20934 \u21644 \u35201 \u27714 \u36827 \u34892 \u22788 \u29702 \u21644 \u20445 \u25252 \u12290 \
\
\uc0\u20116 \u12289 \u25105 \u20204 \u22914 \u20309 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \
\
\uc0\u25105 \u20204 \u20026 \u24744 \u30340 \u20449 \u24687 \u25552 \u20379 \u30456 \u24212 \u30340 \u23433 \u20840 \u20445 \u38556 \u65292 \u20197 \u38450 \u27490 \u20449 \u24687 \u30340 \u20002 \u22833 \u12289 \u19981 \u24403 \u20351 \u29992 \u12289 \u26410 \u32463 \u25480 \u26435 \u35775 \u38382 \u25110 \u25259 \u38706 \u12290 \
5.1 \uc0\u25105 \u20204 \u20005 \u26684 \u36981 \u23432 \u27861 \u24459 \u27861 \u35268 \u20445 \u25252 \u29992 \u25143 \u30340 \u36890 \u20449 \u31192 \u23494 \u12290 \
\
5.2 \uc0\u25105 \u20204 \u23558 \u22312 \u21512 \u29702 \u30340 \u23433 \u20840 \u27700 \u24179 \u20869 \u20351 \u29992 \u21508 \u31181 \u23433 \u20840 \u20445 \u25252 \u25514 \u26045 \u20197 \u20445 \u38556 \u20449 \u24687 \u30340 \u23433 \u20840 \u12290 \u20363 \u22914 \u65292 \u25105 \u20204 \u20351 \u29992 \u21152 \u23494 \u25216 \u26415 \u65288 \u20363 \u22914 \u65306 TLS\u12289 SSL\u65289 \u12289 \u21311 \u21517 \u21270 \u22788 \u29702 \u31561 \u25163 \u27573 \u26469 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
5.3 \uc0\u25105 \u20204 \u24314 \u31435 \u19987 \u38376 \u30340 \u31649 \u29702 \u21046 \u24230 \u12289 \u27969 \u31243 \u21644 \u32452 \u32455 \u30830 \u20445 \u20449 \u24687 \u23433 \u20840 \u12290 \u20363 \u22914 \u65292 \u25105 \u20204 \u20005 \u26684 \u38480 \u21046 \u35775 \u38382 \u20449 \u24687 \u30340 \u20154 \u21592 \u33539 \u22260 \u65292 \u35201 \u27714 \u20182 \u20204 \u36981 \u23432 \u20445 \u23494 \u20041 \u21153 \u65292 \u24182 \u36827 \u34892 \u23457 \u26597 \u12290 \
\
5.4 \uc0\u33509 \u21457 \u29983 \u20010 \u20154 \u20449 \u24687 \u27844 \u38706 \u31561 \u23433 \u20840 \u20107 \u20214 \u65292 \u25105 \u20204 \u20250 \u21551 \u21160 \u24212 \u24613 \u39044 \u26696 \u65292 \u38459 \u27490 \u23433 \u20840 \u20107 \u20214 \u25193 \u22823 \u65292 \u24182 \u20197 \u25512 \u36865 \u36890 \u30693 \u12289 \u20844 \u21578 \u31561 \u24418 \u24335 \u21578 \u30693 \u24744 \u12290 \
\
5.5 \uc0\u25105 \u20204 \u20250 \u37319 \u21462 \u19968 \u20999 \u21512 \u29702 \u21487 \u34892 \u30340 \u25514 \u26045 \u65292 \u30830 \u20445 \u26410 \u25910 \u38598 \u26080 \u20851 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \u25105 \u20204 \u21482 \u20250 \u22312 \u36798 \u25104 \u26412 \u38544 \u31169 \u25919 \u31574 \u25152 \u36848 \u30446 \u30340 \u25152 \u38656 \u30340 \u26399 \u38480 \u20869 \u20445 \u30041 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u38500 \u38750 \u38656 \u35201 \u24310 \u38271 \u20445 \u30041 \u26399 \u25110 \u21463 \u21040 \u27861 \u24459 \u30340 \u20801 \u35768 \u12290 \
\
5.6 \uc0\u20114 \u32852 \u32593 \u24182 \u38750 \u32477 \u23545 \u23433 \u20840 \u30340 \u29615 \u22659 \u65292 \u32780 \u19988 \u30005 \u23376 \u37038 \u20214 \u12289 \u21363 \u26102 \u36890 \u35759 \u12289 \u31038 \u20132 \u36719 \u20214 \u31561 \u19982 \u20854 \u20182 \u29992 \u25143 \u30340 \u20132 \u27969 \u26041 \u24335 \u26080 \u27861 \u30830 \u23450 \u26159 \u21542 \u23436 \u20840 \u21152 \u23494 \u65292 \u25105 \u20204 \u24314 \u35758 \u24744 \u20351 \u29992 \u27492 \u31867 \u24037 \u20855 \u26102 \u35831 \u20351 \u29992 \u22797 \u26434 \u23494 \u30721 \u65292 \u24182 \u27880 \u24847 \u20445 \u25252 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u23433 \u20840 \u12290 \
\
5.7 \uc0\u20114 \u32852 \u32593 \u29615 \u22659 \u24182 \u38750 \u30334 \u20998 \u20043 \u30334 \u23433 \u20840 \u65292 \u25105 \u20204 \u23558 \u23613 \u21147 \u30830 \u20445 \u25110 \u25285 \u20445 \u24744 \u21457 \u36865 \u32473 \u25105 \u20204 \u30340 \u20219 \u20309 \u20449 \u24687 \u30340 \u23433 \u20840 \u24615 \u12290 \u22914 \u26524 \u25105 \u20204 \u30340 \u29289 \u29702 \u12289 \u25216 \u26415 \u12289 \u25110 \u31649 \u29702 \u38450 \u25252 \u35774 \u26045 \u36973 \u21040 \u30772 \u22351 \u65292 \u33268 \u20449 \u24687 \u34987 \u38750 \u25480 \u26435 \u35775 \u38382 \u12289 \u20844 \u24320 \u25259 \u38706 \u12289 \u31713 \u25913 \u12289 \u25110 \u27585 \u22351 \u65292 \u23548 \u33268 \u24744 \u30340 \u21512 \u27861 \u26435 \u30410 \u21463 \u25439 \u65292 \u25105 \u20204 \u23558 \u25215 \u25285 \u30456 \u24212 \u30340 \u27861 \u24459 \u36131 \u20219 \u12290 \
\
\uc0\u20845 \u12289 \u25105 \u20204 \u22914 \u20309 \u20445 \u23384 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \
\
6.1 \uc0\u23384 \u20648 \u20449 \u24687 \u30340 \u22320 \u28857 \u65306 \u25105 \u20204 \u20250 \u25353 \u29031 \u27861 \u24459 \u27861 \u35268 \u35268 \u23450 \u65292 \u23558 \u22659 \u20869 \u25910 \u38598 \u30340 \u29992 \u25143 \u20010 \u20154 \u20449 \u24687 \u23384 \u20648 \u20110 \u20013 \u22269 \u22659 \u20869 \u12290 \u22914 \u28041 \u21450 \u21521 \u22659 \u22806 \u20256 \u36755 \u20010 \u20154 \u20449 \u24687 \u65292 \u25105 \u20204 \u23558 \u26126 \u30830 \u21521 \u24744 \u21578 \u30693 \u20010 \u20154 \u20449 \u24687 \u20986 \u22659 \u30340 \u30446 \u30340 \u12289 \u25509 \u25910 \u26041 \u12289 \u23433 \u20840 \u20445 \u38556 \u25514 \u26045 \u31561 \u24773 \u20917 \u65292 \u24182 \u21478 \u34892 \u24449 \u24471 \u24744 \u30340 \u21516 \u24847 \u12290 \
\
6.2 \uc0\u23384 \u20648 \u20449 \u24687 \u30340 \u26399 \u38480 \u19968 \u33324 \u32780 \u35328 \u65292 \u25105 \u20204 \u20165 \u20026 \u23454 \u29616 \u30446 \u30340 \u25152 \u24517 \u38656 \u30340 \u26368 \u30701 \u26102 \u38388 \u20445 \u30041 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u38500 \u38750 \u27861 \u24459 \u26377 \u24378 \u21046 \u30340 \u30041 \u23384 \u35201 \u27714 \u12290 \u20294 \u22312 \u19979 \u21015 \u24773 \u20917 \u19979 \u65292 \u25105 \u20204 \u26377 \u21487 \u33021 \u22240 \u38656 \u31526 \u21512 \u27861 \u24459 \u35201 \u27714 \u65292 \u26356 \u25913 \u20010 \u20154 \u20449 \u24687 \u30340 \u23384 \u20648 \u26102 \u38388 \u65306 \
A. \uc0\u20026 \u36981 \u23432 \u36866 \u29992 \u30340 \u27861 \u24459 \u27861 \u35268 \u31561 \u26377 \u20851 \u35268 \u23450 \u65307 \
\
B. \uc0\u20026 \u36981 \u23432 \u27861 \u38498 \u21028 \u20915 \u12289 \u35009 \u23450 \u25110 \u20854 \u20182 \u27861 \u24459 \u31243 \u24207 \u30340 \u35268 \u23450 \u65307 \
\
C. \uc0\u20026 \u36981 \u23432 \u30456 \u20851 \u25919 \u24220 \u26426 \u20851 \u25110 \u27861 \u23450 \u25480 \u26435 \u32452 \u32455 \u30340 \u35201 \u27714 \u65307 \
\
D. \uc0\u25105 \u20204 \u26377 \u29702 \u30001 \u30830 \u20449 \u38656 \u35201 \u36981 \u23432 \u27861 \u24459 \u27861 \u35268 \u31561 \u26377 \u20851 \u35268 \u23450 \u65307 \
\
E. \uc0\u20026 \u25191 \u34892 \u30456 \u20851 \u26381 \u21153 \u21327 \u35758 \u25110 \u26412 \u25919 \u31574 \u12289 \u32500 \u25252 \u31038 \u20250 \u20844 \u20849 \u21033 \u30410 \u65292 \u20026 \u20445 \u25252 \u25105 \u20204 \u30340 \u23458 \u25143 \u12289 \u25105 \u20204 \u25110 \u25105 \u20204 \u30340 \u20851 \u32852 \u20844 \u21496 \u12289 \u20854 \u20182 \u29992 \u25143 \u25110 \u38599 \u21592 \u30340 \u20154 \u36523 \u36130 \u20135 \u23433 \u20840 \u25110 \u20854 \u20182 \u21512 \u27861 \u26435 \u30410 \u25152 \u21512 \u29702 \u24517 \u38656 \u30340 \u29992 \u36884 \u12290 \
\
6.3 \uc0\u35831 \u24744 \u27880 \u24847 \u65292 \u24744 \u22312 \u20351 \u29992 \u25105 \u20204 \u30340 \u26381 \u21153 \u26399 \u38388 \u65292 \u25105 \u20204 \u20165 \u22312 \u27861 \u24459 \u27861 \u35268 \u35268 \u23450 \u12289 \u30417 \u31649 \u35201 \u27714 \u21450 \u20026 \u23454 \u29616 \u26412 \u26465 \u27454 \u22768 \u26126 \u30340 \u30446 \u30340 \u25152 \u24517 \u38656 \u30340 \u26102 \u38480 \u20869 \u20445 \u30041 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u22312 \u24744 \u27880 \u38144 \u36134 \u21495 \u21518 \u65292 \u25105 \u20204 \u20165 \u22312 \u27861 \u24459 \u27861 \u35268 \u35268 \u23450 \u12289 \u30417 \u31649 \u35201 \u27714 \u30340 \u26399 \u38480 \u20869 \u20445 \u30041 \u20010 \u20154 \u20449 \u24687 \u65292 \u19988 \u19981 \u20250 \u24320 \u23637 \u38500 \u23384 \u20648 \u21644 \u37319 \u21462 \u24517 \u35201 \u30340 \u23433 \u20840 \u20445 \u25252 \u25514 \u26045 \u20043 \u22806 \u30340 \u22788 \u29702 \u27963 \u21160 \u65292 \u20294 \u24050 \u32463 \u21311 \u21517 \u21270 \u22788 \u29702 \u21518 \u30340 \u20010 \u20154 \u20449 \u24687 \u38500 \u22806 \u12290 \u24403 \u24744 \u25104 \u21151 \u27880 \u38144 \u31508 \u22696 \u35759 \u20351 \u36134 \u25143 \u25110 \u36229 \u20986 \u20445 \u30041 \u26399 \u38388 \u21518 \u65292 \u25105 \u20204 \u23558 \u23545 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u36827 \u34892 \u21024 \u38500 \u25110 \u21311 \u21517 \u21270 \u22788 \u29702 \u12290 \
\
6.4 \uc0\u24403 \u25105 \u20204 \u30340 \u20135 \u21697 \u25110 \u26381 \u21153 \u21457 \u29983 \u20572 \u27490 \u36816 \u33829 \u30340 \u24773 \u24418 \u26102 \u65292 \u25105 \u20204 \u23558 \u37319 \u21462 \u20363 \u22914 \u65292 \u25512 \u36865 \u36890 \u30693 \u12289 \u20844 \u21578 \u31561 \u24418 \u24335 \u36890 \u30693 \u24744 \u65292 \u24182 \u22312 \u21512 \u29702 \u30340 \u26399 \u38480 \u20869 \u21024 \u38500 \u25110 \u21311 \u21517 \u21270 \u22788 \u29702 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
\uc0\u19971 \u12289 \u25105 \u20204 \u22914 \u20309 \u20351 \u29992 Cookie\u21450 \u21516 \u31867 \u25216 \u26415 \
\
7.1 \uc0\u20026 \u23454 \u29616 \u24744 \u32852 \u26426 \u20307 \u39564 \u38656 \u27714 \u65292 \u20351 \u24744 \u33719 \u24471 \u26356 \u36731 \u26494 \u30340 \u35775 \u38382 \u20307 \u39564 \u12290 \u25105 \u20204 \u20250 \u22312 \u24744 \u30340 \u35745 \u31639 \u26426 \u25110 \u31227 \u21160 \u35774 \u22791 \u19978 \u21457 \u36865 \u19968 \u20010 \u25110 \u22810 \u20010 \u21517 \u20026 Cookies\u30340 \u23567 \u25968 \u25454 \u25991 \u20214 \u65292 \u25351 \u23450 \u32473 \u24744 \u30340 Cookies \u26159 \u21807 \u19968 \u30340 \u65292 \u23427 \u21482 \u33021 \u34987 \u23558 Cookies\u21457 \u24067 \u32473 \u24744 \u30340 \u22495 \u20013 \u30340 Web\u26381 \u21153 \u22120 \u35835 \u21462 \u12290 \u25105 \u20204 \u21521 \u24744 \u21457 \u36865 Cookies\u26159 \u20026 \u20102 \u31616 \u21270 \u24744 \u37325 \u22797 \u30331 \u24405 \u30340 \u27493 \u39588 \u12289 \u23384 \u20648 \u24744 \u30340 \u20559 \u22909 \u25968 \u25454 \u36827 \u32780 \u24110 \u21161 \u21028 \u26029 \u24744 \u30340 \u30331 \u24405 \u29366 \u24577 \u20197 \u21450 \u36134 \u25143 \u25110 \u25968 \u25454 \u23433 \u20840 \u12290 \
\
7.2 \uc0\u25105 \u20204 \u19981 \u20250 \u23558 Cookies \u29992 \u20110 \u26412 \u38544 \u31169 \u25919 \u31574 \u25152 \u36848 \u30446 \u30340 \u20043 \u22806 \u30340 \u20219 \u20309 \u29992 \u36884 \u12290 \u24744 \u21487 \u26681 \u25454 \u33258 \u24049 \u30340 \u20559 \u22909 \u31649 \u29702 \u25110 \u21024 \u38500 Cookies\u12290 \u21478 \u22806 \u65292 \u24744 \u20063 \u21487 \u20197 \u28165 \u38500 \u36719 \u20214 \u20869 \u20445 \u23384 \u30340 \u25152 \u26377 Cookies\u12290 \u20294 \u22914 \u26524 \u24744 \u36825 \u20040 \u20570 \u65292 \u24744 \u21487 \u33021 \u38656 \u35201 \u22312 \u27599 \u19968 \u27425 \u35775 \u38382 \u36719 \u20214 \u26102 \u20146 \u33258 \u26356 \u25913 \u29992 \u25143 \u35774 \u32622 \u65292 \u32780 \u19988 \u24744 \u20043 \u21069 \u25152 \u35760 \u24405 \u30340 \u30456 \u24212 \u20449 \u24687 \u20063 \u22343 \u20250 \u34987 \u21024 \u38500 \u65292 \u24182 \u19988 \u21487 \u33021 \u20250 \u23545 \u24744 \u25152 \u20351 \u29992 \u26381 \u21153 \u30340 \u23433 \u20840 \u24615 \u26377 \u19968 \u23450 \u24433 \u21709 \u12290 \
\
7.3 \uc0\u32593 \u32476 Beacon\u21644 \u21516 \u31867 \u25216 \u26415 \u30340 \u20351 \u29992 \u38500 Cookies \u22806 \u65292 \u25105 \u20204 \u36824 \u20250 \u22312 \u32593 \u31449 \u19978 \u20351 \u29992 \u32593 \u32476 Beacon\u31561 \u20854 \u20182 \u21516 \u31867 \u25216 \u26415 \u12290 \u25105 \u20204 \u30340 \u32593 \u39029 \u19978 \u24120 \u20250 \u21253 \u21547 \u19968 \u20123 \u30005 \u23376 \u22270 \u20687 \u65288 \u31216 \u20026 "\u21333 \u20687 \u32032 " GIF \u25991 \u20214 \u25110 "\u32593 \u32476 Beacon"\u65289 \u12290 \u20351 \u29992 \u32593 \u32476 Beacon\u21487 \u20197 \u35745 \u31639 \u27983 \u35272 \u32593 \u39029 \u30340 \u29992 \u25143 \u25110 \u35775 \u38382 \u26576 \u20123 Cookies\u65292 \u24182 \u36890 \u36807 \u24471 \u21040 \u30340 Cookies\u20449 \u24687 \u65292 \u20026 \u24744 \u25552 \u20379 \u26381 \u21153 \u12290 \
\
\uc0\u20843 \u12289 \u24744 \u22914 \u20309 \u31649 \u29702 \u33258 \u24049 \u30340 \u20449 \u24687 \
\
\uc0\u24744 \u21487 \u20197 \u36890 \u36807 \u20197 \u19979 \u26041 \u24335 \u35775 \u38382 \u21450 \u31649 \u29702 \u24744 \u30340 \u20449 \u24687 \u65306 \
\
8.1 \uc0\u26597 \u35810 \u12289 \u26356 \u27491 \u21644 \u34917 \u20805 \u24744 \u30340 \u20449 \u24687 \u24744 \u26377 \u26435 \u26597 \u35810 \u12289 \u26356 \u27491 \u25110 \u34917 \u20805 \u24744 \u30340 \u20449 \u24687 \u12290 \u24744 \u21487 \u20197 \u36890 \u36807 \u20197 \u19979 \u26041 \u24335 \u33258 \u34892 \u36827 \u34892 \u65306 \u30331 \u24405 \u31508 \u22696 \u35759 \u20351 \u23458 \u25143 \u31471 \u65292 \u36827 \u20837 \'93\u25105 \u30340 \'94\u65292 \u28857 \u20987 \u29992 \u25143 \u21517 \u23545 \u20010 \u20154 \u36164 \u26009 \u12289 \u22522 \u26412 \u20449 \u24687 \u12289 \u35748 \u35777 \u20449 \u24687 \u12289 \u20010 \u20154 \u36164 \u26009 \u31561 \u36827 \u34892 \u26597 \u35810 \u12289 \u26356 \u27491 \u25110 \u34917 \u20805 \u65307 \
\
8.2 \uc0\u21024 \u38500 \u24744 \u30340 \u20449 \u24687 \u24744 \u21487 \u20197 \u36890 \u36807 \'938.1\u26597 \u35810 \u12289 \u26356 \u27491 \u21644 \u34917 \u20805 \u24744 \u30340 \u20449 \u24687 \'94\u20013 \u21015 \u26126 \u30340 \u26041 \u24335 \u21024 \u38500 \u24744 \u30340 \u37096 \u20998 \u20449 \u24687 \u12290 \
\
\uc0\u22312 \u20197 \u19979 \u24773 \u24418 \u20013 \u65292 \u24744 \u21487 \u20197 \u21521 \u25105 \u20204 \u25552 \u20986 \u21024 \u38500 \u20010 \u20154 \u20449 \u24687 \u30340 \u35831 \u27714 \u65306 \
\
A. \uc0\u22914 \u26524 \u25105 \u20204 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u30340 \u34892 \u20026 \u36829 \u21453 \u27861 \u24459 \u27861 \u35268 \u65307 \
\
B. \uc0\u22914 \u26524 \u25105 \u20204 \u25910 \u38598 \u12289 \u20351 \u29992 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u21364 \u26410 \u24449 \u24471 \u24744 \u30340 \u26126 \u30830 \u21516 \u24847 \u65307 \
\
C. \uc0\u22914 \u26524 \u25105 \u20204 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u30340 \u34892 \u20026 \u20005 \u37325 \u36829 \u21453 \u20102 \u19982 \u24744 \u30340 \u32422 \u23450 \u65307 \
\
D. \uc0\u22914 \u26524 \u24744 \u19981 \u20877 \u20351 \u29992 \u25105 \u20204 \u30340 \u20135 \u21697 \u25110 \u26381 \u21153 \u65292 \u25110 \u24744 \u20027 \u21160 \u27880 \u38144 \u20102 \u36134 \u21495 \u65307 \
\
E. \uc0\u22914 \u26524 \u25105 \u20204 \u27704 \u20037 \u19981 \u20877 \u20026 \u24744 \u25552 \u20379 \u20135 \u21697 \u25110 \u26381 \u21153 \u12290 \
\uc0\u33509 \u25105 \u20204 \u20915 \u23450 \u21709 \u24212 \u24744 \u30340 \u21024 \u38500 \u35831 \u27714 \u65292 \u25105 \u20204 \u36824 \u23558 \u21516 \u26102 \u23613 \u21487 \u33021 \u36890 \u30693 \u20174 \u25105 \u20204 \u22788 \u33719 \u24471 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u30340 \u20027 \u20307 \u65292 \u24182 \u35201 \u27714 \u20854 \u21450 \u26102 \u21024 \u38500 \u65288 \u38500 \u38750 \u27861 \u24459 \u27861 \u35268 \u21478 \u26377 \u35268 \u23450 \u65292 \u25110 \u36825 \u20123 \u20027 \u20307 \u24050 \u29420 \u31435 \u33719 \u24471 \u24744 \u30340 \u25480 \u26435 \u65289 \u12290 \u24403 \u24744 \u25110 \u25105 \u20204 \u21327 \u21161 \u24744 \u21024 \u38500 \u30456 \u20851 \u20449 \u24687 \u21518 \u65292 \u22240 \u20026 \u36866 \u29992 \u30340 \u27861 \u24459 \u21644 \u23433 \u20840 \u25216 \u26415 \u65292 \u25105 \u20204 \u21487 \u33021 \u26080 \u27861 \u31435 \u21363 \u20174 \u22791 \u20221 \u31995 \u32479 \u20013 \u21024 \u38500 \u30456 \u24212 \u30340 \u20449 \u24687 \u65292 \u25105 \u20204 \u23558 \u23433 \u20840 \u22320 \u23384 \u20648 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u24182 \u23558 \u20854 \u19982 \u20219 \u20309 \u36827 \u19968 \u27493 \u22788 \u29702 \u38548 \u31163 \u65292 \u30452 \u21040 \u22791 \u20221 \u21487 \u20197 \u28165 \u38500 \u25110 \u23454 \u29616 \u21311 \u21517 \u12290 \
\
8.3 \uc0\u25913 \u21464 \u24744 \u25480 \u26435 \u21516 \u24847 \u30340 \u33539 \u22260 \
\
\uc0\u27599 \u20010 \u19994 \u21153 \u21151 \u33021 \u38656 \u35201 \u19968 \u20123 \u22522 \u26412 \u30340 \u20010 \u20154 \u20449 \u24687 \u25165 \u33021 \u24471 \u20197 \u23436 \u25104 \u65288 \u35265 \u26412 \u38544 \u31169 \u25919 \u31574 \'93\u31532 \u19968 \u26465 \u25105 \u20204 \u25910 \u38598 \u30340 \u20449 \u24687 \'94\u65289 \u12290 \u38500 \u27492 \u20043 \u22806 \u65292 \u24744 \u21487 \u20197 \u22312 \u31508 \u22696 \u35759 \u20351 \u23458 \u25143 \u31471 \'93\u25105 \u30340 -\u35774 \u32622 -\u38544 \u31169 \u35774 \u32622 \'94\u20013 \u35774 \u32622 \u25110 \u25913 \u21464 \u24744 \u30340 \u26234 \u33021 \u31227 \u21160 \u35774 \u22791 \u30340 \u35774 \u32622 \u31561 \u26041 \u24335 \u32473 \u20104 \u25110 \u25910 \u22238 \u24744 \u30340 \u25480 \u26435 \u21516 \u24847 \u30340 \u33539 \u22260 \u12290 \u24403 \u24744 \u25910 \u22238 \u21516 \u24847 \u21518 \u65292 \u25105 \u20204 \u23558 \u19981 \u20877 \u22788 \u29702 \u30456 \u24212 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \u20294 \u24744 \u25910 \u22238 \u21516 \u24847 \u30340 \u20915 \u23450 \u65292 \u19981 \u20250 \u24433 \u21709 \u27492 \u21069 \u22522 \u20110 \u24744 \u30340 \u25480 \u26435 \u32780 \u24320 \u23637 \u30340 \u20010 \u20154 \u20449 \u24687 \u22788 \u29702 \u12290 \
\
8.4 \uc0\u27880 \u38144 \u24744 \u30340 \u36134 \u25143 \
\
\uc0\u24744 \u21487 \u20197 \u36890 \u36807 \u20197 \u19979 \u26041 \u24335 \u30003 \u35831 \u27880 \u38144 \u24744 \u30340 \u36134 \u25143 \u65306 \
\
\uc0\u30331 \u24405 \u31508 \u22696 \u35759 \u20351 \u23448 \u32593 \u24182 \u36873 \u25321 \u32852 \u31995 \u23458 \u26381 \u23547 \u27714 \u24110 \u21161 \u65292 \u21327 \u21161 \u24744 \u30003 \u35831 \u27880 \u38144 \u24744 \u30340 \u36134 \u25143 \u12290 \
\
\uc0\u22312 \u24744 \u20027 \u21160 \u27880 \u38144 \u36134 \u25143 \u20043 \u21518 \u65292 \u25105 \u20204 \u23558 \u20572 \u27490 \u20026 \u24744 \u25552 \u20379 \u20135 \u21697 \u25110 \u26381 \u21153 \u65292 \u26681 \u25454 \u36866 \u29992 \u27861 \u24459 \u30340 \u35201 \u27714 \u21024 \u38500 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u25110 \u20351 \u20854 \u21311 \u21517 \u21270 \u22788 \u29702 \u12290 \
\
8.5 \uc0\u32422 \u26463 \u20449 \u24687 \u31995 \u32479 \u33258 \u21160 \u20915 \u31574 \
\
\uc0\u22312 \u26576 \u20123 \u19994 \u21153 \u21151 \u33021 \u20013 \u65292 \u25105 \u20204 \u21487 \u33021 \u20165 \u20381 \u25454 \u20449 \u24687 \u31995 \u32479 \u12289 \u31639 \u27861 \u31561 \u22312 \u20869 \u30340 \u38750 \u20154 \u24037 \u33258 \u21160 \u20915 \u31574 \u26426 \u21046 \u20570 \u20986 \u20915 \u23450 \u12290 \
\
\uc0\u22914 \u26524 \u36825 \u20123 \u20915 \u23450 \u26174 \u33879 \u24433 \u21709 \u24744 \u30340 \u21512 \u27861 \u26435 \u30410 \u65292 \u24744 \u26377 \u26435 \u35201 \u27714 \u25105 \u20204 \u20570 \u20986 \u35299 \u37322 \u65292 \u25105 \u20204 \u20063 \u23558 \u22312 \u19981 \u20405 \u23475 \u31508 \u22696 \u35759 \u20351 \u21830 \u19994 \u31192 \u23494 \u25110 \u20854 \u20182 \u29992 \u25143 \u26435 \u30410 \u12289 \u31038 \u20250 \u20844 \u20849 \u21033 \u30410 \u30340 \u21069 \u25552 \u19979 \u25552 \u20379 \u30003 \u35785 \u26041 \u27861 \u12290 \
\
8.6 \uc0\u21709 \u24212 \u24744 \u30340 \u19978 \u36848 \u35831 \u27714 \
\
\uc0\u20026 \u20445 \u38556 \u23433 \u20840 \u65292 \u24744 \u21487 \u33021 \u38656 \u35201 \u25552 \u20379 \u20070 \u38754 \u35831 \u27714 \u65292 \u25110 \u20197 \u20854 \u20182 \u26041 \u24335 \u35777 \u26126 \u24744 \u30340 \u36523 \u20221 \u12290 \u25105 \u20204 \u21487 \u33021 \u20250 \u20808 \u35201 \u27714 \u24744 \u39564 \u35777 \u33258 \u24049 \u30340 \u36523 \u20221 \u65292 \u28982 \u21518 \u20877 \u22788 \u29702 \u24744 \u30340 \u35831 \u27714 \u12290 \
\
\uc0\u25105 \u20204 \u23558 \u22312 15\u22825 \u20869 \u20570 \u20986 \u31572 \u22797 \u12290 \u22914 \u24744 \u19981 \u28385 \u24847 \u65292 \u36824 \u21487 \u20197 \u36890 \u36807 \u23458 \u26381 \u37038 \u31665 \u21457 \u36215 \u25237 \u35785 \u12290 \
\
\uc0\u23545 \u20110 \u24744 \u21512 \u29702 \u30340 \u35831 \u27714 \u65292 \u25105 \u20204 \u21407 \u21017 \u19978 \u19981 \u25910 \u21462 \u36153 \u29992 \u65292 \u20294 \u23545 \u22810 \u27425 \u37325 \u22797 \u12289 \u36229 \u20986 \u21512 \u29702 \u38480 \u24230 \u30340 \u35831 \u27714 \u65292 \u25105 \u20204 \u23558 \u35270 \u24773 \u25910 \u21462 \u19968 \u23450 \u25104 \u26412 \u36153 \u29992 \u12290 \u23545 \u20110 \u37027 \u20123 \u26080 \u31471 \u37325 \u22797 \u12289 \u38656 \u35201 \u36807 \u22810 \u25216 \u26415 \u25163 \u27573 \u65288 \u20363 \u22914 \u65292 \u38656 \u35201 \u24320 \u21457 \u26032 \u31995 \u32479 \u25110 \u20174 \u26681 \u26412 \u19978 \u25913 \u21464 \u29616 \u34892 \u24815 \u20363 \u65289 \u12289 \u32473 \u20182 \u20154 \u21512 \u27861 \u26435 \u30410 \u24102 \u26469 \u39118 \u38505 \u25110 \u32773 \u38750 \u24120 \u19981 \u20999 \u23454 \u38469 \u30340 \u35831 \u27714 \u65292 \u25105 \u20204 \u21487 \u33021 \u20250 \u20104 \u20197 \u25298 \u32477 \u12290 \
\
\uc0\u22312 \u20197 \u19979 \u24773 \u24418 \u20013 \u65292 \u25353 \u29031 \u27861 \u24459 \u27861 \u35268 \u35201 \u27714 \u65292 \u25105 \u20204 \u23558 \u26080 \u27861 \u21709 \u24212 \u24744 \u30340 \u35831 \u27714 \u65306 \
\
A. \uc0\u19982 \u22269 \u23478 \u23433 \u20840 \u12289 \u22269 \u38450 \u23433 \u20840 \u26377 \u20851 \u30340 \u65307 \
\
B. \uc0\u19982 \u20844 \u20849 \u23433 \u20840 \u12289 \u20844 \u20849 \u21355 \u29983 \u12289 \u37325 \u22823 \u20844 \u20849 \u21033 \u30410 \u26377 \u20851 \u30340 \u65307 \
\
C. \uc0\u19982 \u29359 \u32618 \u20390 \u26597 \u12289 \u36215 \u35785 \u12289 \u23457 \u21028 \u21644 \u25191 \u34892 \u21028 \u20915 \u31561 \u26377 \u20851 \u30340 \u65307 \
\
D. \uc0\u26377 \u20805 \u20998 \u35777 \u25454 \u34920 \u26126 \u20010 \u20154 \u20449 \u24687 \u20027 \u20307 \u23384 \u22312 \u20027 \u35266 \u24694 \u24847 \u25110 \u28389 \u29992 \u26435 \u21033 \u30340 \u65307 \
\
E. \uc0\u21709 \u24212 \u24744 \u30340 \u35831 \u27714 \u23558 \u23548 \u33268 \u24744 \u25110 \u20854 \u20182 \u20010 \u20154 \u12289 \u32452 \u32455 \u30340 \u21512 \u27861 \u26435 \u30410 \u21463 \u21040 \u20005 \u37325 \u25439 \u23475 \u30340 \u65307 \
\
F. \uc0\u28041 \u21450 \u21830 \u19994 \u31192 \u23494 \u30340 \u65307 \
\
G. \uc0\u19982 \u25105 \u20204 \u23653 \u34892 \u27861 \u24459 \u27861 \u35268 \u35268 \u23450 \u30340 \u20041 \u21153 \u30456 \u20851 \u30340 \u65307 \
\
H. \uc0\u20986 \u20110 \u32500 \u25252 \u24744 \u25110 \u20854 \u20182 \u20010 \u20154 \u30340 \u29983 \u21629 \u12289 \u36130 \u20135 \u31561 \u37325 \u22823 \u21512 \u27861 \u26435 \u30410 \u20294 \u21448 \u24456 \u38590 \u24471 \u21040 \u24744 \u26412 \u20154 \u21516 \u24847 \u30340 \u12290 \
\
\uc0\u20061 \u12289 \u24744 \u20849 \u20139 \u30340 \u20449 \u24687 \
\
9.1 \uc0\u24744 \u21487 \u20197 \u22522 \u20110 \u25105 \u20204 \u30340 \u26381 \u21153 \u19982 \u20182 \u20154 \u12289 \u31038 \u20132 \u32593 \u32476 \u20998 \u20139 \u24744 \u30340 \u30456 \u20851 \u20449 \u24687 \u65292 \u36825 \u20854 \u20013 \u21487 \u33021 \u21253 \u21547 \u24744 \u30340 \u20010 \u20154 \u36523 \u20221 \u20449 \u24687 \u12289 \u20010 \u20154 \u36130 \u20135 \u20449 \u24687 \u31561 \u25935 \u24863 \u20449 \u24687 \u65292 \u24182 \u33258 \u20027 \u36873 \u25321 \u20998 \u20139 \u30340 \u23545 \u35937 \u20316 \u20026 \u33021 \u22815 \u30475 \u21040 \u24744 \u20449 \u24687 \u30340 \u31532 \u19977 \u26041 \u12290 \
\
9.2 \uc0\u24403 \u24744 \u19982 \u20182 \u20154 \u12289 \u31038 \u20132 \u32593 \u32476 \u25110 \u31532 \u19977 \u26041 \u26381 \u21153 \u25552 \u20379 \u21830 \u20849 \u20139 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u25110 \u32467 \u21512 \u31532 \u19977 \u26041 \u30340 \u26381 \u21153 \u20351 \u29992 \u12289 \u25110 \u20351 \u29992 \u31532 \u19977 \u26041 \u30340 \u26381 \u21153 \u38142 \u25509 \u26102 \u65292 \u24744 \u21487 \u33021 \u20250 \u25259 \u38706 \u25110 \u20351 \u31532 \u19977 \u26041 \u33719 \u21462 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
9.3 \uc0\u25105 \u20204 \u24314 \u35758 \u24744 \u26597 \u30475 \u27492 \u31867 \u31038 \u20132 \u32593 \u32476 \u25110 \u31532 \u19977 \u26041 \u26381 \u21153 \u25552 \u20379 \u21830 \u30340 \u38544 \u31169 \u22768 \u26126 \u65292 \u20197 \u20102 \u35299 \u20182 \u20204 \u22914 \u20309 \u22788 \u29702 \u24744 \u30340 \u20449 \u24687 \u12290 \u23545 \u20110 \u24744 \u20351 \u29992 \u31532 \u19977 \u26041 \u26381 \u21153 \u26102 \u21521 \u20854 \u25552 \u20379 \u25110 \u19982 \u20182 \u20154 \u20998 \u20139 \u20010 \u20154 \u20449 \u24687 \u32780 \u20135 \u29983 \u30340 \u21518 \u26524 \u30001 \u24744 \u33258 \u34892 \u25215 \u25285 \u65292 \u24182 \u30001 \u30456 \u24212 \u36131 \u20219 \u26041 \u22312 \u27861 \u24459 \u35268 \u23450 \u33539 \u22260 \u20869 \u25215 \u25285 \u36131 \u20219 \u12290 \
\
9.4 \uc0\u24403 \u24744 \u22312 \u31508 \u22696 \u35759 \u20351 \u24179 \u21488 \u20844 \u24320 \u20998 \u20139 \u20219 \u20309 \u20449 \u24687 \u26102 \u65292 \u20219 \u20309 \u20154 \u37117 \u21487 \u33021 \u22312 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u24179 \u21488 \u26102 \u26597 \u30475 \u25110 \u35775 \u38382 \u36825 \u20123 \u20449 \u24687 \u65292 \u22914 \u24744 \u22312 \u31508 \u22696 \u35759 \u20351 \u24179 \u21488 \u19978 \u20256 \u30340 \u29031 \u29255 \u12289 \u35780 \u20215 \u31561 \u20449 \u24687 \u12290 \u24744 \u22312 \u20351 \u29992 \u25105 \u20204 \u26381 \u21153 \u26102 \u33258 \u24895 \u20849 \u20139 \u30340 \u20449 \u24687 \u65292 \u21487 \u33021 \u20250 \u28041 \u21450 \u24744 \u25110 \u20182 \u20154 \u30340 \u20010 \u20154 \u20449 \u24687 \u65292 \u35831 \u24744 \u35880 \u24910 \u36873 \u25321 \u24182 \u33258 \u34892 \u25215 \u25285 \u36131 \u20219 \u12290 \
\
\uc0\u21313 \u12289 \u26410 \u25104 \u24180 \u20154 \u20445 \u25252 \
\
10.1 \uc0\u25105 \u20204 \u38750 \u24120 \u37325 \u35270 \u23545 \u26410 \u25104 \u24180 \u20154 \u20010 \u20154 \u20449 \u24687 \u30340 \u20445 \u25252 \u12290 \u26681 \u25454 \u30456 \u20851 \u27861 \u24459 \u27861 \u35268 \u30340 \u35268 \u23450 \u65292 \u33509 \u24744 \u26159 18\u21608 \u23681 \u20197 \u19979 \u30340 \u26410 \u25104 \u24180 \u20154 \u65292 \u22312 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u30340 \u26381 \u21153 \u21069 \u65292 \u24212 \u20107 \u20808 \u21462 \u24471 \u24744 \u30340 \u23478 \u38271 \u25110 \u27861 \u23450 \u30417 \u25252 \u20154 \u30340 \u21516 \u24847 \u12290 \u33509 \u24744 \u26159 \u26410 \u28385 14\u21608 \u23681 \u30340 \u26410 \u25104 \u24180 \u20154 \u65292 \u24744 \u30340 \u30417 \u25252 \u20154 \u38656 \u35201 \u20180 \u32454 \u38405 \u35835 \u26412 \u25919 \u31574 \u24182 \u21516 \u24847 \u24744 \u20381 \u29031 \u26412 \u25919 \u31574 \u36866 \u29992 \u31508 \u22696 \u35759 \u20351 \u24179 \u21488 \u12290 \
\
10.2 \uc0\u25105 \u20204 \u19981 \u20250 \u20027 \u21160 \u30452 \u25509 \u21521 \u26410 \u25104 \u24180 \u20154 \u25910 \u38598 \u20854 \u20010 \u20154 \u20449 \u24687 \u12290 \u23545 \u20110 \u32463 \u30417 \u25252 \u20154 \u21516 \u24847 \u32780 \u25910 \u38598 \u26410 \u25104 \u24180 \u20154 \u20010 \u20154 \u20449 \u24687 \u30340 \u24773 \u20917 \u65292 \u25105 \u20204 \u21482 \u20250 \u22312 \u21463 \u21040 \u27861 \u24459 \u20801 \u35768 \u12289 \u30417 \u25252 \u20154 \u21516 \u24847 \u25110 \u32773 \u20445 \u25252 \u26410 \u25104 \u24180 \u20154 \u25152 \u24517 \u35201 \u30340 \u24773 \u20917 \u19979 \u20351 \u29992 \u12289 \u20849 \u20139 \u12289 \u36716 \u35753 \u25110 \u25259 \u38706 \u27492 \u20449 \u24687 \u12290 \
\
10.3 \uc0\u22914 \u26524 \u26377 \u20107 \u23454 \u35777 \u26126 \u26410 \u25104 \u24180 \u20154 \u24182 \u26410 \u21462 \u24471 \u30417 \u25252 \u20154 \u21516 \u24847 \u30340 \u24773 \u20917 \u19979 \u27880 \u20876 \u20351 \u29992 \u20102 \u25105 \u20204 \u30340 \u20135 \u21697 \u19982 /\u25110 \u26381 \u21153 \u65292 \u25105 \u20204 \u20250 \u19982 \u30456 \u20851 \u30417 \u25252 \u20154 \u21327 \u21830 \u65292 \u24182 \u35774 \u27861 \u23613 \u24555 \u21024 \u38500 \u30456 \u20851 \u20010 \u20154 \u20449 \u24687 \u12290 \
\
\uc0\u21313 \u19968 \u12289 \u36866 \u29992 \u33539 \u22260 \
\
\uc0\u25105 \u20204 \u30340 \u25152 \u26377 \u26381 \u21153 \u22343 \u36866 \u29992 \u26412 \u25919 \u31574 \u12290 \u20294 \u26576 \u20123 \u26381 \u21153 \u26377 \u20854 \u29305 \u23450 \u30340 \u38544 \u31169 \u25351 \u24341 /\u22768 \u26126 \u65292 \u35813 \u29305 \u23450 \u38544 \u31169 \u25351 \u24341 /\u22768 \u26126 \u26356 \u20855 \u20307 \u22320 \u35828 \u26126 \u25105 \u20204 \u22312 \u35813 \u26381 \u21153 \u20013 \u22914 \u20309 \u22788 \u29702 \u24744 \u30340 \u20449 \u24687 \u12290 \u22914 \u26412 \u25919 \u31574 \u19982 \u29305 \u23450 \u26381 \u21153 \u30340 \u38544 \u31169 \u25351 \u24341 /\u22768 \u26126 \u26377 \u19981 \u19968 \u33268 \u20043 \u22788 \u65292 \u35831 \u20197 \u35813 \u29305 \u23450 \u38544 \u31169 \u25351 \u24341 /\u22768 \u26126 \u20026 \u20934 \u12290 \u35831 \u24744 \u27880 \u24847 \u65292 \u26412 \u25919 \u31574 \u19981 \u36866 \u29992 \u30001 \u20854 \u20182 \u20844 \u21496 \u25110 \u20010 \u20154 \u25552 \u20379 \u30340 \u26381 \u21153 \u12290 \u20363 \u22914 \u65292 \u24744 \u36890 \u36807 \u20351 \u29992 \u31508 \u22696 \u35759 \u20351 \u24080 \u21495 \u30331 \u24405 \u20854 \u20182 \u20844 \u21496 \u25110 \u20010 \u20154 \u25552 \u20379 \u30340 \u26381 \u21153 \u12290 \u24744 \u20351 \u29992 \u35813 \u31561 \u31532 \u19977 \u26041 \u26381 \u21153 \u39035 \u21463 \u20854 \u38544 \u31169 \u25919 \u31574 \u65288 \u32780 \u38750 \u26412 \u25919 \u31574 \u65289 \u32422 \u26463 \u65292 \u24744 \u38656 \u35201 \u20180 \u32454 \u38405 \u35835 \u20854 \u25919 \u31574 \u20869 \u23481 \u12290 \
\
\uc0\u21313 \u20108 \u12289 \u36890 \u30693 \u21644 \u20462 \u35746 \
\
12.1 \uc0\u20026 \u32473 \u24744 \u25552 \u20379 \u26356 \u22909 \u30340 \u26381 \u21153 \u20197 \u21450 \u38543 \u30528 \u31508 \u22696 \u35759 \u20351 \u19994 \u21153 \u30340 \u21457 \u23637 \u65292 \u26412 \u38544 \u31169 \u25919 \u31574 \u20063 \u20250 \u38543 \u20043 \u26356 \u26032 \u12290 \u20294 \u26410 \u32463 \u24744 \u26126 \u30830 \u21516 \u24847 \u65292 \u25105 \u20204 \u19981 \u20250 \u21066 \u20943 \u24744 \u20381 \u25454 \u26412 \u38544 \u31169 \u25919 \u31574 \u25152 \u24212 \u20139 \u26377 \u30340 \u26435 \u21033 \u12290 \u25105 \u20204 \u20250 \u36890 \u36807 \u22312 \u31508 \u22696 \u35759 \u20351 \u23448 \u26041 \u32593 \u31449 \u12289 \u31508 \u22696 \u35759 \u20351 \u31227 \u21160 \u31471 \u19978 \u21457 \u20986 \u26356 \u26032 \u29256 \u26412 \u24182 \u22312 \u29983 \u25928 \u21069 \u36890 \u36807 \u32593 \u31449 \u20844 \u21578 \u25110 \u20197 \u20854 \u20182 \u36866 \u24403 \u26041 \u24335 \u25552 \u37266 \u24744 \u30456 \u20851 \u20869 \u23481 \u30340 \u26356 \u26032 \u65292 \u20063 \u35831 \u24744 \u35775 \u38382 \u21450 \u26102 \u20102 \u35299 \u26368 \u26032 \u30340 \u38544 \u31169 \u25919 \u31574 \u12290 \
\
12.2 \uc0\u23545 \u20110 \u37325 \u22823 \u21464 \u26356 \u65292 \u25105 \u20204 \u36824 \u20250 \u25552 \u20379 \u26356 \u20026 \u26174 \u33879 \u30340 \u36890 \u30693 \u65288 \u25105 \u20204 \u20250 \u36890 \u36807 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u37038 \u20214 \u12289 \u30701 \u20449 \u25110 \u22312 \u27983 \u35272 \u39029 \u38754 \u20570 \u29305 \u21035 \u25552 \u31034 \u31561 \u26041 \u24335 \u65292 \u35828 \u26126 \u38544 \u31169 \u25919 \u31574 \u30340 \u20855 \u20307 \u21464 \u26356 \u20869 \u23481 \u65289 \u12290 \
\
\uc0\u26412 \u38544 \u31169 \u25919 \u31574 \u25152 \u25351 \u30340 \u37325 \u22823 \u21464 \u26356 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \
\
A. \uc0\u25105 \u20204 \u30340 \u26381 \u21153 \u27169 \u24335 \u21457 \u29983 \u37325 \u22823 \u21464 \u21270 \u12290 \u22914 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u30340 \u30446 \u30340 \u12289 \u22788 \u29702 \u30340 \u20010 \u20154 \u20449 \u24687 \u31867 \u22411 \u12289 \u20010 \u20154 \u20449 \u24687 \u30340 \u20351 \u29992 \u26041 \u24335 \u31561 \u65307 \
\
B. \uc0\u25105 \u20204 \u22312 \u25152 \u26377 \u26435 \u32467 \u26500 \u12289 \u32452 \u32455 \u26550 \u26500 \u31561 \u26041 \u38754 \u21457 \u29983 \u37325 \u22823 \u21464 \u21270 \u12290 \u22914 \u19994 \u21153 \u35843 \u25972 \u12289 \u30772 \u20135 \u24182 \u36141 \u31561 \u24341 \u36215 \u30340 \u25152 \u26377 \u32773 \u21464 \u26356 \u31561 \u65307 \
\
C. \uc0\u20010 \u20154 \u20449 \u24687 \u20849 \u20139 \u12289 \u36716 \u35753 \u25110 \u20844 \u24320 \u25259 \u38706 \u30340 \u20027 \u35201 \u23545 \u35937 \u21457 \u29983 \u21464 \u21270 \u65307 \
\
D. \uc0\u24744 \u21442 \u19982 \u20010 \u20154 \u20449 \u24687 \u22788 \u29702 \u26041 \u38754 \u30340 \u26435 \u21033 \u21450 \u20854 \u34892 \u20351 \u26041 \u24335 \u21457 \u29983 \u37325 \u22823 \u21464 \u21270 \u65307 \
\
E. \uc0\u25105 \u20204 \u36127 \u36131 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u23433 \u20840 \u30340 \u36131 \u20219 \u37096 \u38376 \u12289 \u32852 \u32476 \u26041 \u24335 \u21450 \u25237 \u35785 \u28192 \u36947 \u21457 \u29983 \u21464 \u21270 \u26102 \u65307 \
\
F. \uc0\u20010 \u20154 \u20449 \u24687 \u23433 \u20840 \u24433 \u21709 \u35780 \u20272 \u25253 \u21578 \u34920 \u26126 \u23384 \u22312 \u39640 \u39118 \u38505 \u26102 \u12290 \
\
12.3 \uc0\u25105 \u20204 \u36824 \u20250 \u23558 \u26412 \u38544 \u31169 \u25919 \u31574 \u30340 \u26087 \u29256 \u26412 \u23384 \u26723 \u65292 \u20379 \u24744 \u26597 \u38405 \u12290 \
\
\uc0\u21313 \u19977 \u12289 \u26412 \u38544 \u31169 \u25919 \u31574 \u22914 \u20309 \u26356 \u26032 \
\
\uc0\u25105 \u20204 \u30340 \u38544 \u31169 \u20445 \u25252 \u25919 \u31574 \u21487 \u33021 \u21464 \u26356 \u12290 \u26410 \u32463 \u24744 \u26126 \u30830 \u21516 \u24847 \u65292 \u25105 \u20204 \u19981 \u20250 \u38480 \u21046 \u24744 \u25353 \u29031 \u26412 \u38544 \u31169 \u25919 \u31574 \u25152 \u24212 \u20139 \u26377 \u30340 \u26435 \u21033 \u12290 \u23545 \u20110 \u37325 \u22823 \u21464 \u26356 \u65292 \u25105 \u20204 \u20250 \u25552 \u20379 \u26174 \u33879 \u30340 \u36890 \u30693 \u65288 \u21253 \u25324 \u36890 \u36807 \u25105 \u20204 \u20844 \u31034 \u30340 \u26041 \u24335 \u36827 \u34892 \u36890 \u30693 \u29978 \u33267 \u21521 \u24744 \u25552 \u20379 \u24377 \u31383 \u25552 \u31034 \u65289 \u12290 \u26412 \u25919 \u31574 \u25152 \u25351 \u30340 \u37325 \u22823 \u21464 \u26356 \u21253 \u25324 \u20294 \u19981 \u38480 \u20110 \u65306 \
13.1 \uc0\u25105 \u20204 \u30340 \u26381 \u21153 \u27169 \u24335 \u21457 \u29983 \u37325 \u22823 \u21464 \u21270 \u12290 \u22914 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u30340 \u30446 \u30340 \u12289 \u22788 \u29702 \u30340 \u20010 \u20154 \u20449 \u24687 \u31867 \u22411 \u12289 \u20010 \u20154 \u20449 \u24687 \u30340 \u20351 \u29992 \u26041 \u24335 \u31561 \u12290 \
\
13.2 \uc0\u25105 \u20204 \u22312 \u25511 \u21046 \u26435 \u31561 \u26041 \u38754 \u21457 \u29983 \u37325 \u22823 \u21464 \u21270 \u12290 \u22914 \u24182 \u36141 \u12289 \u37325 \u32452 \u31561 \u24341 \u36215 \u30340 \u25152 \u26377 \u32773 \u21464 \u26356 \u31561 \u12290 \
\
13.3 \uc0\u20010 \u20154 \u20449 \u24687 \u20849 \u20139 \u12289 \u36716 \u35753 \u25110 \u20844 \u24320 \u25259 \u38706 \u30340 \u20027 \u35201 \u23545 \u35937 \u21457 \u29983 \u21464 \u21270 \u12290 \
\
13.4 \uc0\u24744 \u21442 \u19982 \u20010 \u20154 \u20449 \u24687 \u22788 \u29702 \u26041 \u38754 \u30340 \u26435 \u21033 \u21450 \u20854 \u34892 \u20351 \u26041 \u24335 \u21457 \u29983 \u37325 \u22823 \u21464 \u21270 \u12290 \
\
13.5 \uc0\u25105 \u20204 \u36127 \u36131 \u22788 \u29702 \u20010 \u20154 \u20449 \u24687 \u23433 \u20840 \u30340 \u36131 \u20219 \u37096 \u38376 \u12289 \u32852 \u32476 \u26041 \u24335 \u21450 \u25237 \u35785 \u28192 \u36947 \u21457 \u29983 \u21464 \u21270 \u26102 \u12290 \
\
13.6 \uc0\u20010 \u20154 \u20449 \u24687 \u23433 \u20840 \u24433 \u21709 \u35780 \u20272 \u25253 \u21578 \u34920 \u26126 \u23384 \u22312 \u39640 \u39118 \u38505 \u26102 \u12290 \
\
\uc0\u25105 \u20204 \u36824 \u20250 \u23558 \u26412 \u38544 \u31169 \u25919 \u31574 \u30340 \u26087 \u29256 \u26412 \u23384 \u26723 \u65292 \u20379 \u24744 \u26597 \u38405 \u12290 \
\
\uc0\u21313 \u22235 \u12289 \u22914 \u20309 \u32852 \u31995 \u25105 \u20204 \
\
14.1 \uc0\u22914 \u24744 \u23545 \u26412 \u38544 \u31169 \u25919 \u31574 \u25110 \u24744 \u20010 \u20154 \u20449 \u24687 \u30340 \u30456 \u20851 \u20107 \u23452 \u26377 \u20219 \u20309 \u38382 \u39064 \u12289 \u24847 \u35265 \u25110 \u24314 \u35758 \u65292 \u35831 \u32852 \u31995 \u31508 \u22696 \u35759 \u20351 \u23458 \u26381 \u65288 \u32852 \u31995 \u37038 \u31665 \u65306 \u12304 fengqunwangluo2025@163.com\u12305 \u65289 \u12290 \
\
14.2 \uc0\u19968 \u33324 \u24773 \u20917 \u19979 \u65292 \u25105 \u20204 \u23558 \u22312 15\u22825 \u20869 \u22238 \u22797 \u21644 \u22788 \u29702 \u24744 \u30340 \u30097 \u38382 \u12289 \u24847 \u35265 \u25110 \u24314 \u35758 \u12290 \u38656 \u27880 \u24847 \u30340 \u26159 \u65292 \u25105 \u20204 \u21487 \u33021 \u19981 \u20250 \u22238 \u22797 \u19982 \u26412 \u25919 \u31574 \u25110 \u24744 \u30340 \u20010 \u20154 \u20449 \u24687 \u26435 \u21033 \u26080 \u20851 \u30340 \u38382 \u39064 \u12290 \u22914 \u26524 \u24744 \u23545 \u25105 \u20204 \u30340 \u22238 \u22797 \u19981 \u28385 \u24847 \u65292 \u29305 \u21035 \u26159 \u25105 \u20204 \u30340 \u20010 \u20154 \u20449 \u24687 \u22788 \u29702 \u34892 \u20026 \u25439 \u23475 \u20102 \u24744 \u30340 \u21512 \u27861 \u26435 \u30410 \u65292 \u24744 \u36824 \u21487 \u20197 \u21521 \u32593 \u20449 \u12289 \u30005 \u20449 \u12289 \u20844 \u23433 \u21450 \u24037 \u21830 \u31561 \u30417 \u31649 \u37096 \u38376 \u36827 \u34892 \u25237 \u35785 \u25110 \u20030 \u25253 \u12290 \u25110 \u36890 \u36807 \u21521 \u25105 \u20204 \u25152 \u22312 \u22320 \u26377 \u31649 \u36758 \u26435 \u30340 \u27861 \u38498 \u25552 \u36215 \u35785 \u35772 \u26469 \u23547 \u27714 \u35299 \u20915 \u26041 \u26696 \u12290 \
\
\uc0\u21313 \u20116 \u12289 \u38468 \u21017 \
15.1 \uc0\u26412 \u25919 \u31574 \u30340 \u35299 \u37322 \u21450 \u20105 \u35758 \u35299 \u20915 \u22343 \u24212 \u36866 \u29992 \u20013 \u21326 \u20154 \u27665 \u20849 \u21644 \u22269 \u27861 \u24459 \u12290 \u19982 \u26412 \u25919 \u31574 \u30456 \u20851 \u30340 \u20219 \u20309 \u32416 \u32439 \u65292 \u21452 \u26041 \u24212 \u32463 \u21451 \u22909 \u21327 \u21830 \u35299 \u20915 \u65307 \u33509 \u19981 \u33021 \u21327 \u21830 \u35299 \u20915 \u65292 \u24744 \u22312 \u27492 \u21516 \u24847 \u23558 \u20105 \u35758 \u25552 \u20132 \u33267 \u25105 \u20204 \u20844 \u21496 \u25152 \u22312 \u22320 \u20154 \u27665 \u27861 \u38498 \u25552 \u36215 \u35785 \u35772 \u12290 \
\
15.2 \uc0\u22914 \u26524 \u26377 \u31649 \u36758 \u26435 \u30340 \u20219 \u20309 \u27861 \u38498 \u35009 \u23450 \u25110 \u21028 \u20915 \u26412 \u25919 \u31574 \u30340 \u20219 \u20309 \u26465 \u27454 \u26080 \u25928 \u65292 \u21017 \u35813 \u26465 \u27454 \u23558 \u20174 \u26412 \u25919 \u31574 \u20013 \u31227 \u38500 \u65292 \u20294 \u35813 \u26465 \u27454 \u30340 \u26080 \u25928 \u19981 \u24433 \u21709 \u26412 \u25919 \u31574 \u20854 \u20313 \u26465 \u27454 \u30340 \u25928 \u21147 \u12290 \u26412 \u25919 \u31574 \u30340 \u20854 \u20313 \u26465 \u27454 \u23558 \u32487 \u32493 \u25191 \u34892 \u12290 \
\
15.3 \uc0\u26412 \u25919 \u31574 \u30340 \u26631 \u39064 \u20165 \u20026 \u26041 \u20415 \u38405 \u35835 \u32780 \u35774 \u35745 \u65292 \u19981 \u24433 \u21709 \u26412 \u25919 \u31574 \u20219 \u20309 \u26465 \u27454 \u30340 \u21547 \u20041 \u25110 \u35299 \u37322 \u12290 \
\
\uc0\u24320 \u21457 \u32773 \u21517 \u31216 \u65306 \u12304 \u28023 \u21475 \u24066 \u26539 \u32676 \u32593 \u32476 \u31185 \u25216 \u26377 \u38480 \u20844 \u21496 \u12305 }
+4
View File
@@ -0,0 +1,4 @@
-- Remove the deprecated Kafka DB outbox table from a tenant database.
-- Run once per tenant business database after deploying the direct Kafka publish path.
DROP TABLE IF EXISTS im_kafka_outbox;
+36
View File
@@ -0,0 +1,36 @@
-- IM Kafka foundation tables for tenant databases.
-- Date: 2026-08-26
-- Scope: run once in every tenant business database, such as tinode.
-- Notes:
-- * Do not run in hotgo. Admin/HotGo side needs its own admin_command tables.
-- * Kafka consume idempotency/retry state is Redis-backed; MySQL only keeps manual dead letters.
DROP TABLE IF EXISTS im_kafka_consume_log;
CREATE TABLE IF NOT EXISTS im_kafka_dead_letters (
id BIGINT NOT NULL AUTO_INCREMENT,
dead_id VARCHAR(96) NOT NULL,
event_id VARCHAR(96) NOT NULL,
parent_event_id VARCHAR(96) NOT NULL DEFAULT '',
source_topic VARCHAR(128) NOT NULL,
consumer_group VARCHAR(128) NOT NULL,
event_type VARCHAR(96) NOT NULL DEFAULT '',
biz_type VARCHAR(64) NOT NULL DEFAULT '',
biz_id VARCHAR(128) NOT NULL DEFAULT '',
envelope JSON NOT NULL,
retry_count INT NOT NULL DEFAULT 0,
last_reason VARCHAR(64) NOT NULL DEFAULT '',
last_error VARCHAR(1000) NOT NULL DEFAULT '',
status VARCHAR(24) NOT NULL DEFAULT 'dead',
replay_count INT NOT NULL DEFAULT 0,
resolved_by VARCHAR(64) NOT NULL DEFAULT '',
resolved_at DATETIME(3),
resolve_note VARCHAR(1000) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY(id),
UNIQUE INDEX uk_im_kafka_dead_letters_dead(dead_id),
INDEX idx_im_kafka_dead_letters_event(event_id),
INDEX idx_im_kafka_dead_letters_status(status, created_at),
INDEX idx_im_kafka_dead_letters_biz(biz_type, biz_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -0,0 +1,12 @@
-- IM SeqId BIGINT upgrade for existing MySQL tenant databases.
-- New schemas have been updated in server/db/mysql/adapter.go and group_schema.go.
ALTER TABLE im_topics MODIFY seqid BIGINT NOT NULL DEFAULT 0;
ALTER TABLE im_subscriptions MODIFY recvseqid BIGINT DEFAULT 0;
ALTER TABLE im_subscriptions MODIFY readseqid BIGINT DEFAULT 0;
ALTER TABLE im_messages MODIFY seqid BIGINT NOT NULL;
ALTER TABLE im_message_revisions MODIFY seqid BIGINT NOT NULL;
ALTER TABLE im_favorites MODIFY source_seq BIGINT NOT NULL;
ALTER TABLE im_abuse_reports MODIFY target_seq BIGINT NOT NULL DEFAULT 0;
ALTER TABLE im_call_records MODIFY seq_id BIGINT NOT NULL;
ALTER TABLE im_group_files MODIFY source_seq BIGINT NOT NULL DEFAULT 0;
@@ -0,0 +1,92 @@
-- 仅用于本地环境:为 IM 业务表补充 tenant_id 字段。
--
-- 本脚本只修改表结构并回填现有数据,不修改索引、唯一约束或应用代码,
-- 也不会启用多商户共库能力。
USE `tinode`;
DELIMITER $$
DROP PROCEDURE IF EXISTS add_im_tenant_columns$$
CREATE PROCEDURE add_im_tenant_columns(IN target_schema VARCHAR(64), IN default_tenant_id BIGINT)
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE target_table VARCHAR(128);
DECLARE has_id INT DEFAULT 0;
DECLARE table_cursor CURSOR FOR
SELECT t.table_name
FROM information_schema.tables t
WHERE t.table_schema = target_schema
AND t.table_type = 'BASE TABLE'
AND t.table_name LIKE 'im\_%'
AND NOT EXISTS (
SELECT 1
FROM information_schema.columns c
WHERE c.table_schema = t.table_schema
AND c.table_name = t.table_name
AND c.column_name = 'tenant_id'
);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN table_cursor;
table_loop: LOOP
FETCH table_cursor INTO target_table;
IF done = 1 THEN
LEAVE table_loop;
END IF;
SELECT COUNT(*) INTO has_id
FROM information_schema.columns
WHERE table_schema = target_schema
AND table_name = target_table
AND column_name = 'id';
SET @position_sql = IF(has_id > 0, ' AFTER `id`', ' FIRST');
SET @alter_sql = CONCAT(
'ALTER TABLE `', target_schema, '`.`', target_table,
'` ADD COLUMN `tenant_id` BIGINT NOT NULL DEFAULT ',
default_tenant_id,
' COMMENT ''租户ID''',
@position_sql
);
PREPARE alter_stmt FROM @alter_sql;
EXECUTE alter_stmt;
DEALLOCATE PREPARE alter_stmt;
END LOOP;
CLOSE table_cursor;
END$$
DELIMITER ;
-- 本地默认业务库,现有数据归属 tenant_id=1。
CALL add_im_tenant_columns('tinode', 1);
-- 本地独立业务库,现有数据归属 tenant_id=2。
CALL add_im_tenant_columns('meeting_im', 2);
DROP PROCEDURE IF EXISTS add_im_tenant_columns;
-- 验证:以下结果中 missing_count 应全部为 0。
SELECT 'tinode' AS db_name, COUNT(*) AS missing_count
FROM information_schema.tables t
WHERE t.table_schema = 'tinode'
AND t.table_type = 'BASE TABLE'
AND t.table_name LIKE 'im\_%'
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns c
WHERE c.table_schema = t.table_schema
AND c.table_name = t.table_name
AND c.column_name = 'tenant_id'
)
UNION ALL
SELECT 'meeting_im', COUNT(*)
FROM information_schema.tables t
WHERE t.table_schema = 'meeting_im'
AND t.table_type = 'BASE TABLE'
AND t.table_name LIKE 'im\_%'
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns c
WHERE c.table_schema = t.table_schema
AND c.table_name = t.table_name
AND c.column_name = 'tenant_id'
);
@@ -0,0 +1,153 @@
-- 仅用于本地环境:将高数据量表迁移为“共库、按商户分表”。
-- 目标命名:
-- im_001_friendships
-- im_001_messages
-- im_001_group_messages_YYYYMM
-- 测试环境不得执行本文件。
-- 1. 商户编号改为事务序列分配,下一编号按当前最大 ID 顺序递增。
USE `hotgo`;
CREATE TABLE IF NOT EXISTS `im_tenant_id_sequence` (
`sequence_name` VARCHAR(32) NOT NULL,
`next_id` BIGINT NOT NULL,
`updated_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (`sequence_name`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
INSERT INTO `im_tenant_id_sequence` (`sequence_name`, `next_id`)
SELECT 'tenant', COALESCE(MAX(`id`), 0) + 1 FROM `im_tenant`
ON DUPLICATE KEY UPDATE `next_id` = GREATEST(`next_id`, VALUES(`next_id`));
-- 禁止绕过序列表使用 AUTO_INCREMENT 生成跳号或历史大整数编号。
ALTER TABLE `hg_admin_member` DROP FOREIGN KEY `fk_hg_admin_member_im_tenant`;
ALTER TABLE `im_tenant_config` DROP FOREIGN KEY `fk_im_tenant_config_tenant`;
ALTER TABLE `im_tenant` MODIFY `id` BIGINT NOT NULL;
ALTER TABLE `hg_admin_member` ADD CONSTRAINT `fk_hg_admin_member_im_tenant`
FOREIGN KEY (`tenant_id`) REFERENCES `im_tenant` (`id`);
ALTER TABLE `im_tenant_config` ADD CONSTRAINT `fk_im_tenant_config_tenant`
FOREIGN KEY (`tenant_id`) REFERENCES `im_tenant` (`id`);
UPDATE `im_tenant` AS target
JOIN `im_tenant` AS shared ON shared.`code`='default'
SET target.`db_name`=shared.`db_name`, target.`db_dsn`=shared.`db_dsn`
WHERE target.`id`<>shared.`id`;
-- 2. 月表路由元数据改为以 tenant_id 为第一维度。
USE `tinode`;
ALTER TABLE `im_group_message_shards`
DROP PRIMARY KEY,
DROP INDEX `uk_im_group_message_shards_table`,
DROP INDEX `idx_im_group_message_shards_status`,
ADD PRIMARY KEY (`tenant_id`, `shard_month`),
ADD UNIQUE INDEX `uk_im_group_message_shards_table` (`tenant_id`, `table_name`),
ADD INDEX `idx_im_group_message_shards_status` (`tenant_id`, `status`, `shard_month`);
DROP TABLE IF EXISTS `im_group_message_topic_shards`;
DROP TABLE IF EXISTS `im_group_message_dedup`;
-- 3. 创建好友与单聊消息分表并迁移现有数据。
CREATE TABLE IF NOT EXISTS `im_001_friendships` LIKE `im_friendships`;
ALTER TABLE `im_001_friendships` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 1 COMMENT '商户ID';
INSERT IGNORE INTO `im_001_friendships` SELECT * FROM `im_friendships` WHERE `tenant_id` = 1;
CREATE TABLE IF NOT EXISTS `im_001_messages` LIKE `im_messages`;
ALTER TABLE `im_001_messages` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 1 COMMENT '商户ID';
INSERT IGNORE INTO `im_001_messages` SELECT * FROM `im_messages` WHERE `tenant_id` = 1;
CREATE TABLE IF NOT EXISTS `im_002_friendships` LIKE `im_friendships`;
ALTER TABLE `im_002_friendships` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 2 COMMENT '商户ID';
INSERT IGNORE INTO `im_002_friendships` SELECT * FROM `meeting_im`.`im_friendships`;
CREATE TABLE IF NOT EXISTS `im_002_messages` LIKE `im_messages`;
ALTER TABLE `im_002_messages` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 2 COMMENT '商户ID';
INSERT IGNORE INTO `im_002_messages` SELECT * FROM `meeting_im`.`im_messages`;
-- 4. 迁移所有已存在的标准月表。过程只识别 im_group_messages_YYYYMM。
DELIMITER $$
DROP PROCEDURE IF EXISTS migrate_group_month_tables$$
CREATE PROCEDURE migrate_group_month_tables(IN source_schema VARCHAR(64), IN source_tenant BIGINT, IN tenant_code CHAR(3))
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE source_table VARCHAR(64);
DECLARE month_code CHAR(6);
DECLARE target_table VARCHAR(64);
DECLARE month_cursor CURSOR FOR
SELECT table_name
FROM information_schema.tables
WHERE table_schema = source_schema
AND table_name REGEXP '^im_group_messages_[0-9]{6}$';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN month_cursor;
month_loop: LOOP
FETCH month_cursor INTO source_table;
IF done = 1 THEN
LEAVE month_loop;
END IF;
SET month_code = RIGHT(source_table, 6);
SET target_table = CONCAT('im_', tenant_code, '_group_messages_', month_code);
SET @create_sql = CONCAT('CREATE TABLE IF NOT EXISTS `tinode`.`', target_table,
'` LIKE `', source_schema, '`.`', source_table, '`');
PREPARE stmt FROM @create_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @alter_sql = CONCAT('ALTER TABLE `tinode`.`', target_table,
'` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT ', source_tenant, ' COMMENT ''商户ID''');
PREPARE stmt FROM @alter_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @copy_sql = CONCAT('INSERT IGNORE INTO `tinode`.`', target_table,
'` SELECT * FROM `', source_schema, '`.`', source_table,
'` WHERE `tenant_id`=', source_tenant);
PREPARE stmt FROM @copy_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE month_cursor;
END$$
DELIMITER ;
CALL migrate_group_month_tables('tinode', 1, '001');
CALL migrate_group_month_tables('meeting_im', 2, '002');
DROP PROCEDURE IF EXISTS migrate_group_month_tables;
-- 旧的非月表数据按创建月份归入 1 号商户月表(本地现有数据为 202608)。
INSERT IGNORE INTO `im_001_group_messages_202608`
(`tenant_id`,`message_id`,`topic`,`group_id`,`seqid`,`from_user`,`client_msg_id`,`msg_type`,`status`,
`content`,`content_nonce`,`key_version`,`head`,`mentioned_user_ids`,`mention_all`,`reply_seqid`,`root_seqid`,
`file_count`,`content_size`,`has_risk`,`audit_status`,`version`,`recalled_at`,`recalled_by`,`deleted_at`,
`expires_at`,`created_at`,`updated_at`)
SELECT 1,`id`,`topic`,`group_id`,`seqid`,`from_user`,`client_msg_id`,`msg_type`,`status`,
`content`,`content_nonce`,`key_version`,`head`,`mentioned_user_ids`,`mention_all`,`reply_seqid`,`root_seqid`,
`file_count`,`content_size`,`has_risk`,`audit_status`,`version`,`recalled_at`,`recalled_by`,`deleted_at`,
`expires_at`,`created_at`,`updated_at`
FROM `im_group_messages`
WHERE `tenant_id` = 1 AND DATE_FORMAT(`created_at`, '%Y%m') = '202608';
-- 5. 修正 1 号商户路由,并合入 2 号商户路由元数据。
UPDATE `im_group_message_shards`
SET `table_name` = CONCAT('im_001_group_messages_', `shard_month`)
WHERE `tenant_id` = 1;
INSERT INTO `im_group_message_shards`
(`tenant_id`,`shard_month`,`table_name`,`shard_start`,`shard_end`,`status`,`row_count`,
`min_created_at`,`max_created_at`,`created_at`,`updated_at`)
SELECT 2,`shard_month`,CONCAT('im_002_group_messages_', `shard_month`),`shard_start`,`shard_end`,`status`,
`row_count`,`min_created_at`,`max_created_at`,`created_at`,`updated_at`
FROM `meeting_im`.`im_group_message_shards`
ON DUPLICATE KEY UPDATE `table_name`=VALUES(`table_name`),`status`=VALUES(`status`);
-- Topic-to-month routes are Redis-backed and are rebuilt by runtime reads.
UPDATE `im_group_message_shards` AS s
SET s.`row_count`=(SELECT COUNT(*) FROM `im_001_group_messages_202608`),
s.`min_created_at`=(SELECT MIN(`created_at`) FROM `im_001_group_messages_202608`),
s.`max_created_at`=(SELECT MAX(`created_at`) FROM `im_001_group_messages_202608`)
WHERE s.`tenant_id`=1 AND s.`shard_month`='202608';
-- 验证结果。
SELECT `sequence_name`,`next_id` FROM `hotgo`.`im_tenant_id_sequence`;
SELECT `table_name`,`table_rows`
FROM information_schema.tables
WHERE table_schema = 'tinode'
AND table_name REGEXP '^im_[0-9]{3}_(friendships|messages|group_messages_[0-9]{6})$'
ORDER BY table_name;
@@ -0,0 +1,272 @@
-- 仅用于测试环境复制库 meeting_im_sharding_preview。
-- 源库 meeting_im 和注册库 hotgo 不在本脚本修改范围内。
USE `meeting_im_sharding_preview`;
-- 1. 共享业务表补充商户维度,复制库全部现有数据归属商户 2。
DELIMITER $$
DROP PROCEDURE IF EXISTS add_preview_tenant_columns$$
CREATE PROCEDURE add_preview_tenant_columns()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE target_table VARCHAR(128);
DECLARE has_id INT DEFAULT 0;
DECLARE table_cursor CURSOR FOR
SELECT t.table_name
FROM information_schema.tables t
WHERE t.table_schema = 'meeting_im_sharding_preview'
AND t.table_type = 'BASE TABLE'
AND t.table_name LIKE 'im\_%'
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns c
WHERE c.table_schema=t.table_schema
AND c.table_name=t.table_name
AND c.column_name='tenant_id'
);
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN table_cursor;
table_loop: LOOP
FETCH table_cursor INTO target_table;
IF done = 1 THEN
LEAVE table_loop;
END IF;
SELECT COUNT(*) INTO has_id
FROM information_schema.columns
WHERE table_schema='meeting_im_sharding_preview'
AND table_name=target_table AND column_name='id';
SET @position_sql = IF(has_id > 0, ' AFTER `id`', ' FIRST');
SET @alter_sql = CONCAT('ALTER TABLE `meeting_im_sharding_preview`.`', target_table,
'` ADD COLUMN `tenant_id` BIGINT NOT NULL DEFAULT 2 COMMENT ''商户ID''',
@position_sql);
PREPARE stmt FROM @alter_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE table_cursor;
END$$
DELIMITER ;
CALL add_preview_tenant_columns();
DROP PROCEDURE IF EXISTS add_preview_tenant_columns;
-- 2. 群消息路由元数据的唯一性增加商户维度。
ALTER TABLE `im_group_message_shards`
DROP PRIMARY KEY,
DROP INDEX `uk_im_group_message_shards_table`,
DROP INDEX `idx_im_group_message_shards_status`,
ADD PRIMARY KEY (`tenant_id`,`shard_month`),
ADD UNIQUE INDEX `uk_im_group_message_shards_table` (`tenant_id`,`table_name`),
ADD INDEX `idx_im_group_message_shards_status` (`tenant_id`,`status`,`shard_month`);
DROP TABLE IF EXISTS `im_group_message_topic_shards`;
DROP TABLE IF EXISTS `im_group_message_dedup`;
-- 3. 创建商户 2 的好友与单聊消息分表并复制快照数据。
CREATE TABLE `im_002_friendships` LIKE `im_friendships`;
ALTER TABLE `im_002_friendships` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 2 COMMENT '商户ID';
INSERT INTO `im_002_friendships` SELECT * FROM `im_friendships` WHERE `tenant_id`=2;
CREATE TABLE `im_002_messages` LIKE `im_messages`;
ALTER TABLE `im_002_messages` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 2 COMMENT '商户ID';
INSERT INTO `im_002_messages` SELECT * FROM `im_messages` WHERE `tenant_id`=2;
-- 4. 将所有现存标准月表复制为商户 2 的月表。
DELIMITER $$
DROP PROCEDURE IF EXISTS migrate_preview_group_month_tables$$
CREATE PROCEDURE migrate_preview_group_month_tables()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE source_table VARCHAR(64);
DECLARE month_code CHAR(6);
DECLARE target_table VARCHAR(64);
DECLARE month_cursor CURSOR FOR
SELECT table_name
FROM information_schema.tables
WHERE table_schema='meeting_im_sharding_preview'
AND table_name REGEXP '^im_group_messages_[0-9]{6}$';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN month_cursor;
month_loop: LOOP
FETCH month_cursor INTO source_table;
IF done = 1 THEN
LEAVE month_loop;
END IF;
SET month_code = RIGHT(source_table, 6);
SET target_table = CONCAT('im_002_group_messages_', month_code);
SET @create_sql = CONCAT('CREATE TABLE `', target_table, '` LIKE `', source_table, '`');
PREPARE stmt FROM @create_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @alter_sql = CONCAT('ALTER TABLE `', target_table,
'` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 2 COMMENT ''商户ID''');
PREPARE stmt FROM @alter_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @copy_sql = CONCAT('INSERT INTO `', target_table, '` SELECT * FROM `', source_table,
'` WHERE `tenant_id`=2');
PREPARE stmt FROM @copy_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE month_cursor;
END$$
DELIMITER ;
CALL migrate_preview_group_month_tables();
DROP PROCEDURE IF EXISTS migrate_preview_group_month_tables;
UPDATE `im_group_message_shards`
SET `table_name`=CONCAT('im_002_group_messages_',`shard_month`)
WHERE `tenant_id`=2;
-- 5. 补齐 default 商户(ID=1,分表编号 001)的核心分表。
-- tinode 是 default 商户的原库,只从该库读取,不修改原表。
CREATE TABLE `im_001_friendships` LIKE `im_friendships`;
ALTER TABLE `im_001_friendships` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 1 COMMENT '商户ID';
INSERT INTO `im_001_friendships` (
`id`,`tenant_id`,`user_id`,`peer_id`,`status`,`remark`,`topic`,`created_at`,`updated_at`
)
SELECT `id`,1,`user_id`,`peer_id`,`status`,`remark`,`topic`,`created_at`,`updated_at`
FROM `tinode`.`im_friendships`;
CREATE TABLE `im_001_messages` LIKE `im_messages`;
ALTER TABLE `im_001_messages` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 1 COMMENT '商户ID';
INSERT INTO `im_001_messages` (
`id`,`tenant_id`,`createdat`,`updatedat`,`deletedat`,`delid`,`seqid`,`topic`,`from`,
`client_msg_id`,`version`,`recalled_at`,`recalled_by`,`head`,`content`,`content_nonce`,`key_version`
)
SELECT `id`,1,`createdat`,`updatedat`,`deletedat`,`delid`,`seqid`,`topic`,`from`,
`client_msg_id`,`version`,`recalled_at`,`recalled_by`,`head`,`content`,`content_nonce`,`key_version`
FROM `tinode`.`im_messages`;
DELIMITER $$
DROP PROCEDURE IF EXISTS migrate_default_group_month_tables$$
CREATE PROCEDURE migrate_default_group_month_tables()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE source_table VARCHAR(64);
DECLARE month_code CHAR(6);
DECLARE target_table VARCHAR(64);
DECLARE month_cursor CURSOR FOR
SELECT table_name
FROM information_schema.tables
WHERE table_schema='tinode'
AND table_name REGEXP '^im_group_messages_[0-9]{6}$';
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN month_cursor;
month_loop: LOOP
FETCH month_cursor INTO source_table;
IF done = 1 THEN
LEAVE month_loop;
END IF;
SET month_code = RIGHT(source_table, 6);
SET target_table = CONCAT('im_001_group_messages_', month_code);
SET @create_sql = CONCAT('CREATE TABLE `', target_table,
'` LIKE `im_group_messages_', month_code, '`');
PREPARE stmt FROM @create_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @alter_sql = CONCAT('ALTER TABLE `', target_table,
'` MODIFY `tenant_id` BIGINT NOT NULL DEFAULT 1 COMMENT ''商户ID''');
PREPARE stmt FROM @alter_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @copy_sql = CONCAT(
'INSERT INTO `', target_table, '` (`tenant_id`,`message_id`,`topic`,`group_id`,`seqid`,',
'`from_user`,`client_msg_id`,`msg_type`,`status`,`content`,`content_nonce`,`key_version`,',
'`head`,`mentioned_user_ids`,`mention_all`,`reply_seqid`,`root_seqid`,`file_count`,',
'`content_size`,`has_risk`,`audit_status`,`version`,`recalled_at`,`recalled_by`,',
'`deleted_at`,`expires_at`,`created_at`,`updated_at`) ',
'SELECT 1,`message_id`,`topic`,`group_id`,`seqid`,`from_user`,`client_msg_id`,`msg_type`,',
'`status`,`content`,`content_nonce`,`key_version`,`head`,`mentioned_user_ids`,`mention_all`,',
'`reply_seqid`,`root_seqid`,`file_count`,`content_size`,`has_risk`,`audit_status`,`version`,',
'`recalled_at`,`recalled_by`,`deleted_at`,`expires_at`,`created_at`,`updated_at` ',
'FROM `tinode`.`', source_table, '`'
);
PREPARE stmt FROM @copy_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE month_cursor;
END$$
DELIMITER ;
CALL migrate_default_group_month_tables();
DROP PROCEDURE IF EXISTS migrate_default_group_month_tables;
INSERT INTO `im_group_message_shards` (
`tenant_id`,`shard_month`,`table_name`,`shard_start`,`shard_end`,`status`,`row_count`,
`min_created_at`,`max_created_at`,`created_at`,`updated_at`
)
SELECT 1,`shard_month`,CONCAT('im_001_group_messages_',`shard_month`),`shard_start`,`shard_end`,
`status`,`row_count`,`min_created_at`,`max_created_at`,`created_at`,`updated_at`
FROM `tinode`.`im_group_message_shards`;
-- Topic-to-month routes are Redis-backed and are rebuilt by runtime reads.
-- 以实际复制后的物理表为准刷新统计,避免源库路由统计滞后。
DELIMITER $$
DROP PROCEDURE IF EXISTS refresh_default_group_shard_stats$$
CREATE PROCEDURE refresh_default_group_shard_stats()
BEGIN
DECLARE done INT DEFAULT 0;
DECLARE month_code CHAR(6);
DECLARE target_table VARCHAR(64);
DECLARE shard_cursor CURSOR FOR
SELECT `shard_month`
FROM `im_group_message_shards`
WHERE `tenant_id`=1;
DECLARE CONTINUE HANDLER FOR NOT FOUND SET done = 1;
OPEN shard_cursor;
shard_loop: LOOP
FETCH shard_cursor INTO month_code;
IF done = 1 THEN
LEAVE shard_loop;
END IF;
SET target_table = CONCAT('im_001_group_messages_', month_code);
SET @stats_sql = CONCAT(
'UPDATE `im_group_message_shards` SET ',
'`row_count`=(SELECT COUNT(*) FROM `', target_table, '`),',
'`min_created_at`=(SELECT MIN(`created_at`) FROM `', target_table, '`),',
'`max_created_at`=(SELECT MAX(`created_at`) FROM `', target_table, '`) ',
'WHERE `tenant_id`=1 AND `shard_month`=''', month_code, ''''
);
PREPARE stmt FROM @stats_sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
END LOOP;
CLOSE shard_cursor;
END$$
DELIMITER ;
CALL refresh_default_group_shard_stats();
DROP PROCEDURE IF EXISTS refresh_default_group_shard_stats;
-- 6. 验证:missing_tenant_id 必须为 0,两个商户的分表与路由均存在。
SELECT COUNT(*) AS missing_tenant_id
FROM information_schema.tables t
WHERE t.table_schema='meeting_im_sharding_preview'
AND t.table_type='BASE TABLE'
AND t.table_name LIKE 'im\_%'
AND NOT EXISTS (
SELECT 1 FROM information_schema.columns c
WHERE c.table_schema=t.table_schema
AND c.table_name=t.table_name
AND c.column_name='tenant_id'
);
SELECT 'friendships' AS data_type,
(SELECT COUNT(*) FROM `im_friendships` WHERE `tenant_id`=2) AS source_count,
(SELECT COUNT(*) FROM `im_002_friendships`) AS target_count
UNION ALL
SELECT 'messages',
(SELECT COUNT(*) FROM `im_messages` WHERE `tenant_id`=2),
(SELECT COUNT(*) FROM `im_002_messages`);
SELECT s.`shard_month`,s.`table_name`,s.`status`,s.`row_count`
FROM `im_group_message_shards` s
WHERE s.`tenant_id` IN (1,2)
ORDER BY s.`tenant_id`,s.`shard_month`;
SELECT t.table_name
FROM information_schema.tables t
WHERE t.table_schema='meeting_im_sharding_preview'
AND t.table_name REGEXP '^im_(001|002)_(friendships|messages|group_messages_[0-9]{6})$'
ORDER BY t.table_name;
@@ -0,0 +1,134 @@
-- MySQL 8.0. Runtime fixes for the shared meeting_im database.
-- Execute with a privileged account that can read hotgo.im_tenant.
CREATE TABLE IF NOT EXISTS im_user_account_sequences (
tenant_id BIGINT NOT NULL,
id TINYINT UNSIGNED NOT NULL,
next_value INT UNSIGNED NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3)
ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (tenant_id, id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- If this table was created by the older independent-database script, its
-- primary key may still be only (id). Shared DB must key by tenant_id too.
ALTER TABLE im_user_account_sequences
DROP PRIMARY KEY,
ADD PRIMARY KEY (tenant_id, id);
INSERT INTO im_user_account_sequences (tenant_id, id, next_value)
SELECT tenant_id, 1, GREATEST(COALESCE(MAX(account_no), 999), 999)
FROM im_users
WHERE tenant_id > 0
GROUP BY tenant_id
ON DUPLICATE KEY UPDATE
next_value = GREATEST(next_value, VALUES(next_value));
INSERT IGNORE INTO im_user_account_sequences (tenant_id, id, next_value)
SELECT id, 1, 999
FROM hotgo.im_tenant
WHERE state = 1;
DROP TABLE IF EXISTS im_login_lockouts;
-- HTTP write idempotency is Redis-backed. Keep it out of MySQL.
DROP TABLE IF EXISTS im_http_idempotency;
DROP TABLE IF EXISTS im_group_idempotency;
DROP TABLE IF EXISTS im_group_message_dedup;
DROP TABLE IF EXISTS im_group_message_topic_shards;
DROP TABLE IF EXISTS im_kafka_outbox;
DROP TABLE IF EXISTS im_kafka_consume_log;
DROP TABLE IF EXISTS im_business_events_outbox;
DROP TABLE IF EXISTS im_group_events_outbox;
DROP TABLE IF EXISTS im_login_failure_logs;
DROP TABLE IF EXISTS im_action_tokens;
-- User identity indexes must be unique inside one tenant, not globally.
ALTER TABLE im_users
DROP INDEX uk_im_users_account_no,
DROP INDEX users_phone,
DROP INDEX users_username,
ADD UNIQUE INDEX uk_im_users_tenant_account_no (tenant_id, account_no),
ADD UNIQUE INDEX uk_im_users_tenant_phone (tenant_id, phone),
ADD UNIQUE INDEX uk_im_users_tenant_username (tenant_id, username);
ALTER TABLE im_auth
DROP INDEX auth_userid,
ADD UNIQUE INDEX auth_tenant_userid (tenant_id, userid);
-- Topic/subscription runtime indexes for shared-database deployments.
-- These tables are on hot paths for topic lookup, message seq updates,
-- unread calculation and subscription permission checks. Keep tenant_id as
-- the leading column so one tenant does not scan another tenant's rows.
SET @schema_name := DATABASE();
SET @sql := IF((
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = @schema_name
AND table_name = 'im_topics'
AND index_name = 'uk_topics_tenant_name'
) = 0,
'ALTER TABLE im_topics ADD UNIQUE INDEX uk_topics_tenant_name(tenant_id, name)',
'SELECT ''skip uk_topics_tenant_name'' AS status'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF((
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = @schema_name
AND table_name = 'im_topics'
AND index_name = 'idx_topics_tenant_name_state_seqid'
) = 0,
'ALTER TABLE im_topics ADD INDEX idx_topics_tenant_name_state_seqid(tenant_id, name, state, seqid)',
'SELECT ''skip idx_topics_tenant_name_state_seqid'' AS status'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF((
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = @schema_name
AND table_name = 'im_topics'
AND index_name = 'idx_topics_tenant_owner'
) = 0,
'ALTER TABLE im_topics ADD INDEX idx_topics_tenant_owner(tenant_id, owner)',
'SELECT ''skip idx_topics_tenant_owner'' AS status'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF((
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = @schema_name
AND table_name = 'im_subscriptions'
AND index_name = 'idx_subscriptions_tenant_topic_deletedat'
) = 0,
'ALTER TABLE im_subscriptions ADD INDEX idx_subscriptions_tenant_topic_deletedat(tenant_id, topic, deletedat)',
'SELECT ''skip idx_subscriptions_tenant_topic_deletedat'' AS status'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
SET @sql := IF((
SELECT COUNT(*) FROM information_schema.statistics
WHERE table_schema = @schema_name
AND table_name = 'im_subscriptions'
AND index_name = 'idx_subscriptions_tenant_user_topic_deletedat'
) = 0,
'ALTER TABLE im_subscriptions ADD INDEX idx_subscriptions_tenant_user_topic_deletedat(tenant_id, userid, topic, deletedat)',
'SELECT ''skip idx_subscriptions_tenant_user_topic_deletedat'' AS status'
);
PREPARE stmt FROM @sql; EXECUTE stmt; DEALLOCATE PREPARE stmt;
-- Default app release rows used by /v1/im/app/releases/latest.
INSERT INTO hotgo.im_app_releases (
platform, channel, latest_version, min_supported_version, update_type,
release_notes, remind_after_seconds
) VALUES
('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
('web', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0)
ON DUPLICATE KEY UPDATE
latest_version = VALUES(latest_version),
min_supported_version = VALUES(min_supported_version),
update_type = VALUES(update_type),
release_notes = VALUES(release_notes),
remind_after_seconds = VALUES(remind_after_seconds),
updated_at = CURRENT_TIMESTAMP(3);
+22
View File
@@ -0,0 +1,22 @@
CREATE TABLE IF NOT EXISTS `im_content_moderation_logs` (
`id` BIGINT NOT NULL AUTO_INCREMENT,
`request_id` VARCHAR(64) NOT NULL,
`scene` VARCHAR(32) NOT NULL,
`topic` VARCHAR(96) NOT NULL DEFAULT '',
`seqid` BIGINT DEFAULT NULL,
`actor_id` BIGINT DEFAULT NULL,
`provider` VARCHAR(32) NOT NULL DEFAULT '',
`provider_request_id` VARCHAR(96) NOT NULL DEFAULT '',
`risk_level` VARCHAR(16) NOT NULL DEFAULT 'none',
`risk_labels` JSON DEFAULT NULL,
`action` VARCHAR(16) NOT NULL,
`content_hash` CHAR(64) NOT NULL,
`latency_ms` BIGINT NOT NULL DEFAULT 0,
`error_code` VARCHAR(64) NOT NULL DEFAULT '',
`created_at` DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_content_moderation_request` (`request_id`),
KEY `idx_im_content_moderation_scene_time` (`scene`,`created_at` DESC,`id` DESC),
KEY `idx_im_content_moderation_actor_time` (`actor_id`,`created_at` DESC,`id` DESC),
KEY `idx_im_content_moderation_topic_seq` (`topic`,`seqid`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
@@ -0,0 +1,66 @@
-- MySQL 8.0. Admin message governance and authentication audit tables.
-- Execute this script in the shared meeting_im database; tenant_id provides tenant isolation.
CREATE TABLE IF NOT EXISTS im_user_login_logs (
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
user_id BIGINT NOT NULL,
username VARCHAR(64) NOT NULL DEFAULT '',
account_type VARCHAR(24) NOT NULL DEFAULT '',
account_hash CHAR(64) NOT NULL DEFAULT '',
account_mask VARCHAR(128) NOT NULL DEFAULT '',
login_type VARCHAR(24) NOT NULL,
event_type VARCHAR(24) NOT NULL DEFAULT 'login',
session_id VARCHAR(96) NOT NULL DEFAULT '',
device_id VARCHAR(255) NOT NULL DEFAULT '',
platform VARCHAR(32) NOT NULL DEFAULT 'unknown',
app_version VARCHAR(32) NOT NULL DEFAULT '',
ip VARCHAR(64) NOT NULL DEFAULT '',
region VARCHAR(128) NOT NULL DEFAULT '',
user_agent VARCHAR(512) NOT NULL DEFAULT '',
risk_level VARCHAR(16) NOT NULL DEFAULT 'low',
request_id VARCHAR(64) NOT NULL DEFAULT '',
login_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
logout_at DATETIME(3) NULL,
metadata JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
INDEX idx_im_user_login_tenant_time (tenant_id, login_at DESC, id DESC),
INDEX idx_im_user_login_session_close (tenant_id, session_id, logout_at),
INDEX idx_im_user_login_user_time (user_id, login_at DESC, id DESC),
INDEX idx_im_user_login_ip_time (ip, login_at DESC, id DESC),
INDEX idx_im_user_login_device_time (device_id(64), login_at DESC),
INDEX idx_im_user_login_request (request_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
DROP TABLE IF EXISTS im_login_failure_logs;
DROP TABLE IF EXISTS im_login_lockouts;
CREATE TABLE IF NOT EXISTS im_message_admin_actions (
id BIGINT NOT NULL AUTO_INCREMENT,
action_id VARCHAR(96) NOT NULL,
command_id VARCHAR(96) NOT NULL,
conversation_type VARCHAR(16) NOT NULL,
topic VARCHAR(64) NOT NULL,
seqid BIGINT NOT NULL,
sender_user_id BIGINT NULL,
action VARCHAR(16) NOT NULL,
action_status VARCHAR(16) NOT NULL DEFAULT 'pending',
reason VARCHAR(500) NOT NULL DEFAULT '',
actor_admin_id BIGINT NOT NULL DEFAULT 0,
request_id VARCHAR(64) NOT NULL DEFAULT '',
before_snapshot JSON NULL,
after_snapshot JSON NULL,
failure_reason VARCHAR(96) NOT NULL DEFAULT '',
failure_message VARCHAR(1000) NOT NULL DEFAULT '',
processed_at DATETIME(3) NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_message_admin_action_id (action_id),
UNIQUE INDEX uk_im_message_admin_command (command_id),
INDEX idx_im_message_admin_target (conversation_type, topic, seqid, id DESC),
INDEX idx_im_message_admin_status (action_status, created_at, id),
INDEX idx_im_message_admin_actor (actor_admin_id, created_at DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
+29
View File
@@ -0,0 +1,29 @@
-- Update workbench module order to match the mobile workbench layout.
-- Order: 打卡、月报、日报、部门框架、举报信箱、会议通知、会议笔记.
UPDATE tinode.im_workbench_apps
SET sort_order = CASE code
WHEN 'attendance' THEN 10
WHEN 'monthly_reports' THEN 20
WHEN 'daily_reports' THEN 30
WHEN 'organization' THEN 40
WHEN 'executive_mailbox' THEN 50
WHEN 'meetings' THEN 60
WHEN 'meeting_notes' THEN 70
ELSE sort_order
END,
name = CASE code
WHEN 'organization' THEN '部门框架'
WHEN 'executive_mailbox' THEN '举报信箱'
ELSE name
END,
updated_at = CURRENT_TIMESTAMP(3)
WHERE code IN (
'attendance',
'monthly_reports',
'daily_reports',
'organization',
'executive_mailbox',
'meetings',
'meeting_notes'
);
@@ -0,0 +1,5 @@
-- Persist client-provided upload filename for downstream business modules.
-- Apply before deploying im_server changes which read/write im_fileuploads.original_name.
ALTER TABLE im_fileuploads
ADD COLUMN original_name VARCHAR(255) NOT NULL DEFAULT '' AFTER location;

Some files were not shown because too many files have changed in this diff Show More