feat: add tenant-aware IM management console
This commit is contained in:
@@ -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 当前工作区已有改动,并保留与本任务无关的用户修改。
|
||||
```
|
||||
@@ -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';
|
||||
Reference in New Issue
Block a user