-- IM group business tables design. -- Scope: chapter 6 group chat business truth tables. -- Database: the currently selected tenant database. -- 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. -- 1. Group profile and governance truth. -- Topic/Subscription remains the communication projection; this table is the product business truth. CREATE TABLE IF NOT EXISTS 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 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 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 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 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 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 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 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 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 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 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 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;