feat: add tenant-aware IM management console

This commit is contained in:
richjohnrambo
2026-08-19 00:31:09 +08:00
parent 642b7ec5da
commit 89ba6bcff8
126 changed files with 11941 additions and 339 deletions
@@ -0,0 +1,747 @@
# IM HotGo 注册短信开关与邀请码管理改造交接文档
> 文档状态:已按数据库直连方案实现,作为现状与后续 Kafka 演进说明
> 对应实施顺序:注册改造计划第 4 项(HotGo 后台)
> HotGo 项目:`/Users/youshunlai/go_workspace/im_manager`
> IM 服务项目:`/Users/youshunlai/go_workspace/im_server`
> 编写日期:2026-08-18
## 1. 文档用途
本文供新的开发会话直接接手 HotGo 项目使用。阅读本文后,应能明确:
1. 为什么要在 HotGo 中增加注册短信开关和邀请码管理。
2. 哪些数据属于 HotGo 控制库,哪些数据属于 IM 租户业务库。
3. HotGo 后端、前端、菜单权限、统一租户身份、审计和配置需要修改什么。
4. HotGo 与 IM 如何通过租户业务库协作,以及后续如何迁移到 Kafka。
5. 如何验证实现没有破坏租户隔离,也没有泄露邀请码明文。
本文指导 HotGo 项目改造。HotGo 负责邀请码生成和管理;IM 仍独占客户端注册、短信验证码和邀请码使用记录事务。
## 2. 开发前必须阅读
在修改 `/Users/youshunlai/go_workspace/im_manager` 前,先阅读以下文档:
1. `/Users/youshunlai/go_workspace/im_server/docs/IM-注册、邀请码与认证数据模型改造需求文档.md`
2. `/Users/youshunlai/go_workspace/im_server/docs/IM-前后端对接文档.md` 中 1.4、1.5 注册章节
3. `/Users/youshunlai/go_workspace/im_server/docs/IM-后台需求文档.md` 中租户权限、用户管理和审计章节
4. `/Users/youshunlai/go_workspace/im_server/docs/IM-独立库多租户改造方案.md`
规则冲突时按以下优先级处理:
1. 本文针对 HotGo 的落地边界和接口约定。
2. 《IM 注册、邀请码与认证数据模型改造需求文档》。
3. 《IM 独立库多租户改造方案》。
4. 其他较早的需求和设计文档。
`IM-服务端多租户改造方案.md` 已废弃,不得作为实现依据。当前多租户方案是“一套 IM 服务、一个 HotGo 控制库、每租户一个独立 IM 业务库”。
## 3. 需求前因后果
### 3.1 注册页面最终规则
客户端注册页面同时展示用户名、手机号、短信验证码、密码和邀请码,但用户名与手机号是二选一关系:
1. 手机号非空时,以手机号方式为准,忽略用户名输入。
2. 手机号方式注册后,`im_users.username` 和 `im_users.phone` 都保存规范化后的 `+86` 手机号。
3. 手机号为空时使用普通用户名,用户名必须匹配 `^[a-z0-9]{1,10}$`,此时 `phone=NULL`。
4. 普通用户名方式不能自助找回密码,只能由企业管理员重置。
5. 邀请码固定为 5 位 ASCII 大小写英文字母,区分大小写、租户内唯一,有效期内可供多人使用。
### 3.2 为什么需要短信开关
短信服务在不同部署环境中可能暂未启用,因此需要平台级开关:
| 场景 | 短信开关开启 | 短信开关关闭 |
| --- | --- | --- |
| 点击获取验证码 | IM 服务真实调用短信服务商 | IM 服务返回正常挑战和倒计时,但不调用服务商 |
| 手机号注册 | 必须验证短信;邀请码不能绕过 | 必须提供有效邀请码,可无验证码注册 |
| 用户名注册 | 不发送短信,必须提供有效邀请码 | 不发送短信,必须提供有效邀请码 |
| 客户端是否知道开关 | 不直接暴露 | 不直接暴露 |
HotGo 只负责管理开关值。是否调用短信服务商、是否允许注册,全部由 IM 服务端判定。
### 3.3 为什么当前改为 HotGo 直接访问租户库
邀请码属于 IM 租户业务数据:
1. 每个租户有独立业务库。
2. 邀请码批次和主表位于 `hotgo` 控制库,使用明细位于对应租户业务库。
3. HotGo 邀请码管理直接访问控制库;密码重置等租户业务功能仍通过 `im_tenant.id` 和服务端保存的 DSN 选择租户库。
4. 当前部署中 HotGo 到 IM 的内部 HTTP 地址不可达,继续维护 HMAC 管理 API 会增加一套没有必要的调用链。
因此当前采用“控制库主数据 + 租户库使用事实”的混合边界:HotGo 负责邀请码生成、查询、导出和撤销;IM 从控制库校验邀请码,并在租户库记录使用关系和 outbox 事件。两端各自只写自己负责的数据。
密码重置等需要 IM 立即感知的操作写入 `im_business_events_outbox`。当前 IM 轮询消费,后续改为 Kafka 时保留事件契约,只替换传输层。
## 4. 系统边界
```mermaid
flowchart LR
A["HotGo 管理后台浏览器"] -->|HotGo 登录 Token| B["HotGo 后端"]
B -->|读写全局开关| C[("hotgo.hg_sys_config")]
B -->|读取管理员 tenant_id| D[("hotgo.im_tenant")]
B -->|管理邀请码| G[("hotgo.im_registration_invite_batches")]
B -->|管理邀请码| H[("hotgo.im_registration_invite_codes")]
B -->|其他租户业务按服务端 DSN| F[("租户 IM 业务库")]
F --> I["im_registration_invite_uses"]
J["IM 注册与发码接口"] -->|短时缓存读取| C
J -->|按 tenant_id 校验| H
J -->|记录使用关系与事件| F
B -->|写 security.password_reset| K["im_business_events_outbox"]
F --> K
K -->|当前轮询,后续 Kafka| J
```
### 4.1 HotGo 负责
1. 在 `hg_sys_config` 保存全局注册短信开关。
2. 只允许平台超级管理员修改开关。
3. 为邀请码提供管理页面、表单、权限和审计入口。
4. 从当前登录管理员身份解析可信的 IM `tenant_id`。
5. 通过 `imtenant.DB()` 直连所选租户业务库,不接受浏览器传入 DSN。
6. 对邀请码明文查询、查看和导出执行独立权限校验。
### 4.2 IM 服务负责
1. 读取并缓存短信开关。
2. 实际发送或抑制短信。
3. 注册时大小写敏感地校验邀请码,并记录邀请码与新用户的使用关系。
4. 邀请码使用后不改变 active 状态。
5. 消费 `security.password_reset` 事件并清退在线会话。
6. 登录时以 `auth_version` 拒绝密码重置前签发的旧 Token。
7. 消费 `registration.invite_used`,将租户库使用统计幂等投影到控制库。
### 4.3 明确禁止
1. 浏览器直接连接租户数据库。
2. 浏览器通过 Header、Query 或 Body 自报可信 `tenant_id`。
3. HotGo 后端根据浏览器传入的 `db_name` 或 `db_dsn` 连接数据库。
4. HotGo 修改 `im_registration_invite_uses`;该表只允许 IM 注册事务写入。
5. HotGo 绕过 outbox 直接修改在线会话状态。
6. 在普通访问日志、错误日志、埋点或 URL 中记录完整邀请码。
## 5. 当前 HotGo 项目现状
目标仓库 `/Users/youshunlai/go_workspace/im_manager` 基于 GoFrame 2、Vue 3 和 Naive UI。
已确认的现状:
1. 通用配置后端位于 `server/api/admin/config`、`server/internal/controller/admin/sys/config.go`、`server/internal/logic/sys/config.go`。
2. 通用配置前端位于 `web/src/views/system/config` 和 `web/src/api/sys/config.ts`。
3. `hg_sys_config` 支持 `group/key/type/value/default_value`,前端不能通过通用接口新增不存在的配置项。
4. `sys_switch` 字典实际定义为“开启=`1`、关闭=`2`”。
5. HotGo 管理接口权限按“路由路径 + HTTP Method”使用 Casbin 校验,前端按钮权限也使用路由路径,例如 `hasPermission(['/notice/edit'])`。
6. `hotgo.im_tenant` 是全系统唯一租户注册表,由 HotGo 维护,不存在另一套需要映射的“IM 租户”。
7. 后台管理员通过 `hg_admin_member.tenant_id` 直接引用 `im_tenant.id`,登录身份必须携带该字段;不再使用独立租户绑定表。
8. `hg_sys_log` 会保存 GET Query 和 POST Body;现有逻辑只自动掩码名称包含 `password` 的 POST 字段,邀请码查询参数会泄漏,必须补充脱敏。
## 6. 建议开发顺序
### 阶段 A:先完成 HotGo 基础边界
1. 增加全局短信开关配置种子和专用读写接口。
2. 增加 `hg_admin_member.tenant_id` 外键与登录身份解析,统一使用 `im_tenant.id`。
3. 增加租户库直连、Snowflake ID 和数据库事件写入能力。
4. 增加邀请码相关菜单、路由权限和后台代理接口。
5. 增加邀请码管理页面。
6. 增加敏感字段日志脱敏和操作审计。
### 阶段 B:与 IM 服务联调
1. 核对 IM 租户库迁移已执行且表结构一致。
2. 核对服务凭证、租户上下文和操作人上下文可被 IM 验证。
3. 联调单个生成、批量生成、分页查询、撤销、详情和导出。
4. 联调短信开关在 IM 服务缓存时间内生效。
IM 管理接口未完成时,可以通过接口 Client 抽象和测试 Stub 开发页面,但不得用 HotGo 直写租户数据库作为临时方案。
## 7. 全局注册短信开关
### 7.1 数据定义
在 HotGo 控制库 `hg_sys_config` 中增加且只保留一条配置:
```text
group = im
name = 注册短信发送
type = bool
key = register_sms_enabled
value = 1
default_value = 1
status = 1
is_default = 1
```
建议的 MySQL 幂等种子:
```sql
INSERT INTO hg_sys_config
(`group`, `name`, `type`, `key`, `value`, `default_value`, `sort`, `tip`, `is_default`, `status`, `created_at`, `updated_at`)
SELECT
'im', '注册短信发送', 'bool', 'register_sms_enabled', '1', '1', 10,
'控制 IM 注册验证码是否真实发送', 1, 1, NOW(), NOW()
WHERE NOT EXISTS (
SELECT 1 FROM hg_sys_config
WHERE `group` = 'im' AND `key` = 'register_sms_enabled'
);
```
同时为 `(group, key)` 增加唯一约束。加索引前先检查并清理重复配置,不能让 IM 服务随机读到多条值。
```sql
ALTER TABLE hg_sys_config
ADD UNIQUE KEY uk_hg_sys_config_group_key (`group`, `key`);
```
新装数据库种子、实际部署迁移及项目保留的 MySQL/PostgreSQL/SQLite 初始化脚本应按项目发布策略同步,不能只改一份示例 SQL。
### 7.2 `sys_switch` 映射陷阱
`sys_switch` 字典值与真实配置值不相同:
| UI 字典值 | UI 含义 | `hg_sys_config.value` |
| --- | --- | --- |
| `1` | 开启 | `1` |
| `2` | 关闭 | `0` |
实现要求:
1. 页面加载时将后端 bool 值映射为字典值:`true -> 1`,`false -> 2`。
2. 页面提交时反向映射:`1 -> true`,`2 -> false`。
3. 不得把字典值 `2` 原样写入 bool 配置;GoFrame 的 bool 转换可能把非零值当成 true。
4. 数据库只允许保存可解释为 `1/0` 的值。
### 7.3 后端接口
不要直接让页面复用通用 `/config/update`。新增专用接口,以便强制执行平台管理员校验:
| 方法 | HotGo 路径 | 权限 |
| --- | --- | --- |
| GET | `/imRegistration/config/get` | 平台超级管理员 |
| POST | `/imRegistration/config/update` | 平台超级管理员 |
读取响应:
```json
{
"registerSmsEnabled": true,
"updatedAt": "2026-08-18T16:00:00+08:00"
}
```
更新请求:
```json
{
"registerSmsEnabled": false
}
```
后端必须在业务逻辑层再次调用 `service.AdminMember().VerifySuperId(ctx, contexts.GetUserId(ctx))`,不能只依赖菜单是否可见。更新时只允许修改固定的 `group=im/key=register_sms_enabled`,不接受前端传 group 或 key。
### 7.4 页面位置
建议在 `web/src/views/system/config/system.vue` 增加“IM 注册”设置项,并新增:
```text
web/src/views/system/config/ImRegistrationSetting.vue
```
页面只展示一个“注册短信发送”开关和保存按钮。选项通过 `dict.loadOptions(['sys_switch'])` 加载,并按 7.2 做显式映射。
### 7.5 IM 服务读取约定
HotGo 不需要额外暴露公开配置接口。IM 服务直接从控制库读取:
```sql
SELECT value
FROM hg_sys_config
WHERE `group` = 'im'
AND `key` = 'register_sms_enabled'
AND status = 1
LIMIT 1;
```
约定:
1. 种子默认值为开启。
2. IM 服务缓存建议为 30 秒,缓存时长可配置。
3. 临时读取失败时优先使用最近一次成功值。
4. 进程启动后无缓存且配置缺失、停用或非法时,按“开启”处理,防止邀请码绕过短信验证,并记录告警。
5. HotGo 更新后不承诺立即生效,验收标准为 30 秒内生效。
## 8. 统一租户身份
### 8.1 唯一租户主键
`hotgo.im_tenant.id` 是 HotGo 和 IM 服务共同使用的唯一 `tenant_id`。HotGo 负责维护租户注册信息和业务库路由,IM 服务根据该 ID 选择租户业务库。不得再创建 `im_admin_tenant_binding` 或其他租户映射表。
### 8.2 管理员租户归属
后台管理员记录直接引用统一租户:
```sql
ALTER TABLE hg_admin_member
ADD COLUMN tenant_id BIGINT NULL COMMENT '统一租户ID,引用 im_tenant.id',
ADD INDEX idx_hg_admin_member_tenant (tenant_id),
ADD CONSTRAINT fk_hg_admin_member_im_tenant
FOREIGN KEY (tenant_id) REFERENCES im_tenant(id);
```
平台账号的 `tenant_id` 可以为空。租户管理员及其创建的下级后台账号必须继承同一个 `im_tenant.id`。只有平台超级管理员可以为后台账号选择或修改租户,租户域管理员提交的 `tenantId` 必须被后端忽略并以当前登录身份为准。
### 8.3 解析规则
租户管理员请求邀请码接口时:
1. 从 HotGo 登录上下文取得 `member_id`、`tenant_id` 和部门类型。
2. 仅允许合法的租户级管理员角色进入;普通用户和商户角色不得管理邀请码。
3. `tenant_id` 必须大于 0,并且对应的 `im_tenant` 必须存在且为 active。
4. 将该 `tenant_id` 写入 IM 服务调用上下文。
5. 忽略并拒绝浏览器提交的 `tenantId`。
平台超级管理员可以跨租户查看邀请码;列表和导出未选择租户时查询全部。创建、批次查看、撤销及其他写操作必须先选择一个 `im_tenant`。此选择只对 `VerifySuperId` 通过的管理员开放,HotGo 后端仍要检查目标租户存在且 active。
后台用户管理应提供仅平台超级管理员可见的租户选择控件。租户管理员未关联 `im_tenant.id` 时返回明确错误“当前管理员未关联租户”,不能回退到默认租户。
## 9. 租户库直连与事件通道
### 9.1 数据库选择
HotGo 从可信登录上下文取得 `im_tenant.id`。邀请码批次和邀请码主表直接通过 `imtenant.RegistryDB()` 访问 HotGo 控制库;只有密码重置等租户业务操作才通过 `imtenant.DB()` 打开租户业务库。浏览器传入的 `tenantId` 只允许平台超级管理员选择,租户管理员始终使用登录关系中的租户 ID;任何请求都不能提交或覆盖 DSN。
若配置了 `database.imRegistry`,租户注册表从该连接读取;未配置时回退到 HotGo 默认库。业务操作始终使用按租户动态建立的连接组。
### 9.2 Snowflake 配置
HotGo 直接新增 IM 表记录时使用与 IM 相同位布局的 Snowflake ID:
```yaml
imDirectDb:
snowflakeWorkerId: 1023
```
也可由 `IM_DIRECT_DB_SNOWFLAKE_WORKER_ID` 覆盖。每个 HotGo 写库实例必须使用唯一 worker ID,并且不能与 IM 节点重叠。当前单实例默认预留 `1023`,多实例部署前必须分配 worker ID。
### 9.3 写入职责
HotGo 可以写:
1. HotGo 控制库中的 `im_registration_invite_batches`、`im_registration_invite_codes`。
2. 管理员重置密码时的 `im_auth.secret` 和 `im_user_security`。
3. 管理操作产生的 `im_business_events_outbox`。
IM 独占写入:
1. 客户端注册事务及用户创建。
2. 租户业务库中的 `im_registration_invite_uses` 以及同事务的 `registration.invite_used` outbox 事件。
3. 客户端登录、改密、短信找回等认证事务。
### 9.4 Kafka 演进
管理员重置密码在数据库事务内写入:
```json
{
"event_type": "security.password_reset",
"payload": {
"user_id": 198000000000000001,
"auth_version": 2,
"operator_id": 10001,
"request_id": "..."
}
}
```
当前 IM 在注册写入 outbox 后立即触发一次消费,并每秒按在线会话涉及的租户轮询 pending 事件。`security.password_reset` 消费后清退该用户会话;`registration.invite_used` 消费时从租户使用明细重新聚合 `COUNT/MAX(used_at)`,覆盖 HotGo 控制库的 `usage_count/last_used_at`,成功后标记 delivered。离线用户不依赖密码重置轮询,后续登录时由 `auth_version` 直接拒绝旧 Token。
邀请码使用事件契约:
```json
{
"event_type": "registration.invite_used",
"payload": {
"tenant_id": 1,
"invite_code_id": 198000000000000001,
"user_id": 198000000000000002,
"request_id": "acc-request-id",
"used_at": "2026-08-19T10:00:00.000Z"
}
}
```
迁移 Kafka 时,生产者继续在业务事务内写 outbox,由独立投递器发布同一事件;消费者保持事件类型与 payload 不变。Kafka 投递确认后再标记 outbox delivered,避免数据库提交成功但消息丢失。
## 10. 邀请码接口契约
### 10.1 数据表契约
HotGo 在控制库直接维护带 `tenant_id` 的 `im_registration_invite_batches` 和 `im_registration_invite_codes`。邀请码状态仅为 `active/expired/revoked`,使用后仍为 active;`im_registration_invite_uses` 只由 IM 注册事务写入租户库。控制库中的 `usage_count/last_used_at` 由 outbox 消费器幂等投影,HotGo 列表不跨租户库做联表聚合。
平台超级管理员在列表和导出接口中可不传 `tenantId` 以查询全部租户;创建、批次查看、撤销等写操作必须明确选择一个 active 租户。租户管理员只能查询和操作登录关系绑定的 `im_tenant.id`。
邀请码明文不得放入 URL。查询完整邀请码时必须使用 HotGo POST Body,普通请求日志需对 `inviteCode` 脱敏。
### 10.2 HotGo 对浏览器接口
沿用 HotGo 现有命名风格,建议增加:
| 方法 | HotGo 路径 | 说明 |
| --- | --- | --- |
| POST | `/imInvite/list` | 分页查询,过滤条件放在 JSON Body |
| POST | `/imInvite/create` | 单个生成 |
| POST | `/imInvite/batchCreate` | 批量生成 |
| GET | `/imInvite/batchView` | 批次详情 |
| POST | `/imInvite/revoke` | 单个撤销 |
| POST | `/imInvite/batchRevoke` | 批量撤销 |
| POST | `/imInvite/export` | 明文导出,过滤条件放在 JSON Body |
HotGo controller 只做 DTO 转换、调用 service 和包装响应。租户解析、平台管理员判断、权限复核、审计和租户库访问放在 logic/service 层。
### 10.3 单个生成
HotGo 请求:
```json
{
"expiresAt": "2026-09-01T00:00:00+08:00",
"remark": "新员工入职",
"idempotencyKey": "7be3de87-994f-46c6-86bd-844ffb54f83f"
}
```
规则:
1. `expiresAt` 必填,必须晚于服务端当前时间。
2. `remark` 最长 255 字符。
3. 推荐单个生成也传幂等键,避免提交成功但响应超时时重复创建。
4. 邀请码由 HotGo 使用 `crypto/rand` 生成并依靠租户库唯一索引兜底,HotGo 不接受用户自定义 code。
响应数据:
```json
{
"id": 198000000000000001,
"code": "AbCdE",
"status": "active",
"expiresAt": "2026-09-01T00:00:00+08:00",
"remark": "新员工入职",
"createdBy": 10001,
"createdAt": "2026-08-18T16:10:00+08:00"
}
```
### 10.4 批量生成
HotGo 请求:
```json
{
"quantity": 100,
"expiresAt": "2026-09-01T00:00:00+08:00",
"remark": "2026年8月新员工入职",
"idempotencyKey": "4dc7f13e-448c-4b49-b056-57a2fde18f95"
}
```
规则:
1. `quantity` 为 1 到 1000,由 HotGo 校验。
2. 同一个提交动作必须复用同一个幂等键,直到得到明确成功响应。
3. 前端可在打开弹窗时生成 UUID,失败重试时复用;成功或关闭弹窗后废弃。
4. HotGo 在批次表保存幂等键;相同参数重试返回原批次,不重复生成。
响应数据:
```json
{
"batchId": 198000000000000100,
"quantity": 100,
"activeCount": 100,
"usageCount": 0,
"revokedCount": 0,
"expiredCount": 0,
"expiresAt": "2026-09-01T00:00:00+08:00",
"createdAt": "2026-08-18T16:15:00+08:00"
}
```
### 10.5 分页查询
列表请求使用 POST JSON Body,支持字段:
```text
page, perPage, inviteCode, status, batchId, createdBy,
createdAtStart, createdAtEnd, expiresAtStart, expiresAtEnd
```
要求:
1. `inviteCode` 仅支持完整 5 位精确查询,不做模糊查询,不转换大小写。
2. `status` 为 `active/expired/revoked`,邀请码使用后仍保持 `active`。
3. HotGo 查询前将 `expires_at <= now` 的 active 记录更新为 expired,不依赖定时任务。
4. 默认按 `created_at DESC, id DESC` 排序。
5. `perPage` 设置合理上限,建议不超过 100。
列表字段至少包括:
```json
{
"id": 198000000000000001,
"batchId": 198000000000000100,
"code": "AbCdE",
"status": "active",
"expiresAt": "2026-09-01T00:00:00+08:00",
"usageCount": 12,
"lastUsedAt": "2026-08-20T09:30:00+08:00",
"createdBy": 10001,
"remark": "新员工入职",
"createdAt": "2026-08-18T16:10:00+08:00"
}
```
### 10.6 撤销
1. 只有 `active` 且未过期的邀请码可撤销;已有使用记录不影响撤销。
2. 已过期、已撤销的邀请码不能恢复为 active。
3. 批量撤销只撤销批次内仍可撤销的码,并返回各状态统计。
4. 页面操作前显示确认框,确认内容不必重复显示完整邀请码。
5. 最终状态由租户库条件更新结果决定,不以前端状态为准。
### 10.7 导出
1. 导出必须具备独立 `/imInvite/export` 权限。
2. 导出过滤条件与列表一致。
3. HotGo 后端分页读取租户库并流式写出,不把整份文件加载到内存。
4. 建议输出带 UTF-8 BOM 的 CSV,字段至少包含邀请码、状态、批次、到期时间、累计使用次数、最后使用时间、创建人、创建时间和备注。
5. 文件名建议为 `registration-invite-codes-YYYYMMDD-HHmmss.csv`。
6. 每次导出必须写审计,但审计详情不得包含邀请码明文列表。
## 11. 页面和交互
建议页面路径:
```text
web/src/views/im/registrationInvite/index.vue
web/src/views/im/registrationInvite/model.ts
web/src/views/im/registrationInvite/CreateModal.vue
web/src/views/im/registrationInvite/BatchCreateModal.vue
web/src/views/im/registrationInvite/BatchViewModal.vue
web/src/api/im/registrationInvite.ts
```
页面放在“IM 管理 / 注册邀请码”菜单下。使用现有 `BasicForm`、`BasicTable`、`TableAction`、`usePermission` 和 Naive UI 组件。
页面必须具备:
1. 完整邀请码、状态、批次 ID、创建人、创建时间和到期时间筛选。
2. 单个生成按钮。
3. 批量生成按钮。
4. 导出按钮。
5. 列表展示完整邀请码、状态、到期时间、批次、使用情况、创建信息和备注。
6. active 行提供撤销操作,其他状态不显示撤销操作。
7. 批次详情展示总数、active、used、expired、revoked 统计。
8. 所有日期时间明确按本地时区展示,提交时发送带时区的 RFC 3339 时间。
9. 加载、空数据、接口错误、重复提交和无权限状态处理完整。
邀请码区分大小写,任何输入组件、复制操作和查询参数都不得自动转为大写或小写。
## 12. 菜单与权限
HotGo 的权限标识使用接口路径。建议创建以下按钮权限:
```text
/imRegistration/config/get
/imRegistration/config/update
/imInvite/list
/imInvite/create
/imInvite/batchCreate
/imInvite/batchView
/imInvite/revoke
/imInvite/batchRevoke
/imInvite/export
```
权限分配:
| 能力 | 平台超级管理员 | 租户管理员 | 其他角色 |
| --- | --- | --- | --- |
| 查看/修改短信开关 | 允许 | 禁止 | 禁止 |
| 选择任意 IM 租户管理邀请码 | 允许 | 禁止 | 禁止 |
| 管理当前绑定租户邀请码 | 允许 | 按权限允许 | 禁止 |
| 查看邀请码明文 | 允许 | 需独立权限 | 禁止 |
| 导出邀请码明文 | 允许 | 需独立权限 | 禁止 |
服务端 Casbin、业务逻辑层租户范围校验、前端按钮隐藏三层都要实现。前端隐藏按钮不是安全边界。
## 13. 日志脱敏与审计
### 13.1 必须修复现有访问日志泄露
`server/internal/logic/sys/log.go` 当前会保存 Query 和 Body,且只掩码 password。至少增加以下字段名的大小写不敏感脱敏:
```text
inviteCode
invite_code
serviceSecret
service_secret
authorization
```
建议抽成统一的递归敏感字段过滤器,同时处理 Query、JSON Body、Form 和 Header。Header 中已有简化逻辑也必须确认不会保存内部服务凭证。
对 `/imInvite/list` 的明文精确查询,邀请码只能放在 POST Body,`hg_sys_log.post_data` 中只能出现 `******`。`hg_sys_log.get_data`、Nginx URL 和其他访问日志中都不能出现真实邀请码。HTTP Client 的 debug 日志同样不得打印完整 URL、Query、请求体或响应体。
### 13.2 业务审计
仅靠普通访问日志不够。建议在 HotGo 控制库增加独立审计表,或者接入项目已有的等价不可变审计设施。最低字段为:
```sql
CREATE TABLE im_admin_operation_audit (
id BIGINT NOT NULL AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL,
tenant_id BIGINT NULL,
operator_id BIGINT NOT NULL,
action VARCHAR(64) NOT NULL,
target_type VARCHAR(32) NOT NULL,
target_id BIGINT NULL,
result VARCHAR(16) NOT NULL,
ip VARCHAR(64) NOT NULL DEFAULT '',
detail_json JSON NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
KEY idx_im_admin_audit_tenant_time (tenant_id, created_at),
KEY idx_im_admin_audit_operator_time (operator_id, created_at),
UNIQUE KEY uk_im_admin_audit_request_action (request_id, action)
);
```
必须审计:
1. 短信开关修改,记录旧值和新值。
2. 单个生成和批量生成,记录 code ID 或 batch ID、数量和到期时间,但不记录 code 明文。
3. 邀请码明文查询和详情查看,记录查询行为,但不记录搜索明文。
4. 单个撤销和批量撤销。
5. 明文导出,记录过滤条件摘要和导出数量,不记录明文内容。
6. 后台管理员租户归属变更。
IM 服务也应写自己的业务审计。HotGo 审计记录后台操作者,IM 审计记录实际租户库操作,两者通过 `request_id` 关联。
## 14. 错误处理
HotGo 应将数据库错误稳定映射为后台可理解的错误,不向浏览器返回 DSN、SQL、堆栈或内部配置。
至少处理:
| 场景 | HotGo 行为 |
| --- | --- |
| 当前管理员未关联租户 | 返回明确业务错误,不使用默认租户 |
| 目标租户停用 | 拒绝操作 |
| 无查看/导出权限 | 403 |
| 邀请码已过期或已撤销 | 显示不可撤销,不本地修改状态 |
| 幂等键重复且请求相同 | 返回原结果 |
| 幂等键重复但参数不同 | 返回幂等冲突 |
| 租户库连接失败 | 返回“租户业务库暂不可用”,保留 Request ID |
| 数据库写入失败 | 事务回滚,返回统一后台错误并记录脱敏日志 |
| 导出数据过大 | 使用服务端限制或异步导出,不耗尽内存 |
## 15. 建议代码落点
遵循现有 HotGo 分层,建议新增或修改:
```text
server/api/admin/imregistration/
server/api/admin/iminvite/
server/internal/controller/admin/admin/im_registration.go
server/internal/controller/admin/admin/im_invite.go
server/internal/model/input/adminin/im_registration.go
server/internal/model/input/adminin/im_invite.go
server/internal/service/im.go
server/internal/logic/admin/im_registration.go
server/internal/logic/admin/im_invite.go
server/internal/library/imtenant/db.go
server/internal/library/imtenant/snowflake.go
server/internal/router/admin.go
server/internal/logic/sys/log.go
web/src/api/im/registrationInvite.ts
web/src/views/im/registrationInvite/
web/src/views/system/config/ImRegistrationSetting.vue
web/src/views/system/config/system.vue
```
如果新增 `im_admin_operation_audit`,按 HotGo 现有 GoFrame DAO 生成流程生成 `dao/do/entity`,不要手工修改标有 “Code generated” 的文件。禁止新增 `im_admin_tenant_binding`。
同时提交:
1. 生产数据库幂等迁移。
2. 新装数据库初始化数据。
3. 菜单和按钮权限种子。
4. 默认角色授权或明确的部署授权步骤。
5. Snowflake worker 配置示例,但不提交真实 DSN。
## 16. 测试清单
### 16.1 短信开关
1. 无配置时 IM 按开启处理并告警。
2. 平台超级管理员可查看和修改。
3. 租户管理员即使手工调用接口也不能修改。
4. UI 选择“关闭”后数据库保存 `0`,不是 `2`。
5. UI 重新加载时 `0` 正确显示为“关闭”。
6. IM 服务在 30 秒内读取到新值。
7. 开关审计包含操作者、旧值、新值和 Request ID。
### 16.2 租户隔离
1. 租户 A 管理员只能看到租户 A 邀请码。
2. 修改 Query、Body 或 Header 中的 tenant ID 不能访问租户 B。
3. 未绑定管理员不能回退到 default 租户。
4. 停用租户不能生成、查询、撤销或导出。
5. 平台超级管理员选择租户后只能操作所选租户,切换时列表立即清空并重新请求。
### 16.3 邀请码
1. 单个生成返回恰好 5 位 `[A-Za-z]`。
2. 批量生成数量正确,最大值限制生效。
3. `AbCdE` 和 `abcde` 可作为不同邀请码精确查询。
4. 重复提交相同幂等键不产生第二个批次。
5. active 可撤销,expired/revoked 不可撤销。
6. 同一 active 邀请码可供多个不同用户注册,累计使用次数正确增长且状态保持 active。
7. 列表、详情和导出权限相互独立。
8. 导出文件数量、过滤条件和明文字段正确。
### 16.4 安全和日志
1. `hg_sys_log.get_data/post_data/header_data` 中找不到真实邀请码和服务 secret。
2. 应用日志中找不到邀请码明文、内部 Token 和 Authorization。
3. 每个敏感操作都有 HotGo 审计和 IM 审计,并可通过 Request ID 对应。
4. 无权限接口返回 403,不能只依赖前端按钮隐藏。
5. 数据库连接和 SQL 错误不泄露内部实现信息。
## 17. 完成标准
满足以下条件才能认为 HotGo 第 4 项完成:
1. 全局注册短信开关可由平台超级管理员配置,关闭值正确保存为 `0`。
2. IM 服务能在约定缓存时间内读取开关,缺失和异常时采用安全默认值。
3. HotGo 管理员身份能可靠映射到 `im_tenant.id`,不存在默认租户回退。
4. 邀请码单个、批量、查询、详情、撤销和导出全部通过所选租户库完成。
5. 平台与租户权限、跨租户隔离和服务端复核全部通过测试。
6. 邀请码明文只出现在授权页面和授权导出中,不进入普通日志。
7. 配置修改和邀请码敏感操作均有可关联的审计记录。
8. 后端 Go 测试、前端类型检查/构建和核心页面联调通过。
## 18. 新会话可直接使用的任务说明
在新的开发会话中可直接发送:
```text
请修改 HotGo 项目 /Users/youshunlai/go_workspace/im_manager。
先完整阅读 /Users/youshunlai/go_workspace/im_server/docs/IM-HotGo注册短信开关与邀请码管理改造交接文档.md,
再阅读其中列出的上游 IM 文档。按照交接文档的开发顺序完成 HotGo 的注册短信全局开关、
统一租户身份、租户库直连、邀请码管理后台、菜单权限、日志脱敏和审计。
只允许通过服务端 `im_tenant` 注册信息选择租户库,不要信任浏览器自报 tenant_id 或 DSN,
不要修改 `im_registration_invite_uses`,不要把邀请码明文写入普通日志。
开始前先检查 im_manager 当前工作区已有改动,并保留与本任务无关的用户修改。
```
+713
View File
@@ -0,0 +1,713 @@
-- IM product database changes summary, chapters 1-11.
-- Target:
-- public backend database: hotgo
-- default tenant business database: tinode
-- Notes:
-- 1. Public tenant registry tables live in hotgo.
-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id.
-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP.
-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs.
SET NAMES utf8mb4;
CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
-- ============================================================
-- Public database: tenant registry.
-- ============================================================
CREATE TABLE IF NOT EXISTS hotgo.im_tenant (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
tenant_desc VARCHAR(256),
db_name VARCHAR(128) NOT NULL DEFAULT '',
db_dsn VARCHAR(1024) NOT NULL DEFAULT '',
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),
UNIQUE INDEX uk_im_tenant_code (code),
INDEX idx_im_tenant_db_name (db_name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS hotgo.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',
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 hotgo.im_tenant(id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 1, tenant-aware login, verification, security.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_user_security (
user_id BIGINT NOT NULL,
auth_version INT NOT NULL DEFAULT 1,
password_updated_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_verification_codes (
id BIGINT NOT NULL AUTO_INCREMENT,
challenge_id VARCHAR(64) NOT NULL,
scene VARCHAR(32) NOT NULL,
action VARCHAR(64) NOT NULL DEFAULT '',
phone_hash CHAR(64) NOT NULL,
phone_mask VARCHAR(32) NOT NULL,
target_user BIGINT,
code_hash CHAR(64) NOT NULL,
verification_token_hash CHAR(64),
state SMALLINT NOT NULL,
attempts INT NOT NULL DEFAULT 0,
max_attempts INT NOT NULL,
expires_at DATETIME(3) NOT NULL,
resend_after DATETIME(3) NOT NULL,
verified_at DATETIME(3),
consumed_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_verification_challenge (challenge_id),
INDEX idx_im_verification_scene_phone_state (scene, phone_hash, state, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
submission_id VARCHAR(64) NOT NULL,
status VARCHAR(16) NOT NULL,
name_cipher TEXT NOT NULL,
id_type VARCHAR(32) NOT NULL,
id_number_cipher TEXT NOT NULL,
id_number_hash CHAR(64) NOT NULL,
masked_name VARCHAR(64) NOT NULL,
masked_id_number VARCHAR(64) NOT NULL,
review_reason VARCHAR(256),
verified_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_realname_submission (submission_id),
INDEX idx_im_realname_user_latest (user_id, created_at),
INDEX idx_im_realname_id_hash (id_number_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_action_tokens (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
token_id VARCHAR(64) NOT NULL,
action VARCHAR(64) NOT NULL,
target VARCHAR(128) NOT NULL DEFAULT '',
token_hash CHAR(64) NOT NULL,
state SMALLINT NOT NULL,
expires_at DATETIME(3) NOT NULL,
consumed_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_action_tokens_token (token_id),
INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings (
user_id BIGINT NOT NULL,
purpose VARCHAR(64) NOT NULL,
password_hash VARBINARY(255) NOT NULL,
enabled TINYINT NOT NULL DEFAULT 0,
policy_version INT NOT NULL DEFAULT 1,
restrictions JSON,
enabled_at DATETIME(3),
failed_attempts INT NOT NULL DEFAULT 0,
locked_until DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id, purpose)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapters 2-7, notifications, messages, friends, files, favorites.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_system_notifications (
id BIGINT NOT NULL AUTO_INCREMENT,
user_id BIGINT NOT NULL,
event_id VARCHAR(64) NOT NULL,
event_type VARCHAR(64) NOT NULL,
title VARCHAR(128) NOT NULL,
summary VARCHAR(512) NOT NULL,
action JSON,
read_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_system_notifications_event (user_id, event_id),
INDEX idx_im_system_notifications_inbox (user_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_message_revisions (
id BIGINT NOT NULL AUTO_INCREMENT,
createdat DATETIME(3) NOT NULL,
updatedat DATETIME(3) NOT NULL,
topic CHAR(25) NOT NULL,
seqid INT NOT NULL,
version INT NOT NULL,
actor BIGINT NOT NULL,
head JSON,
content JSON,
reason VARCHAR(256) NOT NULL DEFAULT '',
PRIMARY KEY (id),
UNIQUE INDEX uk_im_message_revisions_version (topic, seqid, version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_friend_requests (
id BIGINT NOT NULL AUTO_INCREMENT,
request_id VARCHAR(64) NOT NULL,
applicant_id BIGINT NOT NULL,
target_id BIGINT NOT NULL,
message VARCHAR(200) NOT NULL DEFAULT '',
source VARCHAR(32) NOT NULL DEFAULT '',
source_topic VARCHAR(64) NOT NULL DEFAULT '',
status VARCHAR(16) NOT NULL,
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
expires_at DATETIME(3) NOT NULL,
handled_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_friend_requests_request (request_id),
UNIQUE INDEX uk_im_friend_requests_idempotency (applicant_id, idempotency_key),
INDEX idx_im_friend_requests_pair (applicant_id, target_id, status, created_at),
INDEX idx_im_friend_requests_inbox (target_id, status, created_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_friendships (
user_id BIGINT NOT NULL,
peer_id BIGINT NOT NULL,
status VARCHAR(16) NOT NULL,
remark VARCHAR(64) NOT NULL DEFAULT '',
topic CHAR(25) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id, peer_id),
INDEX idx_im_friendships_peer (peer_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_user_blocks (
blocker_id BIGINT NOT NULL,
blocked_id BIGINT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (blocker_id, blocked_id),
INDEX idx_im_user_blocks_blocked (blocked_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_favorites (
id BIGINT NOT NULL AUTO_INCREMENT,
favorite_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
item_type VARCHAR(16) NOT NULL,
source_topic CHAR(25) NOT NULL,
source_seq INT NOT NULL,
snapshot JSON,
attachment_refs JSON,
availability VARCHAR(16) NOT NULL DEFAULT 'available',
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
collected_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_favorites_favorite (favorite_id),
UNIQUE INDEX uk_im_favorites_source (user_id, source_topic, source_seq),
UNIQUE INDEX uk_im_favorites_idempotency (user_id, ((NULLIF(idempotency_key, '')))),
INDEX idx_im_favorites_inbox (user_id, id DESC),
INDEX idx_im_favorites_type (user_id, item_type, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- Existing Tinode tables must match the current schema for chapters 2-7:
-- topics.aux, fileuploads.etag, and filemsglinks.topic/userid are required by current code.
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'client_msg_id') = 0,
'ALTER TABLE tinode.messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'version') = 0,
'ALTER TABLE tinode.messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_at') = 0,
'ALTER TABLE tinode.messages ADD COLUMN recalled_at DATETIME(3) AFTER version',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_by') = 0,
'ALTER TABLE tinode.messages ADD COLUMN recalled_by BIGINT AFTER recalled_at',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND INDEX_NAME = 'messages_sender_client_mid') = 0,
'CREATE UNIQUE INDEX messages_sender_client_mid ON tinode.messages(`from`, client_msg_id)',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- ============================================================
-- Tenant database: chapter 8, realtime call records.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_call_records (
id BIGINT NOT NULL AUTO_INCREMENT,
call_id VARCHAR(64) NOT NULL,
topic CHAR(25) NOT NULL,
seq_id INT NOT NULL,
caller_id BIGINT NOT NULL,
callee_id BIGINT NOT NULL,
call_type VARCHAR(16) NOT NULL,
status VARCHAR(32) NOT NULL,
end_reason VARCHAR(64) NOT NULL DEFAULT '',
duration_ms BIGINT NOT NULL DEFAULT 0,
started_at DATETIME(3) NOT NULL,
accepted_at DATETIME(3),
finished_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_call_records_call (call_id),
UNIQUE INDEX uk_im_call_records_topic_seq (topic, seq_id),
INDEX idx_im_call_records_caller (caller_id, id DESC),
INDEX idx_im_call_records_callee (callee_id, id DESC),
INDEX idx_im_call_records_status (status, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 9, workbench and office collaboration.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps (
id BIGINT NOT NULL AUTO_INCREMENT,
code VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
icon_file_id VARCHAR(64) NOT NULL DEFAULT '',
route VARCHAR(128) NOT NULL,
sort_order INT NOT NULL DEFAULT 0,
unread_key VARCHAR(64) NOT NULL DEFAULT '',
enabled TINYINT NOT NULL DEFAULT 1,
permission VARCHAR(64) NOT NULL DEFAULT 'enabled',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_workbench_apps_code (code),
INDEX idx_im_workbench_apps_enabled_order (enabled, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_org_departments (
id BIGINT NOT NULL AUTO_INCREMENT,
department_id VARCHAR(64) NOT NULL,
name VARCHAR(128) NOT NULL,
parent_id VARCHAR(64) NOT NULL DEFAULT 'root',
sort_order INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
state VARCHAR(16) NOT NULL DEFAULT 'active',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_org_departments_department (department_id),
INDEX idx_im_org_departments_parent (parent_id, state, sort_order, id),
INDEX idx_im_org_departments_name (name)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_org_members (
id BIGINT NOT NULL AUTO_INCREMENT,
department_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
display_name VARCHAR(128) NOT NULL DEFAULT '',
position VARCHAR(128) NOT NULL DEFAULT '',
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
mobile VARCHAR(32) NOT NULL DEFAULT '',
email VARCHAR(128) NOT NULL DEFAULT '',
employment_state VARCHAR(16) NOT NULL DEFAULT 'active',
contactable TINYINT NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_org_members_department_user (department_id, user_id),
INDEX idx_im_org_members_department (department_id, employment_state, sort_order, id),
INDEX idx_im_org_members_user (user_id),
INDEX idx_im_org_members_display_name (display_name),
CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_meetings (
id BIGINT NOT NULL AUTO_INCREMENT,
meeting_id VARCHAR(64) NOT NULL,
organizer_id BIGINT NOT NULL,
title VARCHAR(200) NOT NULL,
start_at DATETIME(3) NOT NULL,
end_at DATETIME(3) NOT NULL,
location VARCHAR(256) NOT NULL DEFAULT '',
state VARCHAR(16) NOT NULL DEFAULT 'scheduled',
reminder_minutes JSON,
idempotency_key VARCHAR(128),
version INT NOT NULL DEFAULT 1,
canceled_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_meetings_meeting (meeting_id),
UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key),
INDEX idx_im_meetings_organizer (organizer_id, start_at),
INDEX idx_im_meetings_state_time (state, start_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants (
id BIGINT NOT NULL AUTO_INCREMENT,
meeting_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
response VARCHAR(16) NOT NULL DEFAULT 'pending',
response_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_meeting_participants_user (meeting_id, user_id),
INDEX idx_im_meeting_participants_user_time (user_id, meeting_id),
INDEX idx_im_meeting_participants_response (meeting_id, response),
CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_daily_reports (
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
author_id BIGINT NOT NULL,
report_date DATE NOT NULL,
content TEXT NOT NULL,
attachments JSON,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
version INT NOT NULL DEFAULT 1,
submitted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_daily_reports_report (report_id),
UNIQUE INDEX uk_im_daily_reports_author_date (author_id, report_date),
INDEX idx_im_daily_reports_author_id (author_id, id DESC),
INDEX idx_im_daily_reports_date_status (report_date, status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports (
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
author_id BIGINT NOT NULL,
report_month CHAR(7) NOT NULL,
work_summary TEXT NOT NULL,
next_plan TEXT NOT NULL,
attachments JSON,
status VARCHAR(16) NOT NULL DEFAULT 'draft',
read_state VARCHAR(16) NOT NULL DEFAULT 'unread',
version INT NOT NULL DEFAULT 1,
submitted_at DATETIME(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_monthly_reports_report (report_id),
UNIQUE INDEX uk_im_monthly_reports_author_month (author_id, report_month),
INDEX idx_im_monthly_reports_author_id (author_id, id DESC),
INDEX idx_im_monthly_reports_month_status (report_month, status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config (
id BIGINT NOT NULL AUTO_INCREMENT,
enabled TINYINT NOT NULL DEFAULT 1,
allow_anonymous TINYINT NOT NULL DEFAULT 1,
required_fields JSON,
max_length INT NOT NULL DEFAULT 5000,
attachment_policy JSON,
notice_title VARCHAR(128) NOT NULL DEFAULT '',
notice_content TEXT NOT NULL,
notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions (
id BIGINT NOT NULL AUTO_INCREMENT,
submission_id VARCHAR(64) NOT NULL,
sender_id BIGINT NOT NULL,
subject VARCHAR(200) NOT NULL,
content TEXT NOT NULL,
anonymous TINYINT NOT NULL DEFAULT 0,
attachment_file_ids JSON,
notice_version VARCHAR(64) NOT NULL DEFAULT '',
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
submitted_at DATETIME(3) NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_mailbox_submissions_submission (submission_id),
UNIQUE INDEX uk_im_mailbox_submissions_idempotency (sender_id, idempotency_key),
INDEX idx_im_mailbox_submissions_sender (sender_id, id DESC),
INDEX idx_im_mailbox_submissions_status (status, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 10, profile, privacy, app release, push, youth mode.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_user_profiles (
user_id BIGINT NOT NULL,
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
nickname VARCHAR(64) NOT NULL DEFAULT '',
birth_date DATE,
signature VARCHAR(256) NOT NULL DEFAULT '',
gender VARCHAR(16) NOT NULL DEFAULT '',
region VARCHAR(128) NOT NULL DEFAULT '',
public_account VARCHAR(64) NOT NULL DEFAULT '',
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings (
user_id BIGINT NOT NULL,
add_me_methods JSON,
profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends',
read_receipts TINYINT NOT NULL DEFAULT 1,
version INT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_app_releases (
id BIGINT NOT NULL AUTO_INCREMENT,
platform VARCHAR(32) NOT NULL,
channel VARCHAR(32) NOT NULL DEFAULT 'stable',
latest_version VARCHAR(64) NOT NULL,
min_supported_version VARCHAR(64) NOT NULL,
update_type VARCHAR(16) NOT NULL DEFAULT 'none',
release_notes TEXT NOT NULL,
store_url VARCHAR(512) NOT NULL DEFAULT '',
package_url VARCHAR(512) NOT NULL DEFAULT '',
checksum VARCHAR(128) NOT NULL DEFAULT '',
remind_after_seconds INT NOT NULL DEFAULT 0,
published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_app_releases_platform_channel (platform, channel)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens (
device_id VARCHAR(128) NOT NULL,
user_id BIGINT NOT NULL,
provider VARCHAR(32) NOT NULL,
token VARCHAR(512) NOT NULL,
app_version VARCHAR(64) NOT NULL DEFAULT '',
platform VARCHAR(32) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (device_id),
UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token),
INDEX idx_im_device_push_tokens_user (user_id, updated_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Tenant database: chapter 11, risk challenge, abuse report, feedback.
-- ============================================================
CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges (
id BIGINT NOT NULL AUTO_INCREMENT,
challenge_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
action VARCHAR(64) NOT NULL,
level VARCHAR(16) NOT NULL,
notice_code VARCHAR(64) NOT NULL,
notice_params JSON,
state VARCHAR(16) NOT NULL DEFAULT 'pending',
expires_at DATETIME(3) NOT NULL,
decision VARCHAR(16) NOT NULL DEFAULT '',
action_token_id VARCHAR(64) NOT NULL DEFAULT '',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_risk_challenges_challenge (challenge_id),
INDEX idx_im_risk_challenges_user_state (user_id, state, expires_at)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons (
id BIGINT NOT NULL AUTO_INCREMENT,
target_type VARCHAR(16) NOT NULL,
reason_code VARCHAR(64) NOT NULL,
label VARCHAR(128) NOT NULL,
requires_description TINYINT NOT NULL DEFAULT 0,
allows_evidence TINYINT NOT NULL DEFAULT 1,
sort_order INT NOT NULL DEFAULT 0,
enabled TINYINT NOT NULL DEFAULT 1,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_abuse_reasons_type_code (target_type, reason_code),
INDEX idx_im_abuse_reasons_type_order (target_type, enabled, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports (
id BIGINT NOT NULL AUTO_INCREMENT,
report_id VARCHAR(64) NOT NULL,
reporter_id BIGINT NOT NULL,
target_type VARCHAR(16) NOT NULL,
target_user_id BIGINT NOT NULL DEFAULT 0,
target_topic VARCHAR(32) NOT NULL DEFAULT '',
target_seq INT NOT NULL DEFAULT 0,
reason_code VARCHAR(64) NOT NULL,
description TEXT NOT NULL,
evidence_file_ids JSON,
snapshot JSON,
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_abuse_reports_report (report_id),
UNIQUE INDEX uk_im_abuse_reports_idempotency (reporter_id, idempotency_key),
INDEX idx_im_abuse_reports_target (target_type, target_topic, target_seq, target_user_id),
INDEX idx_im_abuse_reports_reporter (reporter_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_feedback_config (
id BIGINT NOT NULL AUTO_INCREMENT,
categories JSON,
max_length INT NOT NULL DEFAULT 1000,
attachment_policy JSON,
allow_contact TINYINT NOT NULL DEFAULT 1,
diagnostics_notice TEXT NOT NULL,
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions (
id BIGINT NOT NULL AUTO_INCREMENT,
feedback_id VARCHAR(64) NOT NULL,
user_id BIGINT NOT NULL,
category VARCHAR(64) NOT NULL,
content TEXT NOT NULL,
contact VARCHAR(128) NOT NULL DEFAULT '',
attachment_file_ids JSON,
client_context JSON,
include_diagnostics TINYINT NOT NULL DEFAULT 0,
idempotency_key VARCHAR(128) NOT NULL,
status VARCHAR(32) NOT NULL DEFAULT 'received',
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
PRIMARY KEY (id),
UNIQUE INDEX uk_im_feedback_submissions_feedback (feedback_id),
UNIQUE INDEX uk_im_feedback_submissions_idempotency (user_id, idempotency_key),
INDEX idx_im_feedback_submissions_user (user_id, id DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
-- ============================================================
-- Seed data.
-- ============================================================
INSERT INTO hotgo.im_tenant(id, code, name, tenant_desc, db_name, db_dsn, state, created_by, updated_by)
VALUES (1, 'default', 'Default Tenant', NULL, 'tinode', '', 1, 0, 0)
ON DUPLICATE KEY UPDATE
name = VALUES(name),
db_name = VALUES(db_name),
state = VALUES(state),
updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO hotgo.im_tenant_config(tenant_id, max_users, max_groups, max_group_members, created_by, updated_by)
SELECT id, 0, 0, 0, 0, 0 FROM hotgo.im_tenant WHERE code = 'default'
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission)
VALUES
('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'),
('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'),
('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'),
('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'),
('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled')
ON DUPLICATE KEY UPDATE
name = VALUES(name),
route = VALUES(route),
sort_order = VALUES(sort_order),
unread_key = VALUES(unread_key),
updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content)
VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。')
ON DUPLICATE KEY UPDATE updated_at = updated_at;
INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds)
VALUES
('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0),
('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0)
ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_abuse_reasons(target_type, reason_code, label, requires_description, allows_evidence, sort_order)
VALUES
('user', 'fraud', '诈骗欺诈', 0, 1, 10),
('user', 'harassment', '骚扰辱骂', 0, 1, 20),
('user', 'illegal', '违法违规', 1, 1, 30),
('group', 'fraud', '诈骗欺诈', 0, 1, 10),
('group', 'spam', '垃圾广告', 0, 1, 20),
('group', 'illegal', '违法违规', 1, 1, 30),
('message', 'fraud', '诈骗欺诈', 0, 1, 10),
('message', 'spam', '垃圾广告', 0, 1, 20),
('message', 'harassment', '骚扰辱骂', 0, 1, 30),
('message', 'illegal', '违法违规', 1, 1, 40)
ON DUPLICATE KEY UPDATE
label = VALUES(label),
requires_description = VALUES(requires_description),
allows_evidence = VALUES(allows_evidence),
sort_order = VALUES(sort_order),
updated_at = CURRENT_TIMESTAMP(3);
INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_policy, allow_contact, diagnostics_notice)
VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。')
ON DUPLICATE KEY UPDATE updated_at = updated_at;
-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go.
UPDATE tinode.kvmeta SET `value` = 123 WHERE `key` = 'version';