fix: fan out receipts and align tenant schema
This commit is contained in:
@@ -0,0 +1,96 @@
|
||||
-- Keep device push token schema aligned across tenant business databases.
|
||||
-- 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_device_push_tokens ADD COLUMN sdk_version VARCHAR(64) NOT NULL DEFAULT '''' AFTER platform'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_device_push_tokens'
|
||||
AND column_name = 'sdk_version'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
CONCAT('ALTER TABLE ', @im_schema, '.im_device_push_tokens ADD COLUMN push_env VARCHAR(32) NOT NULL DEFAULT '''' AFTER sdk_version'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_device_push_tokens'
|
||||
AND column_name = 'push_env'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
CONCAT('ALTER TABLE ', @im_schema, '.im_device_push_tokens ADD COLUMN notification_permission VARCHAR(32) NOT NULL DEFAULT '''' AFTER push_env'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_device_push_tokens'
|
||||
AND column_name = 'notification_permission'
|
||||
);
|
||||
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_device_push_tokens ADD COLUMN sdk_version VARCHAR(64) NOT NULL DEFAULT '''' AFTER platform'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_device_push_tokens'
|
||||
AND column_name = 'sdk_version'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
CONCAT('ALTER TABLE ', @im_schema, '.im_device_push_tokens ADD COLUMN push_env VARCHAR(32) NOT NULL DEFAULT '''' AFTER sdk_version'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_device_push_tokens'
|
||||
AND column_name = 'push_env'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
|
||||
SET @im_sql = (
|
||||
SELECT IF(
|
||||
COUNT(*) = 0,
|
||||
CONCAT('ALTER TABLE ', @im_schema, '.im_device_push_tokens ADD COLUMN notification_permission VARCHAR(32) NOT NULL DEFAULT '''' AFTER push_env'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_device_push_tokens'
|
||||
AND column_name = 'notification_permission'
|
||||
);
|
||||
PREPARE im_stmt FROM @im_sql;
|
||||
EXECUTE im_stmt;
|
||||
DEALLOCATE PREPARE im_stmt;
|
||||
@@ -1,14 +1,34 @@
|
||||
-- Add personal privacy switch for requiring approval when others invite the user into a group.
|
||||
-- Default is off to preserve existing 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,
|
||||
'ALTER TABLE tinode.im_user_privacy_settings ADD COLUMN group_invite_verification TINYINT NOT NULL DEFAULT 0 AFTER read_receipts',
|
||||
CONCAT('ALTER TABLE ', @im_schema, '.im_user_privacy_settings ADD COLUMN group_invite_verification TINYINT NOT NULL DEFAULT 0 AFTER read_receipts'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = 'tinode'
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_user_privacy_settings'
|
||||
AND column_name = 'group_invite_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 group_invite_verification TINYINT NOT NULL DEFAULT 0 AFTER read_receipts'),
|
||||
'SELECT 1'
|
||||
)
|
||||
FROM information_schema.columns
|
||||
WHERE table_schema = @im_schema
|
||||
AND table_name = 'im_user_privacy_settings'
|
||||
AND column_name = 'group_invite_verification'
|
||||
);
|
||||
|
||||
@@ -0,0 +1,125 @@
|
||||
# IM system 租户接口冒烟记录 - 2026-09-16
|
||||
|
||||
## 环境
|
||||
|
||||
- Base URL: `https://api-test.118im.com`
|
||||
- Tenant Code: `system`
|
||||
- API Key: 新测试环境 Key,接口解析返回的 WebSocket 地址可用
|
||||
- 测试账号: 本次临时注册 `s832453991` / `17832453991`
|
||||
- 测试时间: `2026-09-16 11:40 CST`
|
||||
|
||||
## 结论
|
||||
|
||||
`system` 租户登录、注册、WebSocket 登录和大部分登录后接口可用;本轮确认 1 个确定后端问题:
|
||||
|
||||
`GET /v1/im/me/privacy` 在 `system` 租户下返回 `500 INTERNAL_ERROR`。该问题已在 `2026-09-16 12:27 CST` 通过补齐 `meeting_im.im_user_privacy_settings.group_invite_verification` 字段修复,复测返回 200。
|
||||
|
||||
初步根因是 `system` 租户实际业务库 `meeting_im` 的 `im_user_privacy_settings` 表缺少 `group_invite_verification` 字段,而当前代码查询和更新都会使用该字段。
|
||||
|
||||
## 确认通过
|
||||
|
||||
| 接口 | 结果 |
|
||||
| --- | --- |
|
||||
| `POST /v1/im/tenants/resolve` | 200,返回 `tenant.code=system` |
|
||||
| `POST /v1/im/auth/verification-codes` | 200 |
|
||||
| `POST /v1/im/auth/verification-codes/{challenge_id}/verify` | 200,固定验证码 `123456` 可用 |
|
||||
| WebSocket `hi` | 201 |
|
||||
| WebSocket `acc user:new` | 200,新账号创建成功 |
|
||||
| WebSocket `login scheme=token` | 200 |
|
||||
| WebSocket `sub me` | 200 |
|
||||
| `GET /v1/im/bootstrap` | 200 |
|
||||
| `GET /v1/im/me` | 200 |
|
||||
| `GET /v1/im/me/profile` | 200 |
|
||||
| `GET /v1/im/security/realname` | 200 |
|
||||
| `GET /v1/im/workbench` | 200,返回 `enterprise.tenant_code=system` |
|
||||
| `GET /v1/im/organization/departments` | 200 |
|
||||
| `GET /v1/im/organization/members?limit=20` | 200 |
|
||||
| `GET /v1/im/organization/search?q=1&limit=20` | 200 |
|
||||
| `GET /v1/im/attendance/config` | 200 |
|
||||
| `GET /v1/im/attendance/records?limit=20` | 200 |
|
||||
| `GET /v1/im/meeting-notes?limit=20` | 200 |
|
||||
| `GET /v1/im/meetings?limit=20` | 200 |
|
||||
| `GET /v1/im/reports/abuse/reasons?target_type=user` | 200 |
|
||||
| `GET /v1/im/reports/abuse/reasons?target_type=group` | 200 |
|
||||
| `GET /v1/im/reports/daily/statistics?date=2026-09-16` | 200 |
|
||||
| `GET /v1/im/reports/daily?limit=20` | 200 |
|
||||
| `GET /v1/im/reports/monthly/statistics?month=2026-09` | 200 |
|
||||
| `GET /v1/im/reports/monthly?limit=20` | 200 |
|
||||
| `GET /v1/im/executive-mailbox/config` | 200 |
|
||||
| `GET /v1/im/feedback/config` | 200 |
|
||||
| `GET /v1/im/app/releases/latest?platform=ios¤t_version=1.0.0&channel=stable` | 200 |
|
||||
| `GET /v1/im/search/conversations?q=&limit=20` | 200 |
|
||||
| `GET /v1/im/forward-targets?q=&limit=20` | 200 |
|
||||
| `GET /v1/im/system-notifications?limit=20` | 200 |
|
||||
| `GET /v1/im/favorites?limit=20` | 200 |
|
||||
| `GET /v1/im/groups?limit=20` | 200 |
|
||||
| `GET /v1/im/calls?limit=20` | 200 |
|
||||
|
||||
## 问题记录
|
||||
|
||||
### P0: `GET /v1/im/me/privacy` 返回 500
|
||||
|
||||
请求:
|
||||
|
||||
```http
|
||||
GET /v1/im/me/privacy
|
||||
X-Tenant-Code: system
|
||||
X-IM-Schema: 1
|
||||
Authorization: Bearer <system 用户 token>
|
||||
```
|
||||
|
||||
响应:
|
||||
|
||||
```json
|
||||
{
|
||||
"code": 90000,
|
||||
"reason": "INTERNAL_ERROR",
|
||||
"message": "internal error"
|
||||
}
|
||||
```
|
||||
|
||||
数据库检查:
|
||||
|
||||
- `hotgo.im_tenant` 中 `system` 租户:`db_name=meeting_im`,`db_dsn=''`。
|
||||
- `meeting_im.im_user_privacy_settings` 当前字段包含:
|
||||
- `id`
|
||||
- `user_id`
|
||||
- `add_me_methods`
|
||||
- `profile_visibility`
|
||||
- `last_seen_visibility`
|
||||
- `read_receipts`
|
||||
- `version`
|
||||
- `created_at`
|
||||
- `updated_at`
|
||||
- `tinode.im_user_privacy_settings` 比 `meeting_im` 多一个字段:
|
||||
- `group_invite_verification`
|
||||
|
||||
代码使用点:
|
||||
|
||||
- `/Users/youshunlai/go_workspace/im_server/server/db/mysql/tenant_adapter.go:6613`
|
||||
- `/Users/youshunlai/go_workspace/im_server/server/db/mysql/tenant_adapter.go:7194`
|
||||
|
||||
判断:
|
||||
|
||||
`system` 租户库 `meeting_im` 未执行最新隐私设置表结构迁移。当前代码查询 `group_invite_verification` 字段时会在该库报 SQL 错误,最终 HTTP 统一包装成 `500 INTERNAL_ERROR`。
|
||||
|
||||
处理结果:
|
||||
|
||||
- 测试库 `mysql.118im.com:30306/tinode` 已有该字段。
|
||||
- 测试库 `mysql.118im.com:30306/meeting_im` 已补字段。
|
||||
- 本地库 `127.0.0.1:3306/tinode` 已补字段。
|
||||
- 本地库 `127.0.0.1:3306/meeting_im` 已补字段。
|
||||
- 复测 `GET /v1/im/me/privacy` 返回 200,响应包含 `group_invite_verification:false`。
|
||||
|
||||
## 误测排除
|
||||
|
||||
以下接口第一次测试时返回 400/405,是因为请求参数或方法不完整,不属于本轮确认问题:
|
||||
|
||||
- `GET /v1/im/reports/abuse/reasons`:需要 `target_type=user|group`。
|
||||
- `GET /v1/im/reports/daily/statistics`:需要 `date=YYYY-MM-DD`。
|
||||
- `GET /v1/im/reports/monthly/statistics`:需要 `month=YYYY-MM`。
|
||||
- `GET /v1/im/reports/daily/reminders`:实现为 `POST`,GET 返回 405 属正常方法限制。
|
||||
|
||||
## 额外注意
|
||||
|
||||
旧 API Key 访问 `system` 或 `default` 都返回 `403 API_KEY_REQUIRED`;新测试环境应使用租户解析接口返回的新 Key 和 WebSocket 地址。
|
||||
@@ -300,7 +300,7 @@ func realtimeMessageDedupKey(target string, msg *ServerComMessage) string {
|
||||
return "data:" + topic + ":" + strconv.FormatInt(msg.Data.SeqId, 10)
|
||||
}
|
||||
if msg.Info != nil {
|
||||
return "info:" + target + ":" + msg.Info.Event + ":" + strconv.FormatInt(msg.Info.SeqId, 10)
|
||||
return "info:" + target + ":" + msg.Info.What + ":" + msg.Info.From + ":" + msg.Info.Event + ":" + strconv.FormatInt(msg.Info.SeqId, 10)
|
||||
}
|
||||
if msg.Pres != nil {
|
||||
return "pres:" + target + ":" + msg.Pres.What + ":" + strconv.FormatInt(msg.Pres.SeqId, 10)
|
||||
|
||||
@@ -149,6 +149,75 @@ func TestRealtimeSessionControlStaleEvictDropped(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealtimeP2PTopicFallbackRewritesVisibleInfoTopic(t *testing.T) {
|
||||
oldStore := globals.sessionStore
|
||||
oldHub := globals.hub
|
||||
defer func() {
|
||||
globals.sessionStore = oldStore
|
||||
globals.hub = oldHub
|
||||
}()
|
||||
|
||||
tenantID := types.TenantID(1)
|
||||
uid1 := types.Uid(1001)
|
||||
uid2 := types.Uid(1002)
|
||||
topic := uid1.P2PName(uid2)
|
||||
if topic == "" {
|
||||
t.Fatal("failed to create p2p topic")
|
||||
}
|
||||
globals.hub = nil
|
||||
globals.sessionStore = realtimeTestSessionStore(
|
||||
realtimeTestSession("sid-u1", tenantID, uid1, uid2.UserId()),
|
||||
realtimeTestSession("sid-u2", tenantID, uid2, uid1.UserId()),
|
||||
)
|
||||
msg := &ServerComMessage{
|
||||
Info: &MsgServerInfo{Topic: topic, From: uid1.UserId(), What: "read", SeqId: 13},
|
||||
}
|
||||
raw, err := json.Marshal(msg)
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
|
||||
deliverRealtimeFanoutEnvelope(&store.RealtimeFanoutEnvelope{
|
||||
TenantID: tenantID,
|
||||
Kind: realtimeKindMessage,
|
||||
TargetType: realtimeTargetTopic,
|
||||
Target: topic,
|
||||
Message: raw,
|
||||
})
|
||||
|
||||
expectTopic := map[string]string{"sid-u1": uid2.UserId(), "sid-u2": uid1.UserId()}
|
||||
for sid, visibleTopic := range expectTopic {
|
||||
sess := globals.sessionStore.Get(sid)
|
||||
select {
|
||||
case got := <-sess.send:
|
||||
info := got.(*ServerComMessage).Info
|
||||
if info.Topic != visibleTopic {
|
||||
t.Fatalf("%s got info topic %q, want %q", sid, info.Topic, visibleTopic)
|
||||
}
|
||||
if info.What != "read" || info.SeqId != 13 || info.From != uid1.UserId() {
|
||||
t.Fatalf("%s got unexpected info %#v", sid, info)
|
||||
}
|
||||
default:
|
||||
t.Fatalf("expected p2p info delivery to %s", sid)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
func TestRealtimeInfoDedupKeySeparatesReadRecv(t *testing.T) {
|
||||
read := realtimeMessageDedupKey("p2pABC", &ServerComMessage{
|
||||
Info: &MsgServerInfo{From: "usrA", What: "read", SeqId: 12},
|
||||
})
|
||||
recv := realtimeMessageDedupKey("p2pABC", &ServerComMessage{
|
||||
Info: &MsgServerInfo{From: "usrA", What: "recv", SeqId: 12},
|
||||
})
|
||||
if read == recv {
|
||||
t.Fatalf("read and recv info dedup keys must differ: %q", read)
|
||||
}
|
||||
if read != "info:p2pABC:read:usrA::12" {
|
||||
t.Fatalf("unexpected read dedup key %q", read)
|
||||
}
|
||||
}
|
||||
|
||||
func realtimeTestSessionStore(sessions ...*Session) *SessionStore {
|
||||
ss := NewSessionStore(time.Minute)
|
||||
for _, sess := range sessions {
|
||||
|
||||
@@ -1852,6 +1852,7 @@ func (t *Topic) handleNoteBroadcast(msg *ClientComMessage) {
|
||||
}
|
||||
|
||||
t.broadcastToSessions(info)
|
||||
publishRealtimeTopicMessage(t.name, info)
|
||||
}
|
||||
|
||||
// handlePresence fans out {pres} messages to recipients in topic.
|
||||
|
||||
Reference in New Issue
Block a user