feat: persist group business state in tables
This commit is contained in:
@@ -156,6 +156,7 @@
|
||||
| 优先级 | 增强项 | 说明 | 建议验收点 |
|
||||
| --- | --- | --- | --- |
|
||||
| P0 | 群业务独立表拆分 | 当前群公告、进群申请、治理配置、禁言记录先存 Topic Aux,后续需要拆为独立表。 | 公告、申请、治理、禁言均有 tenant/topic 维度索引,可稳定分页和审计。 |
|
||||
| P0 | 群业务表设计落地 | 已整理 `docs/IM-群聊业务表设计.sql`,后续需要按该设计接入迁移脚本和 store/adapter。 | 表结构审核通过后,群接口从 Topic Aux 切到业务表。 |
|
||||
| P0 | 群治理强事务状态机 | 邀请、申请审批、管理员变更、群主转让、禁言需在事务内同步业务表、订阅 ACL、成员数和事件。 | 并发审批/转让/禁言不会出现 ACL 与业务状态不一致。 |
|
||||
| P0 | 第六章幂等键落库 | 邀请、申请处理、公告发布、治理 PATCH、管理员批量、群主转让、禁言批量都需要统一幂等。 | 超时重试同一请求返回同一结果,不重复加人、升权或发事件。 |
|
||||
| P0 | 系统事件、Push 与 Outbox | 第六章所有成员/公告/治理变化需要生成可信群系统事件,并通过 Outbox 保证推送。 | 在线端实时刷新;模拟推送失败后可补偿且不重复。 |
|
||||
|
||||
@@ -0,0 +1,280 @@
|
||||
-- IM group business tables design.
|
||||
-- Scope: chapter 6 group chat business truth tables.
|
||||
-- Database: tenant database, default tinode.
|
||||
-- Important: one tenant owns one database, so these tables intentionally do not contain tenant_id.
|
||||
-- This SQL is for review first. Do not execute directly in production before confirming indexes and migration order.
|
||||
|
||||
CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 1. Group profile and governance truth.
|
||||
-- Topic/Subscription remains the communication projection; this table is the product business truth.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_settings (
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
owner_user_id BIGINT NOT NULL,
|
||||
name VARCHAR(128) NOT NULL DEFAULT '',
|
||||
avatar_file_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
description VARCHAR(512) NOT NULL DEFAULT '',
|
||||
group_status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
join_approval TINYINT NOT NULL DEFAULT 0,
|
||||
mute_all TINYINT NOT NULL DEFAULT 0,
|
||||
member_upload_files TINYINT NOT NULL DEFAULT 1,
|
||||
member_private_chat TINYINT NOT NULL DEFAULT 1,
|
||||
member_invite TINYINT NOT NULL DEFAULT 1,
|
||||
max_member_count INT NOT NULL DEFAULT 0 COMMENT '0 means use tenant default or unlimited',
|
||||
max_admin_count INT NOT NULL DEFAULT 10,
|
||||
member_count INT NOT NULL DEFAULT 0,
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
created_by BIGINT NOT NULL,
|
||||
dissolved_by BIGINT,
|
||||
dissolved_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 (topic),
|
||||
INDEX idx_im_group_settings_owner (owner_user_id, group_status),
|
||||
INDEX idx_im_group_settings_status_updated (group_status, updated_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 2. Group members and role truth.
|
||||
-- Role changes must update this table and the Tinode Subscription ACL in the same transaction.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_members (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
role VARCHAR(16) NOT NULL DEFAULT 'member',
|
||||
member_status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
nickname VARCHAR(64) NOT NULL DEFAULT '',
|
||||
pinned TINYINT NOT NULL DEFAULT 0,
|
||||
mute_notifications TINYINT NOT NULL DEFAULT 0,
|
||||
join_source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
||||
joined_by BIGINT,
|
||||
joined_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
left_at DATETIME(3),
|
||||
removed_by BIGINT,
|
||||
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_group_members_topic_user (topic, user_id),
|
||||
INDEX idx_im_group_members_user_status (user_id, member_status, updated_at),
|
||||
INDEX idx_im_group_members_topic_role (topic, role, member_status, id),
|
||||
INDEX idx_im_group_members_topic_status (topic, member_status, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 3. Direct invitations.
|
||||
-- If join_approval is off, accepting an invitation can directly create im_group_members + Subscription.
|
||||
-- If join_approval is on, accepting may create a pending im_group_join_requests record.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_invitations (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
invitation_id VARCHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
inviter_user_id BIGINT NOT NULL,
|
||||
target_user_id BIGINT NOT NULL,
|
||||
message VARCHAR(256) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
result VARCHAR(32) NOT NULL DEFAULT '',
|
||||
join_request_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
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_group_invitations_invitation (invitation_id),
|
||||
UNIQUE INDEX uk_im_group_invitations_idempotency (inviter_user_id, idempotency_key),
|
||||
INDEX idx_im_group_invitations_target (target_user_id, status, expires_at),
|
||||
INDEX idx_im_group_invitations_topic_status (topic, status, id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 4. Join requests.
|
||||
-- Pending users must not have a readable Subscription before approval.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_join_requests (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
request_id VARCHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
applicant_user_id BIGINT NOT NULL,
|
||||
inviter_user_id BIGINT,
|
||||
source VARCHAR(32) NOT NULL DEFAULT 'manual',
|
||||
qr_token_hash CHAR(64) NOT NULL DEFAULT '',
|
||||
message VARCHAR(256) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
reject_reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
handled_by BIGINT,
|
||||
handled_at DATETIME(3),
|
||||
expires_at DATETIME(3),
|
||||
idempotency_key VARCHAR(128) 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 (id),
|
||||
UNIQUE INDEX uk_im_group_join_requests_request (request_id),
|
||||
UNIQUE INDEX uk_im_group_join_requests_idempotency (applicant_user_id, idempotency_key),
|
||||
INDEX idx_im_group_join_requests_topic_status (topic, status, id),
|
||||
INDEX idx_im_group_join_requests_applicant (applicant_user_id, topic, status, id),
|
||||
INDEX idx_im_group_join_requests_expire (status, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 5. Group announcements.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_announcements (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
announcement_id VARCHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
author_user_id BIGINT NOT NULL,
|
||||
content TEXT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'current',
|
||||
version INT NOT NULL DEFAULT 1,
|
||||
published_at DATETIME(3) NOT NULL,
|
||||
deleted_by BIGINT,
|
||||
deleted_at DATETIME(3),
|
||||
idempotency_key VARCHAR(128) 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_group_announcements_announcement (announcement_id),
|
||||
UNIQUE INDEX uk_im_group_announcements_idempotency (topic, author_user_id, idempotency_key),
|
||||
INDEX idx_im_group_announcements_topic_status (topic, status, version DESC, id DESC),
|
||||
INDEX idx_im_group_announcements_topic_time (topic, published_at DESC, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 6. Announcement read receipts.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_announcement_reads (
|
||||
announcement_id VARCHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
read_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 (announcement_id, user_id),
|
||||
INDEX idx_im_group_announcement_reads_topic_user (topic, user_id, read_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 7. Current member mute state.
|
||||
-- Historical changes are stored in im_group_admin_logs.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_mutes (
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
user_id BIGINT NOT NULL,
|
||||
muted_by BIGINT NOT NULL,
|
||||
muted_at DATETIME(3) NOT NULL,
|
||||
expires_at DATETIME(3),
|
||||
reason VARCHAR(256) NOT NULL DEFAULT '',
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
revoked_by BIGINT,
|
||||
revoked_at DATETIME(3),
|
||||
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 (topic, user_id),
|
||||
INDEX idx_im_group_mutes_topic_status (topic, status, expires_at),
|
||||
INDEX idx_im_group_mutes_user_status (user_id, status, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 8. Group QR codes / invite tokens.
|
||||
-- qr_token should remain opaque to clients. Store only token hash.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_qrcodes (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
token_id VARCHAR(64) NOT NULL,
|
||||
token_hash CHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
created_by BIGINT NOT NULL,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
expires_at DATETIME(3) NOT NULL,
|
||||
revoked_by BIGINT,
|
||||
revoked_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_group_qrcodes_token (token_id),
|
||||
UNIQUE INDEX uk_im_group_qrcodes_hash (token_hash),
|
||||
INDEX idx_im_group_qrcodes_topic_status (topic, status, expires_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 9. Group file directory.
|
||||
-- File binary metadata still lives in the existing file table/storage. This is the group-visible index.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_files (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
group_file_id VARCHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
file_id VARCHAR(64) NOT NULL,
|
||||
source_topic VARCHAR(32) NOT NULL DEFAULT '',
|
||||
source_seq INT NOT NULL DEFAULT 0,
|
||||
uploader_id BIGINT NOT NULL,
|
||||
file_name VARCHAR(255) NOT NULL DEFAULT '',
|
||||
mime_type VARCHAR(128) NOT NULL DEFAULT '',
|
||||
file_type VARCHAR(32) NOT NULL DEFAULT 'file',
|
||||
file_size BIGINT NOT NULL DEFAULT 0,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'active',
|
||||
deleted_by BIGINT,
|
||||
deleted_at DATETIME(3),
|
||||
idempotency_key VARCHAR(128) 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_group_files_group_file (group_file_id),
|
||||
UNIQUE INDEX uk_im_group_files_topic_file (topic, file_id),
|
||||
UNIQUE INDEX uk_im_group_files_idempotency (topic, uploader_id, idempotency_key),
|
||||
INDEX idx_im_group_files_topic_type (topic, status, file_type, id DESC),
|
||||
INDEX idx_im_group_files_topic_name (topic, status, file_name),
|
||||
INDEX idx_im_group_files_uploader (uploader_id, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 10. Group operation audit logs.
|
||||
-- Store role changes, governance changes, mutes, announcements, owner transfer, dissolve, file delete, etc.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_admin_logs (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
log_id VARCHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
operator_id BIGINT NOT NULL,
|
||||
target_user_id BIGINT,
|
||||
action VARCHAR(64) NOT NULL,
|
||||
object_type VARCHAR(32) NOT NULL DEFAULT '',
|
||||
object_id VARCHAR(64) NOT NULL DEFAULT '',
|
||||
before_data JSON,
|
||||
after_data JSON,
|
||||
request_id VARCHAR(128) NOT NULL DEFAULT '',
|
||||
idempotency_key VARCHAR(128) NOT NULL DEFAULT '',
|
||||
created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
|
||||
PRIMARY KEY (id),
|
||||
UNIQUE INDEX uk_im_group_admin_logs_log (log_id),
|
||||
INDEX idx_im_group_admin_logs_topic_time (topic, id DESC),
|
||||
INDEX idx_im_group_admin_logs_operator (operator_id, id DESC),
|
||||
INDEX idx_im_group_admin_logs_object (object_type, object_id)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 11. Group operation idempotency.
|
||||
-- This can later be replaced by a global HTTP idempotency table.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_idempotency (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
user_id BIGINT NOT NULL,
|
||||
method VARCHAR(8) NOT NULL,
|
||||
route VARCHAR(256) NOT NULL,
|
||||
idempotency_key VARCHAR(128) NOT NULL,
|
||||
body_hash CHAR(64) NOT NULL,
|
||||
result_code INT NOT NULL DEFAULT 0,
|
||||
result_body JSON,
|
||||
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_group_idempotency_request (user_id, method, route, idempotency_key),
|
||||
INDEX idx_im_group_idempotency_created (created_at)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
|
||||
-- 12. Group outbox events.
|
||||
-- Transactional bridge for group.membership.updated, group.join_request.created,
|
||||
-- group.announcement.updated, group.role.updated, group.mute.updated, group.dissolved, etc.
|
||||
CREATE TABLE IF NOT EXISTS tinode.im_group_events_outbox (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT,
|
||||
event_id VARCHAR(64) NOT NULL,
|
||||
topic VARCHAR(32) NOT NULL,
|
||||
actor_user_id BIGINT NOT NULL DEFAULT 0,
|
||||
event_type VARCHAR(64) NOT NULL,
|
||||
payload JSON,
|
||||
status VARCHAR(16) NOT NULL DEFAULT 'pending',
|
||||
attempts INT NOT NULL DEFAULT 0,
|
||||
next_retry_at DATETIME(3),
|
||||
delivered_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_group_events_outbox_event (event_id),
|
||||
INDEX idx_im_group_events_outbox_pending (status, next_retry_at, id),
|
||||
INDEX idx_im_group_events_outbox_topic (topic, id DESC)
|
||||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;
|
||||
Reference in New Issue
Block a user