-- ====================================================================== -- tencent-im-util 数据库初始化脚本(PostgreSQL) -- 库: tencen_im schema: public -- 说明: 多租户隔离,所有业务表均带 tenant_id;消息主表按月 RANGE 分区 -- ====================================================================== -- ===== 1. 租户表 ===== CREATE TABLE IF NOT EXISTS tenant ( tenant_id varchar(32) NOT NULL, tenant_name varchar(64) NOT NULL, app_key varchar(128) NOT NULL, app_secret varchar(128) NOT NULL, prefix_code varchar(16) NOT NULL, callback_url text, quota_im_qps int NOT NULL DEFAULT 50, quota_trtc_concurrent int NOT NULL DEFAULT 100, status smallint NOT NULL DEFAULT 1, -- 1=启用 0=停用 created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_tenant PRIMARY KEY (tenant_id) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_tenant_app_key ON tenant(app_key); CREATE UNIQUE INDEX IF NOT EXISTS uk_tenant_prefix ON tenant(prefix_code); -- ===== 2. 用户映射(业务用户 ↔ IM 用户) ===== CREATE TABLE IF NOT EXISTS user_mapping ( id bigint GENERATED BY DEFAULT AS IDENTITY, tenant_id varchar(32) NOT NULL, biz_user_id varchar(128) NOT NULL, im_user_id varchar(128) NOT NULL, is_default boolean NOT NULL DEFAULT false, is_global boolean NOT NULL DEFAULT false, status smallint NOT NULL DEFAULT 1, -- 1=正常 0=封禁 nick varchar(128), -- 用户昵称(T17 数据同步拉取自群成员资料) last_synced_at timestamptz, -- 最近一次同步时间(T17) created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_user_mapping PRIMARY KEY (id) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_user_tenant_biz ON user_mapping(tenant_id, biz_user_id); CREATE UNIQUE INDEX IF NOT EXISTS uk_user_im ON user_mapping(im_user_id); -- ===== 3. 群映射 ===== CREATE TABLE IF NOT EXISTS group_mapping ( id bigint GENERATED BY DEFAULT AS IDENTITY, tenant_id varchar(32) NOT NULL, biz_group_id varchar(128) NOT NULL, im_group_id varchar(128) NOT NULL, group_type varchar(16) NOT NULL DEFAULT 'Public', name varchar(256), -- 群名称(T17 数据同步拉取) owner_account varchar(128), -- 群主账号(T17 数据同步拉取) member_count int, -- 群成员数(T17 数据同步拉取) last_synced_at timestamptz, -- 最近一次同步时间(T17) created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_group_mapping PRIMARY KEY (id) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_group_tenant_biz ON group_mapping(tenant_id, biz_group_id); CREATE UNIQUE INDEX IF NOT EXISTS uk_group_im ON group_mapping(im_group_id); -- ===== 4. 消息主表(按月 RANGE 分区) ===== CREATE TABLE IF NOT EXISTS im_message ( msg_key varchar(128) NOT NULL, tenant_id varchar(32) NOT NULL, msg_time timestamptz NOT NULL, conv_type smallint NOT NULL, -- 1=C2C 2=GROUP conv_id varchar(128) NOT NULL, -- C2C=对端账号 GROUP=群ID from_account varchar(128), to_account varchar(128), group_id varchar(128), msg_type varchar(32), msg_body text, source varchar(16) NOT NULL DEFAULT 'CALLBACK', -- CALLBACK/PULL_BACK/IMPORT is_cross_tenant boolean NOT NULL DEFAULT false, dist_status smallint NOT NULL DEFAULT 0, -- 0=待分发 1=已分发 2=失败 created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_im_message PRIMARY KEY (msg_key, msg_time) ) PARTITION BY RANGE (msg_time); -- DEFAULT 分区兜底(应用启动后由 partitionCreateJob 滚动建当月分区) CREATE TABLE IF NOT EXISTS im_message_default PARTITION OF im_message DEFAULT; -- 分区索引(建在父表自动传播到所有子分区) CREATE INDEX IF NOT EXISTS idx_msg_time_brin ON im_message USING BRIN (msg_time); CREATE INDEX IF NOT EXISTS idx_msg_tenant_conv ON im_message (tenant_id, conv_id, msg_time); CREATE INDEX IF NOT EXISTS idx_msg_from ON im_message (from_account, msg_time); CREATE INDEX IF NOT EXISTS idx_msg_dist ON im_message (dist_status) WHERE dist_status = 0; -- ===== 5. 分发队列(替代 MQ,FOR UPDATE SKIP LOCKED 抢占消费) ===== CREATE TABLE IF NOT EXISTS dist_queue ( id bigserial, msg_key varchar(128) NOT NULL, tenant_id varchar(32) NOT NULL, conv_id varchar(128), target_url text, payload text, -- 分发给业务系统的回调快照 status smallint NOT NULL DEFAULT 0, -- 0=pending 1=processing 2=done 3=dead retry_count int NOT NULL DEFAULT 0, next_retry_at timestamptz NOT NULL DEFAULT now(), locked_by varchar(64), locked_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_dist_queue PRIMARY KEY (id) ); CREATE INDEX IF NOT EXISTS idx_dq_pending ON dist_queue (next_retry_at, id) WHERE status = 0; CREATE INDEX IF NOT EXISTS idx_dq_processing ON dist_queue (locked_at) WHERE status = 1; CREATE INDEX IF NOT EXISTS idx_dq_tenant ON dist_queue (tenant_id, status); -- ===== 6. 跨租户授权 ===== CREATE TABLE IF NOT EXISTS cross_tenant_grant ( grant_id bigint GENERATED BY DEFAULT AS IDENTITY, from_tenant_id varchar(32) NOT NULL, from_im_user_id varchar(128), -- NULL=该租户任意账户 to_tenant_id varchar(32) NOT NULL, to_im_user_id varchar(128), -- NULL=目标租户全员 permissions varchar(64) NOT NULL DEFAULT 'send_msg', -- send_msg,add_friend,join_group direction smallint NOT NULL DEFAULT 0, -- 0=单向 1=双向 start_at timestamptz NOT NULL DEFAULT now(), end_at timestamptz, -- NULL=长期有效 status smallint NOT NULL DEFAULT 1, -- 1=active 0=revoked 2=expired approved_by_from varchar(64), approved_by_to varchar(64), created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_cross_grant PRIMARY KEY (grant_id) ); CREATE INDEX IF NOT EXISTS idx_grant_from ON cross_tenant_grant (from_tenant_id, from_im_user_id, status); CREATE INDEX IF NOT EXISTS idx_grant_to ON cross_tenant_grant (to_tenant_id, to_im_user_id, status); -- ===== 7. 跨租户通讯审计 ===== CREATE TABLE IF NOT EXISTS cross_tenant_audit ( id bigserial, grant_id bigint, msg_key varchar(128), from_im_user_id varchar(128), to_im_user_id varchar(128), action_time timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_cross_audit PRIMARY KEY (id) ); CREATE INDEX IF NOT EXISTS idx_cross_audit_time ON cross_tenant_audit (action_time); -- ===== 8. TRTC 音视频房间 ===== CREATE TABLE IF NOT EXISTS trtc_room ( room_id bigint NOT NULL, -- 本工具统一分配(雪花) tenant_id varchar(32) NOT NULL, biz_room_id varchar(128), im_group_id varchar(128), -- 关联 IM 群(可选) status smallint NOT NULL DEFAULT 1, -- 1=进行中 0=已结束 created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_trtc_room PRIMARY KEY (room_id) ); CREATE INDEX IF NOT EXISTS idx_room_tenant_biz ON trtc_room (tenant_id, biz_room_id); -- ===== 9. 录制文件 ===== CREATE TABLE IF NOT EXISTS recording ( file_id varchar(128) NOT NULL, tenant_id varchar(32) NOT NULL, room_id bigint, cos_path text, -- 按 tenant_id 隔离目录 duration int, -- 秒 created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_recording PRIMARY KEY (file_id) ); CREATE INDEX IF NOT EXISTS idx_rec_tenant_room ON recording (tenant_id, room_id); -- ===== 10. 用量统计(计费拆账依据) ===== CREATE TABLE IF NOT EXISTS usage_stat ( id bigint GENERATED BY DEFAULT AS IDENTITY, tenant_id varchar(32) NOT NULL, stat_time timestamptz NOT NULL, stat_level smallint NOT NULL DEFAULT 1, -- 1=小时 2=天 im_msg_count bigint NOT NULL DEFAULT 0, im_dau bigint NOT NULL DEFAULT 0, trtc_duration_sec bigint NOT NULL DEFAULT 0, trtc_max_concurrent_room int NOT NULL DEFAULT 0, api_call_count bigint NOT NULL DEFAULT 0, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_usage_stat PRIMARY KEY (id) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_usage ON usage_stat (tenant_id, stat_time, stat_level); -- ===== 11. API 调用审计 ===== CREATE TABLE IF NOT EXISTS api_call_log ( id bigserial, tenant_id varchar(32) NOT NULL, api_name varchar(64) NOT NULL, params text, result text, caller varchar(64), called_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_api_call_log PRIMARY KEY (id) ); CREATE INDEX IF NOT EXISTS idx_api_tenant_time ON api_call_log (tenant_id, called_at); CREATE INDEX IF NOT EXISTS idx_api_time ON api_call_log (called_at); -- ===== 12. 补拉水位线(PG 持久,Redis 兜底) ===== CREATE TABLE IF NOT EXISTS pull_watermark ( tenant_id varchar(32) NOT NULL, conv_id varchar(128) NOT NULL, conv_type smallint NOT NULL, last_seq bigint NOT NULL DEFAULT 0, last_time timestamptz, updated_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_pull_watermark PRIMARY KEY (tenant_id, conv_id) ); -- ===== 初始化默认租户(示例,可改) ===== INSERT INTO tenant (tenant_id, tenant_name, app_key, app_secret, prefix_code, callback_url) VALUES ('sa', '系统A', 'sa_app_key_secret', 'sa_app_secret_secret', 'sa', NULL) ON CONFLICT (tenant_id) DO NOTHING; -- ===== 13. 管理后台用户(Sa-Token 登录) ===== CREATE TABLE IF NOT EXISTS admin_user ( id bigserial, username varchar(64) NOT NULL, password_hash varchar(200) NOT NULL, -- 格式 iterations:salt:hash(PBKDF2,见 PasswordUtil) role varchar(16) NOT NULL DEFAULT 'admin', status smallint NOT NULL DEFAULT 1, -- 1=启用 0=停用 created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_admin_user PRIMARY KEY (id), CONSTRAINT uk_admin_username UNIQUE (username) ); -- 默认管理员账号由应用首次启动时按 app.yml imutil.admin 配置初始化(不在此写死密码hash) -- ===== 14. 老应用配置(数据迁移源:每个老应用对应主应用一个租户前缀) ===== CREATE TABLE IF NOT EXISTS source_app ( id bigint GENERATED BY DEFAULT AS IDENTITY, app_name varchar(64) NOT NULL, -- 老应用名称 sdk_app_id varchar(32) NOT NULL, -- 老应用 SDKAppID secret_key varchar(128) NOT NULL, -- 老应用密钥(仅本工具持有,迁移拉取用) tenant_id varchar(32) NOT NULL, -- 对应主应用租户(决定 UserID 前缀),关联 tenant.tenant_id status smallint NOT NULL DEFAULT 1,-- 1=启用 0=停用 remark text, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_source_app PRIMARY KEY (id) ); CREATE UNIQUE INDEX IF NOT EXISTS uk_source_app_sdk ON source_app(sdk_app_id); -- ===== 15. 迁移任务记录(支持断点续传与校验) ===== CREATE TABLE IF NOT EXISTS migrate_task ( id bigint GENERATED BY DEFAULT AS IDENTITY, source_app_id bigint NOT NULL, -- 关联 source_app.id object_type varchar(16) NOT NULL, -- USER/GROUP/MSG/ROSTER status smallint NOT NULL DEFAULT 0, -- 0=pending 1=running 2=paused 3=done 4=failed total_count bigint NOT NULL DEFAULT 0, -- 应处理总数 processed_count bigint NOT NULL DEFAULT 0, -- 已处理数 pos_cursor varchar(256), -- 断点游标(时间戳/会话+seq,按对象类型语义不同) verify_result text, -- 校验结果 JSON(老 vs 新 条数对比) error_msg text, -- 失败原因 started_at timestamptz, finished_at timestamptz, created_at timestamptz NOT NULL DEFAULT now(), CONSTRAINT pk_migrate_task PRIMARY KEY (id) ); CREATE INDEX IF NOT EXISTS idx_migrate_task_app ON migrate_task(source_app_id, object_type, status); -- ===== T17 数据同步:老库升级补丁(ADD COLUMN IF NOT EXISTS,幂等可重跑) ===== ALTER TABLE group_mapping ADD COLUMN IF NOT EXISTS name varchar(256); ALTER TABLE group_mapping ADD COLUMN IF NOT EXISTS owner_account varchar(128); ALTER TABLE group_mapping ADD COLUMN IF NOT EXISTS member_count int; ALTER TABLE group_mapping ADD COLUMN IF NOT EXISTS last_synced_at timestamptz; ALTER TABLE user_mapping ADD COLUMN IF NOT EXISTS nick varchar(128); ALTER TABLE user_mapping ADD COLUMN IF NOT EXISTS last_synced_at timestamptz;