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:
yixiong
2026-07-08 20:28:32 +08:00
co-authored by Claude
parent 20bd78baa8
commit d22fdcfaad
80 changed files with 4955 additions and 0 deletions
+213
View File
@@ -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. 分发队列(替代 MQFOR 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:hashPBKDF2,见 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)