fix im server shared database config
This commit is contained in:
+11
-1
@@ -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版需要审核,这两个可能会是卡点
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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。 |
|
||||||
|
|||||||
@@ -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
@@ -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",
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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
|
||||||
|
|||||||
@@ -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)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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:"-"`
|
||||||
|
|||||||
Reference in New Issue
Block a user