From d8d1639d5b5e5d08ae83fa5dcbba2e12c770d620 Mon Sep 17 00:00:00 2001 From: richjohnrambo Date: Sat, 15 Aug 2026 15:48:17 +0800 Subject: [PATCH] fix mysql tenant database schema cleanup --- docs/IM-数据库变更汇总.sql | 735 +++++++++++++++++++++++++++++++ scripts/apply_hotgo_tenant_db.go | 79 +--- scripts/apply_sql_file.go | 49 +++ scripts/query_sql.go | 61 +++ server/db/mysql/adapter.go | 2 - server/session_test.go | 23 +- 6 files changed, 868 insertions(+), 81 deletions(-) create mode 100644 docs/IM-数据库变更汇总.sql create mode 100644 scripts/apply_sql_file.go create mode 100644 scripts/query_sql.go diff --git a/docs/IM-数据库变更汇总.sql b/docs/IM-数据库变更汇总.sql new file mode 100644 index 0000000..3d068e1 --- /dev/null +++ b/docs/IM-数据库变更汇总.sql @@ -0,0 +1,735 @@ +-- IM product database changes summary, chapters 1-11. +-- Target: +-- public backend database: hotgo +-- default tenant business database: tinode +-- Notes: +-- 1. Public tenant registry tables live in hotgo. +-- 2. Tenant-scoped business tables live in each tenant database and do not include tenant_id. +-- 3. This file is for review and manual execution. Existing legacy columns/indexes should be checked before ALTER/DROP. +-- 4. Table definitions are aligned with server/db/mysql/adapter.go plus im_verification_codes used by chapter 1 verification APIs. + +SET NAMES utf8mb4; + +CREATE DATABASE IF NOT EXISTS hotgo CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; +CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Public database: tenant registry. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS hotgo.im_tenant ( + id BIGINT NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + tenant_desc VARCHAR(256), + db_name VARCHAR(128) NOT NULL DEFAULT '', + db_dsn VARCHAR(1024) NOT NULL DEFAULT '', + state SMALLINT NOT NULL, + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE INDEX uk_im_tenant_code (code), + INDEX idx_im_tenant_db_name (db_name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS hotgo.im_tenant_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + tenant_id BIGINT NOT NULL, + max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', + created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), + created_by BIGINT NOT NULL, + updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), + updated_by BIGINT NOT NULL, + PRIMARY KEY (id), + UNIQUE INDEX uk_im_tenant_config_tenant_id (tenant_id), + CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY (tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 1, tenant-aware login, verification, security. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_user_security ( + user_id BIGINT NOT NULL, + auth_version INT NOT NULL DEFAULT 1, + password_updated_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 (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_verification_codes ( + id BIGINT NOT NULL AUTO_INCREMENT, + challenge_id VARCHAR(64) NOT NULL, + scene VARCHAR(32) NOT NULL, + action VARCHAR(64) NOT NULL DEFAULT '', + phone_hash CHAR(64) NOT NULL, + phone_mask VARCHAR(32) NOT NULL, + target_user BIGINT, + code_hash CHAR(64) NOT NULL, + verification_token_hash CHAR(64), + state SMALLINT NOT NULL, + attempts INT NOT NULL DEFAULT 0, + max_attempts INT NOT NULL, + expires_at DATETIME(3) NOT NULL, + resend_after DATETIME(3) NOT NULL, + verified_at DATETIME(3), + consumed_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_verification_challenge (challenge_id), + INDEX idx_im_verification_scene_phone_state (scene, phone_hash, state, expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + submission_id VARCHAR(64) NOT NULL, + status VARCHAR(16) NOT NULL, + name_cipher TEXT NOT NULL, + id_type VARCHAR(32) NOT NULL, + id_number_cipher TEXT NOT NULL, + id_number_hash CHAR(64) NOT NULL, + masked_name VARCHAR(64) NOT NULL, + masked_id_number VARCHAR(64) NOT NULL, + review_reason VARCHAR(256), + verified_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_realname_submission (submission_id), + INDEX idx_im_realname_user_latest (user_id, created_at), + INDEX idx_im_realname_id_hash (id_number_hash) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_action_tokens ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + token_id VARCHAR(64) NOT NULL, + action VARCHAR(64) NOT NULL, + target VARCHAR(128) NOT NULL DEFAULT '', + token_hash CHAR(64) NOT NULL, + state SMALLINT NOT NULL, + expires_at DATETIME(3) NOT NULL, + consumed_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_action_tokens_token (token_id), + INDEX idx_im_action_tokens_user_action (user_id, action, target, state, expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_youth_mode_settings ( + user_id BIGINT NOT NULL, + purpose VARCHAR(64) NOT NULL, + password_hash VARBINARY(255) NOT NULL, + enabled TINYINT NOT NULL DEFAULT 0, + policy_version INT NOT NULL DEFAULT 1, + restrictions JSON, + enabled_at DATETIME(3), + failed_attempts INT NOT NULL DEFAULT 0, + locked_until 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 (user_id, purpose) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapters 2-7, notifications, messages, friends, files, favorites. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_system_notifications ( + id BIGINT NOT NULL AUTO_INCREMENT, + user_id BIGINT NOT NULL, + event_id VARCHAR(64) NOT NULL, + event_type VARCHAR(64) NOT NULL, + title VARCHAR(128) NOT NULL, + summary VARCHAR(512) NOT NULL, + action JSON, + read_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_system_notifications_event (user_id, event_id), + INDEX idx_im_system_notifications_inbox (user_id, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_message_revisions ( + id BIGINT NOT NULL AUTO_INCREMENT, + createdat DATETIME(3) NOT NULL, + updatedat DATETIME(3) NOT NULL, + topic CHAR(25) NOT NULL, + seqid INT NOT NULL, + version INT NOT NULL, + actor BIGINT NOT NULL, + head JSON, + content JSON, + reason VARCHAR(256) NOT NULL DEFAULT '', + PRIMARY KEY (id), + UNIQUE INDEX uk_im_message_revisions_version (topic, seqid, version) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_friend_requests ( + id BIGINT NOT NULL AUTO_INCREMENT, + request_id VARCHAR(64) NOT NULL, + applicant_id BIGINT NOT NULL, + target_id BIGINT NOT NULL, + message VARCHAR(200) NOT NULL DEFAULT '', + source VARCHAR(32) NOT NULL DEFAULT '', + source_topic VARCHAR(64) NOT NULL DEFAULT '', + status VARCHAR(16) NOT NULL, + 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_friend_requests_request (request_id), + UNIQUE INDEX uk_im_friend_requests_idempotency (applicant_id, idempotency_key), + INDEX idx_im_friend_requests_pair (applicant_id, target_id, status, created_at), + INDEX idx_im_friend_requests_inbox (target_id, status, created_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_friendships ( + user_id BIGINT NOT NULL, + peer_id BIGINT NOT NULL, + status VARCHAR(16) NOT NULL, + remark VARCHAR(64) NOT NULL DEFAULT '', + topic CHAR(25) 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 (user_id, peer_id), + INDEX idx_im_friendships_peer (peer_id, status) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_user_blocks ( + blocker_id BIGINT NOT NULL, + blocked_id BIGINT 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 (blocker_id, blocked_id), + INDEX idx_im_user_blocks_blocked (blocked_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_favorites ( + id BIGINT NOT NULL AUTO_INCREMENT, + favorite_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + item_type VARCHAR(16) NOT NULL, + source_topic CHAR(25) NOT NULL, + source_seq INT NOT NULL, + snapshot JSON, + attachment_refs JSON, + availability VARCHAR(16) NOT NULL DEFAULT 'available', + idempotency_key VARCHAR(128) NOT NULL DEFAULT '', + collected_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 (id), + UNIQUE INDEX uk_im_favorites_favorite (favorite_id), + UNIQUE INDEX uk_im_favorites_source (user_id, source_topic, source_seq), + UNIQUE INDEX uk_im_favorites_idempotency (user_id, ((NULLIF(idempotency_key, '')))), + INDEX idx_im_favorites_inbox (user_id, id DESC), + INDEX idx_im_favorites_type (user_id, item_type, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- Existing Tinode tables must match the current schema for chapters 2-7: +-- topics.aux, fileuploads.etag, and filemsglinks.topic/userid are required by current code. + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'client_msg_id') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN client_msg_id VARCHAR(64) AFTER `from`', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'version') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN version INT NOT NULL DEFAULT 1 AFTER client_msg_id', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_at') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN recalled_at DATETIME(3) AFTER version', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND COLUMN_NAME = 'recalled_by') = 0, + 'ALTER TABLE tinode.messages ADD COLUMN recalled_by BIGINT AFTER recalled_at', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'messages' AND INDEX_NAME = 'messages_sender_client_mid') = 0, + 'CREATE UNIQUE INDEX messages_sender_client_mid ON tinode.messages(`from`, client_msg_id)', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- Independent-DB tenant isolation: tenant business tables must not keep tenant_id. +DROP TABLE IF EXISTS tinode.im_tenant_config; +DROP TABLE IF EXISTS tinode.im_tenant; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'auth' AND COLUMN_NAME = 'tenant_id') > 0, + 'ALTER TABLE tinode.auth DROP COLUMN tenant_id', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +SET @im_sql = IF( + (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = 'tinode' AND TABLE_NAME = 'credentials' AND COLUMN_NAME = 'tenant_id') > 0, + 'ALTER TABLE tinode.credentials DROP COLUMN tenant_id', + 'SELECT 1' +); +PREPARE im_stmt FROM @im_sql; +EXECUTE im_stmt; +DEALLOCATE PREPARE im_stmt; + +-- ============================================================ +-- Tenant database: chapter 8, realtime call records. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_call_records ( + id BIGINT NOT NULL AUTO_INCREMENT, + call_id VARCHAR(64) NOT NULL, + topic CHAR(25) NOT NULL, + seq_id INT NOT NULL, + caller_id BIGINT NOT NULL, + callee_id BIGINT NOT NULL, + call_type VARCHAR(16) NOT NULL, + status VARCHAR(32) NOT NULL, + end_reason VARCHAR(64) NOT NULL DEFAULT '', + duration_ms BIGINT NOT NULL DEFAULT 0, + started_at DATETIME(3) NOT NULL, + accepted_at DATETIME(3), + finished_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_call_records_call (call_id), + UNIQUE INDEX uk_im_call_records_topic_seq (topic, seq_id), + INDEX idx_im_call_records_caller (caller_id, id DESC), + INDEX idx_im_call_records_callee (callee_id, id DESC), + INDEX idx_im_call_records_status (status, updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 9, workbench and office collaboration. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_workbench_apps ( + id BIGINT NOT NULL AUTO_INCREMENT, + code VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + icon_file_id VARCHAR(64) NOT NULL DEFAULT '', + route VARCHAR(128) NOT NULL, + sort_order INT NOT NULL DEFAULT 0, + unread_key VARCHAR(64) NOT NULL DEFAULT '', + enabled TINYINT NOT NULL DEFAULT 1, + permission VARCHAR(64) NOT NULL DEFAULT 'enabled', + 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_workbench_apps_code (code), + INDEX idx_im_workbench_apps_enabled_order (enabled, sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_org_departments ( + id BIGINT NOT NULL AUTO_INCREMENT, + department_id VARCHAR(64) NOT NULL, + name VARCHAR(128) NOT NULL, + parent_id VARCHAR(64) NOT NULL DEFAULT 'root', + sort_order INT NOT NULL DEFAULT 0, + version INT NOT NULL DEFAULT 1, + state VARCHAR(16) NOT NULL DEFAULT 'active', + 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_org_departments_department (department_id), + INDEX idx_im_org_departments_parent (parent_id, state, sort_order, id), + INDEX idx_im_org_departments_name (name) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_org_members ( + id BIGINT NOT NULL AUTO_INCREMENT, + department_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + display_name VARCHAR(128) NOT NULL DEFAULT '', + position VARCHAR(128) NOT NULL DEFAULT '', + avatar_file_id VARCHAR(64) NOT NULL DEFAULT '', + mobile VARCHAR(32) NOT NULL DEFAULT '', + email VARCHAR(128) NOT NULL DEFAULT '', + employment_state VARCHAR(16) NOT NULL DEFAULT 'active', + contactable TINYINT NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + 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_org_members_department_user (department_id, user_id), + INDEX idx_im_org_members_department (department_id, employment_state, sort_order, id), + INDEX idx_im_org_members_user (user_id), + INDEX idx_im_org_members_display_name (display_name), + CONSTRAINT fk_im_org_members_department FOREIGN KEY (department_id) REFERENCES tinode.im_org_departments(department_id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_meetings ( + id BIGINT NOT NULL AUTO_INCREMENT, + meeting_id VARCHAR(64) NOT NULL, + organizer_id BIGINT NOT NULL, + title VARCHAR(200) NOT NULL, + start_at DATETIME(3) NOT NULL, + end_at DATETIME(3) NOT NULL, + location VARCHAR(256) NOT NULL DEFAULT '', + state VARCHAR(16) NOT NULL DEFAULT 'scheduled', + reminder_minutes JSON, + idempotency_key VARCHAR(128), + version INT NOT NULL DEFAULT 1, + canceled_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_meetings_meeting (meeting_id), + UNIQUE INDEX uk_im_meetings_idempotency (organizer_id, idempotency_key), + INDEX idx_im_meetings_organizer (organizer_id, start_at), + INDEX idx_im_meetings_state_time (state, start_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_meeting_participants ( + id BIGINT NOT NULL AUTO_INCREMENT, + meeting_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + response VARCHAR(16) NOT NULL DEFAULT 'pending', + response_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_meeting_participants_user (meeting_id, user_id), + INDEX idx_im_meeting_participants_user_time (user_id, meeting_id), + INDEX idx_im_meeting_participants_response (meeting_id, response), + CONSTRAINT fk_im_meeting_participants_meeting FOREIGN KEY (meeting_id) REFERENCES tinode.im_meetings(meeting_id) ON DELETE CASCADE +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_daily_reports ( + id BIGINT NOT NULL AUTO_INCREMENT, + report_id VARCHAR(64) NOT NULL, + author_id BIGINT NOT NULL, + report_date DATE NOT NULL, + content TEXT NOT NULL, + attachments JSON, + status VARCHAR(16) NOT NULL DEFAULT 'draft', + read_state VARCHAR(16) NOT NULL DEFAULT 'unread', + version INT NOT NULL DEFAULT 1, + submitted_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_daily_reports_report (report_id), + UNIQUE INDEX uk_im_daily_reports_author_date (author_id, report_date), + INDEX idx_im_daily_reports_author_id (author_id, id DESC), + INDEX idx_im_daily_reports_date_status (report_date, status, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_monthly_reports ( + id BIGINT NOT NULL AUTO_INCREMENT, + report_id VARCHAR(64) NOT NULL, + author_id BIGINT NOT NULL, + report_month CHAR(7) NOT NULL, + work_summary TEXT NOT NULL, + next_plan TEXT NOT NULL, + attachments JSON, + status VARCHAR(16) NOT NULL DEFAULT 'draft', + read_state VARCHAR(16) NOT NULL DEFAULT 'unread', + version INT NOT NULL DEFAULT 1, + submitted_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_monthly_reports_report (report_id), + UNIQUE INDEX uk_im_monthly_reports_author_month (author_id, report_month), + INDEX idx_im_monthly_reports_author_id (author_id, id DESC), + INDEX idx_im_monthly_reports_month_status (report_month, status, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_mailbox_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + enabled TINYINT NOT NULL DEFAULT 1, + allow_anonymous TINYINT NOT NULL DEFAULT 1, + required_fields JSON, + max_length INT NOT NULL DEFAULT 5000, + attachment_policy JSON, + notice_title VARCHAR(128) NOT NULL DEFAULT '', + notice_content TEXT NOT NULL, + notice_updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(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) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_mailbox_submissions ( + id BIGINT NOT NULL AUTO_INCREMENT, + submission_id VARCHAR(64) NOT NULL, + sender_id BIGINT NOT NULL, + subject VARCHAR(200) NOT NULL, + content TEXT NOT NULL, + anonymous TINYINT NOT NULL DEFAULT 0, + attachment_file_ids JSON, + notice_version VARCHAR(64) NOT NULL DEFAULT '', + idempotency_key VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'received', + submitted_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 (id), + UNIQUE INDEX uk_im_mailbox_submissions_submission (submission_id), + UNIQUE INDEX uk_im_mailbox_submissions_idempotency (sender_id, idempotency_key), + INDEX idx_im_mailbox_submissions_sender (sender_id, id DESC), + INDEX idx_im_mailbox_submissions_status (status, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 10, profile, privacy, app release, push, youth mode. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_user_profiles ( + user_id BIGINT NOT NULL, + avatar_file_id VARCHAR(64) NOT NULL DEFAULT '', + nickname VARCHAR(64) NOT NULL DEFAULT '', + birth_date DATE, + signature VARCHAR(256) NOT NULL DEFAULT '', + gender VARCHAR(16) NOT NULL DEFAULT '', + region VARCHAR(128) NOT NULL DEFAULT '', + public_account VARCHAR(64) 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 (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_user_privacy_settings ( + user_id BIGINT NOT NULL, + add_me_methods JSON, + profile_visibility VARCHAR(32) NOT NULL DEFAULT 'friends', + last_seen_visibility VARCHAR(32) NOT NULL DEFAULT 'friends', + read_receipts TINYINT NOT NULL DEFAULT 1, + 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 (user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_app_releases ( + id BIGINT NOT NULL AUTO_INCREMENT, + platform VARCHAR(32) NOT NULL, + channel VARCHAR(32) NOT NULL DEFAULT 'stable', + latest_version VARCHAR(64) NOT NULL, + min_supported_version VARCHAR(64) NOT NULL, + update_type VARCHAR(16) NOT NULL DEFAULT 'none', + release_notes TEXT NOT NULL, + store_url VARCHAR(512) NOT NULL DEFAULT '', + package_url VARCHAR(512) NOT NULL DEFAULT '', + checksum VARCHAR(128) NOT NULL DEFAULT '', + remind_after_seconds INT NOT NULL DEFAULT 0, + published_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(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_app_releases_platform_channel (platform, channel) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_device_push_tokens ( + device_id VARCHAR(128) NOT NULL, + user_id BIGINT NOT NULL, + provider VARCHAR(32) NOT NULL, + token VARCHAR(512) NOT NULL, + app_version VARCHAR(64) NOT NULL DEFAULT '', + platform VARCHAR(32) 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 (device_id), + UNIQUE INDEX uk_im_device_push_tokens_provider_token (provider, token), + INDEX idx_im_device_push_tokens_user (user_id, updated_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Tenant database: chapter 11, risk challenge, abuse report, feedback. +-- ============================================================ + +CREATE TABLE IF NOT EXISTS tinode.im_risk_challenges ( + id BIGINT NOT NULL AUTO_INCREMENT, + challenge_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + action VARCHAR(64) NOT NULL, + level VARCHAR(16) NOT NULL, + notice_code VARCHAR(64) NOT NULL, + notice_params JSON, + state VARCHAR(16) NOT NULL DEFAULT 'pending', + expires_at DATETIME(3) NOT NULL, + decision VARCHAR(16) NOT NULL DEFAULT '', + action_token_id VARCHAR(64) 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_risk_challenges_challenge (challenge_id), + INDEX idx_im_risk_challenges_user_state (user_id, state, expires_at) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_abuse_reasons ( + id BIGINT NOT NULL AUTO_INCREMENT, + target_type VARCHAR(16) NOT NULL, + reason_code VARCHAR(64) NOT NULL, + label VARCHAR(128) NOT NULL, + requires_description TINYINT NOT NULL DEFAULT 0, + allows_evidence TINYINT NOT NULL DEFAULT 1, + sort_order INT NOT NULL DEFAULT 0, + enabled TINYINT 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_abuse_reasons_type_code (target_type, reason_code), + INDEX idx_im_abuse_reasons_type_order (target_type, enabled, sort_order) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_abuse_reports ( + id BIGINT NOT NULL AUTO_INCREMENT, + report_id VARCHAR(64) NOT NULL, + reporter_id BIGINT NOT NULL, + target_type VARCHAR(16) NOT NULL, + target_user_id BIGINT NOT NULL DEFAULT 0, + target_topic VARCHAR(32) NOT NULL DEFAULT '', + target_seq INT NOT NULL DEFAULT 0, + reason_code VARCHAR(64) NOT NULL, + description TEXT NOT NULL, + evidence_file_ids JSON, + snapshot JSON, + idempotency_key VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'received', + 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_abuse_reports_report (report_id), + UNIQUE INDEX uk_im_abuse_reports_idempotency (reporter_id, idempotency_key), + INDEX idx_im_abuse_reports_target (target_type, target_topic, target_seq, target_user_id), + INDEX idx_im_abuse_reports_reporter (reporter_id, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_feedback_config ( + id BIGINT NOT NULL AUTO_INCREMENT, + categories JSON, + max_length INT NOT NULL DEFAULT 1000, + attachment_policy JSON, + allow_contact TINYINT NOT NULL DEFAULT 1, + diagnostics_notice TEXT 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 (id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +CREATE TABLE IF NOT EXISTS tinode.im_feedback_submissions ( + id BIGINT NOT NULL AUTO_INCREMENT, + feedback_id VARCHAR(64) NOT NULL, + user_id BIGINT NOT NULL, + category VARCHAR(64) NOT NULL, + content TEXT NOT NULL, + contact VARCHAR(128) NOT NULL DEFAULT '', + attachment_file_ids JSON, + client_context JSON, + include_diagnostics TINYINT NOT NULL DEFAULT 0, + idempotency_key VARCHAR(128) NOT NULL, + status VARCHAR(32) NOT NULL DEFAULT 'received', + 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_feedback_submissions_feedback (feedback_id), + UNIQUE INDEX uk_im_feedback_submissions_idempotency (user_id, idempotency_key), + INDEX idx_im_feedback_submissions_user (user_id, id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci; + +-- ============================================================ +-- Seed data. +-- ============================================================ + +INSERT INTO hotgo.im_tenant(id, code, name, tenant_desc, db_name, db_dsn, state, created_by, updated_by) +VALUES (1, 'default', 'Default Tenant', NULL, 'tinode', '', 1, 0, 0) +ON DUPLICATE KEY UPDATE + name = VALUES(name), + db_name = VALUES(db_name), + state = VALUES(state), + updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO hotgo.im_tenant_config(tenant_id, max_users, max_groups, max_group_members, created_by, updated_by) +SELECT id, 0, 0, 0, 0, 0 FROM hotgo.im_tenant WHERE code = 'default' +ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_workbench_apps(code, name, route, sort_order, unread_key, enabled, permission) +VALUES + ('organization', '组织架构', '/organization', 10, 'organization', 1, 'enabled'), + ('meetings', '会议通知', '/meetings', 20, 'meetings', 1, 'enabled'), + ('daily_reports', '日报', '/reports/daily', 30, 'daily_reports', 1, 'enabled'), + ('monthly_reports', '月报', '/reports/monthly', 40, 'monthly_reports', 1, 'enabled'), + ('executive_mailbox', '总经理信箱', '/executive-mailbox', 50, 'executive_mailbox', 1, 'enabled') +ON DUPLICATE KEY UPDATE + name = VALUES(name), + route = VALUES(route), + sort_order = VALUES(sort_order), + unread_key = VALUES(unread_key), + updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_mailbox_config(id, enabled, allow_anonymous, required_fields, max_length, attachment_policy, notice_title, notice_content) +VALUES (1, 1, 1, '["subject","content"]', 5000, '{"max_files":5,"purpose":"executive_mailbox"}', '总经理信箱', '请如实填写诉求,匿名提交将按企业政策保护展示身份。') +ON DUPLICATE KEY UPDATE updated_at = updated_at; + +INSERT INTO tinode.im_app_releases(platform, channel, latest_version, min_supported_version, update_type, release_notes, remind_after_seconds) +VALUES + ('ios', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0), + ('android', 'stable', '1.0.0', '1.0.0', 'none', '当前已是最新版本。', 0) +ON DUPLICATE KEY UPDATE updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_abuse_reasons(target_type, reason_code, label, requires_description, allows_evidence, sort_order) +VALUES + ('user', 'fraud', '诈骗欺诈', 0, 1, 10), + ('user', 'harassment', '骚扰辱骂', 0, 1, 20), + ('user', 'illegal', '违法违规', 1, 1, 30), + ('group', 'fraud', '诈骗欺诈', 0, 1, 10), + ('group', 'spam', '垃圾广告', 0, 1, 20), + ('group', 'illegal', '违法违规', 1, 1, 30), + ('message', 'fraud', '诈骗欺诈', 0, 1, 10), + ('message', 'spam', '垃圾广告', 0, 1, 20), + ('message', 'harassment', '骚扰辱骂', 0, 1, 30), + ('message', 'illegal', '违法违规', 1, 1, 40) +ON DUPLICATE KEY UPDATE + label = VALUES(label), + requires_description = VALUES(requires_description), + allows_evidence = VALUES(allows_evidence), + sort_order = VALUES(sort_order), + updated_at = CURRENT_TIMESTAMP(3); + +INSERT INTO tinode.im_feedback_config(id, categories, max_length, attachment_policy, allow_contact, diagnostics_notice) +VALUES (1, '["bug","suggestion","complaint","other"]', 1000, '{"max_files":5,"purpose":"feedback"}', 1, '仅在你同意时上传脱敏诊断信息。') +ON DUPLICATE KEY UPDATE updated_at = updated_at; + +-- Current MySQL adapter schema version expected by server/db/mysql/adapter.go. +UPDATE tinode.kvmeta SET `value` = 123 WHERE `key` = 'version'; diff --git a/scripts/apply_hotgo_tenant_db.go b/scripts/apply_hotgo_tenant_db.go index cbcd16e..9477807 100644 --- a/scripts/apply_hotgo_tenant_db.go +++ b/scripts/apply_hotgo_tenant_db.go @@ -57,48 +57,9 @@ func main() { CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES hotgo.im_tenant(id) ON DELETE CASCADE ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, "CREATE DATABASE IF NOT EXISTS tinode CHARACTER SET utf8mb4 COLLATE utf8mb4_0900_ai_ci", - // The main server binary only creates tinode.im_tenant/im_tenant_config - // (see createTenantRegistry in server/db/mysql/adapter.go) as part of its - // version-gated schema upgrade chain. A tinode DB that was already at the - // target schema version before this table was introduced never runs that - // step, so the ALTERs below would otherwise fail with "table doesn't - // exist". Create it here too (schema kept identical to createTenantRegistry). - `CREATE TABLE IF NOT EXISTS tinode.im_tenant( - id BIGINT NOT NULL AUTO_INCREMENT, - code VARCHAR(64) NOT NULL, - name VARCHAR(128) NOT NULL, - tenant_desc VARCHAR(256), - db_name VARCHAR(128) NOT NULL DEFAULT '', - db_dsn VARCHAR(1024) NOT NULL DEFAULT '', - state SMALLINT NOT NULL, - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - created_by BIGINT NOT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - updated_by BIGINT NOT NULL, - PRIMARY KEY(id), - UNIQUE INDEX uk_im_tenant_code(code), - INDEX idx_im_tenant_db_name(db_name) - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `CREATE TABLE IF NOT EXISTS tinode.im_tenant_config( - id BIGINT NOT NULL AUTO_INCREMENT, - tenant_id BIGINT NOT NULL, - max_users BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', - max_groups BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', - max_group_members INT UNSIGNED NOT NULL DEFAULT 0 COMMENT '0 means unlimited', - created_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), - created_by BIGINT NOT NULL, - updated_at DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), - updated_by BIGINT NOT NULL, - PRIMARY KEY(id), - UNIQUE INDEX uk_im_tenant_config_tenant_id(tenant_id), - CONSTRAINT fk_im_tenant_config_tenant FOREIGN KEY(tenant_id) REFERENCES tinode.im_tenant(id) ON DELETE CASCADE - ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci`, - `INSERT INTO tinode.im_tenant(id,code,name,tenant_desc,db_name,db_dsn,state,created_by,updated_by) - VALUES(1,'default','Default Tenant',NULL,'tinode','',1,0,0) - ON DUPLICATE KEY UPDATE db_name=VALUES(db_name), updated_at=CURRENT_TIMESTAMP(3)`, - // Same root cause as im_tenant above: createIMSecurityTables, createIMNotificationTables - // and createIMRelationTables (server/db/mysql/adapter.go) are only reached from CreateDb's - // fresh-install path, which our pre-existing tinode DB never runs. Recreate their tables here. + // createIMSecurityTables, createIMNotificationTables and createIMRelationTables + // (server/db/mysql/adapter.go) are only reached from CreateDb's fresh-install + // path, which our pre-existing tinode DB never runs. Recreate their tables here. `CREATE TABLE IF NOT EXISTS tinode.im_realname_verifications( id BIGINT NOT NULL AUTO_INCREMENT, user_id BIGINT NOT NULL, @@ -559,21 +520,10 @@ func main() { "ALTER TABLE hotgo.im_tenant ADD COLUMN tenant_desc VARCHAR(256) AFTER name", "ALTER TABLE hotgo.im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc", "ALTER TABLE hotgo.im_tenant ADD COLUMN db_dsn VARCHAR(1024) NOT NULL DEFAULT '' AFTER db_name", - "ALTER TABLE tinode.im_tenant ADD COLUMN tenant_desc VARCHAR(256) AFTER name", - "ALTER TABLE tinode.im_tenant ADD COLUMN db_name VARCHAR(128) NOT NULL DEFAULT '' AFTER tenant_desc", - "ALTER TABLE tinode.im_tenant ADD COLUMN db_dsn VARCHAR(1024) NOT NULL DEFAULT '' AFTER db_name", "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled TINYINT NOT NULL DEFAULT 0 AFTER password_hash", "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN policy_version INT NOT NULL DEFAULT 1 AFTER enabled", "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN restrictions JSON AFTER policy_version", "ALTER TABLE tinode.im_youth_mode_settings ADD COLUMN enabled_at DATETIME(3) AFTER restrictions", - // auth/credentials: server/db/mysql/adapter.go's CreateDb never defined a - // tenant_id column here even on a fresh install, yet every auth/credentials - // query unconditionally filters/inserts by tenant_id - a latent bug in the - // fresh-schema definition, not something version-skip alone explains. Backfill - // existing rows to the default tenant (id=1); adapter.go's CREATE TABLE was - // fixed alongside this to include the column on future fresh installs too. - "ALTER TABLE tinode.auth ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id", - "ALTER TABLE tinode.credentials ADD COLUMN tenant_id BIGINT NOT NULL DEFAULT 1 AFTER id", } { if _, err = db.Exec(stmt); err != nil && !strings.Contains(err.Error(), "Duplicate column name") { fatal(fmt.Errorf("exec failed: %s: %w", stmt, err)) @@ -611,9 +561,6 @@ func main() { ON DUPLICATE KEY UPDATE updated_at=CURRENT_TIMESTAMP(3)`); err != nil { fatal(err) } - if _, err = db.Exec("UPDATE tinode.im_tenant SET db_name='tinode' WHERE code='default' AND (db_name='' OR db_name IS NULL)"); err != nil { - fatal(err) - } if _, err = db.Exec(`INSERT INTO tinode.im_abuse_reasons(target_type,reason_code,label,requires_description,allows_evidence,sort_order) VALUES ('user','fraud','诈骗欺诈',0,1,10), @@ -644,13 +591,6 @@ func main() { fatal(err) } fmt.Printf("hotgo.im_tenant default: code=%s db_name=%s state=%d\n", code, dbName, state) - var columnCount int - if err = db.QueryRow(`SELECT COUNT(*) FROM information_schema.COLUMNS - WHERE TABLE_SCHEMA='tinode' AND TABLE_NAME='im_tenant' AND COLUMN_NAME IN ('tenant_desc','db_name','db_dsn')`).Scan(&columnCount); err != nil { - fatal(err) - } - fmt.Printf("tinode.im_tenant routing columns: %d/3\n", columnCount) - rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS WHERE TABLE_SCHEMA='tinode' AND COLUMN_NAME='tenant_id' ORDER BY table_name`) @@ -681,17 +621,10 @@ func fatal(v any) { os.Exit(1) } -// keptTenantIDTables lists tables whose tenant_id column must survive -// dropTenantIDColumns: server/db/mysql/adapter.go queries auth and credentials -// by tenant_id unconditionally (not gated behind tenant_db_routing), so unlike -// the newer per-tenant-DB feature tables, these two are row-tagged by design. -var keptTenantIDTables = "'auth','credentials'" - func dropTenantIDColumns(db *sql.DB, schema string) error { fks, err := queryPairs(db, `SELECT DISTINCT table_name, constraint_name FROM information_schema.KEY_COLUMN_USAGE - WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND REFERENCED_TABLE_NAME IS NOT NULL - AND table_name NOT IN (`+keptTenantIDTables+`)`, + WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND REFERENCED_TABLE_NAME IS NOT NULL`, schema) if err != nil { return err @@ -704,8 +637,7 @@ func dropTenantIDColumns(db *sql.DB, schema string) error { indexes, err := queryPairs(db, `SELECT DISTINCT table_name, index_name FROM information_schema.STATISTICS - WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND INDEX_NAME<>'PRIMARY' - AND table_name NOT IN (`+keptTenantIDTables+`)`, + WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' AND INDEX_NAME<>'PRIMARY'`, schema) if err != nil { return err @@ -718,7 +650,6 @@ func dropTenantIDColumns(db *sql.DB, schema string) error { rows, err := db.Query(`SELECT table_name FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=? AND COLUMN_NAME='tenant_id' - AND table_name NOT IN (`+keptTenantIDTables+`) ORDER BY table_name`, schema) if err != nil { return err diff --git a/scripts/apply_sql_file.go b/scripts/apply_sql_file.go new file mode 100644 index 0000000..b032c93 --- /dev/null +++ b/scripts/apply_sql_file.go @@ -0,0 +1,49 @@ +//go:build ignore + +package main + +import ( + "database/sql" + "fmt" + "os" + "strings" + + _ "github.com/go-sql-driver/mysql" +) + +func main() { + dsn := strings.TrimPrefix(os.Getenv("IM_MYSQL_DSN"), "mysql://") + sqlFile := os.Getenv("IM_SQL_FILE") + if dsn == "" || sqlFile == "" { + fatal("IM_MYSQL_DSN and IM_SQL_FILE are required") + } + if !strings.Contains(dsn, "multiStatements=") { + sep := "?" + if strings.Contains(dsn, "?") { + sep = "&" + } + dsn += sep + "multiStatements=true" + } + + body, err := os.ReadFile(sqlFile) + if err != nil { + fatal(err) + } + db, err := sql.Open("mysql", dsn) + if err != nil { + fatal(err) + } + defer db.Close() + if err = db.Ping(); err != nil { + fatal(err) + } + if _, err = db.Exec(string(body)); err != nil { + fatal(err) + } + fmt.Println("applied", sqlFile) +} + +func fatal(v any) { + fmt.Fprintln(os.Stderr, v) + os.Exit(1) +} diff --git a/scripts/query_sql.go b/scripts/query_sql.go new file mode 100644 index 0000000..fddf12a --- /dev/null +++ b/scripts/query_sql.go @@ -0,0 +1,61 @@ +//go:build ignore + +package main + +import ( + "database/sql" + "fmt" + "os" + "strings" + + _ "github.com/go-sql-driver/mysql" +) + +func main() { + dsn := strings.TrimPrefix(os.Getenv("IM_MYSQL_DSN"), "mysql://") + query := os.Getenv("IM_SQL_QUERY") + if dsn == "" || query == "" { + fatal("IM_MYSQL_DSN and IM_SQL_QUERY are required") + } + db, err := sql.Open("mysql", dsn) + if err != nil { + fatal(err) + } + defer db.Close() + rows, err := db.Query(query) + if err != nil { + fatal(err) + } + defer rows.Close() + + cols, err := rows.Columns() + if err != nil { + fatal(err) + } + fmt.Println(strings.Join(cols, "\t")) + for rows.Next() { + values := make([]sql.NullString, len(cols)) + scan := make([]any, len(cols)) + for i := range values { + scan[i] = &values[i] + } + if err := rows.Scan(scan...); err != nil { + fatal(err) + } + out := make([]string, len(cols)) + for i, value := range values { + if value.Valid { + out[i] = value.String + } + } + fmt.Println(strings.Join(out, "\t")) + } + if err := rows.Err(); err != nil { + fatal(err) + } +} + +func fatal(v any) { + fmt.Fprintln(os.Stderr, v) + os.Exit(1) +} diff --git a/server/db/mysql/adapter.go b/server/db/mysql/adapter.go index b0c07e7..a446a85 100644 --- a/server/db/mysql/adapter.go +++ b/server/db/mysql/adapter.go @@ -658,7 +658,6 @@ func (a *adapter) CreateDb(reset bool) error { if _, err = tx.Exec( `CREATE TABLE auth( id INT NOT NULL AUTO_INCREMENT, - tenant_id BIGINT NOT NULL, uname VARCHAR(32) NOT NULL, userid BIGINT NOT NULL, scheme VARCHAR(16) NOT NULL, @@ -836,7 +835,6 @@ func (a *adapter) CreateDb(reset bool) error { if _, err = tx.Exec( `CREATE TABLE credentials( id INT NOT NULL AUTO_INCREMENT, - tenant_id BIGINT NOT NULL, createdat DATETIME(3) NOT NULL, updatedat DATETIME(3) NOT NULL, deletedat DATETIME(3), diff --git a/server/session_test.go b/server/session_test.go index e82eed4..aa36bd4 100644 --- a/server/session_test.go +++ b/server/session_test.go @@ -105,15 +105,28 @@ func TestDispatchHello(t *testing.T) { } } -func TestDispatchHelloRequiresTenant(t *testing.T) { +func TestDispatchHelloDefaultsTenant(t *testing.T) { + oldTenants := store.Tenants + store.Tenants = testTenantStore{tenant: &types.Tenant{ + ID: 1, + Code: "default", + Name: "Default Tenant", + State: types.TenantStateActive, + }} + defer func() { store.Tenants = oldTenants }() + s := &Session{send: make(chan any, 1)} s.dispatch(&ClientComMessage{Hi: &MsgClientHi{Id: "123", Version: "1"}}) resp := (<-s.send).(*ServerComMessage) - if resp.Ctrl.Code != http.StatusBadRequest { - t.Fatalf("response code: expected %d, got %d", http.StatusBadRequest, resp.Ctrl.Code) + if resp.Ctrl.Code != http.StatusCreated { + t.Fatalf("response code: expected %d, got %d", http.StatusCreated, resp.Ctrl.Code) } - if !s.tenantID.IsZero() || s.ver != 0 { - t.Fatalf("invalid hello must not bind session: tenant=%d version=%d", s.tenantID, s.ver) + if s.tenantID != 1 || s.tenantCode != "default" || s.ver == 0 { + t.Fatalf("default hello did not bind session: tenant=%d code=%q version=%d", s.tenantID, s.tenantCode, s.ver) + } + tenantParams := resp.Ctrl.Params.(map[string]any)["tenant"].(map[string]any) + if tenantParams["code"] != "default" || tenantParams["name"] != "Default Tenant" { + t.Fatalf("unexpected tenant params: %#v", tenantParams) } }