docs: update im integration and schema notes

This commit is contained in:
richjohnrambo
2026-09-17 14:59:28 +08:00
parent 50f8581667
commit ed976c76ff
5 changed files with 150 additions and 45 deletions
+56 -38
View File
@@ -451,7 +451,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| 好友申请通过 | 申请人、通过人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.accepted","payload":{"user_id":"usr_actor"}}}` | 刷新新朋友列表、好友列表、P2P 会话 context;申请人当前有系统通知,通过人本人当前无系统通知 |
| 好友申请拒绝 | 申请人、拒绝人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.rejected","payload":{"user_id":"usr_actor"}}}` | 刷新新朋友列表和 `bootstrap.entry_badges`;申请人当前有系统通知,拒绝人本人当前无系统通知 |
| 删除好友 | 被删除方、操作人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.deleted","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表、会话 context,重新判断输入区 `can_send`;被删除方当前有系统通知,操作人本人当前无系统通知 |
| 拉黑/取消拉黑 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.blocked","payload":{"user_id":"usr_actor"}}}` | 刷新黑名单、好友列表和会话 context |
| 拉黑/取消拉黑 | 操作人本人 | 拉黑:`{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.blocked","payload":{"user_id":"usr_actor"}}}`;取消拉黑:`{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.unblocked","payload":{"user_id":"usr_actor"}}}` | 刷新黑名单、好友列表和会话 context |
| 修改好友备注 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"friend.remark.updated","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表和会话头显示名 |
| 消息撤回 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.recalled","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取或本地标记已撤回 |
| 消息编辑 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.edited","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取消息详情或修订记录 |
@@ -494,16 +494,20 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
客户端侧要求:
- App 集成 JPush SDK,初始化使用极光 `AppKey`,不接触 `MasterSecret`。
- 登录 IM 后将极光 alias 绑定为当前 `user_id` 的安全别名:`user_id.replace("-", ".").replace("_", "|")`,例如 `usrd6Ag5opa-ww` 绑定为 `usrd6Ag5opa.ww`;同一用户多设备允许绑定同一个 alias。
- SDK 返回 `registration_id` 后调用 `PUT /v1/im/devices/{device_id}/push-token` 上报,`provider` 固定传 `jpush`。
- 当前测试环境使用 `registration_id` 模式,服务端配置 `jpush_use_alias=0`,不依赖客户端 alias;alias 绑定可保留但不是投递依据。
- SDK 返回 `registration_id` 后调用 `PUT /v1/im/devices/{device_id}/push-token` 上报,`provider` 固定传 `jpush`。`device_id` 必须稳定,同一台设备不要每次启动变化。
- 切换账号、切换租户、清登录态后必须重新上报 Push Token;客户端本地的 `user_id/token/registration_id` 去重缓存必须按 `tenant_code + user_id + device_id + registration_id` 隔离,避免把设备继续绑在旧账号上。
- 若后续切回 alias 模式,登录 IM 后将极光 alias 绑定为当前 `user_id` 的安全别名:`user_id.replace("-", ".").replace("_", "|")`,例如 `usrd6Ag5opa-ww` 绑定为 `usrd6Ag5opa.ww`;同一用户多设备允许绑定同一个 alias。
- 点击 Push 后按 `extras.type` 跳转,再按服务端数据补偿,不能只凭 Push payload 渲染最终状态。
服务端侧要求:
- `jpush_master_secret` 只保存在后台配置 `im_jpush` 分组,IM Server 通过内部配置接口读取并调用极光 REST API。
- 当前测试环境按 `registration_id` 投递:服务端从 `im_device_push_tokens` 按目标 `user_id + provider=jpush` 查询设备 token,并调用极光 `audience.registration_id`。
- 用户在线时优先 WebSocket 实时投递;用户离线、后台或不可投递时生成 Push 任务。
- 万人大群实时 fanout 不靠 Push;离线只对需要提醒的成员生成极光 Push,客户端重连后按 Topic seq 拉取补偿。
- Push payload 默认不带敏感正文,通知栏文案按用户隐私设置降级。
- iOS 测试环境固定使用 `options.apns_production=false`;客户端包也必须是开发 APNs 环境。TestFlight/App Store 生产包需要后端与客户端同时切到生产 APNs。
极光 Push extras 统一格式:
@@ -532,22 +536,11 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
```json
{
"platform": "all",
"platform": "ios",
"audience": {
"alias": ["usr_target"]
"registration_id": ["161a3797c95ee4a8bd3"]
},
"notification": {
"alert": "你收到一条新消息",
"android": {
"title": "MeetingIM",
"alert": "你收到一条新消息",
"extras": {
"type": "im_message",
"tenant_code": "default",
"topic": "p2p...",
"seq": "128"
}
},
"ios": {
"alert": {
"title": "MeetingIM",
@@ -575,25 +568,11 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
```json
{
"platform": "all",
"platform": "ios",
"audience": {
"alias": ["usr_callee"]
"registration_id": ["161a3797c95ee4a8bd3"]
},
"notification": {
"alert": "收到一通语音通话",
"android": {
"title": "语音通话",
"alert": "张三邀请你语音通话",
"extras": {
"type": "call_invite",
"tenant_code": "default",
"topic": "p2p...",
"seq": "30",
"call_id": "call_01K...",
"call_type": "audio",
"caller_id": "usr_caller"
}
},
"ios": {
"alert": {
"title": "语音通话",
@@ -1779,7 +1758,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
**状态:新增。**
- 开启拉黑:`PUT /v1/im/blocks/{user_id}`;解除:`DELETE /v1/im/blocks/{user_id}`,均带幂等键。
- 成功返回 `blocked,relationship,updated_at` 并向当前用户其他设备推送 `privacy.block.updated`。拉黑后服务端拦截对方消息和呼叫。
- 成功返回 `blocked,relationship,updated_at` 并向当前用户其他设备推送 `relationship.blocked`;解除拉黑推送 `relationship.unblocked`。事件 `topic` 为操作人自己的 `usr_actor`,`payload.user_id` 为操作人 ID;客户端收到后刷新黑名单、好友列表和会话 context。拉黑后服务端拦截对方消息和呼叫。
- 解除拉黑只恢复黑名单 ACL;若双方已非好友,`relationship` 仍为 `none`,不能自动恢复好友。
### 5.11 删除好友提示.png
@@ -1985,6 +1964,45 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
- 该昵称仅作用当前群,不能修改全局用户昵称或其他群资料。
- 群成员列表和群成员资料中的 `display_name` 仍由服务端按当前登录人计算,好友备注优先;群内昵称单独返回为 `group_nickname`,前端不要用 `nickname/group_nickname` 覆盖 `display_name`。
### 6.13.1 群成员列表拉取与缓存策略
**状态:已按分页 + 服务端页缓存实现。**
客户端进入普通群聊会话时不要主动全量拉取群成员列表;群详情中的 `member_count`、当前用户权限和会话上下文足够渲染聊天页。只有进入群成员页、管理员候选页、禁言候选页、转让群主候选页或输入 `@` 搜索成员时,才调用 `GET /v1/im/groups/{topic}/members`。
请求参数:
| 参数 | 类型 | 用法 |
| --- | --- | --- |
| `topic` | Path string | 群 Topic,例如 `grpORCbFk4mAzk`。 |
| `query` | Query string,可选 | 成员搜索关键字。空或省略表示拉当前筛选条件下的成员页;有值时服务端按群昵称、用户昵称、备注展示名、用户名等可搜索字段匹配。`@` 人、候选人选择器必须优先传 `query`,不要先拉全量再本地筛。 |
| `cursor` | Query string,可选 | 分页游标。第一页省略或传空;下一页必须使用上一次响应的 `next_cursor`,客户端不得自行拼游标。 |
| `limit` | Query int,可选 | 每页数量,默认 `30`,最大 `100`。万人群场景建议普通列表 `30`,搜索/候选页 `30`,不要循环预拉完整群成员。 |
| `mentionable` | Query bool,可选 | `@` 成员选择器传 `true`,服务端只返回当前可被提及的成员,并在响应里返回 `permissions.can_mention_all`;普通成员列表省略或传 `false`。 |
| `role` | Query enum,可选 | 按现有角色筛选:`owner`、`admin`、`member`。例如管理员列表传 `role=admin`。 |
| `eligible_role` | Query enum,可选 | 候选角色视图:`admin` 表示添加管理员候选,`owner` 表示转让群主候选;响应项会带 `eligible`、`ineligible_reason`。 |
| `mute_eligible` | Query bool,可选 | 禁言候选选择器传 `true`;响应项会带 `eligible`、`ineligible_reason`。该参数用于响应阶段计算候选状态,不改变公共成员页缓存维度。 |
响应重点字段:`items[]` 返回 `user_id,display_name,group_nickname,avatar_file_id,role,mentionable,acs,eligible,ineligible_reason`;`next_cursor` 为空表示没有下一页;`permissions.can_mention_all` 只表示当前登录用户是否可以使用 `@所有人`。
典型调用:
| 场景 | 调用方式 | 说明 |
| --- | --- | --- |
| 进入普通群聊 | 不调用成员列表 | 只用群详情、会话上下文和 `member_count`。 |
| 群成员第一页 | `GET /v1/im/groups/{topic}/members?limit=30` | 打开成员列表时拉第一页。 |
| 群成员下一页 | `GET /v1/im/groups/{topic}/members?cursor={next_cursor}&limit=30` | 只使用服务端返回的 `next_cursor`。 |
| 成员搜索 | `GET /v1/im/groups/{topic}/members?query=张&limit=30` | `query` 变化时清空本地结果,从第一页重新拉。 |
| `@` 选择器 | `GET /v1/im/groups/{topic}/members?query=张&mentionable=true&limit=30` | 只展示 `mentionable=true` 的成员;是否展示 `@所有人` 看 `permissions.can_mention_all`。 |
| 管理员列表 | `GET /v1/im/groups/{topic}/members?role=admin&limit=30` | 群设置页展示管理员。 |
| 添加管理员候选 | `GET /v1/im/groups/{topic}/members?eligible_role=admin&query=张&limit=30` | 用 `eligible=false` 和 `ineligible_reason` 禁用不可选项。 |
| 转让群主候选 | `GET /v1/im/groups/{topic}/members?eligible_role=owner&query=张&limit=30` | 只允许选择符合转让条件的成员。 |
| 禁言候选 | `GET /v1/im/groups/{topic}/members?mute_eligible=true&query=张&limit=30` | 管理员选择禁言对象时使用。 |
客户端本地缓存建议按 `tenant_code + my_user_id + topic + query + cursor + limit + role + eligible_role + mentionable + mute_eligible` 建 key,只缓存已加载页,并且只在当前页面生命周期内复用。收到 `group.member.joined`、`group.member.left`、`group.member.updated`、`group.settings.updated` 等群业务事件后,应清理该 `topic` 的本地成员列表、候选人和 `@` 搜索缓存;只发生用户资料/备注变化时,可刷新当前可见页或对应用户资料。
服务端以 `topic + role + eligible_role + query + cursor + limit + mentionable` 作为 Redis 成员页缓存维度,同一页短时间重复请求会命中页缓存。`mute_eligible`、好友备注、当前查看人关系等 viewer-specific 字段不进入公共页缓存,而是在 HTTP 响应阶段按当前登录人实时补齐。成员加入、退出、角色/昵称/状态变化时按 `topic` 清理相关页缓存。该接口不承载在线状态,在线/离线应走 presence 或会话状态逻辑。
### 6.14 群简介.png
<img src="images/群简介.png" alt="群简介" width="360">
@@ -2010,7 +2028,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
**状态:新增。**
- `POST /v1/im/groups/{topic}/qrcodes`,Body `{"expires_in_seconds":86400}`,返回签名 `qr_token,qr_payload,expires_at,group_snapshot`。
- `POST /v1/im/groups/{topic}/qrcodes`,Body `{"expires_in_seconds":86400}`,返回签名 `qr_token,qr_payload,expires_at,group,group_snapshot`;`group` 与 `group_snapshot` 内容一致,前端优先使用 `group`。
- 扫码调用 `POST /v1/im/qrcodes/resolve`,`purpose:"join_group"`;随后 `POST /v1/im/groups/{topic}/join-requests` 并携带 `qr_token`。
- 群解散、二维码过期或管理员撤销后必须失效;二维码不得直接承载内部群权限。
@@ -2157,7 +2175,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
| `POST /v1/im/groups/{topic}/leave` | `{}` | `{"topic":"grp...","status":"left"}` |
| `GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true` | Query only | `{"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","group_nickname":"小张","avatar_file_id":"","role":"member","mentionable":true,"acs":{"want":"JRWP","given":"JRWP","mode":"JRWP"}}],"next_cursor":"","permissions":{"can_mention_all":true}}` |
| `GET /v1/im/groups/{topic}/members/{user_id}/profile` | Path topic/user;`topic` 必须是 `grp...`,不能传 `usr...` | `{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","relationship":"friend","capabilities":{"can_send":true,"can_call":true}}` |
| `POST /v1/im/groups/{topic}/qrcodes` | `{"expires_in_seconds":86400}` | `{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=join_group","expires_at":"2026-08-21T09:00:00Z","group_snapshot":{"topic":"grp...","name":"项目群","member_count":2}}` |
| `POST /v1/im/groups/{topic}/qrcodes` | `{"expires_in_seconds":86400}` | `{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=join_group","expires_at":"2026-08-21T09:00:00Z","group":{"topic":"grp...","name":"项目群","member_count":2},"group_snapshot":{"topic":"grp...","name":"项目群","member_count":2}}` |
| `POST /v1/im/groups/{topic}/invitations` | `{"user_ids":["usr..."],"message":"进群一起讨论"}` | `{"topic":"grp...","items":[{"user_id":"usr...","status":"joined","invitation_id":"ginv_1"}]}` |
| `POST /v1/im/group-invitations/{invitation_id}/accept` 待实现 | `{}` | `{"invitation_id":"ginv_1","status":"joined","topic":"grp..."}` |
| `POST /v1/im/groups/{topic}/join-requests` | `{"message":"申请入群","source":"qr","qr_token":"token..."}` | `{"request_id":"gjr_1","topic":"grp...","status":"pending","created_at":"2026-08-20T09:00:00Z","version":1}` |
@@ -2994,7 +3012,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
**状态:新增。**
- `GET /v1/im/me/privacy` 返回 `add_me_methods,profile_visibility,last_seen_visibility,read_receipts,group_invite_verification,version` 和各字段 `editable`;`group_invite_verification=true` 表示别人邀请我入群时需要走入群审批。
- `GET /v1/im/me/privacy` 返回 `add_me_methods,profile_visibility,last_seen_visibility,read_receipts,friend_request_verification,group_invite_verification,version` 和各字段 `editable`;`friend_request_verification=true` 表示别人加我为好友时需要我确认,`false` 时符合来源规则的好友申请自动通过;`group_invite_verification=true` 表示别人邀请我入群时需要走入群审批。
- `PATCH /v1/im/me/privacy` 只提交变更字段和 `version`;成功返回完整最新配置并向其他设备推送 `privacy.updated`。
- 黑名单入口使用 5.12。服务端在搜索、主页、回执等实际读写路径执行隐私规则,不能只保存开关。
@@ -3102,8 +3120,8 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
| `GET /v1/im/me` | Query only | `{"user_id":"usr...","profile":{"public_account":"usr...","nickname":"张三","avatar_file_id":"","signature":"","version":1},"profile_version":1,"realname":{"status":"unverified"},"youth_mode":{"enabled":false}}` |
| `GET /v1/im/me/profile` | Query only | `{"username":"zhangsan7","avatar_file_id":"","nickname":"张三","birth_date":"1990-01-01","signature":"签名","gender":"male","region":"上海","email":"a@example.com","public_account":"usr...","phone_mask":"136****3333","version":1,"field_rules":{"nickname":{"min_length":1,"max_length":32},"signature":{"max_length":120}}}` |
| `PATCH /v1/im/me/profile` | `{"nickname":"新昵称","signature":"新签名","version":1}` | `{"avatar_file_id":"","nickname":"新昵称","signature":"新签名","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"group_invite_verification":false,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true,"group_invite_verification":true}}` |
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"group_invite_verification":true,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"group_invite_verification":true,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"friend_request_verification":true,"group_invite_verification":false,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true,"friend_request_verification":true,"group_invite_verification":true}}` |
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"friend_request_verification":false,"group_invite_verification":true,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"friend_request_verification":false,"group_invite_verification":true,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/settings/youth-mode` | Query only | `{"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":false},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":false},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/settings/independent-password` | `{"password":"1234","purpose":"youth_mode","action_token":"action_token"}` | `{"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"}` |
| `POST /v1/im/settings/independent-password/verify` | `{"password":"1234","purpose":"close_youth_mode"}` | `{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |
+1
View File
@@ -712,6 +712,7 @@ CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
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),
+10
View File
@@ -423,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 收藏
@@ -0,0 +1,37 @@
-- Add personal privacy switch for requiring approval when others add the user as a friend.
-- Default is on to preserve the current "friend request needs approval" behavior.
-- Run this for every tenant business database. The current test environment uses:
-- default -> tinode
-- system -> meeting_im
SET @im_schema = 'tinode';
SET @im_sql = (
SELECT IF(
COUNT(*) = 0,
CONCAT('ALTER TABLE ', @im_schema, '.im_user_privacy_settings ADD COLUMN friend_request_verification TINYINT NOT NULL DEFAULT 1 AFTER read_receipts'),
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = @im_schema
AND table_name = 'im_user_privacy_settings'
AND column_name = 'friend_request_verification'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_schema = 'meeting_im';
SET @im_sql = (
SELECT IF(
COUNT(*) = 0,
CONCAT('ALTER TABLE ', @im_schema, '.im_user_privacy_settings ADD COLUMN friend_request_verification TINYINT NOT NULL DEFAULT 1 AFTER read_receipts'),
'SELECT 1'
)
FROM information_schema.columns
WHERE table_schema = @im_schema
AND table_name = 'im_user_privacy_settings'
AND column_name = 'friend_request_verification'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
@@ -411,7 +411,7 @@ X-MeetingIM-APIKey: <预置的普通 API Key>
| 好友申请通过 | 申请人、通过人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.accepted","payload":{"user_id":"usr_actor"}}}` | 刷新新朋友列表、好友列表、P2P 会话 context;申请人当前有系统通知,通过人本人当前无系统通知 |
| 好友申请拒绝 | 申请人、拒绝人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.request.rejected","payload":{"user_id":"usr_actor"}}}` | 刷新新朋友列表和 `bootstrap.entry_badges`;申请人当前有系统通知,拒绝人本人当前无系统通知 |
| 删除好友 | 被删除方、操作人本人 | `{"info":{"topic":"usr_target","from":"usr_actor","what":"im","event":"friend.deleted","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表、会话 context,重新判断输入区 `can_send`;被删除方当前有系统通知,操作人本人当前无系统通知 |
| 拉黑/取消拉黑 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.blocked","payload":{"user_id":"usr_actor"}}}` | 刷新黑名单、好友列表和会话 context |
| 拉黑/取消拉黑 | 操作人本人 | 拉黑:`{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.blocked","payload":{"user_id":"usr_actor"}}}`;取消拉黑:`{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"relationship.unblocked","payload":{"user_id":"usr_actor"}}}` | 刷新黑名单、好友列表和会话 context |
| 修改好友备注 | 操作人本人 | `{"info":{"topic":"usr_actor","from":"usr_actor","what":"im","event":"friend.remark.updated","payload":{"user_id":"usr_actor"}}}` | 刷新好友列表和会话头显示名 |
| 消息撤回 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.recalled","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取或本地标记已撤回 |
| 消息编辑 | Topic 在线成员 | `{"info":{"topic":"p2p_or_grp","from":"usr_actor","what":"im","event":"message.edited","payload":{"topic":"p2p_or_grp","seq":12,"version":2}}}` | 按 `topic+seq+version` 重新拉取消息详情或修订记录 |
@@ -1710,7 +1710,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
**状态:新增。**
- 开启拉黑:`PUT /v1/im/blocks/{user_id}`;解除:`DELETE /v1/im/blocks/{user_id}`,均带幂等键。
- 成功返回 `blocked,relationship,updated_at` 并向当前用户其他设备推送 `privacy.block.updated`。拉黑后服务端拦截对方消息和呼叫。
- 成功返回 `blocked,relationship,updated_at` 并向当前用户其他设备推送 `relationship.blocked`;解除拉黑推送 `relationship.unblocked`。事件 `topic` 为操作人自己的 `usr_actor`,`payload.user_id` 为操作人 ID;客户端收到后刷新黑名单、好友列表和会话 context。拉黑后服务端拦截对方消息和呼叫。
- 解除拉黑只恢复黑名单 ACL;若双方已非好友,`relationship` 仍为 `none`,不能自动恢复好友。
### 5.11 删除好友提示.png
@@ -1915,6 +1915,45 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
- 服务端校验成员资格、长度和敏感词,返回 `nickname,version,updated_at` 并推送成员资料变化。
- 该昵称仅作用当前群,不能修改全局用户昵称或其他群资料。
### 6.13.1 群成员列表拉取与缓存策略
**状态:已按分页 + 服务端页缓存实现。**
客户端进入普通群聊会话时不要主动全量拉取群成员列表;群详情中的 `member_count`、当前用户权限和会话上下文足够渲染聊天页。只有进入群成员页、管理员候选页、禁言候选页、转让群主候选页或输入 `@` 搜索成员时,才调用 `GET /v1/im/groups/{topic}/members`。
请求参数:
| 参数 | 类型 | 用法 |
| --- | --- | --- |
| `topic` | Path string | 群 Topic,例如 `grpORCbFk4mAzk`。 |
| `query` | Query string,可选 | 成员搜索关键字。空或省略表示拉当前筛选条件下的成员页;有值时服务端按群昵称、用户昵称、备注展示名、用户名等可搜索字段匹配。`@` 人、候选人选择器必须优先传 `query`,不要先拉全量再本地筛。 |
| `cursor` | Query string,可选 | 分页游标。第一页省略或传空;下一页必须使用上一次响应的 `next_cursor`,客户端不得自行拼游标。 |
| `limit` | Query int,可选 | 每页数量,默认 `30`,最大 `100`。万人群场景建议普通列表 `30`,搜索/候选页 `30`,不要循环预拉完整群成员。 |
| `mentionable` | Query bool,可选 | `@` 成员选择器传 `true`,服务端只返回当前可被提及的成员,并在响应里返回 `permissions.can_mention_all`;普通成员列表省略或传 `false`。 |
| `role` | Query enum,可选 | 按现有角色筛选:`owner`、`admin`、`member`。例如管理员列表传 `role=admin`。 |
| `eligible_role` | Query enum,可选 | 候选角色视图:`admin` 表示添加管理员候选,`owner` 表示转让群主候选;响应项会带 `eligible`、`ineligible_reason`。 |
| `mute_eligible` | Query bool,可选 | 禁言候选选择器传 `true`;响应项会带 `eligible`、`ineligible_reason`。该参数用于响应阶段计算候选状态,不改变公共成员页缓存维度。 |
响应重点字段:`items[]` 返回 `user_id,display_name,group_nickname,avatar_file_id,role,mentionable,acs,eligible,ineligible_reason`;`next_cursor` 为空表示没有下一页;`permissions.can_mention_all` 只表示当前登录用户是否可以使用 `@所有人`。
典型调用:
| 场景 | 调用方式 | 说明 |
| --- | --- | --- |
| 进入普通群聊 | 不调用成员列表 | 只用群详情、会话上下文和 `member_count`。 |
| 群成员第一页 | `GET /v1/im/groups/{topic}/members?limit=30` | 打开成员列表时拉第一页。 |
| 群成员下一页 | `GET /v1/im/groups/{topic}/members?cursor={next_cursor}&limit=30` | 只使用服务端返回的 `next_cursor`。 |
| 成员搜索 | `GET /v1/im/groups/{topic}/members?query=张&limit=30` | `query` 变化时清空本地结果,从第一页重新拉。 |
| `@` 选择器 | `GET /v1/im/groups/{topic}/members?query=张&mentionable=true&limit=30` | 只展示 `mentionable=true` 的成员;是否展示 `@所有人` 看 `permissions.can_mention_all`。 |
| 管理员列表 | `GET /v1/im/groups/{topic}/members?role=admin&limit=30` | 群设置页展示管理员。 |
| 添加管理员候选 | `GET /v1/im/groups/{topic}/members?eligible_role=admin&query=张&limit=30` | 用 `eligible=false` 和 `ineligible_reason` 禁用不可选项。 |
| 转让群主候选 | `GET /v1/im/groups/{topic}/members?eligible_role=owner&query=张&limit=30` | 只允许选择符合转让条件的成员。 |
| 禁言候选 | `GET /v1/im/groups/{topic}/members?mute_eligible=true&query=张&limit=30` | 管理员选择禁言对象时使用。 |
客户端本地缓存建议按 `tenant_code + my_user_id + topic + query + cursor + limit + role + eligible_role + mentionable + mute_eligible` 建 key,只缓存已加载页,并且只在当前页面生命周期内复用。收到 `group.member.joined`、`group.member.left`、`group.member.updated`、`group.settings.updated` 等群业务事件后,应清理该 `topic` 的本地成员列表、候选人和 `@` 搜索缓存;只发生用户资料/备注变化时,可刷新当前可见页或对应用户资料。
服务端以 `topic + role + eligible_role + query + cursor + limit + mentionable` 作为 Redis 成员页缓存维度,同一页短时间重复请求会命中页缓存。`mute_eligible`、好友备注、当前查看人关系等 viewer-specific 字段不进入公共页缓存,而是在 HTTP 响应阶段按当前登录人实时补齐。成员加入、退出、角色/昵称/状态变化时按 `topic` 清理相关页缓存。该接口不承载在线状态,在线/离线应走 presence 或会话状态逻辑。
### 6.14 群简介.png
<img src="images/群简介.png" alt="群简介" width="360">
@@ -1940,7 +1979,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
**状态:新增。**
- `POST /v1/im/groups/{topic}/qrcodes`,Body `{"expires_in_seconds":86400}`,返回签名 `qr_token,qr_payload,expires_at,group_snapshot`。
- `POST /v1/im/groups/{topic}/qrcodes`,Body `{"expires_in_seconds":86400}`,返回签名 `qr_token,qr_payload,expires_at,group,group_snapshot`;`group` 与 `group_snapshot` 内容一致,前端优先使用 `group`。
- 扫码调用 `POST /v1/im/qrcodes/resolve`,`purpose:"join_group"`;随后 `POST /v1/im/groups/{topic}/join-requests` 并携带 `qr_token`。
- 群解散、二维码过期或管理员撤销后必须失效;二维码不得直接承载内部群权限。
@@ -2087,7 +2126,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
| `POST /v1/im/groups/{topic}/leave` | `{}` | `{"topic":"grp...","status":"left"}` |
| `GET /v1/im/groups/{topic}/members?query=&cursor=&mentionable=true` | Query only | `{"topic":"grp...","items":[{"user_id":"usr...","display_name":"张三","avatar_file_id":"","role":"member","mentionable":true,"acs":{"want":"JRWP","given":"JRWP","mode":"JRWP"}}],"next_cursor":"","permissions":{"can_mention_all":true}}` |
| `GET /v1/im/groups/{topic}/members/{user_id}/profile` | Path topic/user;`topic` 必须是 `grp...`,不能传 `usr...` | `{"topic":"grp...","user_id":"usr...","display_name":"张三","group_nickname":"小张","role":"member","relationship":"friend","capabilities":{"can_send":true,"can_call":true}}` |
| `POST /v1/im/groups/{topic}/qrcodes` | `{"expires_in_seconds":86400}` | `{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=join_group","expires_at":"2026-08-21T09:00:00Z","group_snapshot":{"topic":"grp...","name":"项目群","member_count":2}}` |
| `POST /v1/im/groups/{topic}/qrcodes` | `{"expires_in_seconds":86400}` | `{"qr_token":"token...","qr_payload":"im://qr?token=...&purpose=join_group","expires_at":"2026-08-21T09:00:00Z","group":{"topic":"grp...","name":"项目群","member_count":2},"group_snapshot":{"topic":"grp...","name":"项目群","member_count":2}}` |
| `POST /v1/im/groups/{topic}/invitations` | `{"user_ids":["usr..."],"message":"进群一起讨论"}` | `{"topic":"grp...","items":[{"user_id":"usr...","status":"joined","invitation_id":"ginv_1"}]}` |
| `POST /v1/im/group-invitations/{invitation_id}/accept` 待实现 | `{}` | `{"invitation_id":"ginv_1","status":"joined","topic":"grp..."}` |
| `POST /v1/im/groups/{topic}/join-requests` | `{"message":"申请入群","source":"qr","qr_token":"token..."}` | `{"request_id":"gjr_1","topic":"grp...","status":"pending","created_at":"2026-08-20T09:00:00Z","version":1}` |
@@ -2904,7 +2943,7 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
**状态:新增。**
- `GET /v1/im/me/privacy` 返回 `add_me_methods,profile_visibility,last_seen_visibility,read_receipts,group_invite_verification,version` 和各字段 `editable`;`group_invite_verification=true` 表示别人邀请我入群时需要走入群审批。
- `GET /v1/im/me/privacy` 返回 `add_me_methods,profile_visibility,last_seen_visibility,read_receipts,friend_request_verification,group_invite_verification,version` 和各字段 `editable`;`friend_request_verification=true` 表示别人加我为好友时需要我确认,`false` 时符合来源规则的好友申请自动通过;`group_invite_verification=true` 表示别人邀请我入群时需要走入群审批。
- `PATCH /v1/im/me/privacy` 只提交变更字段和 `version`;成功返回完整最新配置并向其他设备推送 `privacy.updated`。
- 黑名单入口使用 5.12。服务端在搜索、主页、回执等实际读写路径执行隐私规则,不能只保存开关。
@@ -3012,8 +3051,8 @@ GET /v1/im/topics/{topic}/message-mutations?since_version={localMutationVersion}
| `GET /v1/im/me` | Query only | `{"user_id":"usr...","profile":{"public_account":"usr...","nickname":"张三","avatar_file_id":"","signature":"","version":1},"profile_version":1,"realname":{"status":"unverified"},"youth_mode":{"enabled":false}}` |
| `GET /v1/im/me/profile` | Query only | `{"avatar_file_id":"","nickname":"张三","birth_date":"1990-01-01","signature":"签名","gender":"male","region":"上海","email":"a@example.com","public_account":"usr...","version":1,"field_rules":{"nickname":{"min_length":1,"max_length":32},"signature":{"max_length":120}}}` |
| `PATCH /v1/im/me/profile` | `{"nickname":"新昵称","signature":"新签名","version":1}` | `{"avatar_file_id":"","nickname":"新昵称","signature":"新签名","version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"group_invite_verification":false,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true,"group_invite_verification":true}}` |
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"group_invite_verification":true,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"group_invite_verification":true,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/me/privacy` | Query only | `{"add_me_methods":{"account":true,"phone":true,"qr":true,"group":true},"profile_visibility":"friends","last_seen_visibility":"friends","read_receipts":true,"friend_request_verification":true,"group_invite_verification":false,"version":1,"editable":{"add_me_methods":true,"profile_visibility":true,"last_seen_visibility":true,"read_receipts":true,"friend_request_verification":true,"group_invite_verification":true}}` |
| `PATCH /v1/im/me/privacy` | `{"add_me_methods":{"phone":false},"read_receipts":false,"friend_request_verification":false,"group_invite_verification":true,"version":1}` | `{"add_me_methods":{"account":true,"phone":false,"qr":true,"group":true},"read_receipts":false,"friend_request_verification":false,"group_invite_verification":true,"version":2,"updated_at":"2026-08-20T09:00:00Z"}` |
| `GET /v1/im/settings/youth-mode` | Query only | `{"enabled":false,"policy_version":1,"restrictions":["night_quiet","content_filter","payment_guard"],"restriction_items":[{"code":"night_quiet","title":"深夜免打扰","description":"夜间减少消息提醒打扰,帮助用户保持健康作息。","enforced":false},{"code":"content_filter","title":"内容过滤","description":"对不适合青少年的内容进行识别和限制展示。","enforced":false},{"code":"payment_guard","title":"支付守护","description":"限制或确认支付、充值等敏感消费行为。","enforced":false}],"password_set":false,"can_enable":false,"updated_at":"2026-08-20T09:00:00Z"}` |
| `POST /v1/im/settings/independent-password` | `{"password":"1234","purpose":"youth_mode","action_token":"action_token"}` | `{"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"}` |
| `POST /v1/im/settings/independent-password/verify` | `{"password":"1234","purpose":"close_youth_mode"}` | `{"action_token":"action_token","action":"close_youth_mode","expires_at":"2026-08-20T09:05:00Z"}` |