fix im server shared database config

This commit is contained in:
richjohnrambo
2026-09-22 16:45:24 +08:00
parent b8c93e8d6a
commit e4416da04a
11 changed files with 114 additions and 140 deletions
+11 -1
View File
@@ -143,7 +143,7 @@ tinode 移除
全局禁止加好友 比如下班时间到了 不能加 从群成员去看 不能显示个人信息 个人名片也不能看 二维码也不能加 全局禁止加好友 比如下班时间到了 不能加 从群成员去看 不能显示个人信息 个人名片也不能看 二维码也不能加
阿里云的oss放一个域名清单 要加密 阿里云的oss放一个域名清单 要加密 apikey动态更换
设备类型 ip 全部采集入库 设备类型 ip 全部采集入库
离线时间 和上线时间的采集 离线时间 和上线时间的采集
检查一下多处存同一个信息的。尤其是用户的 检查一下多处存同一个信息的。尤其是用户的
@@ -201,4 +201,14 @@ Bundle ID:com.ysl.IM
KEY ID:J4ZUVHW59G KEY ID:J4ZUVHW59G
Team ID:2DN6KF37CH Team ID:2DN6KF37CH
单号登录 多设备顶号 已经实现
多标签登录的消息到达率
建一个千人大群
telegram报警功能
加上ai翻译的功能 ai笔记
后端要做一下 redis 的 sentinel适配, 不然重新选举之后可能会写错节点报错
https://api.118im.com 简单弄一个官网,https://api.118im.com/legal/privacy-policy需要把名字改对,TestFlight的beta版需要审核,这两个可能会是卡点  
+4 -2
View File
@@ -51,7 +51,7 @@ ENV SAMPLE_DATA=$SAMPLE_DATA
ENV DEFAULT_COUNTRY_CODE=US ENV DEFAULT_COUNTRY_CODE=US
# The MySQL DSN connection. # 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_ADDR=
ENV REDIS_USERNAME= ENV REDIS_USERNAME=
ENV REDIS_PASSWORD= ENV REDIS_PASSWORD=
@@ -97,10 +97,12 @@ ENV KAFKA_TOPIC_FILE_SECURITY=im.file.security
ENV KAFKA_TOPIC_CACHE_INVALIDATE=im.cache.invalidate ENV KAFKA_TOPIC_CACHE_INVALIDATE=im.cache.invalidate
ENV KAFKA_TOPIC_AUDIT_EVENT=im.audit.event ENV KAFKA_TOPIC_AUDIT_EVENT=im.audit.event
ENV KAFKA_TOPIC_DEAD_LETTER=im.dead.letter ENV KAFKA_TOPIC_DEAD_LETTER=im.dead.letter
ENV INTERNAL_API_ENABLED=false ENV INTERNAL_API_ENABLED=true
ENV INTERNAL_API_TIME_WINDOW_SECONDS=300 ENV INTERNAL_API_TIME_WINDOW_SECONDS=300
ENV INTERNAL_API_HOTGO_SERVICE_NAME=hotgo-admin ENV INTERNAL_API_HOTGO_SERVICE_NAME=hotgo-admin
ENV INTERNAL_API_HOTGO_SECRET= 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_ENABLED=false
ENV ADMIN_INTERNAL_API_BASE_URL= ENV ADMIN_INTERNAL_API_BASE_URL=
ENV ADMIN_INTERNAL_API_SERVICE_NAME=im-server ENV ADMIN_INTERNAL_API_SERVICE_NAME=im-server
+4 -2
View File
@@ -77,10 +77,12 @@ ENV KAFKA_TOPIC_FILE_SECURITY=im.file.security
ENV KAFKA_TOPIC_CACHE_INVALIDATE=im.cache.invalidate ENV KAFKA_TOPIC_CACHE_INVALIDATE=im.cache.invalidate
ENV KAFKA_TOPIC_AUDIT_EVENT=im.audit.event ENV KAFKA_TOPIC_AUDIT_EVENT=im.audit.event
ENV KAFKA_TOPIC_DEAD_LETTER=im.dead.letter ENV KAFKA_TOPIC_DEAD_LETTER=im.dead.letter
ENV INTERNAL_API_ENABLED=false ENV INTERNAL_API_ENABLED=true
ENV INTERNAL_API_TIME_WINDOW_SECONDS=300 ENV INTERNAL_API_TIME_WINDOW_SECONDS=300
ENV INTERNAL_API_HOTGO_SERVICE_NAME=hotgo-admin ENV INTERNAL_API_HOTGO_SERVICE_NAME=hotgo-admin
ENV INTERNAL_API_HOTGO_SECRET= 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_ENABLED=false
ENV ADMIN_INTERNAL_API_BASE_URL= ENV ADMIN_INTERNAL_API_BASE_URL=
ENV ADMIN_INTERNAL_API_SERVICE_NAME=im-server ENV ADMIN_INTERNAL_API_SERVICE_NAME=im-server
@@ -88,7 +90,7 @@ ENV ADMIN_INTERNAL_API_SECRET=
ENV ADMIN_INTERNAL_API_TIMEOUT_MS=3000 ENV ADMIN_INTERNAL_API_TIMEOUT_MS=3000
# Tenant registry lives in the hotgo database (shared with im_manager); tenant # 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 TENANT_REGISTRY_DB=hotgo
ENV MEDIA_HANDLER=sys_config ENV MEDIA_HANDLER=sys_config
+9 -5
View File
@@ -77,7 +77,7 @@
"use_adapter": "$STORE_USE_ADAPTER", "use_adapter": "$STORE_USE_ADAPTER",
"adapters": { "adapters": {
"mysql": { "mysql": {
"database": "tinode", "database": "meeting_im",
"dsn": "$MYSQL_DSN", "dsn": "$MYSQL_DSN",
"max_open_conns": 64, "max_open_conns": 64,
"max_idle_conns": 64, "max_idle_conns": 64,
@@ -87,7 +87,7 @@
"tenant_db_routing": true "tenant_db_routing": true
}, },
"postgres": { "postgres": {
"database": "tinode", "database": "meeting_im",
"dsn": "$POSTGRES_DSN" "dsn": "$POSTGRES_DSN"
}, },
"rethinkdb": { "rethinkdb": {
@@ -154,12 +154,16 @@
} }
}, },
"internal_api": { "internal_api": {
"enabled": true, "enabled": $INTERNAL_API_ENABLED,
"time_window_seconds": 300, "time_window_seconds": $INTERNAL_API_TIME_WINDOW_SECONDS,
"services": { "services": {
"hotgo-admin": { "$INTERNAL_API_HOTGO_SERVICE_NAME": {
"secret": "$INTERNAL_API_HOTGO_SECRET", "secret": "$INTERNAL_API_HOTGO_SECRET",
"allow": ["admin_query"] "allow": ["admin_query"]
},
"$INTERNAL_API_CULTIVATE_SERVICE_NAME": {
"secret": "$INTERNAL_API_CULTIVATE_SECRET",
"allow": ["cultivate"]
} }
} }
}, },
@@ -1,6 +1,31 @@
# IM 极光推送前后端对接文档 # IM 极光推送前后端对接文档
更新时间:2026-09-04 更新时间:2026-09-IM API → PUT https://api.118im.com/v1/im/devices/2386C7EB-FC30-4849-9FC8-7F2B50229F02/push-token [req=ab961f26-e3a9-4f5f-b0f6-2813b8af3597]
请求体 {
"sdk_version": "6.2.0",
"push_env": "test",
"app_version": "1.0",
"token": "1517bfd3f69c6928135",
"platform": "ios",
"notification_permission": "granted",
"provider": "jpush"
}
IM API ← 成功 PUT https://api.118im.com/v1/im/devices/2386C7EB-FC30-4849-9FC8-7F2B50229F02/push-token [req=ab961f26-e3a9-4f5f-b0f6-2813b8af3597, status=200]
响应体 {
"code": 0,
"data": {
"app_version": "1.0",
"device_id": "2386C7EB-FC30-4849-9FC8-7F2B50229F02",
"notification_permission": "granted",
"platform": "ios",
"provider": "jpush",
"push_env": "test",
"sdk_version": "6.2.0",
"updated_at": "2026-09-14T09:49:19.139Z"
},
"request_id": "ab961f26-e3a9-4f5f-b0f6-2813b8af3597",
"server_time": "2026-09-14T09:49:19.167Z"
}04
## 1. 对接边界 ## 1. 对接边界
@@ -24,7 +49,11 @@
- iOS:配置 APNs 证书或 Token Authentication,申请通知权限。 - iOS:配置 APNs 证书或 Token Authentication,申请通知权限。
- HarmonyOS:按鸿蒙 SDK 文档初始化并配置通知分类。 - HarmonyOS:按鸿蒙 SDK 文档初始化并配置通知分类。
客户端启动后取得 `registration_id`,登录 IM 后把 alias 绑定为当前 `user_id` 的极光安全别名。同一用户多设备允许绑定同一个 alias。 客户端启动后取得 `registration_id`,登录 IM 后调用 IM 后端设备接口上报 Push Token。当前测试环境服务端配置 `jpush_use_alias=0`,使用 `registration_id` 投递,不依赖 alias。
`device_id` 必须稳定,同一台设备不要每次启动变化。切换账号、切换租户、清登录态后必须重新上报 Push Token;客户端本地的上报去重缓存必须按 `tenant_code + user_id + device_id + registration_id` 隔离,避免设备继续绑定在旧账号上。
如果后续切回 alias 模式,客户端再把 alias 绑定为当前 `user_id` 的极光安全别名。同一用户多设备允许绑定同一个 alias。
极光 alias 不支持中横线 `-` 和下划线 `_`,而 IM `user_id` 可能包含这两个字符。客户端和后端统一使用以下映射: 极光 alias 不支持中横线 `-` 和下划线 `_`,而 IM `user_id` 可能包含这两个字符。客户端和后端统一使用以下映射:
@@ -124,12 +153,14 @@ IM Server 从后台配置读取 `im_jpush` 分组:
"jpush_apns_production": "false", "jpush_apns_production": "false",
"jpush_default_ttl_seconds": "86400", "jpush_default_ttl_seconds": "86400",
"jpush_call_ttl_seconds": "60", "jpush_call_ttl_seconds": "60",
"jpush_use_alias": "true", "jpush_use_alias": "false",
"jpush_test_model": "false", "jpush_test_model": "false",
"jpush_classification": "1" "jpush_classification": "1"
} }
``` ```
测试环境当前使用 `registration_id` 模式,`jpush_use_alias=false`。服务端从 `im_device_push_tokens` 按目标用户查询 `provider=jpush` 的 token,并调用极光 `audience.registration_id`。如改为 alias 模式,需要客户端 SDK alias 绑定回调成功,并保持 alias 映射规则一致。
### 3.2 设备记录 ### 3.2 设备记录
设备表建议至少支持: 设备表建议至少支持:
@@ -185,22 +216,11 @@ Content-Type: application/json
```json ```json
{ {
"platform": "all", "platform": "ios",
"audience": { "audience": {
"alias": ["usr_target"] "registration_id": ["registration_id_from_jpush"]
}, },
"notification": { "notification": {
"alert": "你收到一条新消息",
"android": {
"title": "MeetingIM",
"alert": "你收到一条新消息",
"extras": {
"type": "im_message",
"tenant_code": "default",
"topic": "p2p...",
"seq": "128"
}
},
"ios": { "ios": {
"alert": { "alert": {
"title": "MeetingIM", "title": "MeetingIM",
@@ -228,25 +248,11 @@ Content-Type: application/json
```json ```json
{ {
"platform": "all", "platform": "ios",
"audience": { "audience": {
"alias": ["usr_callee"] "registration_id": ["registration_id_from_jpush"]
}, },
"notification": { "notification": {
"alert": "收到一通视频通话",
"android": {
"title": "视频通话",
"alert": "张三邀请你视频通话",
"extras": {
"type": "call_invite",
"tenant_code": "default",
"topic": "p2p...",
"seq": "30",
"call_id": "call_01K...",
"call_type": "video",
"caller_id": "usr_caller"
}
},
"ios": { "ios": {
"alert": { "alert": {
"title": "视频通话", "title": "视频通话",
@@ -314,8 +320,8 @@ Content-Type: application/json
| 步骤 | 操作 | 验证 | | 步骤 | 操作 | 验证 |
| --- | --- | --- | | --- | --- | --- |
| 1 | 客户端初始化 JPush SDK | 能拿到 `registration_id`。 | | 1 | 客户端初始化 JPush SDK | 能拿到 `registration_id`。 |
| 2 | 登录 IM 并绑定 alias=`user_id.replace("-", ".").replace("_", "|")` | 极光控制台能查到别名或设备。 | | 2 | 登录 IM 后上报 `registration_id` | `im_device_push_tokens` 中 `device_id/token/user_id` 与当前登录用户一致。 |
| 3 | 调用设备上报接口 | 服务端设备记录 provider=`jpush`。 | | 3 | 调用设备上报接口 | 服务端设备记录 provider=`jpush`,并更新到当前登录用户。 |
| 4 | 目标用户退出或断网 | IM 判断为离线或不可 WS 投递。 | | 4 | 目标用户退出或断网 | IM 判断为离线或不可 WS 投递。 |
| 5 | 发送 P2P 消息 | 离线用户收到 Push。 | | 5 | 发送 P2P 消息 | 离线用户收到 Push。 |
| 6 | 点击 Push | App 打开对应会话并补齐 `seq`。 | | 6 | 点击 Push | App 打开对应会话并补齐 `seq`。 |
@@ -323,3 +329,4 @@ Content-Type: application/json
| 8 | 通话取消后点击旧 Push | 客户端查询后显示已取消或未接,不进入通话中。 | | 8 | 通话取消后点击旧 Push | 客户端查询后显示已取消或未接,不进入通话中。 |
| 9 | 关闭会话免打扰 | 普通消息不发 Push,@提醒按策略执行。 | | 9 | 关闭会话免打扰 | 普通消息不发 Push,@提醒按策略执行。 |
| 10 | 极光返回无效 token | 后端禁用设备 token,重新上报后恢复。 | | 10 | 极光返回无效 token | 后端禁用设备 token,重新上报后恢复。 |
| 11 | iOS APNs 环境 | 测试环境 `apns_production=false` 且客户端为开发 APNs;生产包需同步切换为 true。 |
-4
View File
@@ -51,8 +51,6 @@ type adminInternalTenantDTO struct {
Code string `json:"code"` Code string `json:"code"`
Name string `json:"name"` Name string `json:"name"`
TenantDesc *string `json:"tenant_desc,omitempty"` TenantDesc *string `json:"tenant_desc,omitempty"`
DBName string `json:"db_name"`
DBDSN string `json:"db_dsn"`
State types.TenantState `json:"state"` State types.TenantState `json:"state"`
CreatedAt time.Time `json:"created_at"` CreatedAt time.Time `json:"created_at"`
CreatedBy int64 `json:"created_by"` CreatedBy int64 `json:"created_by"`
@@ -422,8 +420,6 @@ func (t adminInternalTenantDTO) toStoreTenant() *types.Tenant {
Code: strings.TrimSpace(t.Code), Code: strings.TrimSpace(t.Code),
Name: t.Name, Name: t.Name,
TenantDesc: t.TenantDesc, TenantDesc: t.TenantDesc,
DBName: t.DBName,
DBDSN: t.DBDSN,
State: t.State, State: t.State,
CreatedAt: t.CreatedAt, CreatedAt: t.CreatedAt,
CreatedBy: t.CreatedBy, CreatedBy: t.CreatedBy,
+37 -81
View File
@@ -56,8 +56,8 @@ const (
adpVersion = 123 adpVersion = 123
adapterName = "mysql" adapterName = "mysql"
defaultDSN = "root:@tcp(localhost:3306)/tinode?parseTime=true" defaultDSN = "root:@tcp(localhost:3306)/meeting_im?parseTime=true"
defaultDatabase = "tinode" defaultDatabase = "meeting_im"
defaultMaxResults = 1024 defaultMaxResults = 1024
// This is capped by the Session's send queue limit (128). // This is capped by the Session's send queue limit (128).
@@ -421,17 +421,11 @@ func (a *adapter) TenantGetByCode(code string) (*t.Tenant, error) {
db = a.registryDB db = a.registryDB
} }
err := db.GetContext(ctx, &tenant, err := db.GetContext(ctx, &tenant,
`SELECT id, code, name, tenant_desc, IFNULL(db_name,'') AS db_name, IFNULL(db_dsn,'') AS db_dsn, `SELECT id, code, name, tenant_desc, state, created_at, created_by, updated_at, updated_by
state, created_at, created_by, updated_at, updated_by
FROM im_tenant WHERE code=? LIMIT 1`, code) FROM im_tenant WHERE code=? LIMIT 1`, code)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, t.ErrNotFound return nil, t.ErrNotFound
} }
if isUnknownColumn(err) {
err = db.GetContext(ctx, &tenant,
`SELECT id, code, name, tenant_desc, state, created_at, created_by, updated_at, updated_by
FROM im_tenant WHERE code=? LIMIT 1`, code)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -460,14 +454,8 @@ func (a *adapter) TenantListActive(limit int) ([]t.Tenant, error) {
} }
var tenants []t.Tenant var tenants []t.Tenant
err := db.SelectContext(ctx, &tenants, err := db.SelectContext(ctx, &tenants,
`SELECT id, code, name, tenant_desc, IFNULL(db_name,'') AS db_name, IFNULL(db_dsn,'') AS db_dsn, `SELECT id, code, name, tenant_desc, state, created_at, created_by, updated_at, updated_by
state, created_at, created_by, updated_at, updated_by
FROM im_tenant WHERE state=? ORDER BY id ASC LIMIT ?`, t.TenantStateActive, limit) FROM im_tenant WHERE state=? ORDER BY id ASC LIMIT ?`, t.TenantStateActive, limit)
if isUnknownColumn(err) {
err = db.SelectContext(ctx, &tenants,
`SELECT id, code, name, tenant_desc, state, created_at, created_by, updated_at, updated_by
FROM im_tenant WHERE state=? ORDER BY id ASC LIMIT ?`, t.TenantStateActive, limit)
}
return tenants, err return tenants, err
} }
@@ -518,8 +506,8 @@ func (a *adapter) TenantAdapterFor(tenantID t.TenantID) (dbadapter.TenantBusines
baseConfig: a.baseConfig, baseConfig: a.baseConfig,
tenantDBRouting: false, tenantDBRouting: false,
tenantID: tenantID, tenantID: tenantID,
// A dedicated database is still tenant-owned. Keep tenant predicates and // Tenant adapters share the same database. Keep tenant predicates and
// explicit tenant_id writes enabled even though databases are not shared. // explicit tenant_id writes enabled for logical isolation.
tenantScopedDB: false, tenantScopedDB: false,
} }
a.tenantMu.Lock() a.tenantMu.Lock()
@@ -543,17 +531,11 @@ func (a *adapter) tenantByID(tenantID t.TenantID) (*t.Tenant, error) {
} }
var tenant t.Tenant var tenant t.Tenant
err := db.GetContext(ctx, &tenant, err := db.GetContext(ctx, &tenant,
`SELECT id, code, name, tenant_desc, IFNULL(db_name,'') AS db_name, IFNULL(db_dsn,'') AS db_dsn, `SELECT id, code, name, tenant_desc, state, created_at, created_by, updated_at, updated_by
state, created_at, created_by, updated_at, updated_by
FROM im_tenant WHERE id=? LIMIT 1`, tenantID) FROM im_tenant WHERE id=? LIMIT 1`, tenantID)
if err == sql.ErrNoRows { if err == sql.ErrNoRows {
return nil, t.ErrNotFound return nil, t.ErrNotFound
} }
if isUnknownColumn(err) {
err = db.GetContext(ctx, &tenant,
`SELECT id, code, name, tenant_desc, state, created_at, created_by, updated_at, updated_by
FROM im_tenant WHERE id=? LIMIT 1`, tenantID)
}
if err != nil { if err != nil {
return nil, err return nil, err
} }
@@ -563,7 +545,7 @@ func (a *adapter) tenantByID(tenantID t.TenantID) (*t.Tenant, error) {
return &tenant, nil return &tenant, nil
} }
func createTenantRegistry(tx *sql.Tx, registryDB, businessDB string) error { func createTenantRegistry(tx *sql.Tx, registryDB, _ string) error {
if _, err := tx.Exec("CREATE DATABASE IF NOT EXISTS " + quoteIdent(registryDB) + " CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"); err != nil { if _, err := tx.Exec("CREATE DATABASE IF NOT EXISTS " + quoteIdent(registryDB) + " CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci"); err != nil {
return err return err
} }
@@ -572,52 +554,40 @@ func createTenantRegistry(tx *sql.Tx, registryDB, businessDB string) error {
} }
if _, err := tx.Exec( if _, err := tx.Exec(
`CREATE TABLE IF NOT EXISTS im_tenant( `CREATE TABLE IF NOT EXISTS im_tenant(
id BIGINT NOT NULL, id BIGINT NOT NULL,
code VARCHAR(64) NOT NULL, code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL, name VARCHAR(128) NOT NULL,
tenant_desc VARCHAR(256), tenant_desc VARCHAR(256),
db_name VARCHAR(128) NOT NULL DEFAULT '', max_users BIGINT UNSIGNED NOT NULL DEFAULT 0,
db_dsn VARCHAR(1024) NOT NULL DEFAULT '', max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0,
state SMALLINT NOT NULL, max_group_members INT UNSIGNED NOT NULL DEFAULT 0,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), realname_review_policy TINYINT(1) NOT NULL DEFAULT 0,
created_by BIGINT NOT NULL, realname_provider_mode VARCHAR(32) NOT NULL DEFAULT 'id_card_2factor',
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), realname_required TINYINT(1) NOT NULL DEFAULT 0,
updated_by BIGINT NOT NULL, realname_allow_skip TINYINT(1) NOT NULL DEFAULT 1,
realname_remind_after VARCHAR(32) NOT NULL DEFAULT '24h',
aliyun_moderation_enabled TINYINT(1) NOT NULL DEFAULT 0,
local_moderation_enabled TINYINT(1) NOT NULL DEFAULT 0,
message_encryption_enabled TINYINT(1) NOT NULL DEFAULT 0,
default_policy_group_id BIGINT,
security_policy_version INT UNSIGNED NOT NULL DEFAULT 0,
global_friend_enabled TINYINT(1) NOT NULL DEFAULT 1,
message_curfew_enabled TINYINT(1) NOT NULL DEFAULT 0,
state SMALLINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_by BIGINT NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NOT NULL,
PRIMARY KEY(id), PRIMARY KEY(id),
UNIQUE INDEX uk_im_tenant_code(code), UNIQUE INDEX uk_im_tenant_code(code)
INDEX idx_im_tenant_db_name(db_name)
)`); err != nil {
return err
}
if _, err := tx.Exec(
`CREATE TABLE IF NOT EXISTS im_tenant_config(
id BIGINT NOT NULL AUTO_INCREMENT,
tenant_id BIGINT NOT NULL,
max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited',
realname_review_policy TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Real-name review policy: 0 none, 1 auto then manual',
realname_provider_mode VARCHAR(32) NOT NULL DEFAULT 'id_card_2factor' COMMENT 'id_card_2factor or smart_verify',
realname_required TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Require real-name verification',
realname_allow_skip TINYINT(1) NOT NULL DEFAULT 1 COMMENT 'Allow skipping real-name reminder',
realname_remind_after VARCHAR(32) NOT NULL DEFAULT '24h' COMMENT 'Real-name reminder interval',
aliyun_moderation_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Enable Aliyun content moderation',
local_moderation_enabled TINYINT(1) NOT NULL DEFAULT 0 COMMENT 'Enable local content moderation',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_by BIGINT NOT NULL,
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
updated_by BIGINT NOT NULL,
PRIMARY KEY(id),
UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id),
CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES im_tenant(id) ON DELETE CASCADE
)`); err != nil { )`); err != nil {
return err return err
} }
_, err := tx.Exec( _, err := tx.Exec(
`INSERT INTO im_tenant(id,code,name,tenant_desc,db_name,db_dsn,state,created_by,updated_by) `INSERT INTO im_tenant(id,code,name,tenant_desc,state,created_by,updated_by)
VALUES(1,'default','Default Tenant',NULL,?,'',?,0,0) VALUES(1,'default','Default Tenant',NULL,?,0,0)
ON DUPLICATE KEY UPDATE db_name=VALUES(db_name), state=VALUES(state), updated_by=VALUES(updated_by)`, ON DUPLICATE KEY UPDATE state=VALUES(state), updated_by=VALUES(updated_by)`,
businessDB, t.TenantStateActive) t.TenantStateActive)
return err return err
} }
@@ -1007,20 +977,6 @@ func createIMUserProductTables(tx *sql.Tx) error {
return nil return nil
} }
func ensureTenantRoutingColumns(a *adapter) error {
statements := []string{
"ALTER TABLE im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc",
"ALTER TABLE im_tenant ADD COLUMN db_dsn VARCHAR(1024) NOT NULL DEFAULT '' AFTER db_name",
"UPDATE im_tenant SET db_name='tinode' WHERE code='default' AND db_name=''",
}
for _, stmt := range statements {
if _, err := a.db.Exec(stmt); err != nil && !isDuplicateColumn(err) {
return err
}
}
return nil
}
func upgradeIMMessageActions(a *adapter) error { func upgradeIMMessageActions(a *adapter) error {
statements := []string{ statements := []string{
"ALTER TABLE im_messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id", "ALTER TABLE im_messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id",
+2 -2
View File
@@ -26,9 +26,9 @@ func (a *adapter) RealNamePolicy(tenantID t.TenantID) (*t.RealNamePolicy, error)
defer cancel() defer cancel()
} }
var policy t.RealNamePolicy var policy t.RealNamePolicy
err := db.GetContext(ctx, &policy, `SELECT tenant_id, realname_review_policy, realname_provider_mode, err := db.GetContext(ctx, &policy, `SELECT id AS tenant_id, realname_review_policy, realname_provider_mode,
realname_required, realname_allow_skip, realname_remind_after realname_required, realname_allow_skip, realname_remind_after
FROM im_tenant_config WHERE tenant_id=? LIMIT 1`, tenantID) FROM im_tenant WHERE id=? LIMIT 1`, tenantID)
if errors.Is(err, sql.ErrNoRows) { if errors.Is(err, sql.ErrNoRows) {
policy = *defaultRealNamePolicy(tenantID) policy = *defaultRealNamePolicy(tenantID)
return &policy, nil return &policy, nil
+3 -4
View File
@@ -6,16 +6,15 @@
# Current MySQL initialization is generated by server/db/mysql/adapter.go: # Current MySQL initialization is generated by server/db/mysql/adapter.go:
# #
# - Control database: hotgo # - Control database: hotgo
# - im_tenant # - im_tenant (includes tenant feature and policy configuration)
# - im_tenant_config
# - im_public_uploads, im_tenant_applications and im_tenant_entry_feedbacks # - im_public_uploads, im_tenant_applications and im_tenant_entry_feedbacks
# for unauthenticated enterprise registration and tenant-entry feedback # for unauthenticated enterprise registration and tenant-entry feedback
# #
# - Default tenant business database: tinode # - Shared tenant business database: meeting_im
# - im_users, im_topics, im_subscriptions, im_messages and extension tables # - im_users, im_topics, im_subscriptions, im_messages and extension tables
# - im_http_idempotency for HTTP write idempotency # - im_http_idempotency for HTTP write idempotency
# - im_file_review_tasks for asynchronous content safety and malware checks # - im_file_review_tasks for asynchronous content safety and malware checks
# - business tables do not store tenant_id in independent-database mode # - business tables store tenant_id; high-volume tables are physically partitioned by tenant
# #
# The previous shared-table tenant_id draft has been removed from this file to # The previous shared-table tenant_id draft has been removed from this file to
# avoid accidental execution. If standalone SQL initialization scripts are # avoid accidental execution. If standalone SQL initialization scripts are
+3 -3
View File
@@ -263,13 +263,13 @@ func TestTenantAuthIsolation(t *testing.T) {
} }
} }
} }
createTenant := func(code, dbName string) types.TenantID { createTenant := func(code, _ string) types.TenantID {
var id int64 var id int64
if err := db.Get(&id, "SELECT COALESCE(MAX(id),0)+1 FROM im_tenant"); err != nil { if err := db.Get(&id, "SELECT COALESCE(MAX(id),0)+1 FROM im_tenant"); err != nil {
t.Fatal(err) t.Fatal(err)
} }
_, err := db.Exec("INSERT INTO im_tenant(id,code,name,db_name,state,created_by,updated_by) VALUES(?,?,?,?,?,?,?)", _, err := db.Exec("INSERT INTO im_tenant(id,code,name,state,created_by,updated_by) VALUES(?,?,?,?,?,?)",
id, code+suffix, code, dbName, types.TenantStateActive, 0, 0) id, code+suffix, code, types.TenantStateActive, 0, 0)
if err != nil { if err != nil {
t.Fatal(err) t.Fatal(err)
} }
-2
View File
@@ -35,8 +35,6 @@ type Tenant struct {
Code string `db:"code" json:"code"` Code string `db:"code" json:"code"`
Name string `db:"name" json:"name"` Name string `db:"name" json:"name"`
TenantDesc *string `db:"tenant_desc" json:"tenant_desc,omitempty"` TenantDesc *string `db:"tenant_desc" json:"tenant_desc,omitempty"`
DBName string `db:"db_name" json:"-"`
DBDSN string `db:"db_dsn" json:"-"`
State TenantState `db:"state" json:"-"` State TenantState `db:"state" json:"-"`
CreatedAt time.Time `db:"created_at" json:"-"` CreatedAt time.Time `db:"created_at" json:"-"`
CreatedBy int64 `db:"created_by" json:"-"` CreatedBy int64 `db:"created_by" json:"-"`