主体(T17 数据同步,/admin/sync 三个独立按钮): - 同步群组:get_appid_group_list 全量 Next 分页 + 逐群 get_group_info → group_mapping - 同步群成员(→用户):遍历该租户群取 MemberList → user_mapping(腾讯无全量用户API,靠群成员反推) - 同步群消息:遍历该租户群 getGroupMsg + IsFinished 滚动全量 → im_message(source=SYNC) - DB: group_mapping 加 name/owner_account/member_count/last_synced_at;user_mapping 加 nick/last_synced_at (init.sql 建表 + ADD COLUMN IF NOT EXISTS 老库升级补丁,幂等) - TencentImClient: 新增 getAppidGroupList(limit,next[,sdkAppId,secretKey]) - 已知限制:C2C单聊无全量会话API;超大群成员需换 get_group_member_info 分页 附带收尾此前未提交的改动: - 回调字段名修正(FromAccount→From_Account 等腾讯标准字段) + pickMsgRandom/Time/Type 兼容字段差异 - 租户识别重构:前缀经 TenantService.getByPrefixCode 反查 tenantId(主键雪花化与前缀解耦) - FreeMarker java.time ?string 坑修复(usage/queue Controller 预格式化) + 消息记录"全部"状态修复 - 新增项目 CLAUDE.md + .claude/memory 基建(gitignore 含密钥记忆,不进 git) Co-Authored-By: Claude <noreply@anthropic.com>
261 lines
13 KiB
SQL
261 lines
13 KiB
SQL
-- ======================================================================
|
||
-- 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;
|