feat: 初始化腾讯IM分发工具骨架,含回调分发链路与历史消息补拉
- 应用骨架: Solon + MyBatis-Plus + PostgreSQL + Redis + Sa-Token - 核心链路: 回调网关 → 消息落库(幂等) → 分发队列(FOR UPDATE SKIP LOCKED) → worker消费/重试/死信 - 多租户隔离: 前缀法账号映射, 所有业务表带 tenant_id - 管理后台: FreeMarker 渲染, 租户/授权/队列/用量管理页面 - 历史消息补拉: 水位线驱动, getRoamMsg/getGroupMsg 增量拉取, 兜底回调丢失 - msg_key 统一算法(回调/补拉共享, 跨路径去重) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -0,0 +1,213 @@
|
||||
-- ======================================================================
|
||||
-- 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);
|
||||
|
||||
-- ===== 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=封禁
|
||||
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',
|
||||
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)
|
||||
Reference in New Issue
Block a user