chore: seed migration objects batch 3

This commit is contained in:
dev
2026-10-18 13:35:32 +08:00
parent bbb3c6a0f8
commit 8c25f7c11e
36 changed files with 9080 additions and 77 deletions
Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 24 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 19 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 18 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 17 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 16 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 27 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 28 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 26 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 15 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 21 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 20 KiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 25 KiB

@@ -0,0 +1,350 @@
-- Admin Server schema changes for the GedaDa management-console adaptation.
-- Target: MySQL 8.0+, control database `hotgo`.
-- Scope: control-plane configuration only. Do not create tenant business data here.
-- Review before execution. DDL statements auto-commit in MySQL.
SET NAMES utf8mb4;
USE `hotgo`;
-- ============================================================
-- 1. User policy groups.
-- status: 1=enabled, 2=disabled.
-- policy_json action values: allow, deny, challenge, audit_only.
-- Friend entry values: search, profile_card, group_member, qrcode,
-- invite_auto_add, recommendation.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_user_policy_groups` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global default',
`group_code` varchar(64) NOT NULL COMMENT 'Stable policy group code',
`group_name` varchar(128) NOT NULL COMMENT 'Policy group name',
`priority` int NOT NULL DEFAULT 0 COMMENT 'Higher value has higher priority',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled',
`policy_json` json NOT NULL COMMENT 'Login, friend, message, group and file policies',
`ip_whitelist` json DEFAULT NULL COMMENT 'IPv4/IPv6 or CIDR list',
`ip_blacklist` json DEFAULT NULL COMMENT 'IPv4/IPv6 or CIDR list',
`version` int unsigned NOT NULL DEFAULT 1 COMMENT 'Optimistic-lock/config version',
`remark` varchar(500) NOT NULL DEFAULT '',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint NOT NULL DEFAULT 0,
`deleted_at` datetime(3) DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_user_policy_group_code` (`tenant_id`, `group_code`),
KEY `idx_im_user_policy_group_list` (`tenant_id`, `status`, `priority`, `id`),
KEY `idx_im_user_policy_group_deleted` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM user policy groups';
CREATE TABLE IF NOT EXISTS `im_user_policy_group_members` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL DEFAULT 0,
`group_id` bigint NOT NULL,
`user_id` bigint NOT NULL COMMENT 'User ID in the tenant database',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled',
`valid_from` datetime(3) DEFAULT NULL,
`valid_until` datetime(3) DEFAULT NULL,
`remark` varchar(500) NOT NULL DEFAULT '',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint NOT NULL DEFAULT 0,
`deleted_at` datetime(3) DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_user_policy_group_member` (`group_id`, `user_id`),
KEY `idx_im_user_policy_member_user` (`tenant_id`, `user_id`, `status`),
KEY `idx_im_user_policy_member_validity` (`status`, `valid_from`, `valid_until`),
KEY `idx_im_user_policy_member_deleted` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM user-to-policy-group assignments';
-- ============================================================
-- 2. Sensitive word dictionary.
-- match_type: exact, contains, regex, pinyin.
-- action: replace, block, audit_only, manual_review.
-- risk_level: low, medium, high.
-- scene_json values include private_message, group_message, user_profile,
-- group_profile, feedback, report.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_sensitive_word_groups` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global dictionary',
`group_code` varchar(64) NOT NULL,
`group_name` varchar(128) NOT NULL,
`priority` int NOT NULL DEFAULT 0,
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled',
`remark` varchar(500) NOT NULL DEFAULT '',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint NOT NULL DEFAULT 0,
`deleted_at` datetime(3) DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_sensitive_word_group_code` (`tenant_id`, `group_code`),
KEY `idx_im_sensitive_word_group_list` (`tenant_id`, `status`, `priority`, `id`),
KEY `idx_im_sensitive_word_group_deleted` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM sensitive word groups';
CREATE TABLE IF NOT EXISTS `im_sensitive_words` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global dictionary',
`group_id` bigint NOT NULL,
`word` varchar(255) NOT NULL,
`word_hash` char(64) NOT NULL COMMENT 'SHA-256 of normalized word',
`match_type` varchar(24) NOT NULL DEFAULT 'contains',
`scene_json` json NOT NULL COMMENT 'Enabled scenes',
`action` varchar(24) NOT NULL DEFAULT 'block',
`replacement` varchar(255) NOT NULL DEFAULT '',
`risk_level` varchar(16) NOT NULL DEFAULT 'medium',
`priority` int NOT NULL DEFAULT 0,
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled',
`remark` varchar(500) NOT NULL DEFAULT '',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint NOT NULL DEFAULT 0,
`deleted_at` datetime(3) DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_sensitive_word_normalized` (`tenant_id`, `group_id`, `word_hash`),
KEY `idx_im_sensitive_word_list` (`tenant_id`, `status`, `risk_level`, `id`),
KEY `idx_im_sensitive_word_group` (`group_id`, `status`, `priority`),
KEY `idx_im_sensitive_word_deleted` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM sensitive words';
-- ============================================================
-- 3. Versioned security policy.
-- publish_status: draft, published, archived, failed.
-- The latest published row is the source of truth. The summary switches in
-- im_tenant_config are updated in the same application transaction on publish.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_security_policy` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL DEFAULT 0 COMMENT '0 means global fallback policy',
`policy_version` int unsigned NOT NULL,
`policy_name` varchar(128) NOT NULL DEFAULT '',
`publish_status` varchar(16) NOT NULL DEFAULT 'draft',
`password_policy` json DEFAULT NULL,
`login_rate_limit` json DEFAULT NULL,
`sensitive_word_policy` json DEFAULT NULL,
`file_review_policy` json DEFAULT NULL,
`realname_policy` json DEFAULT NULL,
`risk_challenge_policy` json DEFAULT NULL,
`global_friend_enabled` tinyint(1) NOT NULL DEFAULT 1 COMMENT 'Backend master switch for every add-friend entry',
`friend_disabled_policy` json DEFAULT NULL COMMENT 'Profile masking, QR and group-member restrictions',
`message_curfew_enabled` tinyint(1) NOT NULL DEFAULT 0 COMMENT 'Backend master switch for message curfew',
`remark` varchar(500) NOT NULL DEFAULT '',
`published_at` datetime(3) DEFAULT NULL,
`published_by` bigint DEFAULT NULL,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint NOT NULL DEFAULT 0,
`deleted_at` datetime(3) DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_security_policy_version` (`tenant_id`, `policy_version`),
KEY `idx_im_security_policy_publish` (`tenant_id`, `publish_status`, `published_at`),
KEY `idx_im_security_policy_deleted` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Versioned IM security policies';
CREATE TABLE IF NOT EXISTS `im_message_curfew_rules` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL DEFAULT 0,
`policy_id` bigint NOT NULL,
`rule_code` varchar(64) NOT NULL,
`rule_name` varchar(128) NOT NULL,
`date_type` varchar(16) NOT NULL DEFAULT 'daily' COMMENT 'daily, weekday, weekend, custom',
`custom_dates` json DEFAULT NULL COMMENT 'YYYY-MM-DD values for custom date type',
`weekdays` json DEFAULT NULL COMMENT 'ISO weekdays 1 through 7',
`timezone` varchar(64) NOT NULL DEFAULT 'Asia/Shanghai',
`start_time` time NOT NULL,
`end_time` time NOT NULL COMMENT 'Less than start_time means crossing midnight',
`scope` varchar(24) NOT NULL DEFAULT 'private_message' COMMENT 'private_message or group_message',
`whitelist_user_ids` json DEFAULT NULL,
`whitelist_group_ids` json DEFAULT NULL,
`message` varchar(255) NOT NULL DEFAULT '当前时段暂不能发送消息',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled',
`sort` int NOT NULL DEFAULT 0,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint NOT NULL DEFAULT 0,
`deleted_at` datetime(3) DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_message_curfew_rule` (`policy_id`, `rule_code`),
KEY `idx_im_message_curfew_policy` (`tenant_id`, `policy_id`, `status`, `sort`),
KEY `idx_im_message_curfew_deleted` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Message curfew rules';
CREATE TABLE IF NOT EXISTS `im_security_ip_rules` (
`id` bigint NOT NULL AUTO_INCREMENT,
`tenant_id` bigint NOT NULL DEFAULT 0,
`rule_type` varchar(16) NOT NULL COMMENT 'whitelist or blacklist',
`ip_cidr` varchar(64) NOT NULL COMMENT 'IPv4, IPv6 or CIDR',
`action` varchar(24) NOT NULL DEFAULT 'block' COMMENT 'allow, challenge, block, audit_only',
`scene_json` json NOT NULL COMMENT 'login, register, message, upload and other scenes',
`risk_level` varchar(16) NOT NULL DEFAULT 'high',
`status` tinyint NOT NULL DEFAULT 1 COMMENT '1 enabled, 2 disabled',
`expires_at` datetime(3) DEFAULT NULL,
`remark` varchar(500) NOT NULL DEFAULT '',
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_by` bigint NOT NULL DEFAULT 0,
`updated_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3),
`updated_by` bigint NOT NULL DEFAULT 0,
`deleted_at` datetime(3) DEFAULT NULL,
`deleted_by` bigint DEFAULT NULL,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_security_ip_rule` (`tenant_id`, `rule_type`, `ip_cidr`),
KEY `idx_im_security_ip_rule_list` (`tenant_id`, `status`, `rule_type`, `id`),
KEY `idx_im_security_ip_rule_expiry` (`status`, `expires_at`),
KEY `idx_im_security_ip_rule_deleted` (`deleted_at`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='IM IP whitelist and blacklist rules';
-- ============================================================
-- 4. Lightweight published-policy summary in im_tenant_config.
-- These columns are read optimizations, not the policy source of truth.
-- The conditional ALTER blocks are safe to rerun on MySQL 8.0.
-- ============================================================
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_tenant_config'
AND COLUMN_NAME = 'default_policy_group_id') = 0,
'ALTER TABLE `im_tenant_config` ADD COLUMN `default_policy_group_id` BIGINT DEFAULT NULL COMMENT ''Default user policy group'' AFTER `max_group_members`',
'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 = DATABASE() AND TABLE_NAME = 'im_tenant_config'
AND COLUMN_NAME = 'security_policy_version') = 0,
'ALTER TABLE `im_tenant_config` ADD COLUMN `security_policy_version` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''Published security policy version'' AFTER `default_policy_group_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 = DATABASE() AND TABLE_NAME = 'im_tenant_config'
AND COLUMN_NAME = 'global_friend_enabled') = 0,
'ALTER TABLE `im_tenant_config` ADD COLUMN `global_friend_enabled` TINYINT(1) NOT NULL DEFAULT 1 COMMENT ''Published global friend switch'' AFTER `security_policy_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 = DATABASE() AND TABLE_NAME = 'im_tenant_config'
AND COLUMN_NAME = 'message_curfew_enabled') = 0,
'ALTER TABLE `im_tenant_config` ADD COLUMN `message_curfew_enabled` TINYINT(1) NOT NULL DEFAULT 0 COMMENT ''Published message curfew switch'' AFTER `global_friend_enabled`',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- ============================================================
-- 5. Invitation-code compatibility for the 6-digit target format.
-- Existing 5-letter codes remain stored and valid during the application
-- rollout. New code generation must switch to exactly six ASCII digits, while
-- Admin and IM Server temporarily accept both legacy 5-letter and new 6-digit
-- values. max_uses=0 preserves the current unlimited-use behavior.
-- ============================================================
SET @im_sql = IF(
(SELECT CHARACTER_MAXIMUM_LENGTH FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_registration_invite_codes'
AND COLUMN_NAME = 'code') = 5,
'ALTER TABLE `im_registration_invite_codes` MODIFY COLUMN `code` CHAR(6) CHARACTER SET ascii COLLATE ascii_bin NOT NULL',
'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 = DATABASE() AND TABLE_NAME = 'im_registration_invite_codes'
AND COLUMN_NAME = 'max_uses') = 0,
'ALTER TABLE `im_registration_invite_codes` ADD COLUMN `max_uses` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''0 means unlimited'' AFTER `expires_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.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_registration_invite_batches'
AND COLUMN_NAME = 'max_uses_per_code') = 0,
'ALTER TABLE `im_registration_invite_batches` ADD COLUMN `max_uses_per_code` INT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''0 means unlimited'' AFTER `quantity`',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- ============================================================
-- 6. Safe global fallback policy.
-- Tenant-specific published policies override tenant_id=0.
-- This keeps existing behavior permissive until an administrator publishes
-- the first tenant policy.
-- ============================================================
INSERT INTO `im_security_policy` (
`tenant_id`, `policy_version`, `policy_name`, `publish_status`,
`global_friend_enabled`, `friend_disabled_policy`,
`message_curfew_enabled`, `remark`, `published_at`, `published_by`,
`created_by`, `updated_by`
)
SELECT
0, 1, '全局默认安全策略', 'published',
1,
JSON_OBJECT(
'hide_profile', TRUE,
'block_qrcode', TRUE,
'block_group_member_entry', TRUE,
'blocked_entries', JSON_ARRAY(
'search', 'profile_card', 'group_member',
'qrcode', 'invite_auto_add', 'recommendation'
),
'message', '当前时段暂不允许添加好友'
),
0, '系统初始化:保持原有好友和消息行为', CURRENT_TIMESTAMP(3), 0, 0, 0
WHERE NOT EXISTS (
SELECT 1 FROM `im_security_policy`
WHERE `tenant_id` = 0 AND `publish_status` = 'published' AND `deleted_at` IS NULL
);
UPDATE `im_security_policy`
SET `friend_disabled_policy` = JSON_SET(
`friend_disabled_policy`,
'$.blocked_entries',
JSON_ARRAY('search', 'profile_card', 'group_member', 'qrcode', 'invite_auto_add', 'recommendation')
)
WHERE `tenant_id` = 0
AND `publish_status` = 'published'
AND `deleted_at` IS NULL
AND JSON_CONTAINS_PATH(`friend_disabled_policy`, 'one', '$.blocked_entries') = 0;
-- Post-deployment checks (read-only):
-- SELECT tenant_id, group_code, status, priority FROM im_user_policy_groups;
-- SELECT tenant_id, policy_version, publish_status,
-- global_friend_enabled, message_curfew_enabled
-- FROM im_security_policy ORDER BY tenant_id, policy_version DESC;
-- SELECT tenant_id, policy_id, rule_code, date_type, start_time, end_time, scope
-- FROM im_message_curfew_rules WHERE deleted_at IS NULL;
@@ -0,0 +1,464 @@
-- IM Server schema changes for the GedaDa management-console adaptation.
-- Target: MySQL 8.0+, one independent tenant business database.
-- Default local tenant database: `tinode`.
-- IMPORTANT: execute this migration once for every tenant database. Tenant
-- business tables intentionally do not contain tenant_id.
-- Review before execution. DDL statements auto-commit in MySQL.
SET NAMES utf8mb4;
USE `tinode`;
-- ============================================================
-- 0. User account number.
-- account_no is the short user number shown in Admin. The real primary key
-- im_users.id remains the internal Tinode/Snowflake user ID.
-- Existing users are backfilled from 1000 by created time; new users are
-- assigned by IM Server in the user creation transaction.
-- ============================================================
SET @sql := IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_users' AND COLUMN_NAME = 'account_no') = 0,
'ALTER TABLE `im_users` ADD COLUMN `account_no` int unsigned NOT NULL DEFAULT 0 COMMENT ''用户编号'' AFTER `id`',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
SET @im_user_account_no := (
SELECT GREATEST(COALESCE(MAX(`account_no`), 999), 999)
FROM `im_users`
WHERE `account_no` > 0
);
UPDATE `im_users` AS u
JOIN (
SELECT ordered.`id`, (@im_user_account_no := @im_user_account_no + 1) AS next_no
FROM (
SELECT `id`
FROM `im_users`
WHERE `account_no` = 0
ORDER BY `createdat`, `id`
) AS ordered
) AS seq ON seq.`id` = u.`id`
SET u.`account_no` = seq.next_no
WHERE u.`account_no` = 0;
SET @sql := IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_users' AND INDEX_NAME = 'uk_im_users_account_no') = 0,
'CREATE UNIQUE INDEX `uk_im_users_account_no` ON `im_users` (`account_no`)',
'SELECT 1'
);
PREPARE stmt FROM @sql;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;
-- ============================================================
-- 1. Applied runtime policy snapshots.
-- policy_type: security, user_policy_groups, sensitive_words.
-- The application uses the latest row with apply_status=applied and also keeps
-- an in-memory cache. This table makes restart and Kafka replay deterministic.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_runtime_policy_snapshots` (
`id` bigint NOT NULL AUTO_INCREMENT,
`policy_type` varchar(32) NOT NULL,
`policy_version` bigint unsigned NOT NULL,
`payload` json NOT NULL,
`payload_sha256` char(64) NOT NULL,
`source_event_id` varchar(96) DEFAULT NULL COMMENT 'NULL for a manual bootstrap snapshot',
`apply_status` varchar(16) NOT NULL DEFAULT 'applied' COMMENT 'applied or failed',
`apply_error` varchar(1000) NOT NULL DEFAULT '',
`applied_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_runtime_policy_version` (`policy_type`, `policy_version`),
UNIQUE KEY `uk_im_runtime_policy_event` (`source_event_id`),
KEY `idx_im_runtime_policy_latest` (`policy_type`, `apply_status`, `policy_version` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Last applied Admin policy payloads';
-- ============================================================
-- 2. User login history and login failure history.
-- Never store plaintext passwords, verification codes, access tokens or refresh
-- tokens. account_hash is SHA-256 of the normalized login account; account_mask
-- is the value shown in Admin.
-- login_type: password, sms_code, sso, qr_code, token_refresh.
-- event_type: login, refresh, logout, kicked, session_expired.
-- failure_type: password_error, code_error, account_not_found,
-- account_disabled, rate_limited, risk_blocked.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_user_login_logs` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint NOT NULL,
`username` varchar(64) NOT NULL DEFAULT '',
`account_type` varchar(24) NOT NULL DEFAULT '',
`account_hash` char(64) NOT NULL DEFAULT '',
`account_mask` varchar(128) NOT NULL DEFAULT '',
`login_type` varchar(24) NOT NULL,
`event_type` varchar(24) NOT NULL DEFAULT 'login',
`session_id` varchar(96) NOT NULL DEFAULT '' COMMENT 'Opaque session identifier, never an auth token',
`device_id` varchar(255) NOT NULL DEFAULT '',
`platform` varchar(32) NOT NULL DEFAULT 'unknown',
`app_version` varchar(32) NOT NULL DEFAULT '',
`ip` varchar(64) NOT NULL DEFAULT '',
`region` varchar(128) NOT NULL DEFAULT '',
`user_agent` varchar(512) NOT NULL DEFAULT '',
`risk_level` varchar(16) NOT NULL DEFAULT 'low',
`request_id` varchar(64) NOT NULL DEFAULT '',
`login_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`logout_at` datetime(3) DEFAULT NULL,
`metadata` json DEFAULT NULL,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
KEY `idx_im_user_login_user_time` (`user_id`, `login_at` DESC, `id` DESC),
KEY `idx_im_user_login_ip_time` (`ip`, `login_at` DESC, `id` DESC),
KEY `idx_im_user_login_device_time` (`device_id`(64), `login_at` DESC),
KEY `idx_im_user_login_request` (`request_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='User login and session event history';
CREATE TABLE IF NOT EXISTS `im_login_failure_logs` (
`id` bigint NOT NULL AUTO_INCREMENT,
`user_id` bigint DEFAULT NULL COMMENT 'NULL when the account cannot be resolved',
`account_type` varchar(24) NOT NULL DEFAULT '',
`account_hash` char(64) NOT NULL DEFAULT '',
`account_mask` varchar(128) NOT NULL DEFAULT '',
`failure_type` varchar(32) NOT NULL,
`reason` varchar(96) NOT NULL DEFAULT '' COMMENT 'Stable backend reason code',
`detail` varchar(500) NOT NULL DEFAULT '' COMMENT 'Sanitized detail without credentials',
`ip` varchar(64) NOT NULL DEFAULT '',
`region` varchar(128) NOT NULL DEFAULT '',
`device_id` varchar(255) NOT NULL DEFAULT '',
`platform` varchar(32) NOT NULL DEFAULT 'unknown',
`app_version` varchar(32) NOT NULL DEFAULT '',
`user_agent` varchar(512) NOT NULL DEFAULT '',
`risk_level` varchar(16) NOT NULL DEFAULT 'medium',
`risk_action` varchar(24) NOT NULL DEFAULT 'audit_only',
`request_id` varchar(64) NOT NULL DEFAULT '',
`attempt_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
`metadata` json DEFAULT NULL,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
KEY `idx_im_login_failure_account_time` (`account_hash`, `attempt_at` DESC, `id` DESC),
KEY `idx_im_login_failure_user_time` (`user_id`, `attempt_at` DESC, `id` DESC),
KEY `idx_im_login_failure_ip_time` (`ip`, `attempt_at` DESC, `id` DESC),
KEY `idx_im_login_failure_type_time` (`failure_type`, `attempt_at` DESC, `id` DESC),
KEY `idx_im_login_failure_request` (`request_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Authentication failure and risk-block history';
-- ============================================================
-- 3. Message administration actions.
-- Messages remain in im_messages/im_group_messages. This table is an immutable
-- action overlay so hide/delete can be audited and restored.
-- conversation_type: private, group.
-- action: hide, unhide, delete, restore.
-- action_status: pending, success, failed, dead.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_message_admin_actions` (
`id` bigint NOT NULL AUTO_INCREMENT,
`action_id` varchar(96) NOT NULL,
`command_id` varchar(96) NOT NULL,
`conversation_type` varchar(16) NOT NULL,
`topic` varchar(64) NOT NULL,
`seqid` bigint NOT NULL,
`sender_user_id` bigint DEFAULT NULL,
`action` varchar(16) NOT NULL,
`action_status` varchar(16) NOT NULL DEFAULT 'pending',
`reason` varchar(500) NOT NULL DEFAULT '',
`actor_admin_id` bigint NOT NULL DEFAULT 0,
`request_id` varchar(64) NOT NULL DEFAULT '',
`before_snapshot` json DEFAULT NULL,
`after_snapshot` json DEFAULT NULL,
`failure_reason` varchar(96) NOT NULL DEFAULT '',
`failure_message` varchar(1000) NOT NULL DEFAULT '',
`processed_at` datetime(3) DEFAULT 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 KEY `uk_im_message_admin_action_id` (`action_id`),
UNIQUE KEY `uk_im_message_admin_command` (`command_id`),
KEY `idx_im_message_admin_target` (`conversation_type`, `topic`, `seqid`, `id` DESC),
KEY `idx_im_message_admin_status` (`action_status`, `created_at`, `id`),
KEY `idx_im_message_admin_actor` (`actor_admin_id`, `created_at` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Auditable Admin message actions';
-- ============================================================
-- 4. Sensitive-word hits and policy decisions.
-- Store only a digest and a short masked excerpt. Do not duplicate complete
-- private messages in log tables.
-- policy decision reasons include FRIEND_DISABLED and MESSAGE_CURFEW_ACTIVE.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_sensitive_word_hits` (
`id` bigint NOT NULL AUTO_INCREMENT,
`word_id` bigint NOT NULL DEFAULT 0 COMMENT 'Admin control-database word ID',
`word_group_id` bigint NOT NULL DEFAULT 0,
`word_snapshot` varchar(255) NOT NULL DEFAULT '' COMMENT 'Masked word snapshot',
`scene` varchar(32) NOT NULL,
`user_id` bigint NOT NULL DEFAULT 0,
`target_user_id` bigint DEFAULT NULL,
`topic` varchar(64) NOT NULL DEFAULT '',
`seqid` bigint DEFAULT NULL,
`content_sha256` char(64) NOT NULL DEFAULT '',
`matched_excerpt` varchar(500) NOT NULL DEFAULT '',
`match_type` varchar(24) NOT NULL DEFAULT 'contains',
`action` varchar(24) NOT NULL,
`risk_level` varchar(16) NOT NULL DEFAULT 'medium',
`request_id` varchar(64) NOT NULL DEFAULT '',
`metadata` json DEFAULT NULL,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
KEY `idx_im_sensitive_hit_scene_time` (`scene`, `created_at` DESC, `id` DESC),
KEY `idx_im_sensitive_hit_user_time` (`user_id`, `created_at` DESC, `id` DESC),
KEY `idx_im_sensitive_hit_target` (`topic`, `seqid`, `id` DESC),
KEY `idx_im_sensitive_hit_word` (`word_id`, `created_at` DESC),
KEY `idx_im_sensitive_hit_request` (`request_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Sensitive-word match events';
CREATE TABLE IF NOT EXISTS `im_policy_decision_logs` (
`id` bigint NOT NULL AUTO_INCREMENT,
`policy_type` varchar(32) NOT NULL COMMENT 'security or user_policy_group',
`policy_version` bigint unsigned NOT NULL DEFAULT 0,
`policy_group_id` bigint NOT NULL DEFAULT 0,
`scene` varchar(64) NOT NULL COMMENT 'friend_request, qrcode_resolve, private_message and so on',
`action` varchar(24) NOT NULL COMMENT 'deny, challenge or audit_only',
`reason_code` varchar(96) NOT NULL COMMENT 'For example FRIEND_DISABLED or MESSAGE_CURFEW_ACTIVE',
`user_id` bigint NOT NULL DEFAULT 0,
`target_user_id` bigint DEFAULT NULL,
`topic` varchar(64) NOT NULL DEFAULT '',
`request_id` varchar(64) NOT NULL DEFAULT '',
`ip` varchar(64) NOT NULL DEFAULT '',
`metadata` json DEFAULT NULL,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
KEY `idx_im_policy_decision_user_time` (`user_id`, `created_at` DESC, `id` DESC),
KEY `idx_im_policy_decision_reason_time` (`reason_code`, `created_at` DESC, `id` DESC),
KEY `idx_im_policy_decision_scene_time` (`scene`, `created_at` DESC, `id` DESC),
KEY `idx_im_policy_decision_request` (`request_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Runtime policy deny/challenge decisions';
-- ============================================================
-- 5. Group event history and daily IP/account statistics.
-- im_group_events_outbox remains the delivery table; this log is the queryable
-- business history after the operation result is known.
-- ============================================================
CREATE TABLE IF NOT EXISTS `im_group_event_logs` (
`id` bigint NOT NULL AUTO_INCREMENT,
`event_id` varchar(96) NOT NULL,
`topic` varchar(64) NOT NULL,
`group_id` bigint DEFAULT NULL,
`event_type` varchar(64) NOT NULL,
`actor_user_id` bigint NOT NULL DEFAULT 0,
`target_user_id` bigint DEFAULT NULL,
`result` varchar(16) NOT NULL DEFAULT 'success',
`reason` varchar(500) NOT NULL DEFAULT '',
`request_id` varchar(64) NOT NULL DEFAULT '',
`before_snapshot` json DEFAULT NULL,
`after_snapshot` json DEFAULT NULL,
`metadata` json DEFAULT NULL,
`created_at` datetime(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3),
PRIMARY KEY (`id`),
UNIQUE KEY `uk_im_group_event_log_event` (`event_id`),
KEY `idx_im_group_event_topic_time` (`topic`, `created_at` DESC, `id` DESC),
KEY `idx_im_group_event_type_time` (`event_type`, `created_at` DESC, `id` DESC),
KEY `idx_im_group_event_actor_time` (`actor_user_id`, `created_at` DESC, `id` DESC),
KEY `idx_im_group_event_request` (`request_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Queryable group event audit history';
CREATE TABLE IF NOT EXISTS `im_ip_account_stats_daily` (
`id` bigint NOT NULL AUTO_INCREMENT,
`stat_date` date NOT NULL,
`ip` varchar(64) NOT NULL,
`region` varchar(128) NOT NULL DEFAULT '',
`account_count` int unsigned NOT NULL DEFAULT 0,
`user_count` int unsigned NOT NULL DEFAULT 0,
`login_success_count` int unsigned NOT NULL DEFAULT 0,
`login_failure_count` int unsigned NOT NULL DEFAULT 0,
`first_seen_at` datetime(3) DEFAULT NULL,
`last_seen_at` datetime(3) DEFAULT NULL,
`risk_level` varchar(16) NOT NULL DEFAULT 'low',
`risk_action` varchar(24) NOT NULL DEFAULT 'audit_only',
`sample_user_ids` json DEFAULT NULL COMMENT 'Bounded sample only, not an unlimited array',
`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 KEY `uk_im_ip_account_stats_day_ip` (`stat_date`, `ip`),
KEY `idx_im_ip_account_stats_ip_day` (`ip`, `stat_date` DESC),
KEY `idx_im_ip_account_stats_risk_day` (`risk_level`, `stat_date` DESC, `id` DESC)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='Daily IP and account aggregation';
-- ============================================================
-- 6. Compatible enhancements to existing tables.
-- Every ALTER is guarded by information_schema and can be rerun.
-- ============================================================
-- Message management queries need topic/time and sender/time indexes.
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_messages'
AND INDEX_NAME = 'idx_im_messages_topic_created') = 0,
'ALTER TABLE `im_messages` ADD INDEX `idx_im_messages_topic_created` (`topic`, `createdat`, `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.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_messages'
AND INDEX_NAME = 'idx_im_messages_from_created') = 0,
'ALTER TABLE `im_messages` ADD INDEX `idx_im_messages_from_created` (`from`, `createdat`, `id`)',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- Extend the existing Tinode device record for device and login-history views.
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_devices'
AND COLUMN_NAME = 'firstseen') = 0,
'ALTER TABLE `im_devices` ADD COLUMN `firstseen` DATETIME(3) DEFAULT NULL AFTER `platform`',
'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 = DATABASE() AND TABLE_NAME = 'im_devices'
AND COLUMN_NAME = 'lastip') = 0,
'ALTER TABLE `im_devices` ADD COLUMN `lastip` VARCHAR(64) NOT NULL DEFAULT '''' AFTER `lastseen`',
'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 = DATABASE() AND TABLE_NAME = 'im_devices'
AND COLUMN_NAME = 'useragent') = 0,
'ALTER TABLE `im_devices` ADD COLUMN `useragent` VARCHAR(512) NOT NULL DEFAULT '''' AFTER `lastip`',
'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 = DATABASE() AND TABLE_NAME = 'im_devices'
AND COLUMN_NAME = 'appversion') = 0,
'ALTER TABLE `im_devices` ADD COLUMN `appversion` VARCHAR(32) NOT NULL DEFAULT '''' AFTER `useragent`',
'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 = DATABASE() AND TABLE_NAME = 'im_devices'
AND COLUMN_NAME = 'status') = 0,
'ALTER TABLE `im_devices` ADD COLUMN `status` VARCHAR(16) NOT NULL DEFAULT ''active'' AFTER `appversion`',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- Feedback handling fields required by the Admin processing workflow.
SET @im_sql = IF(
(SELECT COUNT(*) FROM information_schema.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions'
AND COLUMN_NAME = 'assigned_to') = 0,
'ALTER TABLE `im_feedback_submissions` ADD COLUMN `assigned_to` BIGINT DEFAULT NULL AFTER `status`',
'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 = DATABASE() AND TABLE_NAME = 'im_feedback_submissions'
AND COLUMN_NAME = 'handled_by') = 0,
'ALTER TABLE `im_feedback_submissions` ADD COLUMN `handled_by` BIGINT DEFAULT NULL AFTER `assigned_to`',
'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 = DATABASE() AND TABLE_NAME = 'im_feedback_submissions'
AND COLUMN_NAME = 'handled_at') = 0,
'ALTER TABLE `im_feedback_submissions` ADD COLUMN `handled_at` DATETIME(3) DEFAULT NULL AFTER `handled_by`',
'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 = DATABASE() AND TABLE_NAME = 'im_feedback_submissions'
AND COLUMN_NAME = 'resolution') = 0,
'ALTER TABLE `im_feedback_submissions` ADD COLUMN `resolution` VARCHAR(1000) NOT NULL DEFAULT '''' AFTER `handled_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.COLUMNS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_feedback_submissions'
AND COLUMN_NAME = 'admin_remark') = 0,
'ALTER TABLE `im_feedback_submissions` ADD COLUMN `admin_remark` VARCHAR(1000) NOT NULL DEFAULT '''' AFTER `resolution`',
'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 = DATABASE() AND TABLE_NAME = 'im_feedback_submissions'
AND INDEX_NAME = 'idx_im_feedback_status_time') = 0,
'ALTER TABLE `im_feedback_submissions` ADD INDEX `idx_im_feedback_status_time` (`status`, `created_at`, `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.STATISTICS
WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'im_audit_logs'
AND INDEX_NAME = 'idx_im_audit_logs_ip_time') = 0,
'ALTER TABLE `im_audit_logs` ADD INDEX `idx_im_audit_logs_ip_time` (`ip`, `created_at`, `id`)',
'SELECT 1'
);
PREPARE im_stmt FROM @im_sql;
EXECUTE im_stmt;
DEALLOCATE PREPARE im_stmt;
-- Post-deployment checks (read-only):
-- SELECT policy_type, policy_version, apply_status, applied_at
-- FROM im_runtime_policy_snapshots ORDER BY policy_type, policy_version DESC;
-- SELECT reason_code, scene, COUNT(*) AS deny_count
-- FROM im_policy_decision_logs
-- WHERE created_at >= CURRENT_DATE
-- GROUP BY reason_code, scene;
-- SELECT stat_date, ip, account_count, login_failure_count, risk_level
-- FROM im_ip_account_stats_daily ORDER BY stat_date DESC, login_failure_count DESC;
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,570 @@
# 服务端消息加密与内容审核改造文档
> 文档状态:待开发
> 编制日期:2026-09-13
> 适用项目:`im_manager`(Admin Server)、`im_server`(IM Server)
> 本文档是后续数据库设计、后端开发、联调、迁移和验收的共同依据。
## 1. 建设目标
本次改造同时建设两项能力:
1. 内容审核:IM Server 在保存或修改内容前,根据商户策略执行本地敏感词检查和阿里云文本内容审核。
2. 消息加密存储:审核通过的消息在 IM Server 内部使用 AES-256-GCM 加密,国内 IM 业务库只保存密文;经授权的后台查询由 IM Server 解密后返回明文。
本次不做以下内容:
- 不做客户端 AES 二次加密。
- 不做端到端加密(E2EE)。
- 不改变现有 HTTPS/WSS 传输协议。
- 不允许 Admin Server 直接读取 IM 数据库并自行解密。
- 不在日志、Kafka 事件、死信或监控标签中保存消息明文。
## 2. 安全边界和部署前提
### 2.1 部署分工
| 区域 | 系统 | 保存数据 |
| --- | --- | --- |
| 国外 | HotGo / Admin Server | 商户审核策略、阿里云配置、字典根密钥、商户消息密钥 |
| 国内 | IM Server | 用户、会话、消息密文、审核结果和处置记录 |
该方案主要防护目标是:任意一侧数据库单独泄露时,不能直接恢复消息明文。
已知边界:HotGo 数据库、IM 数据库和完整代码同时泄露不在本方案的主要防护范围内。
### 2.2 传输安全
- 客户端调用必须使用 `https://` 和 `wss://`。
- 阿里云审核请求必须使用 HTTPS。
- Admin Server 与 IM Server 的内部配置和查询接口必须使用 HTTPS,并保留现有签名鉴权;跨公网部署时建议叠加 mTLS 或固定出口 IP 白名单。
- TLS 只解决传输保护,AES-256-GCM 解决消息落库后的静态保护。
## 3. 总体架构
```text
客户端
| HTTPS/WSS明文业务数据(TLS通道内不可见)
v
IM Server
|-- 实时权限、宵禁、用户状态等校验
|-- 获取商户审核策略
|-- 本地敏感词补充检查
|-- 阿里云 TextModerationPlus 审核
|-- AES-256-GCM 加密
`-- 密文写入国内IM库
Admin Server(国外HotGo)
|-- 管理阿里云配置
|-- 管理商户审核策略
|-- 管理商户消息密钥
|-- 通过内部接口下发配置/密钥
`-- 通过IM内部查询接口查看解密后消息
```
## 4. 内容审核设计
### 4.1 审核范围
| 场景 | 阿里云 Service | 审核时机 |
| --- | --- | --- |
| 用户昵称 | `nickname_detection_pro` | 更新用户资料前 |
| 群昵称 | `nickname_detection_pro` | 更新群成员资料前 |
| 群名称 | `nickname_detection_pro` | 创建群或修改群资料前 |
| 用户签名 | `nickname_detection_pro` | 更新用户资料前 |
| 单聊文本 | `chat_detection_pro` | 消息入库及广播前 |
| 群聊文本 | `chat_detection_pro` | 消息入库及广播前 |
| 编辑后消息 | `chat_detection_pro` | 新版本入库前 |
| 群公告 | `chat_detection_pro` | 发布或修改前 |
图片、视频和文件继续走文件审核链路,不属于本文本审核接口范围。
### 4.2 商户范围
- 第一阶段只为 `tenant.code=default` 的上架默认商户开启阿里云内容审核。
- 其他团队商户默认关闭,不调用阿里云,不产生审核费用。
- 数据模型保留按商户开启的能力,但只有平台管理员可变更。
- 商户端管理员不得看到阿里云 AK/SK,不得修改平台级审核服务配置。
### 4.3 后台配置项
#### 4.3.1 阿里云服务配置
复用 `im_external_config` 的 `service=moderation`,由平台管理员维护:
| 配置 | 说明 |
| --- | --- |
| `provider` | 固定为 `aliyun_green` |
| `endpoint` | 默认 `green-cip.cn-shanghai.aliyuncs.com` |
| `enabled` | 平台总开关 |
| `config_json.region_id` | 默认 `cn-shanghai` |
| `config_json.nickname_service` | 默认 `nickname_detection_pro` |
| `config_json.chat_service` | 默认 `chat_detection_pro` |
| `config_json.timeout_ms` | 单次请求超时,建议 1000~2000 ms |
| `config_json.failure_mode` | `allow` 或 `block` |
| `secret_json.access_key_id` | 只写不回显 |
| `secret_json.access_key_secret` | 只写不回显 |
“检查配置”必须真实调用阿里云测试文本,不能只检查字段非空。返回仅包含成功与否、请求耗时、RequestID 和脱敏错误。
#### 4.3.2 商户审核策略
可在 `im_security_policy.sensitive_word_policy` 中保存,避免新增重复策略表。JSON 结构固定为:
```json
{
"enabled": true,
"aliyun_enabled": true,
"local_words_enabled": true,
"scenes": {
"user_nickname": true,
"user_signature": true,
"group_name": true,
"group_nickname": true,
"private_message": true,
"group_message": true,
"group_announcement": true
},
"risk_actions": {
"high": "block",
"medium": "block",
"low": "audit_only",
"none": "allow"
},
"failure_mode": "allow",
"block_message": "内容不符合平台规范,请修改后重试"
}
```
策略发布后使用现有 `im.config.event` 发布配置事件,IM Server 按商户清理本地缓存。
### 4.4 文本提取
审核输入不能直接对任意 JSON 执行字符串化,应按 Drafty 或消息类型提取用户可见文本:
- 普通文本:提取完整文本。
- Drafty 富文本:提取 `txt` 及按钮、链接等用户可见标题。
- 图片/文件/语音:审核文件名、说明和附带文本,文件本体走文件审核。
- 系统消息、音视频信令和已签名服务端消息不调用文本审核。
- 空文本直接放行。
### 4.5 阿里云结果处置
IM Server 统一映射为:
| 内部结果 | 动作 |
| --- | --- |
| `allow` | 继续加密、入库和广播 |
| `block` | 不入库、不广播,返回统一业务错误 |
| `audit_only` | 允许发送,但写入审核命中日志 |
| `retryable_error` | 根据 `failure_mode` 放行或拦截 |
不只依赖 `RiskLevel/Label` 一个字段,需保留与验证阿里云原始结果中的扩展判断,针对引流、诈骗等样本建立回归测试集。
### 4.6 超时与降级
- 阿里云请求不得无限等待。
- 建议默认 `failure_mode=allow`,阿里云异常时允许发送并记录告警,防止第三方故障造成 IM 全局不可用。
- 连续失败达到阈值后进入短时熔断;熔断期间按失败策略处理。
- 阿里云调用不得在数据库事务中执行。
## 5. 消息加密设计
### 5.1 算法
- 算法:AES-256-GCM。
- DEK 长度:32 字节。
- Nonce:每次加密使用 `crypto/rand` 生成的独立 12 字节随机值。
- GCM 标签:使用 Go 标准库默认 16 字节认证标签。
- 明文编码:将原 `content` JSON 规范化序列化为 UTF-8 字节。
- 不允许固定 Nonce,不允许 AES-ECB,不使用未认证的 AES-CBC/CTR。
### 5.2 AAD
每条消息加密时绑定以下 AAD:
```text
tenant_id | topic | seqid | from | client_msg_id | key_version | content
```
`content` 是字面标识,用于与未来可能加密的 `head` 区分。AAD 按固定字段顺序和长度前缀编码,禁止使用可产生拼接歧义的简单字符串。
### 5.3 密钥层级
```text
HotGo字典根密钥(KEK)
-> 加密每个商户、每个版本的DEK
-> DEK加密该商户消息
```
根密钥使用 HotGo `hg_sys_dict_data` 保存,但必须满足:
1. 使用独立的内部字典类型,不出现在通用字典管理页。
2. 后端字典列表、选项和导出接口必须对该类型做服务端拒绝,不能仅依赖菜单隐藏。
3. 根密钥为 32 字节强随机值的 Base64 编码,不使用口令、UUID 或可读文本。
4. 根密钥只由 Admin Server 密钥服务读取,不通过通用配置接口返回。
5. 根密钥不写日志,不在后台页面回显,不进入 Kafka 消息。
### 5.4 商户密钥管理表
在 HotGo 库新增 `im_message_encryption_keys`:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `id` | bigint | 主键 |
| `tenant_id` | bigint | 商户 ID |
| `key_version` | int | 密钥版本,从 1 递增 |
| `algorithm` | varchar(32) | 固定 `AES-256-GCM` |
| `encrypted_dek` | blob | KEK 加密后的 DEK |
| `dek_nonce` | varbinary(12) | 加密 DEK 时的随机 Nonce |
| `status` | varchar(16) | `pending` / `active` / `retired` / `disabled` |
| `created_at` | datetime | 创建时间 |
| `activated_at` | datetime | 启用时间 |
| `retired_at` | datetime | 退役时间 |
| `created_by` | bigint | 操作人 |
| `remark` | varchar(255) | 备注 |
约束:
- 唯一键:`tenant_id + key_version`。
- 每个商户同时只能有一把 `active` 密钥。
- `retired` 密钥仍可用于解密历史消息,不得再用于新消息加密。
- `disabled` 只用于安全事故应急,禁用前必须完成历史消息转密钥。
### 5.5 IM 消息表改造
`im_messages` 和 `im_message_revisions` 新增:
| 字段 | 类型 | 说明 |
| --- | --- | --- |
| `content_ciphertext` | longblob | 消息密文,包含 GCM 认证标签 |
| `content_nonce` | varbinary(12) | 本条消息随机 Nonce |
| `key_version` | int | 商户 DEK 版本 |
| `encryption_alg` | varchar(32) | `AES-256-GCM` |
| `encrypted` | tinyint | `0` 明文兼容,`1` 密文 |
迁移完成前保留现有 `content JSON`,规则如下:
- 新写入:`content=NULL`、`encrypted=1`,只保存密文。
- 旧数据:`content IS NOT NULL`、`encrypted=0`,按原逻辑读取。
- 解密成功后还原为原 `any` JSON 结构,不修改对客户端的协议。
- `head` 第一阶段不加密,但禁止在 `head` 中放置消息正文、联系方式等敏感明文。
`im_group_messages` 是群消息派生查询表,不得另存一份明文。实现时二选一:
1. 推荐:仅保存索引、状态和统计字段,通过 `topic + seqid` 关联 `im_messages` 解密。
2. 兼容:同步增加密文字段,但加密输入和密文必须与主消息表保持一致。
不允许 `im_messages` 已加密而 `im_group_messages.content` 仍保留明文。
### 5.6 加密和解密时机
#### 新消息
```text
校验消息
-> 提取审核文本
-> 内容审核
-> 分配seqid/组装AAD
-> 序列化content
-> 使用当前active DEK加密
-> 事务写入密文和消息元数据
-> 向在线订阅者广播原内存明文
```
#### 历史消息和离线同步
```text
从数据库读取密文
-> 按tenant_id + key_version获取DEK
-> 重建AAD
-> GCM验签并解密
-> JSON反序列化
-> 按原Tinode协议返回
```
任何验签失败均必须当作数据篡改或密钥错误,禁止返回部分内容或尝试忽略错误。
### 5.7 密钥获取和缓存
- IM Server 不得每发送一条消息都跨境请求 HotGo。
- IM Server 按 `tenant_id + key_version` 在内存中缓存已解密 DEK。
- 新消息使用当前 `active` 密钥,历史消息按记录中的 `key_version` 取密钥。
- 密钥变更发布 `message.encryption.key.updated` 配置事件,IM Server 按商户刷新。
- HotGo 短时不可用时继续使用内存密钥。
- IM Server 重启后如无法取得当前密钥,该商户禁止写入新消息,不得降级为明文落库。
### 5.8 密钥轮换
1. Admin Server 生成新 DEK,使用 KEK 加密后以 `pending` 状态入库。
2. 通知 IM Server 拉取并验证新密钥。
3. 验证成功后,在事务中将旧密钥设为 `retired`,新密钥设为 `active`。
4. 新消息立即使用新版本,旧消息仍按原版本解密。
5. 历史转密钥作为单独低速任务执行,不阻塞即时通信。
## 6. Admin Server 改造
### 6.1 内容审核页面
将现有“审核插件”配置补齐为可用功能:
- 阿里云基础配置。
- 真实连通性检查。
- 商户审核开关和场景开关。
- 风险等级与处置动作映射。
- 异常降级策略。
- 配置发布版本和生效时间。
非默认商户显示“未开启内容审核”,不显示阿里云密钥编辑项。
### 6.2 消息加密管理页面
增加平台级“消息加密”页面,只允许最高权限平台管理员使用:
- 查看商户加密状态、当前密钥版本和最后轮换时间。
- 初始化商户密钥。
- 启用/暂停商户消息加密。
- 轮换密钥。
- 查看历史迁移进度。
- 密钥明文、根密钥和 `encrypted_dek` 均不展示。
### 6.3 内部接口
建议增加:
| 方法 | 路径 | 用途 |
| --- | --- | --- |
| GET | `/internal/im-config/tenants/{code}/moderation-policy` | IM Server 拉取商户审核策略 |
| GET | `/internal/im-config/moderation/provider` | IM Server 拉取阿里云配置 |
| GET | `/internal/im-config/tenants/{code}/message-keys/active` | 获取当前消息 DEK |
| GET | `/internal/im-config/tenants/{code}/message-keys/{version}` | 获取指定版本 DEK |
| POST | `/internal/im-config/tenants/{code}/message-keys/ack` | IM Server 确认新密钥可用 |
密钥接口返回的是服务端使用的 DEK,不允许从浏览器或通用 Admin API 调用。接口必须校验服务身份、时间窗、签名、请求 ID 和重放。
### 6.4 后台消息查询
- Admin Server 继续调用 IM Server internal admin-query,不直连 IM 业务库解密。
- IM Server 在商户数据权限通过后批量解密当页消息。
- 每次查看明文记录管理员 ID、商户、IP、查询条件、返回数量、时间和 TraceID。
- 列表只解密当页,禁止一次加载全部历史消息。
## 7. IM Server 改造
### 7.1 新增内部模块
```text
server/moderation/
provider.go 审核Provider接口
policy.go 商户策略和缓存
text.go 文本提取与结果映射
aliyun/text.go TextModerationPlus实现
server/messagecrypto/
cipher.go AES-256-GCM加解密
key_provider.go 密钥获取接口
key_cache.go tenant/version内存缓存
serializer.go content规范序列化
```
不将加密和阿里云调用直接写进 MySQL adapter;adapter 只保存已加密的存储模型和解密所需元数据。
### 7.2 消息发送接入点
在 `Topic.handlePubBroadcast` 中,于 `saveAndBroadcastMessage` 之前执行:
1. 过滤 WebRTC/系统信令等不审核内容。
2. 提取文本。
3. 根据商户策略执行审核。
4. 拦截时通过 Tinode ctrl 回复业务错误,不进入保存和广播。
5. 放行时进入加密保存。
进程内广播使用本次请求已有明文,不为广播执行“加密后立即解密”。
### 7.3 资料修改接入点
用户昵称/签名、群名称/公告、群昵称的 HTTP 和 Tinode meta 修改通道都必须接入同一 moderation service,不能只修改某一个 HTTP API 而留下协议绕过路径。
### 7.4 存储层改造
- `types.Message` 和 `types.MessageRevision` 增加密文及密钥元数据字段。
- MySQL 的普通库和共享库/多商户 adapter 必须同时改造。
- 新建库 schema、启动 ensure 迁移和手工 SQL 脚本必须保持一致。
- Save、GetOne、GetAll、编辑、撤回、删除、历史修订、管理后台查询均需覆盖。
- 解密层应在业务存储服务边界统一执行,避免上层遗漏。
### 7.5 缓存与多 Pod
- 每个 Pod 保持本地密钥和审核策略缓存。
- Kafka 配置事件使所有 Pod 同时失效。
- 事件处理必须幂等,重复消息不得导致重复创建密钥。
- 新旧密钥切换期间,所有 Pod 必须同时具备旧版本解密和新版本加密能力。
## 8. 审核和解密日志
在 IM 业务库增加 `im_content_moderation_logs`:
| 字段 | 说明 |
| --- | --- |
| `id` | 主键 |
| `request_id` | 审核请求 ID |
| `scene` | 昵称、单聊、群聊等 |
| `topic` / `seqid` | 关联消息,拦截时 `seqid` 可为空 |
| `actor_id` | 内容提交人 |
| `provider` | `local` / `aliyun_green` |
| `provider_request_id` | 第三方 RequestID |
| `risk_level` | 统一风险等级 |
| `risk_labels` | 命中标签 JSON |
| `action` | `allow` / `block` / `audit_only` |
| `content_hash` | SHA-256 摘要,不保存明文 |
| `latency_ms` | 审核耗时 |
| `error_code` | 脱敏错误码 |
| `created_at` | 创建时间 |
后台明文查看继续写入统一审计事件,必须区分“列表解密”、“详情解密”和“导出解密”。默认不提供明文批量导出;需要开放时必须增加独立权限和审批。
## 9. 关键词查询限制
消息加密后,MySQL 无法对密文执行原有 `LIKE '%keyword%'`。第一阶段采用:
1. 后台查询必须先提供商户、时间范围,并尽量提供用户或群号。
2. IM Server 先用明文元数据缩小候选集。
3. 对有上限的候选集批量解密,再在内存中匹配关键词。
4. 限制查询最大时间范围和候选消息数,超限返回明确提示。
不建议在第一阶段建设明文分词索引,否则会降低加密保护效果。若后续必须支持全库高性能检索,再单独评审 HMAC 盲索引方案。
## 10. 历史数据迁移
### 10.1 发布顺序
1. 发布 HotGo 表结构、根密钥和密钥管理能力。
2. 为默认商户初始化第 1 版 DEK,但不开启加密。
3. 发布 IM Server 双读兼容版本,验证密钥获取、加解密和内容审核。
4. 开启新消息加密写入,保持旧数据明文可读。
5. 启动历史数据分批迁移。
6. 对比数量、抽样解密和 GCM 验签,完成后清空明文 `content`。
7. 观察期结束后,再决定是否删除原明文字段。
### 10.2 迁移任务要求
- 按主键或 `topic + seqid` 分批,不长时锁表。
- 每批使用独立事务,记录游标、成功数、失败数和最后错误。
- 重复执行必须幂等,`encrypted=1` 的记录直接跳过。
- 先写入密文并验证可解密,再将明文字段置空。
- 失败记录保留明文并进入重试列表,不得丢失原数据。
- 迁移速率可配置,并监控数据库负载和消息延迟。
## 11. 错误码和客户端兼容
客户端不参与加解密,不修改消息协议。新增业务错误建议:
| 错误 | 说明 | 客户端处理 |
| --- | --- | --- |
| `CONTENT_REJECTED` | 内容审核拒绝 | 保留未发送文本并提示修改 |
| `CONTENT_REVIEW_UNAVAILABLE` | 审核不可用且策略要求拦截 | 提示稍后重试 |
| `MESSAGE_ENCRYPTION_UNAVAILABLE` | 未取得加密密钥 | 提示服务暂时不可用 |
| `MESSAGE_DECRYPT_FAILED` | 历史消息验签/解密失败 | 展示“消息暂时无法加载”,不展示乱码 |
不向客户端返回阿里云原始错误、风险标签详情、密钥版本内部信息或解密堆栈。
## 12. 监控指标
### 12.1 内容审核
- 按商户/场景统计请求量、放行数、拦截数、降级数。
- 阿里云耗时 P50/P95/P99。
- 超时率、错误率、熔断状态。
- 各风险等级和标签命中数。
### 12.2 消息加密
- 加密/解密成功数、失败数和耗时。
- 各商户当前密钥版本。
- 密钥获取成功率、缓存命中率和缓存最后刷新时间。
- 明文历史记录剩余数、迁移速率和迁移失败数。
- GCM 验签失败必须产生高优先级告警。
监控标签不得使用消息正文、密文、Nonce、DEK 或 KEK。
## 13. 验收标准
### 13.1 审核验收
- `default` 商户开启后,单聊、群聊、用户昵称、群名称和群昵称均实际调用对应阿里云 Service。
- 其他商户关闭时不调用阿里云。
- 拦截内容不进入 `im_messages` 和 `im_group_messages`,也不向订阅者广播。
- 本地敏感词与阿里云开关可独立启停。
- 阿里云超时严格按 `failure_mode` 处理。
- 配置发布后所有 IM Pod 在规定时间内生效。
### 13.2 加密验收
- 开启商户的新消息在数据库中不存在可读正文。
- 相同内容连续发送两次,Nonce 和密文必须不同。
- 修改密文、Nonce、AAD 关联字段的任意一处后,解密必须失败。
- 在线发送、离线同步、多端漫游、编辑、回复、撤回、删除保持原有业务行为。
- Admin 有权限账号可查看解密明文,无权限账号被拒绝。
- 查看明文必须形成完整审计记录。
- 旧密钥退役后仍可解密旧版本消息,新消息只使用新版本。
- HotGo 暂时断开时,已有内存密钥的 IM Pod 仍可工作;无密钥时绝不允许明文降级落库。
### 13.3 性能验收
- 纯 AES 加密不得成为消息链路主要瓶颈。
- 密钥缓存命中时不发生跨境 HotGo 请求。
- 审核关闭的商户不增加阿里云网络耗时。
- 审核开启时应分别统计本地处理、阿里云调用和消息入库耗时。
- 历史迁移期间即时消息发送和数据库负载不得超过上线阈值。
## 14. 开发分批建议
### 批次一:配置和密钥基础
- Admin 阿里云内容审核配置、商户策略、密钥表和密钥管理页。
- 字典根密钥隔离和专用密钥服务。
- Admin internal config/key API 及 Kafka 失效事件。
### 批次二:IM 文本审核
- 阿里云 `TextModerationPlus` Go 客户端。
- 策略缓存、文本提取、消息和资料修改接入。
- 审核日志、错误码、告警和回归测试。
### 批次三:新消息加密
- AES-256-GCM 模块、密钥缓存和存储模型。
- 新消息加密写入和历史消息解密读取。
- 单聊、群聊、修订、后台查询的一致性验证。
### 批次四:历史迁移和轮换
- 历史明文分批加密。
- 迁移仪表盘、少量失败重试和数据对账。
- 密钥轮换和历史转密钥测试。
- 完成观察后清理明文。
## 15. 开发前必须确认的决策
| 决策 | 文档默认值 |
| --- | --- |
| 阿里云异常时放行还是拦截 | 放行并告警 |
| 是否审核系统消息 | 不审核 |
| 是否加密 `head` | 第一阶段不加密 |
| 第一阶段启用商户 | 仅 `code=default` |
| 是否允许无密钥时明文降级 | 不允许 |
| 后台是否允许明文导出 | 默认不允许 |
| 历史消息是否迁移 | 是,分批迁移 |
| 根密钥位置 | HotGo 内部字典数据,严禁通用接口暴露 |
## 16. 现有实现对照
| 能力 | 现状 | 本次改造 |
| --- | --- | --- |
| 阿里云文本审核 | 文档已人工验证,IM Server 无 Go 调用实现 | 增加 TextModerationPlus 客户端和业务接入 |
| 本地敏感词 | Admin 已有 CRUD,IM Server 未消费 | 作为可选补充审核 |
| 审核配置 | `im_external_config` 已有 `moderation` 占位 | 增加真实检查、商户策略和发布 |
| 配置同步 | 已有 internal config 客户端、Kafka 配置事件和缓存失效 | 复用并扩展新的策略/密钥类型 |
| 消息存储 | `im_messages.content` 和修订表 `content` 为 JSON 明文 | 改为 AES-256-GCM 密文且双读迁移 |
| 群消息派生表 | `im_group_messages.content` 为 JSON 明文 | 去明文副本或同步密文化 |
| 后台消息查询 | IM internal-query 返回库内内容 | 增加权限校验、解密和查看审计 |
@@ -0,0 +1,460 @@
# 鸽达达管理后台改造测试用例
> 依据:`需求文档.md`、`Admin-Server改造文档.md`、`IM-Server改造文档.md`及当前 Admin Server、IM Server 代码。
> 测试环境:`https://admin.118im.com/admin#/dashboard/console`
> 文档日期:2026-09-07
## 1. 测试目标与范围
本测试用于验证鸽达达管理后台改造后的页面操作、Admin Server 接口、IM Server 内部查询与命令执行是否一致,并重点验证以下闭环:
1. 后台查询条件、表格字段、详情、导出及权限控制正确。
2. Admin Server 能正确访问 IM Server,真实展示概览、消息和登录审计数据。
3. 消息隐藏、删除经 Kafka 下发后由 IM Server 幂等执行并记录结果。
4. 全局禁止加好友发布后,移动端所有加好友入口均受后端控制,不能绕过。
5. 宵禁发布后,命中规则的单聊/私聊不能发送消息;配置群聊规则时群聊也必须受限。
6. 登录成功、失败及退出均产生安全审计数据,且账号、口令和验证码等敏感信息不泄露。
### 1.1 本轮交付范围
| 页面/能力 | 本轮状态 | 测试结论口径 |
|---|---|---|
| 3.1 数据概览 | 已改造 | 本轮必测,需联调 Admin 与 IM |
| 3.3 用户分组 | Admin 已改造为用户策略分组 | 后台 CRUD 本轮必测;IM 策略消费待补齐 |
| 3.7 单聊消息 | 已改造 | 本轮必测,需验证 Kafka 处置闭环 |
| 3.8 群聊消息 | 已改造 | 本轮必测,需验证 Kafka 处置闭环 |
| 3.9 敏感词库 | Admin 管理端已改造 | 后台 CRUD 本轮必测;IM 运行时拦截待补齐 |
| 3.16 用户登录历史 | 已改造 | 本轮必测,数据来自 IM Server |
| 3.18 登录失败日志 | 已改造 | 本轮必测,包含加入 IP 黑名单、封禁用户 |
| 3.20 安全策略 | Admin 管理与发布已改造,IM 执行端未完成 | P0 验收目标;当前不得判定端到端通过 |
| 其余 3.2、3.4-3.6、3.10-3.15、3.17、3.19 | 原系统能力/后续改造 | 执行基础回归,不作为本轮新增功能全部交付的判定依据 |
### 1.2 代码核查结论
本节是对当前代码的静态核查结论,不等同于测试环境已经执行通过:
| 服务 | 已实现内容 | 当前缺口/测试关注点 |
|---|---|---|
| Admin Server | 数据概览、单聊/群聊消息查询与处置、用户登录历史、登录失败日志;用户策略分组、敏感词、安全策略和 IP 规则的管理页面、接口、权限与数据表 | 对 IM 的依赖异常、异步命令最终状态、租户隔离仍需按本文实测 |
| IM Server | 概览/趋势、单聊/群聊消息、登录成功/失败日志的 Internal Query;`message.hide`、`message.delete` 消费、幂等处置及动作落库;真实登录成功、失败和退出审计 | 未找到 `security.policy.updated`、用户策略分组、敏感词策略的运行时消费;未找到全局禁加好友、资料隐藏、二维码拦截和消息宵禁在 IM 请求链路的执行代码 |
| 联调结论 | 消息治理、概览和登录审计具备端到端测试基础 | 安全策略页面“发布成功”目前只能证明 Admin 保存并发出事件,不能证明移动端已生效;`FRIEND-*`、`CURFEW-*`、敏感词运行时用例预计失败或阻塞,须补齐 IM 实现后回归 |
因此,测试报告必须分别记录“Admin 配置成功”“事件发布成功”“IM 接收成功”“移动端行为生效”四个结果,禁止合并为一个“发布成功”。
### 1.3 优先级与结果
| 标识 | 说明 |
|---|---|
| P0 | 核心链路或安全控制,失败时禁止上线 |
| P1 | 主要页面功能,失败时原则上阻断上线 |
| P2 | 辅助功能、兼容性或易用性问题 |
| 通过 | 实际结果与预期完全一致,接口、数据库及异步结果无异常 |
| 失败 | 页面、接口、数据库或 IM 最终行为任一不符合预期 |
| 阻塞 | 环境、账号、数据或依赖不可用,无法执行 |
## 2. 测试准备
### 2.1 账号、权限与数据
准备以下数据,所有测试数据使用唯一前缀 `QA_YYYYMMDD_`:
| 数据 | 用途 |
|---|---|
| 超级管理员 A | 验证全部菜单与操作权限 |
| 只读管理员 B | 仅授予列表、详情权限,验证写按钮隐藏和接口拒绝 |
| 租户管理员 C | 验证租户隔离,不得查看或操作其他租户数据 |
| 移动端用户 U1、U2、U3 | U1/U2 为普通用户,U3 用作好友或宵禁白名单 |
| 群组 G1 | U1、U2、U3 入群,用于群消息及群成员入口测试 |
| 消息样本 | 文本、图片、视频、语音、文件、名片、系统消息各至少 1 条 |
| 登录样本 | 密码成功、密码错误、账号不存在、账号禁用、退出各至少 1 条 |
执行前确认:
- Admin Server 的 IM Internal API 地址与共享密钥配置正确。
- Admin Server 和 IM Server 的 Kafka broker、topic 和鉴权配置一致。
- Admin 库已存在策略、敏感词、策略分组及消息命令相关表。
- IM 库已存在 `im_user_login_logs`、`im_login_failure_logs`、`im_message_admin_actions`。
- 浏览器清理旧缓存后重新登录,确认当前构建版本与待测提交一致。
### 2.2 通用检查
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| COM-001 | P0 | 使用超级管理员登录并展开 IM 管理菜单 | 已交付的 8 个功能域可见,页面可打开,无 404、白屏和控制台致命错误 |
| COM-002 | P0 | 使用只读管理员打开各页面 | 可查询和查看;新增、编辑、删除、发布、隐藏、封禁等无权限按钮不可见或禁用;直接请求写接口返回无权限 |
| COM-003 | P0 | 使用租户管理员查询列表、详情和导出 | 后端强制按当前租户过滤,修改 `tenantId` 不能越权读取或操作其他租户数据 |
| COM-004 | P1 | 对列表执行查询、重置、翻页、切换每页条数 | 查询条件生效;重置恢复默认值;总数和分页正确;每页超过 200 时接口明确报错 |
| COM-005 | P1 | 查询无匹配数据、模拟 IM 超时或不可用 | 空数据展示为空列表而非报错;依赖异常有明确提示,不把历史缓存伪装成实时成功 |
| COM-006 | P1 | 连续快速点击保存、发布、删除等按钮 | 提交期间按钮有加载/防重复状态,不产生重复记录或重复处置 |
| COM-007 | P1 | 检查新增写操作后的操作日志 | 操作者、租户、目标对象、动作、时间、结果和原因可追溯,不记录密码、验证码、Token 或完整敏感账号 |
## 3. 页面测试用例
### 3.1 数据概览
当前按钮和字段:`刷新`,时间范围 `近7天/近14天/近30天`;指标包含总用户数、活跃用户、总群组数、总会话数、总消息数、今日消息;趋势表包含日期、新增用户、新增消息。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| DASH-001 | P0 | 打开数据概览 | 调用 `/imDashboard/overview` 和 `/imDashboard/trends`;六项指标展示真实数字,不显示 `undefined`、`NaN` 或静态占位值 |
| DASH-002 | P0 | 记录 IM 库统计结果后刷新页面 | 总用户、活跃用户、群组、会话、消息及今日消息与 IM Server 同口径统计一致 |
| DASH-003 | P1 | 分别选择近 7、14、30 天 | 趋势行数和日期区间正确,日期连续;切换后重新请求,新增用户和新增消息按日聚合 |
| DASH-004 | P1 | 在 IM 端新增用户并发送一条消息,再点刷新 | 对应总量和当日趋势增加;刷新按钮显示加载状态且不会重复并发请求 |
| DASH-005 | P1 | 检查跨天边界和无数据日期 | 按 `Asia/Shanghai` 自然日统计;无数据日期补 0,不遗漏日期 |
| DASH-006 | P0 | 使用租户管理员查看概览 | 只统计本租户数据;不能通过修改请求参数获得全局或其他租户数据 |
### 3.2 用户管理
本页属于既有能力回归。重点回归查询、详情、状态修改、重置密码、IP 白名单、强制下线、注销、批量封禁、按 IP 封禁和导入。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| USER-001 | P1 | 按用户 ID、用户名、昵称、手机号、状态和时间查询 | 条件可组合,表格字段与详情一致,手机号等敏感字段按权限脱敏 |
| USER-002 | P1 | 新增或编辑用户,切换启用/禁用 | 校验必填项和唯一性;状态写入成功,禁用用户不能继续登录 |
| USER-003 | P1 | 执行重置密码、强制下线、注销 | 均需二次确认;成功后客户端会话失效;操作日志记录动作但不记录明文密码 |
| USER-004 | P1 | 执行批量封禁及按 IP 封禁 | 仅命中目标租户和目标用户;返回成功、跳过、失败数量,重复提交保持幂等 |
### 3.3 用户分组
当前页面为“用户策略分组”,按钮包括新增/编辑、成员、启用/禁用、删除;成员弹窗支持添加成员、移除成员。表格重点字段:分组编码、分组名称、优先级、状态、版本、成员数、备注、更新时间。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| UPG-001 | P1 | 按租户、编码/名称关键字、状态查询并重置 | 条件生效;状态 `1=启用、2=禁用` 显示字典标签;成员数与成员关系表一致 |
| UPG-002 | P1 | 新增分组,填写合法编码、名称、优先级、策略 JSON、IP 白/黑名单 | 保存成功并生成版本;编码以字母开头且仅含字母、数字、下划线、中划线,长度 2-64 |
| UPG-003 | P1 | 输入数字开头、空名称、非法 JSON、超过 128 字名称 | 前后端拒绝保存并给出明确中文错误,数据库不产生脏数据 |
| UPG-004 | P1 | 编辑分组策略后保存 | 原记录更新,版本按实现规则变化;详情与列表一致;其他租户同编码记录不受影响 |
| UPG-005 | P1 | 点击状态开关切换启用/禁用 | 调用 `/imUserPolicyGroup/status`;页面状态即时刷新,非法状态值被后端拒绝 |
| UPG-006 | P1 | 打开成员列表,添加 U1,设置有效开始/结束时间 | 成员成功加入,成员数 +1;结束时间早于开始时间时拒绝保存 |
| UPG-007 | P1 | 重复添加同一用户,然后移除成员 | 不产生重复有效关系;移除后成员列表和成员数同步更新 |
| UPG-008 | P1 | 删除含成员的分组 | 显示二次确认;按业务约束拒绝删除或级联解除关系,不能遗留指向不存在分组的成员关系 |
### 3.4 群组管理
本页属于既有能力回归,验证查询、详情、封禁/解封、解散、成员、文件和快速建群。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| GROUP-001 | P1 | 按群 ID、群号、群名、群主、状态查询 | 列表与 IM 群数据一致,分页及租户隔离正确 |
| GROUP-002 | P1 | 封禁 G1 后尝试群内发消息,再解封 | 封禁后发送被 IM Server 拒绝;解封后恢复;后台和客户端状态一致 |
| GROUP-003 | P1 | 查看成员、文件并删除一个群文件 | 只展示 G1 数据;删除需确认且客户端不可再访问已删除文件 |
| GROUP-004 | P1 | 快速创建群组并解散 | 成员、群主校验正确;解散后群不可继续使用,操作有审计记录 |
### 3.5 群成员管理
本页属于既有能力回归,重点验证踢出、禁言、解除禁言,以及全局禁加好友时的群成员隐私联动。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| GMEM-001 | P1 | 查询 G1 成员并按用户筛选 | 角色、入群时间、禁言状态正确,只返回该群成员 |
| GMEM-002 | P1 | 对 U2 执行禁言、解除禁言、踢出 | 对应客户端行为立即生效;群主/管理员等受保护角色遵守业务限制 |
| GMEM-003 | P0 | 发布“全局禁止加好友”策略后,U1 在群成员列表点击 U2 | 不显示可识别的个人详情或个人名片,不显示加好友入口;抓包直接访问资料/加好友接口也被后端拒绝 |
### 3.6 邀请码管理
本页属于既有能力回归。邀请码创建、删除、使用记录必须与全局好友开关联动。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| INV-001 | P1 | 创建邀请码并设置有效期、次数、状态 | 校验正确,创建成功后可查询,过期或耗尽后不可继续使用 |
| INV-002 | P1 | 查看使用记录并删除未使用邀请码 | 使用人、时间、状态正确;删除需确认,已删除邀请码失效 |
| INV-003 | P0 | 全局禁止加好友期间通过邀请码注册或自动加好友 | 注册能力按私域设置执行,但 `invite_auto_add` 入口必须被后端阻断,不能自动建立好友关系 |
### 3.7 单聊消息
筛选项:租户、会话 Topic、发送用户 ID、接收用户 ID、内容类型、消息状态、内容关键词、发送时间。表格字段:Topic、消息序号、发送人、接收人、类型、内容摘要、状态、发送时间;按钮:详情、隐藏、删除、导出。
内容类型枚举:`text` 文本、`image` 图片、`video` 视频、`audio/voice` 语音、`file` 文件、`card` 名片、`system` 系统。状态枚举:`normal` 正常、`deleted` 后台删除、`recalled` 用户撤回、`blocked` 风控拦截、`hidden` 已隐藏。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| PM-001 | P0 | 打开页面并点击查询 | `/imPrivateMessage/list` 返回真实 IM 消息;单聊双方、Topic、Seq、类型、摘要、状态和时间与 IM 库一致 |
| PM-002 | P1 | 组合发送人、接收人、类型、状态、关键词和时间查询 | 所有条件正确下传并组合生效;非法类型、非法状态、每页大于 200 被后端拒绝 |
| PM-003 | P1 | 点击详情 | `/imPrivateMessage/view` 返回消息头、完整内容、元数据、回复序号、@用户等;空 Topic 或 `seqId<=0` 被拒绝 |
| PM-004 | P0 | 点击隐藏,输入处置原因并确认 | 原因必填且不超过 500 字;Admin 生成唯一 command/event,返回待处理状态,Kafka 发布 `message.hide` |
| PM-005 | P0 | 等待隐藏命令完成后刷新并在移动端查看 | Admin 命令状态变为成功;IM `im_message_admin_actions` 有一条成功记录;列表状态为 `hidden`,普通客户端不再展示该消息 |
| PM-006 | P0 | 点击删除,确认后等待执行 | Kafka 发布 `message.delete`;IM 幂等落库并清除相关缓存;列表状态为 `deleted`,详情/客户端不能再获得可展示原文 |
| PM-007 | P0 | 使用同一 command/event 重放隐藏或删除命令 | 不重复执行、不新增重复有效记录,最终状态保持一致,并可追踪重复消费结果 |
| PM-008 | P1 | Kafka 不可用时执行隐藏/删除 | 页面明确提示失败或命令进入失败/待重试状态,不显示为已成功;恢复后可按设计重试 |
| PM-009 | P1 | 按当前筛选条件导出 | 导出字段含 Topic、Seq、双方 ID、类型、摘要、状态、发送时间;数据范围与筛选及租户权限一致 |
### 3.8 群聊消息
筛选项:租户、群 Topic、发送用户 ID、内容类型、消息状态、关键词、发送时间。表格除公共消息字段外还应展示群名称、群号;按钮与单聊一致。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| GM-001 | P0 | 按 G1 Topic 查询 | `/imGroupMessage/list` 仅返回群消息,群名称、群号、发送人、Seq、类型、摘要和状态正确 |
| GM-002 | P1 | 按发送人、类型、状态、关键词、时间组合查询并查看详情 | 条件准确,详情完整;不能用单聊 Topic 越界取得群消息 |
| GM-003 | P0 | 隐藏一条群消息并在多个群成员客户端刷新 | `message.hide` 执行成功;所有普通成员均不可见,后台状态和 IM 动作记录一致 |
| GM-004 | P0 | 删除一条群消息 | `message.delete` 执行成功;群消息缓存被清除,各客户端不能继续通过历史缓存看到原内容 |
| GM-005 | P1 | 导出群消息 | 导出包含群名、Topic、Seq、发送人、类型、摘要、状态、时间,范围与查询一致 |
| GM-006 | P0 | 重放相同命令、乱序投递隐藏与删除命令 | IM Server 幂等处理;删除是最终不可见状态,不因迟到的隐藏命令恢复内容 |
### 3.9 敏感词库
按钮:新增、编辑、启用/禁用、删除、批量导入、分组管理。筛选字段:租户、分组、敏感词、匹配类型、命中动作、风险等级、状态。
匹配枚举:`exact` 精确、`contains` 包含、`regex` 正则、`pinyin` 拼音。动作枚举:`replace` 替换、`block` 拦截、`audit_only` 仅审计、`manual_review` 人工审核。风险等级:`low/medium/high`。场景:单聊、群聊、用户资料、群资料、反馈、举报。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| SW-001 | P1 | 新建敏感词分组 | 编码格式、名称 1-128 字和状态校验正确;同租户编码保持唯一 |
| SW-002 | P1 | 新增敏感词并逐项选择匹配方式、动作、风险、场景 | 保存后列表字典显示正确;敏感词长度 1-255;至少选择一个生效场景 |
| SW-003 | P1 | 动作选择“替换”但不填替换文本 | 前后端均拒绝并提示“替换动作必须填写替换文本” |
| SW-004 | P1 | 输入非法匹配类型、动作、风险、状态或无效正则 | 后端拒绝;无效正则不得发布到 IM 运行时 |
| SW-005 | P1 | 切换启用/禁用并发送命中消息 | 禁用后不执行该词规则;启用后按所选场景和动作处理,后台状态与 IM 行为一致 |
| SW-006 | P1 | 批量导入含空行、重复词和已存在词的数据 | 单次仅允许 1-5000 条;返回 `inserted` 和 `skipped`,重复数据不产生多条记录 |
| SW-007 | P1 | 删除敏感词后再次发送命中内容 | 删除需确认;运行时规则及时失效,且操作有审计记录 |
### 3.10 文件管理
本页属于既有能力回归。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| FILE-001 | P1 | 按上传人、文件类型、大小、来源、审核状态、时间查询 | 表格元数据、下载地址、引用对象与存储记录一致,租户隔离正确 |
| FILE-002 | P1 | 预览、下载、删除文件 | 权限校验正确;删除需确认,引用中的已删除文件不能继续匿名访问 |
| FILE-003 | P1 | 执行审核通过/拒绝及批量处置 | 状态流转正确,拒绝原因必填,相关消息或群文件同步更新 |
### 3.11 用户反馈
本页属于既有能力回归。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| FB-001 | P1 | 按反馈类型、状态、优先级、用户和时间查询 | 查询、分页、详情及附件展示正确 |
| FB-002 | P1 | 修改处理状态、优先级和回复内容 | 状态流转符合约束,处理人和处理时间自动记录 |
| FB-003 | P2 | 批量处理并导出 | 成功/失败数量准确,导出范围与当前筛选一致 |
### 3.12 操作日志
本页属于既有能力回归,也是所有后台写操作的审计入口。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| OPLOG-001 | P0 | 完成策略发布、消息删除、用户封禁后查询日志 | 三类操作均可按模块、操作者、目标和时间检索,结果、原因、请求 ID 完整 |
| OPLOG-002 | P0 | 检查请求参数与响应摘要 | 密码、验证码、Token、共享密钥、完整手机号/邮箱等不以明文出现 |
| OPLOG-003 | P1 | 只读管理员尝试越权写接口 | 记录拒绝结果及必要上下文,不记录敏感正文,不改变业务数据 |
### 3.13 用户账号审计
本页属于既有能力回归。动作至少覆盖修改资料、修改密码、注销、管理员改用户名/状态、重置密码、更新安全权限、强制下线、强制注销、调整策略分组、批量封禁、按 IP 封禁。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| UAUD-001 | P1 | 分别触发用户自助和管理员账号变更 | 动作枚举显示中文,操作者类型、目标用户、变更前后摘要和时间正确 |
| UAUD-002 | P0 | 执行重置密码并查看审计详情 | 仅记录“已重置”等安全摘要,绝不保存新旧明文密码 |
| UAUD-003 | P1 | 调整 U1 策略分组 | 产生 `assign_policy_group` 审计,旧分组、新分组及管理员可追溯 |
### 3.14 群事件审计
本页属于既有能力回归。事件包括建群、加入/退出/踢出、禁言/解禁、转让群主、增删管理员、改群名、封禁/解封和解散。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| GAUD-001 | P1 | 在 G1 触发踢人、禁言、改群名 | 对应事件各产生一条审计,操作人、目标人、群、时间和原因正确 |
| GAUD-002 | P1 | 系统执行群封禁或解散 | 操作人显示系统或实际管理员,事件类型与最终群状态一致 |
### 3.15 管理员登录历史
本页属于 HotGo 既有能力回归。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| ALOGIN-001 | P1 | 管理员成功登录、失败登录、退出后查询 | 账号、IP、浏览器、系统、结果和时间正确;失败原因不泄露密码 |
| ALOGIN-002 | P1 | 使用日期、账号、IP、状态筛选并导出 | 列表和导出范围一致,普通管理员不能查看超出权限的数据 |
### 3.16 用户登录历史
筛选项:租户、用户 ID、脱敏账号、登录方式、事件类型、客户端平台、IP、登录时间。表格字段:ID、用户、脱敏账号、登录方式、事件、客户端、版本、IP、地区、风险等级、登录时间、退出时间。
登录方式:`password` 密码、`sms_code` 短信验证码、`sso` 单点登录、`qr_code` 二维码、`token_refresh` 令牌刷新。事件:`login` 登录、`refresh` 刷新会话、`logout` 主动退出、`kicked` 被踢下线、`session_expired` 会话过期。平台:iOS、Android、Web、桌面端、未知。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| ULOGIN-001 | P0 | U1 密码登录成功后在后台查询 | IM `im_user_login_logs` 新增成功记录;后台展示用户、脱敏账号、会话、设备、平台、IP、请求 ID 和登录时间 |
| ULOGIN-002 | P0 | U1 主动退出后刷新列表 | 对应会话 `logout_at` 被更新或产生退出事件;退出时间不早于登录时间 |
| ULOGIN-003 | P1 | 分别模拟刷新会话、被踢、会话过期 | 事件字典和发生时间正确,同一 session 可串联追踪 |
| ULOGIN-004 | P1 | 按全部筛选项组合查询 | Admin 参数正确透传到 IM Internal API;结果按登录时间倒序,分页不重复不漏项 |
| ULOGIN-005 | P0 | 检查列表、详情、Admin/IM 日志及数据库 | 只保存账号掩码和不可逆哈希;密码、短信验证码、Token、Cookie 不得出现 |
| ULOGIN-006 | P1 | 导出当前筛选结果 | 含 ID、用户、脱敏账号、事件、平台、IP、地区、风险、原因、时间;租户隔离和脱敏规则不因导出而放宽 |
### 3.17 IP 账号统计
本页属于后续改造/回归能力。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| IPSTAT-001 | P1 | 按 IP、账号数、风险和时间查询 | 同一 IP 关联账号数、成功/失败次数和最近时间统计准确 |
| IPSTAT-002 | P1 | 查看关联用户并按 IP 封禁 | 关联列表受租户隔离;封禁需确认并记录命中用户数、成功数和失败数 |
### 3.18 登录失败日志
筛选项:租户、用户 ID、脱敏账号、失败类型、平台、IP、尝试时间。表格字段:ID、用户、脱敏账号、失败类型、原因、IP、地区、客户端、版本、风险等级、风控动作、尝试时间;按钮:详情、加入 IP 黑名单、封禁用户、导出。
失败类型:`password_error` 密码错误、`code_error` 验证码错误、`account_not_found` 账号不存在、`account_disabled` 账号已禁用、`rate_limited` 访问频率受限、`risk_blocked` 风控拦截。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| LFAIL-001 | P0 | 使用错误密码登录 U1 | IM `im_login_failure_logs` 新增 `password_error`;后台展示脱敏账号、IP、设备、平台、风险及尝试时间 |
| LFAIL-002 | P0 | 分别触发验证码错误、账号不存在、账号禁用、限频和风控拦截 | 失败类型、原因码、详情、风险动作正确;未知账号场景不泄露账号是否真实存在 |
| LFAIL-003 | P1 | 点击详情 | 根据日志 ID 返回完整安全上下文;`id<=0` 或跨租户 ID 被拒绝 |
| LFAIL-004 | P0 | 点击“加入 IP 黑名单”并确认 | IP 必填;在安全策略 IP 黑名单生成规则,不重复插入相同有效规则,后续登录按场景被阻断 |
| LFAIL-005 | P0 | 对有关联用户的记录点击“封禁用户”,填写原因 | 用户 ID 和原因必填;用户状态变为禁用,已有会话按设计失效,账号审计和操作日志可追溯 |
| LFAIL-006 | P1 | 对账号不存在、无关联用户的记录点击封禁 | 按钮不可用或接口明确提示无法关联用户,不得误封其他用户 |
| LFAIL-007 | P1 | 导出失败日志 | 结果与筛选一致,账号继续脱敏,不包含提交的密码、验证码和完整鉴权数据 |
### 3.19 私域设置
本页属于既有/后续能力回归,重点验证注册与验证码开关不绕过安全策略。
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| TENANT-001 | P1 | 修改短信验证码、邀请码注册等开关并保存 | 保存后重新进入值保持一致,移动端登录/注册入口按租户配置变化 |
| TENANT-002 | P0 | 关闭短信验证码并尝试短信登录、忘记密码、验证码注册 | 后端拒绝被关闭的流程,不能仅靠隐藏移动端按钮 |
| TENANT-003 | P0 | 全局禁加好友期间通过注册流程触发自动加好友 | 注册成功与否按私域设置判断,但任何自动加好友均被安全策略统一拒绝 |
### 3.20 安全策略
页面包含“策略版本”和“IP 黑白名单”两个页签。策略按钮:新建草稿、编辑/查看、发布、恢复默认;策略内含好友控制、消息宵禁和其他 JSON 策略。IP 规则按钮:新增、编辑、删除。
发布状态:`draft` 草稿、`published` 已发布、`archived` 已归档、`failed` 发布失败。IP 规则类型:白名单/黑名单;动作:`allow` 放行、`challenge` 二次验证、`block` 阻断、`audit_only` 仅审计;场景:登录、注册、发消息、上传文件;风险:低、中、高。
#### 3.20.1 策略版本与发布
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| SEC-001 | P0 | 点击新建草稿,填写策略名称并保存 | `/imSecurityPolicy/save` 创建 `draft`;只有草稿可编辑,已发布版本只读 |
| SEC-002 | P0 | 修改草稿但不发布,在移动端验证旧策略 | 草稿不影响 IM 运行时,移动端继续执行最后一个已发布版本 |
| SEC-003 | P0 | 点击发布并确认 | 发布成功后当前版本为 `published`,原已发布版本归档;IM Server 收到策略更新并按新版本执行 |
| SEC-004 | P0 | 模拟 Kafka/IM 策略应用失败 | 不得提示完全成功;版本进入失败或可重试状态,运行时继续使用上一个有效版本 |
| SEC-005 | P1 | 点击恢复默认 | 仅创建一个默认草稿,不直接发布、不改变当前生效策略 |
| SEC-006 | P1 | 输入非法 JSON、空策略名或超过 500 字发布原因 | 前后端拒绝提交并明确指出错误字段 |
#### 3.20.2 全局禁止加好友
关闭“允许加好友”时,后端必须强制设置:隐藏个人信息、禁止二维码、禁止群成员入口,并封锁 `search`、`profile_card`、`group_member`、`qrcode`、`invite_auto_add`、`recommendation` 六类入口。该限制必须由后端执行,移动端隐藏按钮不能作为唯一控制。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| FRIEND-001 | P0 | 关闭“允许加好友”并观察表单 | 隐藏资料、禁止二维码、禁止群成员入口自动开启且不可取消;六类入口全部选中 |
| FRIEND-002 | P0 | 仅保存草稿,不发布;U1 从用户搜索添加 U2 | 仍按旧策略执行,证明草稿不直接生效 |
| FRIEND-003 | P0 | 发布禁加好友策略;U1 从用户搜索添加 U2 | 页面不展示添加入口或提示“当前时段暂不允许添加好友”;直接调用加好友接口同样被拒绝 |
| FRIEND-004 | P0 | U1 打开 U2 个人名片并尝试添加 | 个人资料按策略隐藏,不展示可用个人名片及加好友入口;直接请求资料/好友接口不可绕过 |
| FRIEND-005 | P0 | U1 从 G1 群成员列表点击 U2 | 不显示 U2 可识别的个人信息、个人名片和加好友入口 |
| FRIEND-006 | P0 | U1 扫描 U2 二维码并构造二维码加好友请求 | 二维码入口不可用,后端拒绝建立好友关系 |
| FRIEND-007 | P0 | 使用邀请码注册并触发自动添加,再查看推荐联系人 | `invite_auto_add` 和 `recommendation` 均被阻断,不产生好友关系 |
| FRIEND-008 | P0 | 修改客户端请求绕过 UI、伪造入口类型或不传入口类型 | 后端按全局开关统一拒绝,不能因缺失/伪造入口字段放行 |
| FRIEND-009 | P0 | 将 U3 配置为例外用户(若本版本支持)并重复六入口测试 | 仅 U3 按明确白名单规则放行,U1/U2 仍被阻断;未实现例外时页面和接口不得暗示支持 |
| FRIEND-010 | P0 | 重新开启并发布,再从六入口验证 | 限制解除,资料展示及加好友恢复为常规权限;已被拒绝的历史请求不会自动补建好友关系 |
#### 3.20.3 消息宵禁
规则字段:规则编码、名称、日期类型、作用范围、开始/结束时间、时区、状态、提示语、白名单用户 ID;支持每天、工作日、周末、自定义日期,范围为单聊/私聊或群聊。默认时区为 `Asia/Shanghai`,支持跨午夜时间段。
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| CURFEW-001 | P0 | 开启宵禁但不添加规则并保存 | 后端拒绝;开启时至少存在一条规则,且至少一条启用的单聊/私聊规则 |
| CURFEW-002 | P0 | 新增工作日 18:00-09:00、范围单聊、状态启用的规则并发布 | 保存和发布成功,规则编码合法且唯一,IM Server 使用 `Asia/Shanghai` 解释跨午夜区间 |
| CURFEW-003 | P0 | 命中宵禁时段,U1 向 U2 发送文本、图片、语音、文件和名片 | 所有单聊/私聊消息均由后端拒绝,不写入可投递消息;客户端显示配置的宵禁提示语 |
| CURFEW-004 | P0 | 在开始时刻前 1 秒、开始时刻、结束时刻前 1 秒、结束时刻发送 | 边界按统一规则执行;开始时刻进入限制,结束时刻解除,Admin 与 IM 时间口径一致 |
| CURFEW-005 | P0 | 分别验证每天、工作日、周末、自定义日期 | 仅命中日期生效;自定义日期为空或格式非法时拒绝保存 |
| CURFEW-006 | P0 | 将 U3 加入白名单,在命中时段由 U3 与 U1 发消息 | 白名单方向规则符合产品定义且保持一致;非白名单 U1/U2 仍被拒绝 |
| CURFEW-007 | P0 | 增加群聊范围规则并发布,在 G1 发消息 | 命中时群消息被后端拒绝;只有单聊规则时群聊不受影响 |
| CURFEW-008 | P0 | 禁用规则或关闭宵禁并发布 | 消息恢复发送;旧策略缓存及时失效,不需重启 IM Server |
| CURFEW-009 | P0 | 仅保存新的时间规则但不发布 | IM Server 继续执行最后已发布规则,不能提前生效 |
| CURFEW-010 | P0 | 客户端修改本地时间、时区或直接调用发消息接口 | 后端以策略时区和服务器可信时间判定,无法绕过宵禁 |
#### 3.20.4 IP 黑白名单
| 用例 ID | 优先级 | 操作步骤 | 预期结果及核验点 |
|---|---|---|---|
| IPRULE-001 | P1 | 新增黑名单规则,填写单 IP、阻断、登录场景、高风险 | 保存成功,列表字段及字典显示正确;登录请求从该 IP 发起时被阻断 |
| IPRULE-002 | P1 | 新增 CIDR 白名单并选择多个场景 | CIDR 校验正确,命中范围内 IP 按白名单优先级执行,其他场景不受影响 |
| IPRULE-003 | P1 | 输入非法 IP/CIDR、空场景、非法动作/风险/状态 | 后端拒绝且不产生记录 |
| IPRULE-004 | P1 | 编辑规则后重新触发对应场景 | 新规则生效,旧匹配缓存失效;详情和列表一致 |
| IPRULE-005 | P1 | 删除规则并确认 | 记录删除且运行时规则失效;取消确认不产生变化 |
| IPRULE-006 | P0 | 从登录失败日志加入同一 IP 黑名单 | 安全策略页可查询到规则;重复操作不产生冲突规则,审计来源可追踪 |
## 4. Admin Server 与 IM Server 联调核验
### 4.1 接口映射
| 后台操作 | Admin Server | IM Server Internal / Kafka |
|---|---|---|
| 概览指标 | `GET /imDashboard/overview` | `GET /internal/admin-query/tenants/{tenant}/dashboard/overview` |
| 趋势数据 | `GET /imDashboard/trends` | `GET /internal/admin-query/tenants/{tenant}/dashboard/trends` |
| 单聊列表 | `GET /imPrivateMessage/list` | `GET /internal/admin-query/tenants/{tenant}/private-messages` |
| 群聊列表 | `GET /imGroupMessage/list` | `GET /internal/admin-query/tenants/{tenant}/group-messages` |
| 消息详情 | `GET /imPrivateMessage/view` 或 `/imGroupMessage/view` | `GET /internal/admin-query/tenants/{tenant}/messages/{topic}/{seq}` |
| 隐藏/删除消息 | `POST .../hide`、`POST .../delete` | Kafka `message.hide`、`message.delete` |
| 用户登录历史 | `GET /imUserLoginHistory/list` | `GET /internal/admin-query/tenants/{tenant}/user-login-logs` |
| 登录失败列表/详情 | `GET /imLoginFailureLog/list|view` | `GET /internal/admin-query/tenants/{tenant}/login-failure-logs[/{id}]` |
### 4.2 数据库抽查
以下 SQL 仅用于测试环境核验,查询条件必须带测试租户或测试数据标识:
```sql
-- IM 登录成功/退出审计
SELECT id, tenant_id, user_id, account_mask, login_type, event_type,
session_id, platform, ip, risk_level, login_at, logout_at
FROM im_user_login_logs
WHERE tenant_id = ? AND user_id = ?
ORDER BY id DESC LIMIT 20;
-- IM 登录失败审计
SELECT id, tenant_id, user_id, account_mask, failure_type, reason,
ip, platform, risk_level, risk_action, attempt_at
FROM im_login_failure_logs
WHERE tenant_id = ? AND ip = ?
ORDER BY id DESC LIMIT 20;
-- IM 消息后台处置结果
SELECT tenant_id, command_id, event_id, action, topic, seq_id,
status, reason, created_at, updated_at
FROM im_message_admin_actions
WHERE tenant_id = ? AND topic = ? AND seq_id = ?
ORDER BY id DESC;
```
核验要求:同一幂等键不得产生多条有效执行;失败必须保留错误状态和可追踪信息;表中不得出现密码、验证码、Token 或完整认证请求。
## 5. 非功能与异常测试
| 用例 ID | 优先级 | 操作步骤 | 预期结果 |
|---|---|---|---|
| NFT-001 | P1 | 列表准备 10 万级数据并执行常用筛选 | 首屏和翻页在项目约定时间内返回,无全表扫描导致的持续高负载 |
| NFT-002 | P1 | 并发 20 次概览、消息、登录审计查询 | 服务稳定,连接池无耗尽,租户数据不串读 |
| NFT-003 | P0 | 伪造 Internal API 请求、错误共享密钥或重放过期请求 | IM Server 拒绝未授权请求并记录安全日志,不返回业务数据 |
| NFT-004 | P0 | 在 Topic、关键词、原因、备注中输入 SQL/XSS 字符串 | 使用参数化查询;页面转义展示,不执行脚本,不污染导出文件 |
| NFT-005 | P1 | Admin 与 IM 短时网络中断后恢复 | 查询可恢复;异步命令状态可追踪,不丢命令、不重复执行 |
| NFT-006 | P1 | 检查 PC 常用分辨率及窄屏 | 搜索区、表格、弹窗和操作列无重叠;横向滚动可用,按钮文本不被截断为不可辨识状态 |
## 6. 上线验收门槛
满足以下条件方可通过本轮验收:
1. 所有 P0 用例通过,尤其是 `FRIEND-*`、`CURFEW-*`、消息处置、登录审计和租户隔离。
2. P1 无阻断主流程的问题;遗留问题有负责人、修复版本和回归日期。
3. Admin Server、IM Server、Kafka 和两侧数据库结果一致,不以页面提示代替最终状态核验。
4. 全局禁加好友和宵禁均通过“直接调用后端接口”的绕过测试。
5. 数据库、应用日志、操作日志和导出文件均通过敏感信息检查。
6. 其余未纳入本轮改造的页面必须完成基本回归,但不应据此宣称 20 个页面已全部完成重构。
## 7. 执行记录
| 字段 | 内容 |
|---|---|
| Admin Server 版本/提交 | `16cc456`(功能提交:`bb87d01`) |
| IM Server 版本/提交 | `b92154b`(功能提交:`6b20a63`) |
| 前端版本/构建时间 | |
| 测试环境 | `https://admin.118im.com` |
| 测试租户 | |
| 测试人员 | |
| 执行日期 | |
| P0 通过/失败/阻塞 | |
| P1 通过/失败/阻塞 | |
| 缺陷链接 | |
| 最终结论 | 通过 / 有条件通过 / 不通过 |
@@ -0,0 +1,353 @@
# 鸽达达管理后台测试问题记录
> 测试环境:`https://admin.118im.com/admin#/dashboard/console`
> 执行日期:2026-09-07
> 参考用例:`测试用例文档.md`
> 测试方式:浏览器黑盒测试、HTTP 只读探测、测试库只读核验、当前代码静态核查
## 1. 执行摘要
| 项目 | 结果 | 说明 |
|---|---|---|
| 测试站点可达性 | 通过 | `/admin/` 返回 HTTP 200,响应约 0.2-0.5 秒 |
| 前端功能部署 | 通过 | 部署包包含 `imDashboard`、`imPrivateMessage`、`imGroupMessage`、`imUserLoginHistory`、`imLoginFailureLog`、`imSecurityPolicy`、`imSensitiveWord`、`imUserPolicyGroup` |
| Admin 新接口路由 | 通过 | 8 个列表/概览接口均已注册;无登录态请求统一返回业务码 `61`“登录身份已失效” |
| 浏览器页面操作 | 已恢复 | 已接管用户登录标签页,完成 8 个新增菜单及查询/刷新按钮的只读复测 |
| Admin 测试库结构 | 已修复 | 已在指定测试库 `hotgo` 创建 7 张策略相关表、8 个页面菜单及按钮权限 |
| IM 测试库结构 | 通过 | `tinode` 已存在 3 张新增审计/处置表 |
| IM 登录审计数据 | 部分通过 | IM Internal API 已恢复,成功登录历史返回 77 条;失败日志返回成功空列表,仍缺少失败场景验收数据 |
| Kafka Admin 命令 | 风险 | 13 条历史命令中 12 条长期待处理、1 条死信,需检查消费链路 |
| 消息隐藏/删除 | 未执行 | `im_message_admin_actions` 为 0;尚未准备专用消息数据,且 Kafka 命令链路存在长期积压风险 |
| 好友总开关/宵禁 | 未通过实现核查 | Admin 可配置和发布,但当前 IM Server 未找到运行时消费及拦截代码 |
当前结论:**不通过。页面菜单和 Admin 本地策略表已可用,但 IM Internal 查询接口不可用,真实概览、消息和登录日志无法展示;IM 运行时策略执行也尚未完成。**
## 2. 问题清单
### BUG-001 Admin 测试库缺少策略相关表
| 字段 | 内容 |
|---|---|
| 严重级别 | P0 |
| 类型 | 数据库部署/环境配置 |
| 状态 | 已修复并完成只读页面复测 |
| 关联用例 | `UPG-*`、`SW-*`、`SEC-*`、`FRIEND-*`、`CURFEW-*`、`IPRULE-*` |
**操作步骤**
1. 连接用户指定测试数据库 `64.118.137.182:53306`。
2. 查询 `information_schema.tables` 中 `hotgo` 库的本轮新增表。
**实际结果**
以下表均不存在:
- `im_security_policy`
- `im_message_curfew_rules`
- `im_security_ip_rules`
- `im_sensitive_word_groups`
- `im_sensitive_words`
- `im_user_policy_groups`
- `im_user_policy_group_members`
**预期结果**
测试环境 Admin Server 所连接的数据库应包含全部新增表,并已执行初始化数据。
**影响**
若线上 Admin Server 确实连接此 `hotgo` 库,用户策略分组、敏感词、安全策略、宵禁和 IP 规则页面会查询失败或无法保存。如果 Admin Server 实际连接其他数据库,则说明当前提供的测试库地址与部署配置不一致,需要明确真实连接库后重新核验。
**建议**
核对 Admin Server 生产配置中的数据库 DSN;确认无误后执行已提交的 Admin 建表 SQL,并对表结构、索引和初始化数据做一次部署校验。
**处理记录(2026-09-07)**
已在 `hotgo` 执行 `docs/admin-adfahi-research/sql/Admin-Server改造.sql`。7 张表均创建成功,`im_security_policy` 已初始化 1 条全局默认已发布策略,其余新增业务表初始为空。页面复测中,安全策略正确展示该默认策略,敏感词和用户分组正确展示空列表;写操作尚未执行。
### BUG-002 测试库缺少 8 个新增页面菜单记录
| 字段 | 内容 |
|---|---|
| 严重级别 | P0 |
| 类型 | 菜单/权限初始化 |
| 状态 | 已修复并完成超级管理员复测 |
| 关联用例 | `COM-001`、`COM-002` |
**实际结果**
在 `hotgo.hg_admin_menu` 中未查询到以下菜单:
- `imDashboardIndex`
- `imPrivateMessageIndex`
- `imGroupMessageIndex`
- `imUserLoginHistoryIndex`
- `imLoginFailureLogIndex`
- `imUserPolicyGroupIndex`
- `imSensitiveWordIndex`
- `imSecurityPolicyIndex`
部署前端包已经包含对应页面模块,后端接口也已注册,说明代码已部署但数据库菜单初始化可能未执行。
**预期结果**
超级管理员登录后可在 IM 管理下看到 8 个页面;功能权限记录完整,普通角色按授权显示按钮。
**建议**
确认部署库后执行:
- `server/storage/data/generate/im_adfahi_batch2_menu.sql`
- `server/storage/data/generate/im_adfahi_policy_menu.sql`
执行后分别用超级管理员、只读管理员和租户管理员回归菜单及按钮权限。
**处理记录(2026-09-07)**
已执行 `im_adfahi_policy_menu.sql` 和 `im_adfahi_batch2_menu.sql`。8 个页面菜单均启用并挂载到 `imManager`(ID 2431)下;页面与按钮权限合计 43 条。用户需重新登录或刷新权限缓存后复测菜单显示。
**页面复测(2026-09-07)**
超级管理员展开“IM管理”后,数据概览、用户分组、单聊消息、群聊消息、用户登录历史、登录失败日志、敏感词库、安全策略 8 个页面菜单均可见并可打开。`COM-001` 的菜单可见性通过;只读管理员和租户管理员权限仍需使用对应账号复测。
### BUG-003 Kafka Admin 命令存在长期 Pending 和 Dead 记录
| 字段 | 内容 |
|---|---|
| 严重级别 | P0 |
| 类型 | Kafka/异步命令链路 |
| 状态 | 待处理 |
| 关联用例 | `PM-004` 至 `PM-008`、`GM-003` 至 `GM-006`、`NFT-005` |
**实际结果**
`hotgo.im_admin_commands` 共 13 条记录:
| 事件类型 | 状态 | 数量 |
|---|---:|---:|
| `user.password.reset` | `pending` | 12 |
| `user.password.reset` | `dead` | 1 |
最新死信记录时间为 2026-08-28,原因 `RESOURCE_NOT_FOUND`,消息 `not found`。其余 Pending 记录同样创建于 2026-08-28,长期未进入成功或失败终态。
**预期结果**
命令应在约定时间内进入 `succeeded`、`failed` 或 `dead` 终态;Pending 不应长期积压。死信应可查看、重放、忽略或解决。
**影响**
消息隐藏/删除、用户治理等 Kafka 命令可能出现后台提示已提交但 IM 不执行,属于治理能力失效风险。
**建议**
检查 Admin outbox worker、Kafka topic、consumer group、IM consumer 启动配置及结果回传 topic;处理历史积压后再执行消息隐藏/删除端到端测试。
### BUG-004 IM Server 尚未执行安全策略、敏感词和用户策略分组
| 字段 | 内容 |
|---|---|
| 严重级别 | P0 |
| 类型 | 功能未完成 |
| 状态 | 待开发 |
| 关联用例 | `SW-005`、`SW-007`、`FRIEND-*`、`CURFEW-*`、`IPRULE-*` |
**代码核查结果**
Admin Server 已实现策略数据保存、发布和 `security.policy.updated` 等事件写出;当前 IM Server 仅发现 `message.hide`、`message.delete` 等 Admin 命令消费,未发现:
- `security.policy.updated` 事件消费与策略缓存刷新。
- 全局禁止加好友在用户搜索、个人名片、群成员、二维码、邀请自动添加、推荐联系人六类入口的后端校验。
- 禁加好友时个人资料、个人名片的隐藏处理。
- 消息发送路径中的宵禁日期、时区、跨午夜、范围和白名单判定。
- 敏感词及用户策略分组在 IM 运行时的加载与执行。
**预期结果**
后台发布后,IM Server 接收事件、加载对应版本,并在实际移动端请求链路强制执行;直接调用接口也不能绕过。
**影响**
后台可以显示“发布成功”,但移动端行为不会变化。全局禁止加好友和宵禁属于明确要求,未完成时禁止上线。
### DATA-001 IM 测试数据不足,无法覆盖失败日志和消息处置
| 字段 | 内容 |
|---|---|
| 严重级别 | P1 |
| 类型 | 测试数据 |
| 状态 | 待准备 |
| 关联用例 | `PM-*`、`GM-*`、`LFAIL-*` |
**实际结果**
- `tinode.im_user_login_logs`:43 条。
- `tinode.im_login_failure_logs`:0 条。
- `tinode.im_message_admin_actions`:0 条。
登录日志分布以 `password/login` 和 `token_refresh/login` 为主,尚不足以覆盖主动退出、被踢、会话过期等事件。账号掩码和 metadata 的只读抽查未发现手机号全明文或密码、验证码、Token 键值。
**建议**
准备专用测试账号,分别触发密码错误、验证码错误、账号不存在、账号禁用、限频和风控拦截;准备单聊和群聊消息后执行隐藏、删除及幂等重放。
### BUG-005 IM Internal 查询接口返回 resource not found
| 字段 | 内容 |
|---|---|
| 严重级别 | P0 |
| 类型 | 服务部署/路由联调 |
| 状态 | 已修复,接口复测通过 |
| 关联用例 | `DASH-001`、`DASH-002`、`PM-001`、`GM-001`、`ULOGIN-001`、`ULOGIN-004`、`LFAIL-001` |
**操作步骤**
1. 使用超级管理员进入“IM管理”。
2. 打开数据概览并点击“刷新”。
3. 分别进入单聊消息、群聊消息、用户登录历史、登录失败日志并点击“查询”。
**实际结果**
- 数据概览六项指标全部显示 0,趋势表显示“无数据”,页面提示 `resource not found`。
- 单聊消息、群聊消息、用户登录历史、登录失败日志均显示 0 条,查询后提示 `resource not found`。
- 浏览器控制台在每次查询后记录同名接口错误。
- 数据库已有 `tinode.im_user_login_logs` 43 条,因此用户登录历史页面的 0 条不是正常空数据。
**预期结果**
Admin Server 应能调用 IM Server Internal API,概览、消息及登录日志返回真实数据;确实无数据的接口应返回成功的空列表,不能返回资源不存在。
**根因定位(2026-09-07)**
- 当前 IM Server 源码已注册 `/internal/admin-query/`,并已实现 dashboard overview/trends、private-messages、group-messages、user-login-logs、login-failure-logs 路由,不是五个查询分别缺少实现。
- `authorizeInternalAPI` 在 `internal_api.enabled=false` 时固定返回 HTTP 404、业务码 `30004`、消息 `resource not found`;该响应与页面错误完全一致。
- 对测试环境 `https://api.118im.com/internal/admin-query/tenants/probe/dashboard/overview` 进行未签名只读探测,实际返回 HTTP 404、业务码 `30004`、消息 `resource not found`。若 Internal API 已启用,同一未签名请求应返回 HTTP 401、消息 `internal signature required`。
- Gitea Actions 第 66 次部署(run 99,提交 `b92154b`)日志显示工作流调用 `deploy.sh` 时已经设置 `INTERNAL_API_ENABLED=true`,且部署成功切换到 green 实例;但部署后的公开实例仍表现为 disabled。
由此确认:提交 `c68c5ac` 将 Docker 配置模板中原本固定启用的 Internal API 改成了额外环境开关,但现有蓝绿部署并不会把这些新增变量注入容器,导致容器继承镜像默认值 `false`。这是新增启动配置造成的部署回归,不应要求部署机再增加一组开关配置。
**建议**
已将 `docker/tinode/config.template` 恢复为改造前的启动方式:Internal API 固定开启、签名窗口固定 300 秒、调用方固定为 `hotgo-admin`,继续复用原有共享密钥配置;同时移除部署工作流中新增的开关、窗口和服务名变量。部署机无须增加 Compose 配置。
重新构建并部署 IM Server 后,先执行未签名探测:预期由当前的 HTTP 404 `resource not found` 变为 HTTP 401 `internal signature required`;再使用正确签名验证 overview、trends、private/group message、login history、login failure 六类接口,最后回归后台页面。
**接口复测(2026-09-07,IM `35da302`)**
- Gitea Actions run 100 部署成功,线上未签名探测由 HTTP 404 变为预期的 HTTP 401 `internal signature required`。
- 正确签名请求全部返回 HTTP 200、业务码 `0`:概览返回总用户 211、活跃用户 12、群组 95、会话 197、消息 2182、今日消息 167;7 日趋势返回 7 行。
- 单聊列表返回 1528 条,群聊列表返回 654 条;分页、关键词筛选及首条消息详情均通过。
- 用户登录历史返回 77 条;登录方式、事件、脱敏账号及组合筛选均通过。
- 登录失败日志返回 HTTP 200 的空列表,总数 0,属于当前无测试数据,不再是接口错误。
- 不存在租户正确返回 HTTP 404、业务码 `30010`、`tenant unavailable`。
- Admin Server 的 IM 签名客户端和字段转换相关 Go 测试通过。
### BUG-006 新页面调用的字典 Store API 与部署版本不兼容
| 字段 | 内容 |
|---|---|
| 严重级别 | P1 |
| 类型 | 前端兼容性 |
| 状态 | 已发布,静态包复测通过,待登录态页面点击确认 |
| 关联用例 | `UPG-001`、`SW-002`、`SEC-001`、`COM-004` |
**实际结果**
- 用户分组页面加载及点击“查询”时抛出 `TypeError: D.getOptions is not a function`。
- 敏感词库页面抛出 3 次 `c.getOptions is not a function`。
- 安全策略页面抛出 `q.getOptions is not a function`。
- 安全策略表格仍能显示 1 条默认策略,敏感词和用户分组仍能显示空列表,但字典筛选项和后续编辑表单存在失效风险。
**预期结果**
页面应通过当前项目字典 Store 的公开方法加载选项,控制台无异常;状态、类型、动作、风险等级等枚举应正确展示和筛选。
**建议**
已对照当前 `useDictStore` 实际 API,将用户分组、敏感词、安全策略页面统一改为 `dict.getOption()`;额外排查并修复 IM 文件策略同类调用,需过滤选项时使用 `dict.getOptionUnRef()`。源码扫描已无业务页面调用 `dict.getOptions()`,ESLint 和生产构建通过。重新发布 Admin 前端后回归筛选、编辑弹窗和字典标签。
**发布复测(2026-09-07,Admin `7839dce`)**
- Gitea Actions run 103 发布成功,线上入口包已切换到 `index-Ch8uxbvK.js`。
- 用户分组、敏感词、安全策略、文件策略四个线上动态页面包均可下载,且均不再包含 `getOptions` 调用,确认不是旧分包或 CDN 缓存。
- 十条相关 Admin 路由的未登录请求均返回业务码 `61`,鉴权边界正常。
- 本轮无法取得内置浏览器已登录标签页的会话控制,筛选下拉、编辑弹窗和写操作仍需登录态页面点击确认,不能仅凭静态包核对标记为完整通过。
### BLOCK-001 浏览器安全检查阻塞自动化页面测试
| 字段 | 内容 |
|---|---|
| 严重级别 | 环境阻塞,非产品缺陷 |
| 状态 | 已解除 |
| 关联用例 | 所有需要登录态和页面点击的用例 |
已识别到 Codex 内置浏览器中的测试环境标签页,页面标题为“主控台”,URL 和登录状态由用户确认。但自动化接管域名时,Codex 浏览器安全检查返回“permission request could not complete”,因此未读取或复用登录 Token,也未绕过安全控制。
恢复浏览器授权后,应从 `COM-001` 开始继续,并优先执行只读查询。涉及删除消息、删除规则等操作时仅使用专用测试数据。
**复测记录(2026-09-07,建表后)**
再次读取内置浏览器状态,测试标签页仍为“主控台”且 URL 正确;调用浏览器接管接口时仍返回 `Auto-review could not complete for https://admin.118im.com`。本次复测未绕过安全控制,页面用例继续标记为阻塞。
**解除记录(2026-09-07)**
再次连接后浏览器授权恢复,已成功接管同一登录标签页并完成菜单展开、页面切换、查询/刷新和表格读取。该项不再阻塞后续自动化测试。
## 3. 已执行用例
| 执行 ID | 对应测试 | 结果 | 实际结果 |
|---|---|---|---|
| RUN-001 | 站点可达性 | 通过 | `/admin/` HTTP 200,HTML 和静态资源可加载 |
| RUN-002 | 前端部署完整性 | 通过 | 8 个新增页面模块均存在于部署 JS 包 |
| RUN-003 | 未登录接口鉴权 | 通过 | 8 条接口统一返回业务码 `61`,未泄露业务数据 |
| RUN-004 | COM-001 菜单检查 | 通过 | 超级管理员可见并可打开 8 个新增页面;其他角色权限待测 |
| RUN-005 | Admin 新表检查 | 失败 | 7 张策略相关表缺失 |
| RUN-006 | IM 新表检查 | 通过 | 3 张表均存在 |
| RUN-007 | ULOGIN-001 数据检查 | 失败 | 数据库有 43 条登录记录,后台查询为 0 条并提示 `resource not found` |
| RUN-008 | ULOGIN-005 敏感字段抽查 | 通过 | 当前 SQL 规则未发现可疑明文记录 |
| RUN-009 | LFAIL-001 至 LFAIL-007 | 部分阻塞 | 页面可打开和查询,但接口返回 `resource not found`;失败日志表为 0 条,缺少动作数据 |
| RUN-010 | PM/GM 消息处置 | 阻塞 | 动作表为 0 条,Kafka 另有长期积压风险 |
| RUN-011 | FRIEND/CURFEW | 失败(实现核查) | IM 运行时执行代码未完成 |
| RUN-012 | BUG-001 建表复测 | 通过 | 7 张 Admin 策略表均存在,默认安全策略 1 条 |
| RUN-013 | BUG-002 菜单复测 | 通过 | 8 个页面菜单、43 条页面/按钮权限存在且状态启用 |
| RUN-014 | BLOCK-001 浏览器接管复测 | 阻塞 | 建表后重试仍被 Codex 域名安全检查拒绝,未执行页面点击 |
| RUN-015 | BLOCK-001 浏览器接管再次复测 | 通过 | 授权恢复,已完成登录态页面操作 |
| RUN-016 | DASH-001、DASH-002 | 失败 | 刷新后六项指标均为 0,趋势无数据,接口返回 `resource not found` |
| RUN-017 | UPG-001 | 失败 | 表格正确显示 0 条,但查询触发 `getOptions is not a function` |
| RUN-018 | PM-001 | 失败 | 单聊消息显示 0 条,查询接口返回 `resource not found` |
| RUN-019 | GM-001 | 失败 | 群聊消息显示 0 条,查询接口返回 `resource not found` |
| RUN-020 | ULOGIN-001、ULOGIN-004 | 失败 | 数据库 43 条,页面为 0 条,查询接口返回 `resource not found` |
| RUN-021 | LFAIL-001 页面查询 | 失败 | 页面可操作,但查询接口返回 `resource not found`;暂无失败登录测试数据 |
| RUN-022 | SW 页面只读查询 | 部分通过 | 正确返回 0 条;控制台存在 3 个字典 `getOptions` 异常 |
| RUN-023 | SEC 页面只读查询 | 部分通过 | 正确展示默认已发布策略 1 条;控制台存在字典 `getOptions` 异常 |
| RUN-024 | BUG-005 Internal API 启用复测 | 通过 | 未签名请求返回预期的 HTTP 401,不再返回 `resource not found` |
| RUN-025 | DASH-001、DASH-002 接口复测 | 通过 | 概览返回 211 用户、95 群组、2182 消息;7 日趋势返回 7 行 |
| RUN-026 | PM-001、GM-001 接口复测 | 通过 | 单聊 1528 条、群聊 654 条;分页、关键词和详情查询通过 |
| RUN-027 | ULOGIN 接口复测 | 通过 | 登录历史 77 条;登录方式、事件、脱敏账号及组合筛选通过 |
| RUN-028 | LFAIL-001 接口复测 | 部分通过 | 接口成功返回空列表;仍需准备失败登录数据验证详情及处置按钮 |
| RUN-029 | BUG-006 代码复测 | 通过 | 4 个页面同类字典调用已修复,ESLint 和生产构建通过;待发布后页面复测 |
| RUN-030 | BUG-006 发布包复测 | 部分通过 | run 103 发布成功,4 个线上动态页面包无 `getOptions`;待登录态页面点击确认 |
## 4. 后续复测顺序
1. 回归 BUG-005 对应后台页面,确认概览卡片和四个列表正确显示接口数据。
2. 发布 BUG-006 前端修复并回归筛选、编辑弹窗和字典标签。
3. 修复 Kafka Pending/Dead 链路,验证命令可进入终态。
4. 补齐 IM Server 安全策略、好友入口、宵禁和敏感词运行时实现。
5. 使用对应角色账号回归租户隔离、按钮权限、筛选、详情、分页和导出。
6. 使用专用数据执行新增、编辑、发布、隐藏、删除、IP 黑名单和封禁用户。
7. 使用移动端完成好友六入口、直接接口绕过、宵禁边界及白名单验收。
## 5. 复测记录
| 日期 | 版本 | 问题编号 | 复测结果 | 证据/备注 | 测试人 |
|---|---|---|---|---|---|
| 2026-09-07 | Admin `16cc456` | BUG-001 | 通过 | 7 张表存在,默认策略 1 条 | Codex |
| 2026-09-07 | Admin `16cc456` | BUG-002 | 通过 | 8 个菜单、43 条权限启用 | Codex |
| 2026-09-07 | Admin `16cc456` | BLOCK-001 | 阻塞 | 浏览器域名安全检查仍不可用 | Codex |
| 2026-09-07 | 测试环境部署版本 | BLOCK-001 | 通过 | 浏览器授权恢复并完成页面点击 | Codex |
| 2026-09-07 | 测试环境部署版本 | BUG-005 | 失败 | 5 类 IM 数据页面和概览统一返回 `resource not found` | Codex |
| 2026-09-07 | 测试环境部署版本 | BUG-006 | 失败 | 用户分组、敏感词、安全策略存在字典 Store API 异常 | Codex |
| 2026-09-07 | IM `35da302` / run 100 | BUG-005 | 通过 | 六类签名查询均为 HTTP 200;分页、筛选和消息详情通过 | Codex |
| 2026-09-07 | Admin 待发布版本 | BUG-006 | 代码通过 | 修复 4 个页面的字典 Store API 调用,生产构建通过 | Codex |
| 2026-09-07 | Admin `7839dce` / run 103 | BUG-006 | 部分通过 | 线上入口及 4 个动态页面包已更新,无错误方法调用;待登录态页面确认 | Codex |
File diff suppressed because it is too large Load Diff
@@ -0,0 +1,582 @@
# IM Admin 内部查询接口缺口清单
> 目标:梳理 HotGo/Admin 迁移到“Admin 只访问 hotgo,IM Server 只访问租户库”后,IM Server 需要补齐的 `/internal/admin-query/*` 只读接口。
> 当前日期:2026-08-27
> 适用项目:`im_manager` Admin 后台、`im_server` IM 服务端。
## 1. 背景
Kafka 写链路已经在 Admin 侧补齐:Admin 写操作落 `im_admin_commands`、`im_admin_outbox`,通过 Kafka 发给 IM Server,IM Server 执行业务后回传 `im.admin.command.result`。
查询链路还没有完全隔离。Admin 当前仍有不少页面通过 `imtenant.DB()` 或 `resolveImTenantDB()` 直连租户库。若要严格满足目标架构,Admin 查询租户业务数据时应调用 IM Server 的内部只读接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/...
```
IM Server 当前已经具备部分内部查询接口,但还不能覆盖 Admin 后台页面。
## 2. 已有接口
以下接口在 IM Server 当前实现中已经存在,可供 Admin 先迁移:
| 接口 | 用途 | 备注 |
|---|---|---|
| `GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}` | 用户详情 | 需要增强字段,见 P0。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}` | 群详情和治理配置 | 需要增强 topic 基础字段,见 P0。 |
| `GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/members` | 群成员列表 | 当前偏 cursor 分页,Admin 表格还需要 total。 |
| `GET /internal/admin-query/tenants/{tenant_code}/messages/{topic}/{seq_id}` | 消息详情 | 当前 Admin 暂未完整迁移消息页面。 |
| `GET /internal/admin-query/tenants/{tenant_code}/files/{file_id}` | 文件详情 | 还缺文件列表和引用列表。 |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters` | Kafka 死信列表 | Admin 已接入。 |
| `GET /internal/admin-query/tenants/{tenant_code}/kafka/dead-letters/{dead_id}` | Kafka 死信详情 | Admin 已接入。 |
## 3. 统一约定
### 3.1 鉴权
沿用 IM Server 内部 API HMAC 鉴权:
```text
X-Service-Name
X-Timestamp
X-Nonce
X-Signature
```
服务端需要校验:
1. service 是否存在。
2. service 是否包含 `admin_query` capability。
3. timestamp 是否在允许窗口内。
4. nonce 是否防重放。
5. 签名是否匹配。
### 3.2 分页返回
Admin 的 `BasicTable` 更适合 page/per_page/total 格式。建议所有列表接口统一返回:
```json
{
"code": 0,
"data": {
"items": [],
"total": 0,
"page": 1,
"per_page": 20
}
}
```
如果 IM Server 内部更适合 cursor,也建议同时返回:
```json
{
"code": 0,
"data": {
"items": [],
"total": 0,
"page": 1,
"per_page": 20,
"next_cursor": "",
"has_more": false
}
}
```
### 3.3 详情返回
详情接口统一返回:
```json
{
"code": 0,
"data": {}
}
```
### 3.4 时间与 ID
1. 时间统一返回 RFC3339/RFC3339Nano 字符串。
2. 雪花 ID、Tinode 用户 ID、19 位整数 ID 建议以字符串返回,避免前端丢精度。
3. 入参同时兼容字符串 ID 和纯数字 ID 时,IM Server 内部自行解析。
## 4. P0:优先补齐
P0 是 Admin 当前核心页面迁移所需接口,补完后可以先迁移用户、群组、文件等高频页面。
### 4.1 用户列表
```text
GET /internal/admin-query/tenants/{tenant_code}/users
```
用途:IM 用户管理列表。
建议查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `user_id` | string | 用户 ID,支持精确查询。 |
| `nickname` | string | 昵称模糊查询。 |
| `auth_name` | string | 登录标识,匹配 username/phone/email/凭证。 |
| `state` | int | 用户状态。 |
| `created_at_start` | string | 创建时间开始。 |
| `created_at_end` | string | 创建时间结束。 |
| `last_seen_start` | string | 最后在线开始。 |
| `last_seen_end` | string | 最后在线结束。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量,最大建议 200。 |
建议返回字段:
| 字段 | 说明 |
|---|---|
| `user_id` | 字符串用户 ID。 |
| `username` | 用户名。 |
| `phone` | 手机号。 |
| `nickname` | 昵称。 |
| `public_account` | 公开账号。 |
| `state` | 用户状态。 |
| `gender` | 性别。 |
| `region` | 地区。 |
| `auth_names` | 登录标识摘要。 |
| `credentials` | 凭证摘要。 |
| `auth_version` | 鉴权版本。 |
| `password_updated_at` | 密码更新时间。 |
| `last_seen` | 最后在线时间。 |
| `user_agent` | 最后设备 UA。 |
| `created_at` | 创建时间。 |
| `updated_at` | 更新时间。 |
当前 Admin 直连涉及表:`im_users`、`im_user_profiles`、`im_user_security`、`im_credentials`。
### 4.2 用户详情增强
现有接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}
```
建议增强返回字段:
| 字段 | 说明 |
|---|---|
| `profile` | 结构化用户资料。 |
| `public` | Tinode public JSON。 |
| `trusted` | Tinode trusted JSON。 |
| `access` | 访问权限 JSON。 |
| `tags` | 标签。 |
| `security` | `auth_version/password_updated_at` 等安全信息。 |
| `credentials` | 凭证列表,包含 method/value/done。 |
### 4.3 用户设备列表
```text
GET /internal/admin-query/tenants/{tenant_code}/users/{user_id}/devices
```
用途:用户详情中的登录设备列表。
建议查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `device_id` | string | 设备 ID。 |
| `platform` | string | 平台。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量。 |
建议返回字段:
| 字段 | 说明 |
|---|---|
| `device_id` | 设备 ID。 |
| `platform` | 平台。 |
| `user_agent` | UA。 |
| `last_seen` | 最后在线。 |
| `created_at` | 创建时间。 |
| `updated_at` | 更新时间。 |
当前 Admin 直连涉及表:`im_devices`。
### 4.4 群列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups
```
用途:IM 群组管理列表。
建议查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `topic` | string | 群 Topic。 |
| `owner_id` | string | 群主 ID。 |
| `state` | int | Tinode topic 状态。 |
| `group_status` | string | 业务群状态。 |
| `created_at_start` | string | 创建时间开始。 |
| `created_at_end` | string | 创建时间结束。 |
| `touched_at_start` | string | 活跃时间开始。 |
| `touched_at_end` | string | 活跃时间结束。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量。 |
建议返回字段:
| 字段 | 说明 |
|---|---|
| `topic` | 群 Topic。 |
| `state` | Tinode 状态。 |
| `owner_id` | 群主 ID。 |
| `seq_id` | 最新消息序号。 |
| `del_id` | 删除序号。 |
| `sub_count` | Tinode 订阅成员数。 |
| `touched_at` | 活跃时间。 |
| `created_at` | 创建时间。 |
| `updated_at` | 更新时间。 |
| `public` | Tinode public JSON。 |
| `trusted` | Tinode trusted JSON。 |
| `tags` | 标签。 |
| `aux` | 辅助 JSON。 |
| `group_name` | 群名称。 |
| `avatar_file_id` | 群头像文件 ID。 |
| `description` | 群简介。 |
| `group_status` | 业务群状态。 |
| `join_approval` | 入群审批开关。 |
| `mute_all` | 全员禁言。 |
| `member_upload_files` | 成员上传文件。 |
| `member_private_chat` | 成员私聊。 |
| `member_invite` | 成员邀请。 |
| `max_member_count` | 最大成员数。 |
| `max_admin_count` | 最大管理员数。 |
| `business_member_count` | 业务成员数。 |
| `settings_version` | 治理配置版本。 |
| `settings_updated_at` | 治理配置更新时间。 |
当前 Admin 直连涉及表:`im_topics`、`im_group_settings`。
### 4.5 群详情增强
现有接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}
```
当前接口主要返回 `im_group_settings`。建议增强 Tinode topic 基础字段:
| 字段 | 说明 |
|---|---|
| `state` | Topic 状态。 |
| `seq_id` | 最新消息序号。 |
| `del_id` | 删除序号。 |
| `sub_count` | 订阅数量。 |
| `public` | public JSON。 |
| `trusted` | trusted JSON。 |
| `access` | access JSON。 |
| `tags` | tags JSON。 |
| `aux` | aux JSON。 |
| `touched_at` | 活跃时间。 |
### 4.6 群成员列表增强
现有接口:
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/members
```
建议增强查询参数:
| 参数 | 类型 | 说明 |
|---|---|---|
| `user_id` | string | 用户 ID 精确过滤。 |
| `role` | string | `owner/admin/member`。 |
| `member_status` | string | 成员状态。 |
| `q` | string | 昵称、公开账号等关键词。 |
| `eligible_role` | string | 候选角色筛选。 |
| `page` | int | 页码。 |
| `per_page` | int | 每页数量。 |
| `cursor` | string | 兼容 cursor 分页。 |
| `limit` | int | 兼容 cursor 分页。 |
建议增强返回:
| 字段 | 说明 |
|---|---|
| `total` | 总数,Admin 表格需要。 |
| `items[].user_id` | 用户 ID。 |
| `items[].nickname` | 昵称。 |
| `items[].state` | 用户状态。 |
| `items[].mode_want` | 期望权限。 |
| `items[].mode_given` | 授予权限。 |
| `items[].recv_seq_id` | 接收序号。 |
| `items[].read_seq_id` | 已读序号。 |
| `items[].del_id` | 删除序号。 |
| `items[].role` | 业务角色。 |
| `items[].member_status` | 成员状态。 |
| `items[].group_nickname` | 群昵称。 |
| `items[].pinned` | 置顶。 |
| `items[].mute_notifications` | 免打扰。 |
| `items[].join_source` | 加入来源。 |
| `items[].joined_by` | 邀请或处理人。 |
| `items[].joined_at` | 加入时间。 |
| `items[].left_at` | 离开时间。 |
| `items[].removed_by` | 移除人。 |
| `items[].version` | 版本。 |
当前 Admin 直连涉及表:`im_group_members`、`im_subscriptions`、`im_users`、`im_user_profiles`。
### 4.7 入群申请列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/join-requests
```
建议查询参数:`status`、`applicant_user_id`、`page`、`per_page`。
建议返回字段:`request_id`、`topic`、`applicant_user_id`、`inviter_user_id`、`source`、`message`、`status`、`reject_reason`、`handled_by`、`handled_at`、`expires_at`、`version`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_join_requests`。
### 4.8 群邀请列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/invitations
```
建议查询参数:`status`、`target_user_id`、`inviter_user_id`、`page`、`per_page`。
建议返回字段:`invitation_id`、`topic`、`inviter_user_id`、`target_user_id`、`message`、`status`、`result`、`join_request_id`、`expires_at`、`handled_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_invitations`。
### 4.9 群公告列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/announcements
```
建议查询参数:`status`、`page`、`per_page`。
建议返回字段:`announcement_id`、`topic`、`author_user_id`、`content`、`status`、`version`、`published_at`、`deleted_by`、`deleted_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_announcements`。
### 4.10 群禁言列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/mutes
```
建议查询参数:`status`、`user_id`、`page`、`per_page`。
建议返回字段:`topic`、`user_id`、`muted_by`、`muted_at`、`expires_at`、`reason`、`status`、`revoked_by`、`revoked_at`、`version`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_mutes`。
### 4.11 群管理日志
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/admin-logs
```
建议查询参数:`action`、`operator_id`、`page`、`per_page`。
建议返回字段:`log_id`、`topic`、`operator_id`、`target_user_id`、`action`、`object_type`、`object_id`、`before_data`、`after_data`、`request_id`、`idempotency_key`、`created_at`。
当前 Admin 直连涉及表:`im_group_admin_logs`。
### 4.12 群文件列表
```text
GET /internal/admin-query/tenants/{tenant_code}/groups/{topic}/files
```
用途:群治理页面中的群文件列表。不要和已有单文件详情接口混淆。
建议查询参数:`status`、`file_type`、`file_name`、`uploader_id`、`page`、`per_page`。
建议返回字段:`group_file_id`、`topic`、`file_id`、`source_topic`、`source_seq`、`uploader_id`、`file_name`、`mime_type`、`file_type`、`file_size`、`status`、`deleted_by`、`deleted_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_group_files`。
### 4.13 文件列表
```text
GET /internal/admin-query/tenants/{tenant_code}/files
```
用途:IM 文件管理列表。
建议查询参数:`file_id`、`user_id`、`status`、`mime_type`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`file_id`、`user_id`、`mime_type`、`file_size`、`file_name`、`location`、`etag`、`status`、`quarantine_reason`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_fileuploads`。
### 4.14 文件引用列表
```text
GET /internal/admin-query/tenants/{tenant_code}/files/{file_id}/links
```
用途:文件详情中展示消息引用。
建议返回字段:`topic`、`seq_id`、`user_id`、`created_at`。
当前 Admin 直连涉及表:`im_filemsglinks`。
## 5. P1:后台审核和运营页面
### 5.1 审计日志
```text
GET /internal/admin-query/tenants/{tenant_code}/audit-logs
GET /internal/admin-query/tenants/{tenant_code}/audit-logs/{id}
```
建议查询参数:`actor_user_id`、`action`、`target_type`、`target_id`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`request_id`、`actor_user_id`、`action`、`target_type`、`target_id`、`ip`、`user_agent`、`detail`、`created_at`。
当前 Admin 直连涉及表:`im_audit_logs`。
### 5.2 实名审核
```text
GET /internal/admin-query/tenants/{tenant_code}/realname-verifications
GET /internal/admin-query/tenants/{tenant_code}/realname-verifications/{id}
```
建议查询参数:`user_id`、`status`、`real_name`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`user_id`、`real_name`、`id_card_no_masked`、`front_file_id`、`back_file_id`、`status`、`reject_reason`、`reviewed_by`、`reviewed_at`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_realname_verifications`。
### 5.3 举报工单
```text
GET /internal/admin-query/tenants/{tenant_code}/abuse-reports
GET /internal/admin-query/tenants/{tenant_code}/abuse-reports/{id}
GET /internal/admin-query/tenants/{tenant_code}/reports/abuse/{report_id}/target
```
建议查询参数:`reporter_user_id`、`target_type`、`target_id`、`status`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`report_id`、`reporter_user_id`、`target_type`、`target_id`、`reason_code`、`description`、`evidence_file_ids`、`status`、`handler_id`、`handled_at`、`handle_result`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_abuse_reports`。
### 5.4 意见反馈
```text
GET /internal/admin-query/tenants/{tenant_code}/feedback-submissions
GET /internal/admin-query/tenants/{tenant_code}/feedback-submissions/{id}
```
建议查询参数:`user_id`、`category`、`status`、`created_at_start`、`created_at_end`、`page`、`per_page`。
建议返回字段:`id`、`feedback_id`、`user_id`、`category`、`content`、`contact`、`file_ids`、`status`、`handler_id`、`handled_at`、`reply`、`created_at`、`updated_at`。
当前 Admin 直连涉及表:`im_feedback_submissions`。
### 5.5 业务 Outbox
```text
GET /internal/admin-query/tenants/{tenant_code}/outbox/business-events
GET /internal/admin-query/tenants/{tenant_code}/outbox/group-events
GET /internal/admin-query/tenants/{tenant_code}/outbox/business-events/{id}
GET /internal/admin-query/tenants/{tenant_code}/outbox/group-events/{id}
```
建议查询参数:`event_type`、`status`、`target_type`、`target`、`created_at_start`、`created_at_end`、`page`、`per_page`。
当前 Admin 直连涉及表:`im_business_events_outbox`、`im_group_events_outbox`。
## 6. P2:如果要完全消除租户库直连
这些不是 Kafka 联调第一优先级,但如果最终要求 Admin 完全不碰租户库,也需要 IM Server 提供内部查询或改为 hotgo 摘要表。
### 6.1 组织架构
```text
GET /internal/admin-query/tenants/{tenant_code}/org/departments
GET /internal/admin-query/tenants/{tenant_code}/org/members
```
当前 Admin 直连涉及表:`im_org_departments`、`im_org_members`、`im_users`。
### 6.2 工作台应用
```text
GET /internal/admin-query/tenants/{tenant_code}/workbench/apps
```
当前 Admin 直连涉及工作台相关租户表。
### 6.3 租户任务看板摘要
```text
GET /internal/admin-query/tenants/{tenant_code}/dashboard/summary
```
当前 Admin 会统计多个租户库中的待处理实名、举报、反馈、Outbox 等数量。
### 6.4 租户配置类查询
如果这些配置仍放在租户库,也需要补接口;如果后续迁到 hotgo 控制库,则不需要通过 IM Server 查询。
```text
GET /internal/admin-query/tenants/{tenant_code}/feedback-config
GET /internal/admin-query/tenants/{tenant_code}/mailbox-config
GET /internal/admin-query/tenants/{tenant_code}/abuse-reasons
GET /internal/admin-query/tenants/{tenant_code}/app-releases
```
涉及模块:反馈配置、总经理信箱配置、举报原因、App 发布策略。
## 7. Admin 侧迁移建议
### 7.1 第一批迁移
在 IM Server 补完 P0 后,Admin 优先迁移:
1. 用户列表、用户详情、用户设备。
2. 群列表、群详情、群成员。
3. 群入群申请、邀请、公告、禁言、管理日志、群文件。
4. 文件列表、文件详情、文件引用。
### 7.2 第二批迁移
在 IM Server 补完 P1 后,Admin 再迁移:
1. 审计日志。
2. 实名审核。
3. 举报工单。
4. 意见反馈。
5. 业务 Outbox。
### 7.3 保留 hotgo 查询的范围
以下数据天然属于 hotgo 控制库,Admin 可继续查 hotgo,不需要走 IM Server:
1. 租户表和租户绑定表。
2. Admin 命令表:`im_admin_commands`。
3. Admin Outbox 表:`im_admin_outbox`。
4. Admin Kafka 审计事件表:`im_audit_events`。
5. 菜单、角色、管理员、系统配置、字典等 HotGo 系统表。
## 8. 验收标准
1. Admin 查询租户业务数据时,不再出现新的 `imtenant.DB()` 调用。
2. Admin 查询 IM Server 内部接口时,统一使用 HMAC 签名。
3. 列表接口支持 `page/per_page/total`,兼容后台表格分页。
4. 详情接口返回结构化 JSON,不返回数据库内部敏感字段。
5. 用户 ID、雪花 ID 等大整数以字符串返回。
6. IM Server 为内部查询写审计日志,至少包含 `tenant_code`、`service_name`、`path`、`query`、`trace_id`、`status`、`duration_ms`。
7. 关闭 IM Server 内部查询能力时,Admin 应得到明确错误,不泄露租户库信息。
+51 -2
View File
@@ -1,7 +1,7 @@
# IM Kafka 接入方案
> 日期:2026-08-26
> 关联文档:[IM Kafka 接入范围识别](./IM-Kafka接入范围识别.md)、[IM 前后端逐图对接协议](./IM-前后端对接文档.md)
> 日期:2026-08-26
> 关联文档:[IM Kafka 接入范围识别](./IM-Kafka接入范围识别.md)、[IM 前后端逐图对接协议](./IM-前后端对接文档.md)
> 目标:明确 Admin 后台、IM 服务、HotGo 控制库、租户业务库之间的 Kafka 接入边界、查询方式、鉴权方式和首批落地范围。
## 0. 与范围识别文档的关系
@@ -786,6 +786,55 @@ Admin 处理命令:
2. IM 按死信记录中的 `source_topic` 重新投递原 envelope。
3. 投递成功后把死信标记为 `replayed`,并累计 `replay_count`。
4. 如果重放后仍失败,普通消费者会再次写入新的 dead 记录,保留完整处理痕迹。
### 11.12 IM Server 已接入的用户治理命令
IM Server 已在 `im.admin.command` 中接入用户治理类后台动作。Admin 侧写 hotgo 操作单并发送 Kafka Command,IM 消费后修改租户库用户状态、踢在线连接、清理缓存并回传 `im.admin.command.result`。
支持命令:
| `event_type` | 行为 | Payload 关键字段 |
| --- | --- | --- |
| `user.ban` | 封禁用户,设置 `StateSuspended` 并踢所有在线会话。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.unban` | 解封用户,设置 `StateOK`。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.freeze` | 冻结账号,设置 `StateSuspended` 并踢所有在线会话。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.unfreeze` | 恢复账号,设置 `StateOK`。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.device.kick` | 踢指定设备在线连接,并删除设备推送记录。 | `actor_user_id`、`user_id` 或 `target_user_id`、`device_id`、`reason` |
| `user.logout_all` | 踢用户所有在线连接,不修改账号状态。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
| `user.account.cancel` | 注销账号,默认软删除;`hard=true` 时硬删除。 | `actor_user_id`、`user_id` 或 `target_user_id`、`hard`、`reason` |
| `user.account.delete` | 删除账号,按硬删除处理。 | `actor_user_id`、`user_id` 或 `target_user_id`、`reason` |
命令示例:
```json
{
"event_id": "admin:user:ban:default:usrabc:001",
"tenant_code": "default",
"source_service": "hotgo-admin",
"target_service": "im-server",
"event_type": "user.ban",
"biz_type": "user_governance",
"biz_id": "usrabc",
"idempotency_key": "admin-operation-id",
"occurred_at": "2026-08-26T10:10:00.000Z",
"payload": {
"command_id": "admin-operation-id",
"actor_user_id": "usrAdmin",
"user_id": "usrabc",
"reason": "违规处理"
}
}
```
处理说明:
1. 封禁/冻结共用 Tinode 用户状态 `StateSuspended`;解封/恢复共用 `StateOK`。
2. 状态变更后 IM 会清理用户缓存,并向本节点 Hub 投递用户状态变更。
3. 踢设备只影响匹配 `device_id` 的在线连接,同时删除该设备 Push token 记录。
4. 全端登出只踢在线连接,不修改用户状态。
5. 注销/删除会停止用户相关 Topic、清理缓存并调用 `store.Users.Delete`。
6. 命令执行成功后统一回传 `im.admin.command.result`,并写入 `im.audit.event`。
7. 群消息消费成功后继续写入 Redis 热消息缓存,便于 24 小时内热点拉取。
事件示例:
File diff suppressed because it is too large Load Diff
+3 -10
View File
@@ -1,7 +1,7 @@
# IM Kafka 接入范围识别
> 日期:2026-08-26
> 依据:[IM 前后端逐图对接协议](./IM-前后端对接文档.md)
> 日期:2026-08-26
> 依据:[IM 前后端逐图对接协议](./IM-前后端对接文档.md)
> 目标:从前后端对接文档逐章识别适合接入 Kafka 的链路,明确 Kafka 与 WebSocket、Redis、MySQL、OSS 的边界。
## 1. 总体边界
@@ -73,14 +73,7 @@ Kafka 消息统一携带 `event_id`、`trace_id`、`tenant_code`、`biz_type`、
3. 总经理信箱提交生产 `im.workbench.event` 和 `im.audit.event`,后台处理结果再回推 C 端。
4. 工作台模块配置变更生产 `im.cache.invalidate`,IM 节点刷新进程内配置缓存。
## 7. 已补充:缓存失效与 Admin 配置查询
1. `im.cache.invalidate` 已接入 IM 消费端,支持租户、注册短信、第三方配置、字典配置、Redis 对象缓存、群热消息缓存失效。
2. IM 已增加 Admin 内部配置查询客户端,开启 `admin_internal_api.enabled=true` 后优先通过 Admin 查询 hotgo 侧配置,旧数据库读取作为灰度兜底。
3. Admin 侧需要补齐 `/internal/im-config/*` 只读接口,并与 IM 使用同一套 HMAC 签名密钥。
4. Kafka 死信已补齐查询、忽略、解决、重放闭环:查询走 IM 内部 API,处理走 `im.admin.command`。
## 8. 首批建议实现
## 7. 首批建议实现
1. 先实现 Kafka 基础生产者、消费者、配置、健康检查和消费幂等表。
2. 接入 `im.group.admin.event`,解决 Admin 后台群操作通过 Kafka 投递到 C 端。
@@ -0,0 +1,570 @@
# IM 后台全量需求文档
> 版本:V1.0 草案
> 编写日期:2026-08-21
> 适用范围:HotGo 管理后台、IM 服务端、IM 客户端动态配置、控制库、租户业务库
> 输入来源:`docs/im/IM-前后端对接文档.md`、`docs/im/IM-注册、邀请码与认证数据模型改造需求文档.md`、`docs/im/IM-HotGo注册短信开关与邀请码管理改造交接文档.md`、`docs/backstage/IM-后台需求文档.md`
## 1. 文档目标
本文从前后端对接协议、注册/邀请码/认证改造文档中,完整识别 IM 项目需要由后台管理系统承载的能力,包括:
1. 后台页面和业务管理模块。
2. 平台级、租户级开关和配置。
3. 后端统一维护、前端动态加载的字典枚举。
4. 定时任务、Outbox、异步投递、重试和补偿任务。
5. 客户端通过 `bootstrap`、配置接口、字典接口动态渲染的 UI 能力。
6. 权限、审计、日志脱敏、导出和安全边界。
本文是后台全量需求盘点,不替代具体接口设计和数据库迁移 SQL。后续开发时,应按 HotGo 项目规范拆成独立 CRUD 模块、配置模块、任务模块和权限菜单。
## 2. 基础原则
### 2.1 多租户边界
当前多租户采用“一个 HotGo 控制库 + 每租户一个 IM 独立业务库”方案。
| 数据类型 | 存储位置 | 是否带 `tenant_id` | 说明 |
| --- | --- | --- | --- |
| 租户元数据 | HotGo 控制库 | 是 | `im_tenant`、租户配置、平台审计、邀请码主数据 |
| 平台全局配置 | HotGo 控制库 | 否或按配置域区分 | 例如 `hg_sys_config.group=im` |
| 邀请码批次和主表 | HotGo 控制库 | 是 | 后台生成、查询、撤销、导出 |
| 租户用户、群、消息、文件、组织、工单 | 租户 IM 业务库 | 否 | 通过登录态租户上下文选择库,不在业务表里保存 `tenant_id` |
| 租户内使用事实和业务 Outbox | 租户 IM 业务库 | 否 | 例如 `im_registration_invite_uses`、`im_business_events_outbox` |
浏览器不能传入可信 `tenant_id`、`db_name`、`db_dsn`。平台管理员跨租户操作时,由后端基于登录身份和权限选择租户;租户管理员只能使用自身绑定的租户上下文。
### 2.2 HotGo 与 IM 服务职责
| 系统 | 职责 |
| --- | --- |
| HotGo 后台 | 菜单权限、后台页面、平台控制库、租户元数据、平台配置、邀请码管理、运维诊断、后台审计 |
| IM 服务端 | 客户端注册登录、实时消息、业务状态机、租户业务库事务、WebSocket、Outbox 消费、客户端动态配置下发 |
| 客户端 | 根据服务端返回的配置、权限、字典和策略动态渲染,不硬编码业务开关和枚举 |
原则上,租户内用户、群、文件、实名、举报、反馈等业务状态变更优先通过 IM 服务端管理 API 或受控业务通道完成,避免绕过 IM 的状态机和审计。当前注册邀请码和密码重置存在明确的 HotGo 直连数据库边界,必须严格按交接文档执行。
### 2.3 配置分层
| 层级 | 示例 | 管理入口 |
| --- | --- | --- |
| 平台全局配置 | 注册短信发送开关、短信服务商、对象存储默认配置、通用风控模板 | 平台后台 |
| 租户基础配置 | 租户状态、企业码、名称、数据库、配额、品牌、终端地址 | 平台后台,部分租户只读 |
| 租户业务策略 | 消息策略、文件策略、群治理、隐私默认值、工作台模块 | 企业后台 |
| 用户个人配置 | 隐私开关、青少年模式状态、设备推送 Token | 客户端为主,后台只读或安全处置 |
| 前端动态 UI 配置 | `bootstrap.features`、`settings_menu`、`workbench.modules`、原因字典 | IM 服务端返回,来源于后台配置和权限 |
## 3. 后台全量模块总览
| 模块 | 平台后台 | 企业后台 | 类型 | 优先级 |
| --- | --- | --- | --- | --- |
| 租户生命周期 | 是 | 否 | 管理页面、任务、审计 | P0 |
| 租户配置与配额 | 是 | 部分只读或申请变更 | 配置、开关、动态 UI | P0 |
| 注册短信开关 | 是 | 否 | 全局开关、审计 | P0 |
| 邀请码管理 | 平台可代管 | 是 | CRUD、导出、审计 | P0 |
| 管理员、角色、权限 | 是 | 是 | 权限、字典、审计 | P0 |
| 用户与账号安全 | 平台可运维 | 是 | 管理页面、Outbox | P0 |
| 组织架构 | 否 | 是 | CRUD、导入导出、动态 UI | P1 |
| 群治理 | 平台可审计 | 是 | 配置、管理页面、字典 | P1 |
| 消息策略与系统消息 | 平台模板 | 租户配置 | 配置、模板、任务 | P1 |
| 文件、收藏、搜索治理 | 平台运维 | 是 | 配置、任务、审计 | P1 |
| Push、短信、TURN、对象存储 | 是 | 租户专属配置 | 敏感配置、状态检查 | P1 |
| 工作台应用 | 平台模板 | 是 | 动态 UI 配置 | P2 |
| 会议 | 平台审计 | 是 | 管理页面、提醒任务 | P2 |
| 日报、月报 | 平台审计 | 是 | 模板、统计、提醒任务 | P2 |
| 总经理信箱 | 平台审计 | 是 | 配置、工单、动态文案 | P2 |
| 实名认证 | 平台审计 | 是 | 审核、策略、字典 | P2 |
| 青少年模式 | 平台模板 | 是 | 策略、动态文案、限制 | P2 |
| 反诈与内容审核 | 平台策略 | 租户策略与处置 | 风控、字典、审计 | P2 |
| 举报与反馈 | 平台可审计 | 是 | 原因字典、工单、导出 | P2 |
| App 版本更新 | 是 | 可按租户覆盖 | 配置、动态 UI | P2 |
| Outbox 与后台任务 | 是 | 只读本租户 | 运维、重试、死信 | P0 |
| 审计与日志脱敏 | 是 | 只读本租户 | 安全基础能力 | P0 |
## 4. 开关与配置清单
### 4.1 平台全局开关
| 配置键 | 类型 | 默认 | 管理权限 | 使用方 | 说明 |
| --- | --- | --- | --- | --- | --- |
| `im.register_sms_enabled` | bool | true | 平台超级管理员 | IM 注册发码接口 | 控制注册验证码是否真实发送;关闭时仍返回倒计时,不暴露给客户端 |
| `im.invite_batch_max_quantity` | int | 1000 | 平台管理员 | HotGo 邀请码管理 | 单批邀请码生成数量上限 |
| `im.invite_export_max_rows` | int | 待定 | 平台管理员 | HotGo 导出 | 防止明文邀请码大批量泄露 |
| `im.sms_provider_enabled` | bool | true | 平台管理员 | IM 发码服务 | 是否启用短信服务商,不等同于注册短信业务开关 |
| `im.audit_retention_days` | int | 待定 | 平台管理员 | 审计任务 | 平台审计保留期 |
| `im.outbox_retry_enabled` | bool | true | 平台运维 | Outbox 任务 | 是否自动重试平台/租户 Outbox |
`register_sms_enabled` 存在 HotGo `hg_sys_config`,真实值必须保存为 `1/0`。前端复用 `sys_switch` 时要显式映射:UI `1=开启`、`2=关闭`,不能把 `2` 直接写成 bool。
### 4.2 租户基础配置
| 配置 | 类型 | 是否动态下发 | 管理入口 | 说明 |
| --- | --- | --- | --- | --- |
| 租户状态 | enum | 是 | 平台后台 | `provisioning/active/suspended/deleting/deleted` |
| 企业码 | string | 登录前解析 | 平台后台 | 客户端 `tenants/resolve` 使用 |
| 租户名称、Logo、品牌色 | string/file | 是 | 平台后台 | 登录页、工作台、关于页展示 |
| `db_name/db_dsn` | string | 否 | 平台后台 | 只允许后端使用,不给浏览器 |
| 用户配额 | int | 是 | 平台后台 | 注册、导入用户、邀请加入时强校验 |
| 群数量配额 | int | 是 | 平台后台 | 建群时校验 |
| 群成员上限 | int | 是 | 平台/企业后台 | 建群、邀请、审批通过时校验 |
| 文件存储配额 | int64 | 是 | 平台/企业后台 | 上传、转发、收藏、群文件共用 |
| 终端 API 地址 | url | 登录前解析 | 平台后台 | HTTP、WebSocket、文件服务地址 |
| 可用登录方式 | array enum | 登录前解析 | 平台后台 | 密码、手机号、企业码等 |
| 密码策略 | json | 登录/注册/改密 | 平台/企业后台 | 长度、复杂度、历史密码、过期策略 |
| App 更新策略 | json | 是 | 平台后台 | platform/channel 维度版本策略 |
### 4.3 客户端 Bootstrap 动态配置
`GET /v1/im/bootstrap` 是客户端登录后的 UI 和能力裁剪总入口,后台必须为以下字段提供配置来源。
| 字段 | 后台配置项 | 客户端影响 |
| --- | --- | --- |
| `features.create_group` | 是否允许用户建群 | 隐藏或禁用建群入口 |
| `features.user_search` | 是否允许搜索用户 | 隐藏添加好友搜索入口 |
| `features.scan_qr` | 是否启用扫码 | 隐藏二维码/扫码入口 |
| `features.system_notifications` | 系统通知开关 | 展示系统通知 Topic |
| `features.message_recall` | 消息撤回开关 | 长按菜单是否显示撤回 |
| `features.message_edit` | 消息编辑开关 | 长按菜单是否显示编辑 |
| `features.organization` | 组织架构开关 | 工作台/通讯录组织入口 |
| `features.meetings` | 会议开关 | 工作台会议入口 |
| `features.daily_report/monthly_report` | 日报/月报开关 | 工作台报表入口 |
| `features.executive_mailbox` | 总经理信箱开关 | 工作台信箱入口 |
| `message_policy.recall_window_seconds` | 撤回时间窗口 | 超时隐藏操作 |
| `message_policy.edit_window_seconds` | 编辑时间窗口 | 超时隐藏操作 |
| `message_policy.supported_actions` | 消息操作集合 | 转发、收藏、复制、删除等 |
| `limits.max_file_size` | 单文件大小 | 上传前校验 |
| `limits.max_text_length` | 文本最大长度 | 输入框限制 |
| `limits.max_forward_targets` | 转发目标上限 | 多选转发限制 |
| `settings_menu` | 设置菜单配置 | iOS/Android 设置页动态入口 |
| `workbench.modules` | 工作台模块配置 | 工作台排序、图标、路由、角标 |
### 4.4 注册、认证与邀请码配置
| 配置 | 作用 | 后台入口 | 备注 |
| --- | --- | --- | --- |
| 注册短信发送 | 是否真实调用短信服务商 | 平台后台“IM 注册” | 全局开关 |
| 用户名正则 | 普通用户名规则 | 平台只读配置或代码常量 | 当前固定 `^[a-z0-9]{1,10}$` |
| 固定国家号 | 手机号补 `+86` | 平台只读配置 | 本期固定中国 |
| 邀请码长度和字符集 | 生成规则 | 平台只读配置 | 当前固定 5 位 `[A-Za-z]` |
| 邀请码批量上限 | 批量生成限制 | 平台后台 | 默认 1000 |
| 邀请码有效期 | 单批/单码字段 | 企业后台 | 创建时必填,必须晚于当前时间 |
| 邀请码明文查看权限 | 控制列表/详情是否显示明文 | 菜单权限 | 查看、查询、导出需独立审计 |
| 密码重置策略 | 管理员重置密码规则 | 企业后台 | 写 `security.password_reset` Outbox |
| 登录 Token 失效策略 | `auth_version` 校验 | 后台只读/运维 | 重置密码后清退旧会话 |
### 4.5 消息与会话策略
| 配置 | 类型 | 说明 |
| --- | --- | --- |
| 文本最大长度 | int | 输入框、服务端发送校验 |
| 消息撤回开关和时间窗 | bool/int | P2P、群聊均需服务端校验 |
| 消息编辑开关和时间窗 | bool/int | 编辑后产生版本或审计 |
| 多选转发目标上限 | int | 单选、多选、合并转发共用 |
| 支持消息操作 | array enum | copy、forward、favorite、recall、edit、delete |
| 已读回执策略 | enum/bool | 可受隐私和企业强制策略影响 |
| 链接识别与风险检测 | bool/json | URL 发送前后检查,触发反诈挑战 |
| 系统通知 Topic | string/json | 系统消息列表和角标 |
| 消息频控 | json | 防刷、风控、举报联动 |
### 4.6 通讯录、好友与隐私策略
| 配置 | 类型 | 客户端影响 |
| --- | --- | --- |
| 是否允许添加好友 | bool | 添加好友入口 |
| 是否允许精准账号搜索 | bool | 用户 ID/用户名/手机号搜索 |
| 添加我的方式 | enum array | 账号、手机号、二维码、群聊 |
| 好友申请验证策略 | enum | 直接添加、需要验证、禁止 |
| 黑名单能力 | bool | 黑名单入口和服务端拦截 |
| 资料可见性默认值 | enum | 主页字段裁剪 |
| 最近在线可见性 | enum | 会话状态展示 |
| 已读回执默认值 | bool | 会话回执 |
| 企业强制隐私字段 | json | `editable=false` 时前端禁用 |
### 4.7 群治理配置
| 配置 | 类型 | 说明 |
| --- | --- | --- |
| 是否允许建群 | bool | bootstrap 和建群接口 |
| 群成员上限 | int | 加群、邀请、审批事务内校验 |
| 入群策略 | enum | free、approval、invite_only |
| 成员邀请权限 | enum/bool | 群成员是否可邀请别人 |
| 成员上传文件 | bool | 群文件上传入口 |
| 成员私聊 | bool | 群成员主页是否允许发起会话 |
| 成员互加好友 | bool | 群资料页添加好友入口 |
| 全员禁言 | bool | 输入框禁用 |
| 成员禁言时长 | enum/int | 后台和群管理员使用 |
| 管理员数量上限 | int | 添加管理员时校验 |
| 群二维码有效期 | int | 二维码页和失效任务 |
| 群公告策略 | json | 发布、置顶、已读统计 |
### 4.8 文件、收藏与搜索策略
| 配置 | 类型 | 说明 |
| --- | --- | --- |
| 单文件大小上限 | int64 | 上传前后校验 |
| 允许 MIME/扩展名 | array | 图片、视频、语音、普通文件按 purpose 区分 |
| 文件 purpose 策略 | json | avatar、chat、group_file、report、abuse_evidence、feedback |
| 文件保留期 | int | 过期清理任务 |
| 孤儿文件清理时间窗 | int | 上传未绑定业务时清理 |
| 文件病毒扫描 | bool/json | 同步拦截或异步隔离 |
| 收藏总量/容量上限 | int/int64 | 收藏入口和服务端校验 |
| 搜索开关 | bool | 聊天记录、文件、链接搜索入口 |
| 搜索索引策略 | json | 重建、增量、失败重试 |
### 4.9 通话、Push 和外部服务配置
| 配置 | 类型 | 说明 |
| --- | --- | --- |
| 音视频通话开关 | bool | 呼叫入口 |
| 通话超时 | int | invite 超时时间 |
| TURN/STUN 服务器 | secret/json | WebRTC ICE 配置 |
| 通话记录保留期 | int | 后台查询和清理 |
| APNS/FCM 配置 | secret/json | 推送服务 |
| Push 锁屏隐私 | enum/bool | 通知内容是否显示明文 |
| Push 模板 | json | 系统消息、好友申请、会议提醒等 |
| 短信服务商配置 | secret/json | 验证码、通知短信 |
| 对象存储配置 | secret/json | 文件服务 |
| 外部 REST 插件配置 | secret/json | 扩展业务调用 |
### 4.10 办公协同配置
| 模块 | 配置 | 说明 |
| --- | --- | --- |
| 工作台 | 模块 code、名称、图标、路由、排序、启用、权限 | `GET /v1/im/workbench` 动态返回 |
| 组织架构 | 组织可见范围、是否允许跨部门搜索 | 通讯录和工作台组织入口 |
| 会议 | 是否启用、提醒提前量、可见范围、取消策略 | 会议通知和提醒任务 |
| 日报 | 模板、必填规则、最大长度、补交天数、提醒时间 | 普通用户和管理员统计 |
| 月报 | 模板、必填规则、最大长度、重开流程 | 月报统计和详情 |
| 总经理信箱 | 是否启用、匿名、必填字段、最大长度、附件策略、提示文案 | 文案必须服务端下发,不由客户端硬编码 |
### 4.11 安全、审核、举报与反馈配置
| 模块 | 配置 | 说明 |
| --- | --- | --- |
| 实名认证 | 是否强制、允许跳过、提醒周期、重复身份策略、审核字段 | 影响 `bootstrap.security.realname` |
| 青少年模式 | 策略版本、限制集合、提示文案、是否可启用 | 客户端确认时携带 `policy_version` |
| 反诈风控 | 高风险动作、提示文案、二次确认、阻断策略、Token TTL | 返回 `RISK_CONFIRMATION_REQUIRED` |
| 内容审核 | 审核模式、词库、插件、异步处置 | sync_block、async_hold、async_post_audit |
| 举报 | target_type 原因字典、证据规则、合并策略、限频 | `GET /v1/im/reports/abuse/reasons` |
| 反馈 | 类别、最大长度、联系方式、附件策略、诊断授权文案 | `GET /v1/im/feedback/config` |
### 4.12 App 更新配置
| 配置 | 类型 | 说明 |
| --- | --- | --- |
| platform | enum | ios、android、desktop 等 |
| channel | string/enum | stable、beta、enterprise |
| latest_version | string | 最新版本 |
| min_supported | string | 最低支持版本 |
| update_type | enum | none、optional、recommended、force |
| package_url/store_url | url | 安装来源 |
| release_notes | text | 更新说明 |
| remind_after_seconds | int | 非强制更新再次提醒间隔 |
## 5. 字典与枚举清单
后台新增枚举必须由后端统一注册字典,前端通过字典接口加载,不能在页面硬编码文本。
### 5.1 平台与租户
| 字典键建议 | 枚举 |
| --- | --- |
| `im_tenant_status` | provisioning、active、suspended、deleting、deleted |
| `im_tenant_plan` | free、standard、enterprise、custom |
| `im_tenant_config_scope` | platform、tenant、user |
| `im_operation_result` | success、failed、partial |
### 5.2 注册、认证、邀请码
| 字典键建议 | 枚举 |
| --- | --- |
| `im_register_sms_status` | enabled、disabled |
| `im_invite_status` | active、expired、revoked |
| `im_invite_action` | create、batch_create、view、query_exact、revoke、batch_revoke、export |
| `im_auth_scene` | register、login、forgot_password、bind_phone、close_youth_mode |
| `im_auth_channel` | sms、password、independent_password |
| `im_challenge_status` | pending、verified、expired、consumed、failed |
| `im_user_status` | normal、disabled、locked、deleted |
| `im_auth_level` | anonymous、authenticated、admin |
### 5.3 好友、群与消息
| 字典键建议 | 枚举 |
| --- | --- |
| `im_friend_request_status` | pending、accepted、rejected、expired、cancelled |
| `im_friend_add_method` | account、phone、qr、group |
| `im_profile_visibility` | public、friends、none |
| `im_group_role` | owner、admin、member |
| `im_group_join_policy` | free、approval、invite_only |
| `im_group_request_status` | pending、approved、rejected、expired |
| `im_group_mute_type` | all、member |
| `im_message_action` | copy、forward、favorite、recall、edit、delete、multi_select |
| `im_message_content_type` | text、image、audio、video、file、card、link、system |
| `im_read_state` | unread、read |
### 5.4 文件、搜索、通话
| 字典键建议 | 枚举 |
| --- | --- |
| `im_file_purpose` | chat、user_avatar、group_avatar、group_file、daily_report、monthly_report、abuse_evidence、feedback |
| `im_file_status` | uploaded、bound、quarantined、deleted、expired |
| `im_file_scan_status` | pending、clean、risky、failed |
| `im_search_index_status` | pending、indexed、failed、deleted |
| `im_call_type` | audio、video |
| `im_call_status` | ringing、accepted、declined、missed、cancelled、ended、timeout |
### 5.5 办公协同
| 字典键建议 | 枚举 |
| --- | --- |
| `im_workbench_module_status` | enabled、disabled |
| `im_meeting_status` | scheduled、cancelled、finished |
| `im_meeting_response` | pending、accepted、declined、tentative |
| `im_report_type` | daily、monthly |
| `im_report_status` | draft、submitted、reopened、archived |
| `im_mailbox_submission_status` | received、assigned、replied、closed |
### 5.6 安全、审核、举报、反馈
| 字典键建议 | 枚举 |
| --- | --- |
| `im_realname_status` | unverified、pending、approved、rejected |
| `im_youth_restriction` | night_quiet、content_filter、payment_guard、contact_limit |
| `im_risk_level` | low、medium、high |
| `im_risk_decision` | continue、cancel |
| `im_moderation_mode` | off、sync_block、async_hold、async_post_audit |
| `im_moderation_status` | pending、passed、blocked、removed |
| `im_abuse_target_type` | user、group、message |
| `im_abuse_report_status` | received、processing、resolved、rejected、merged |
| `im_abuse_reason` | fraud、harassment、pornography、violence、spam、other |
| `im_feedback_category` | bug、suggestion、complaint、other |
| `im_feedback_status` | received、processing、resolved、closed |
### 5.7 终端、外部服务与错误
| 字典键建议 | 枚举 |
| --- | --- |
| `im_app_platform` | ios、android、desktop、web |
| `im_app_channel` | stable、beta、enterprise |
| `im_update_type` | none、optional、recommended、force |
| `im_push_provider` | apns、fcm、vendor |
| `im_outbox_status` | pending、processing、done、failed、dead |
| `im_error_reason` | TENANT_UNAVAILABLE、TENANT_MISMATCH、AUTH_FAILED、TOKEN_EXPIRED、ACCOUNT_DISABLED、RATE_LIMITED、VALIDATION_FAILED、IDEMPOTENCY_CONFLICT、RESOURCE_NOT_FOUND、FORBIDDEN、FRIEND_RELATION_REQUIRED、MESSAGE_NOT_ALLOWED、GROUP_MUTED、GROUP_MEMBER_LIMIT_EXCEEDED、TENANT_USER_QUOTA_EXCEEDED、TENANT_GROUP_QUOTA_EXCEEDED、FILE_TOO_LARGE、FILE_FORBIDDEN、FILE_UNAVAILABLE、STORAGE_QUOTA_EXCEEDED、REALNAME_REQUIRED、YOUTH_MODE_RESTRICTED、RISK_CONFIRMATION_REQUIRED、RISK_ACTION_BLOCKED、REPORT_RATE_LIMITED |
## 6. 定时任务与异步任务
### 6.1 P0 基础任务
| 任务 | 触发方式 | 作用 | 后台能力 |
| --- | --- | --- | --- |
| 租户业务库初始化 | 创建租户后异步执行 | 创建库、执行 schema、初始化系统数据 | 进度、失败原因、重试 |
| 租户健康检查 | 定时 | 检查 DB 连接、schema version、基础 Topic | 状态看板、告警 |
| Outbox 消费状态巡检 | 定时 | 检查积压、失败、死信 | 列表、重试、死信 |
| `security.password_reset` 消费 | IM 轮询或 Kafka | 清退会话、拒绝旧 Token | 事件查询、重试 |
| 注册邀请码使用投影 | IM Outbox 或投影任务 | 将租户库 `invite_uses` 汇总到控制库主表 | 使用次数、最后使用时间、幂等补偿 |
| 审计日志脱敏与落库 | 同步/异步 | 保存操作审计,避免明文泄露 | 查询、导出、保留期 |
### 6.2 业务任务
| 任务 | 说明 |
| --- | --- |
| 邀请码过期状态刷新 | 邀请码可查询时实时判断过期;可增加定时任务做状态冗余和统计,不作为注册校验唯一依据 |
| 邀请码批量生成 | 大批量生成应可异步化,支持进度、失败重试、幂等键 |
| 文件孤儿清理 | 上传后未绑定业务的文件定期清理 |
| 文件过期清理 | 按 purpose 和保留期清理 |
| 病毒扫描和隔离处理 | 异步扫描结果更新文件状态并通知用户 |
| 搜索索引重建 | 消息、文件、链接索引的全量/增量重建 |
| 系统通知投递 | 系统消息、公告、风控提醒、会议提醒的 Push 和站内信投递 |
| Push 重试和死信 | 第三方推送失败后的重试、死信、凭证异常告警 |
| 会议提醒 | 按会议配置提醒参会人,支持提醒未响应者 |
| 日报/月报提醒 | 按租户配置提醒未提交者,生成统计 |
| 总经理信箱通知 | 新提交、分派、回复、关闭通知 |
| 实名审核超时提醒 | 提醒审核员处理 |
| 风控 challenge 过期 | 清理过期 challenge,记录取消/过期审计 |
| 举报和反馈 SLA 提醒 | 超时未处理提醒,支持工单状态流转 |
| App 更新配置缓存刷新 | 版本策略变更后刷新缓存 |
### 6.3 任务后台需求
1. 任务列表:任务名、租户、状态、进度、最近执行时间、耗时、错误原因。
2. 任务详情:输入参数、幂等键、关联审计、关联 Outbox、重试次数。
3. 操作:手动重试、暂停、恢复、标记死信、导出错误明细。
4. 安全:租户管理员只能看本租户任务;平台运维可跨租户查看但必须审计。
## 7. 前端 UI 动态配置需求
### 7.1 客户端动态 UI
客户端不能硬编码以下 UI 能力,必须由服务端返回:
1. 登录前:租户品牌、企业码解析结果、终端地址、认证方式、密码策略、App 更新策略。
2. 登录后:首页入口、`features`、`permissions`、`settings_menu`、工作台模块、系统通知 Topic。
3. 聊天页:可执行消息操作、撤回/编辑时间窗、上传限制、转发上限、已读回执策略。
4. 通讯录:组织架构入口、用户搜索入口、添加我的方式、黑名单入口。
5. 群聊:入群策略、群文件入口、群成员私聊、成员邀请、禁言状态、管理员入口。
6. 工作台:模块名称、图标、路由、排序、角标、启用状态和权限。
7. 日报/月报:模板、校验、补交天数、是否展示管理员统计。
8. 总经理信箱:提示文案、匿名说明、必填字段、附件规则。
9. 实名、青少年模式、反诈:策略版本、提示文案、限制集合、确认 Token。
10. 举报与反馈:原因、类别、最大长度、附件规则、诊断授权文案。
### 7.2 HotGo 管理后台动态 UI
HotGo 后台自身也需要动态化:
1. 平台管理员显示租户选择器;租户管理员隐藏租户选择器并固定当前租户。
2. 按权限显示按钮:邀请码明文查看、导出、撤销、密码重置、跨租户查看必须独立权限。
3. 表格状态、筛选下拉、表单选项必须从后端字典加载。
4. 敏感配置字段只允许写入,不默认回显明文;回显使用脱敏值和“已配置”状态。
5. 导出前必须展示范围、数量、敏感字段提示,二次确认并写审计。
6. 表单提交不接受浏览器传入 `tenant_id/db_name/db_dsn` 作为可信路由依据。
## 8. 后台页面需求
### 8.1 平台后台
| 页面 | 核心能力 |
| --- | --- |
| 租户管理 | 创建、初始化、启停、删除流程、配置 DB、查看用量 |
| 租户配置 | 配额、品牌、终端地址、登录方式、密码策略、功能开关 |
| IM 注册设置 | 注册短信发送开关、批量邀请码上限、导出限制 |
| 平台管理员 | 平台账号、角色、权限、二次认证 |
| 外部服务配置 | SMS、Push、TURN、对象存储、审核插件 |
| App 版本 | 按 platform/channel 配置版本更新策略 |
| 运维看板 | 租户健康、Outbox、任务、缓存、连接状态 |
| 平台审计 | 跨租户操作、敏感配置变更、导出记录 |
### 8.2 企业后台
| 页面 | 核心能力 |
| --- | --- |
| 邀请码管理 | 单个生成、批量生成、列表、精确查询、批次详情、撤销、导出、使用记录 |
| 企业管理员 | 添加管理员、设置角色、移除管理员、权限范围 |
| 用户管理 | 用户列表、状态、资料、重置密码、撤销会话、设备、安全事件 |
| 组织架构 | 部门树、成员、移动、导入导出、组织权限 |
| 群管理 | 群列表、详情、成员、管理员、禁言、转让群主、解散、群文件 |
| 消息与系统通知 | 模板、公告、系统通知、投递记录 |
| 文件治理 | 文件列表、用途、大小、状态、隔离、删除、下载审计 |
| 搜索治理 | 索引状态、重建、失败明细 |
| 工作台 | 模块配置、排序、图标、角标来源、权限绑定 |
| 会议 | 列表、详情、取消、提醒未响应者、统计 |
| 日报/月报 | 模板、统计、明细、提醒、重开流程 |
| 总经理信箱 | 配置、提交列表、分派、回复、关闭 |
| 实名审核 | 提交列表、详情、通过、拒绝、策略 |
| 青少年模式 | 策略、文案、用户状态、独立密码重置 |
| 反诈与审核 | 风控策略、challenge、内容审核队列、处置 |
| 举报工单 | 原因配置、举报列表、证据、处置、合并 |
| 意见反馈 | 类别配置、反馈列表、诊断附件、处理状态 |
| 租户审计 | 本租户后台操作、敏感查看、导出、业务处置 |
## 9. 数据与接口边界
### 9.1 控制库建议承载
| 表或配置 | 说明 |
| --- | --- |
| `im_tenant` | 租户元数据、状态、企业码、DB 路由 |
| `im_tenant_config` | 租户配置、配额、功能开关、品牌 |
| `hg_sys_config` | 平台全局配置,例如注册短信发送 |
| `im_registration_invite_batches` | 邀请码批次 |
| `im_registration_invite_codes` | 邀请码主表、明文码、状态、使用统计投影 |
| `im_admin_operation_audit` | IM 后台业务审计 |
| `hg_admin_member.tenant_id` | 后台管理员绑定 IM 租户 |
| 平台 Outbox/任务表 | 平台任务、投递、重试、死信 |
### 9.2 租户业务库建议承载
| 表 | 说明 |
| --- | --- |
| `im_users` | 用户主数据,目标包含 `username` 非空唯一、`phone` 可空唯一 |
| `im_auth` | 只按 `userid` 保存 Basic 密码凭据,删除 `scheme/uname` |
| `im_user_security` | `auth_version`、安全状态 |
| `im_registration_invite_uses` | 邀请码使用事实,只由 IM 注册事务写 |
| `im_business_events_outbox` | 租户内业务事件,例如密码重置、邀请码使用 |
| 用户、组织、群、文件、会议、报表、信箱、实名、举报、反馈业务表 | 不带 `tenant_id`,由选库隔离 |
### 9.3 注册认证迁移后台需求
1. 增加迁移状态看板:每个租户当前 schema version、迁移批次、成功/失败数。
2. 存量用户 username 回填报告:冲突、非法字符、保留账号、自动生成结果。
3. phone 规范化报告:可转为 `+86`、重复冲突、格式异常。
4. `im_auth` 双读/切换/清理阶段状态展示。
5. 迁移前后登录回归数据:用户名登录、手机号登录、忘记密码、管理员重置密码。
6. 失败租户支持重试和导出错误明细。
## 10. 权限、审计与日志脱敏
### 10.1 权限点
P0 必须独立拆分以下权限:
1. 注册短信开关读取、修改。
2. 邀请码列表、创建、批量创建、明文查看、精确查询、撤销、批量撤销、导出。
3. 租户创建、启停、DB 配置查看、DB 配置修改。
4. 用户查看、状态修改、重置密码、撤销会话、设备查看。
5. 审计查看、审计导出。
6. Outbox 查看、重试、标记死信。
7. 外部服务敏感配置查看状态、修改。
### 10.2 审计范围
必须审计:
1. 注册短信开关变更。
2. 邀请码生成、批量生成、明文查询/查看、导出、撤销。
3. 管理员重置密码、撤销会话、禁用用户。
4. 租户创建、启停、删除流程、DB 路由配置变更。
5. 群管理员变更、禁言、转让群主、解散群。
6. 文件下载敏感证据、隔离、删除。
7. 实名审核、举报处置、反馈处理。
8. 风控挑战确认、取消、阻断。
9. 任何跨租户查看、导出和代操作。
审计字段至少包含:`request_id`、`tenant_id`、`operator_id`、`operator_role`、`action`、`target_type`、`target_id`、`result`、`ip`、`user_agent`、`detail_json`、`created_at`。
### 10.3 日志脱敏
访问日志、错误日志、审计摘要、导出任务日志不得记录以下明文:
1. 邀请码、验证码、密码、独立密码。
2. `Authorization`、`serviceSecret`、短信/Push/对象存储密钥。
3. `db_dsn`、数据库密码。
4. 身份证件、实名原始图片、敏感证据文件临时 URL。
字段名至少覆盖:`inviteCode`、`invite_code`、`code`、`verification_code`、`password`、`secret`、`serviceSecret`、`service_secret`、`authorization`、`db_dsn`。
## 11. 验收清单
### 11.1 P0
1. 平台管理员可管理租户生命周期、配置和配额,租户管理员无法越权。
2. 每个租户业务库独立,租户业务表不依赖 `tenant_id` 过滤。
3. 注册短信开关只有平台超级管理员可改,值保存为 `1/0`,IM 服务缓存期内生效。
4. 邀请码支持单个、批量、列表、精确查询、撤销、导出和使用统计。
5. 邀请码明文查看、查询、导出都有独立权限和审计。
6. 管理员重置密码写入 `im_auth`、递增 `auth_version`、写 `security.password_reset` Outbox。
7. `bootstrap` 能返回功能开关、消息策略、限制、设置菜单、工作台基础配置。
8. `hg_sys_log` 和业务日志不会泄露邀请码、密码、验证码、Token、密钥和 DSN。
### 11.2 P1
1. 用户、组织、群治理、文件治理、系统通知、Push/SMS/TURN/对象存储配置可后台管理。
2. 群成员上限、用户配额、文件配额、消息策略在并发事务中强校验。
3. 文件清理、Outbox 重试、Push 重试、搜索索引任务可观察、可重试。
4. 前端所有状态、下拉、按钮权限从字典和权限接口动态加载。
### 11.3 P2
1. 工作台、会议、日报、月报、总经理信箱配置能动态影响客户端。
2. 实名、青少年模式、反诈、举报、反馈具备策略配置、工单处理、审计。
3. App 更新策略按 platform/channel 下发,异常时不误触发强制更新。
4. 敏感证据、实名资料、诊断包具备权限、保留期和下载审计。
## 12. 已发现冲突与待确认项
1. 注册规则存在文档冲突:`IM-注册、邀请码与认证数据模型改造需求文档.md` 定义用户名和手机号是独立字段,二者同时填写时都保存;`IM-HotGo注册短信开关与邀请码管理改造交接文档.md` 的 3.1 仍写“手机号非空时忽略用户名”。建议以新的注册模型文档为准,并同步修正交接文档。
2. 邀请码批次和主表目前按交接文档放在 HotGo 控制库,带 `tenant_id`;邀请码使用事实放在租户业务库。该设计不违反“租户业务表不带 tenant_id”,但需要在实现中明确控制库和业务库边界。
3. 邀请码过期是否需要物理状态刷新任务待定。注册校验必须实时判断 `expires_at`,定时任务只能做冗余状态和统计。
4. 注册短信开关当前是平台全局配置;未来若需要企业级差异,需迁移到 `im_tenant_config` 并定义优先级。
5. 当前交接文档允许 HotGo 对部分租户库直连操作;长期是否统一收敛到 IM 管理 API 或 Kafka,需要在运维网络条件稳定后再评审。
6. 开发/测试环境固定验证码 `123456` 不能进入生产;生产必须使用强随机验证码和真实短信服务商响应。
@@ -0,0 +1,418 @@
# IM 后台菜单覆盖测试文档
## 1. 测试范围
本文档用于覆盖 IM 后台菜单从「组织架构」开始,沿左侧菜单向下逐个页面点击验证。
当前覆盖菜单顺序:
1. 组织架构
2. 文件治理
3. 注册邀请码
4. 文件策略
5. 通知模板
6. 工作台配置
7. 外部服务配置
8. 版本更新
9. 举报原因
10. 总经理信箱配置
11. 意见反馈配置
12. 任务看板
13. Kafka 命令
14. Kafka 死信
15. Kafka 审计事件
## 2. 测试环境
| 项目 | 地址 |
| --- | --- |
| 前端 | `http://127.0.0.1:8001/` |
| 后端 | `http://127.0.0.1:8000/admin` |
| 登录账号 | 使用具备 IM 管理权限的后台账号 |
| 浏览器 | Chrome,打开 DevTools Network 和 Console |
## 3. 通用检查项
每进入一个菜单页面,都先执行以下检查:
| 编号 | 检查项 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| COMMON-001 | 点击左侧菜单 | URL、页签、面包屑、页面标题正确变化 | 待测 |
| COMMON-002 | 页面首次加载 | 无白屏,无永久 loading,无明显样式错乱 | 待测 |
| COMMON-003 | Console | 无新的 JS Error,例如组件方法不存在、字典方法不存在 | 待测 |
| COMMON-004 | Network | 页面主列表接口 HTTP 200,业务响应 `code=0` | 待测 |
| COMMON-005 | 查询按钮 | 点击后列表刷新,请求参数包含搜索表单值 | 待测 |
| COMMON-006 | 重置按钮 | 搜索条件清空,列表回到默认查询 | 待测 |
| COMMON-007 | 展开/收起 | 搜索区域展开、收起正常,不遮挡表格 | 待测 |
| COMMON-008 | 表格刷新图标 | 点击后重新请求列表接口 | 待测 |
| COMMON-009 | 表格密度/列设置 | 可打开设置,切换后表格仍正常显示 | 待测 |
| COMMON-010 | 分页 | 切换页码和每页数量,请求参数正确,页面不报错 | 待测 |
说明:涉及删除、撤销、重放、清理等副作用操作时,优先使用测试数据。没有测试数据时,至少验证弹窗、参数校验、取消流程;正式执行前确认不会影响线上有效数据。
## 4. 组织架构
菜单路径:`IM管理 / 组织架构`
主要区域:部门列表、成员列表。
### 4.1 搜索与列表
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| ORG-001 | 打开「组织架构」 | 页面标题显示「组织架构」,部门列表和成员列表正常渲染 | 待测 |
| ORG-002 | 选择租户后查询部门 | 部门列表请求成功,显示部门 ID、部门名称、状态、成员数等字段 | 待测 |
| ORG-003 | 输入部门名称、状态后点击查询 | 部门列表按条件刷新 | 待测 |
| ORG-004 | 输入成员关键词、在职状态后点击查询 | 成员列表按条件刷新 | 待测 |
| ORG-005 | 点击部门行「成员」 | 成员列表切换到当前部门范围,添加成员按钮可用 | 待测 |
### 4.2 部门按钮
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| ORG-010 | 点击「新增部门」 | 打开部门编辑弹窗 | 待测 |
| ORG-011 | 新增部门弹窗点击「取消」 | 弹窗关闭,列表不刷新或数据不变化 | 待测 |
| ORG-012 | 新增部门弹窗直接点击「保存」 | 必填项校验正常展示 | 待测 |
| ORG-013 | 填写部门名称、上级部门、排序、状态后点击「保存」 | 保存成功,部门列表刷新,新部门出现 | 待测 |
| ORG-014 | 点击部门行「编辑」 | 打开编辑弹窗,已有数据正确回填 | 待测 |
| ORG-015 | 修改部门信息后点击「保存」 | 保存成功,列表展示新值 | 待测 |
| ORG-016 | 点击部门行「删除」再点取消 | 弹窗关闭,部门不删除 | 待测 |
| ORG-017 | 点击部门行「删除」并确认 | 删除成功,列表刷新;有子部门或成员时应给出清晰错误 | 待测 |
| ORG-018 | 点击「导出部门」 | 触发文件下载,导出条件与当前查询一致 | 待测 |
### 4.3 成员按钮
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| ORG-020 | 未选择部门时观察「添加成员」 | 按钮禁用或提示先选择部门 | 待测 |
| ORG-021 | 选择部门后点击「添加成员」 | 打开成员编辑弹窗,部门信息默认带入 | 待测 |
| ORG-022 | 成员弹窗直接点击「保存」 | 必填项校验正常展示 | 待测 |
| ORG-023 | 填写用户 ID、岗位、排序、可联系等信息后保存 | 保存成功,成员列表刷新 | 待测 |
| ORG-024 | 点击成员行「编辑」 | 打开编辑弹窗,成员信息正确回填 | 待测 |
| ORG-025 | 修改成员信息后保存 | 保存成功,列表展示新值 | 待测 |
| ORG-026 | 点击成员行「移除」再取消 | 弹窗关闭,成员仍保留 | 待测 |
| ORG-027 | 点击成员行「移除」并确认 | 移除成功,成员列表刷新 | 待测 |
| ORG-028 | 点击「导出成员」 | 触发文件下载,导出条件与当前查询一致 | 待测 |
## 5. 文件治理
菜单路径:`IM管理 / 文件治理`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| FILE-001 | 打开页面 | 文件列表正常展示,包含文件 ID、状态、用途、上传用户、MIME、大小、引用数等字段 | 待测 |
| FILE-002 | 按租户、文件 ID、上传用户、状态、用途、MIME、Topic、更新时间查询 | 列表按条件刷新 | 待测 |
| FILE-003 | 点击行操作「详情」 | 打开详情弹窗,展示文件基础信息和引用列表 | 待测 |
| FILE-004 | 详情弹窗关闭 | 弹窗正常关闭,列表状态不丢失 | 待测 |
| FILE-005 | 点击行操作「隔离」再取消 | 确认框正常,取消后文件状态不变 | 待测 |
| FILE-006 | 点击行操作「隔离」并确认 | 文件状态变为隔离或列表刷新后状态正确 | 待测 |
| FILE-007 | 点击行操作「删除」再取消 | 确认框正常,取消后文件不删除 | 待测 |
| FILE-008 | 点击行操作「删除」并确认 | 删除成功,列表刷新 | 待测 |
| FILE-009 | 点击「清理孤儿文件」再取消 | 确认框正常,取消后不触发清理 | 待测 |
| FILE-010 | 点击「清理孤儿文件」并确认 | 清理请求成功,页面给出成功或结果提示 | 待测 |
## 6. 注册邀请码
菜单路径:`IM管理 / 注册邀请码`
实际菜单路由:`/#/im/registrationInvite`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| INVITE-001 | 打开页面 | 邀请码列表正常展示,包含租户、邀请码、状态、批次 ID、到期时间、使用次数等字段 | 待测 |
| INVITE-002 | 平台管理员不选择租户点击「添加」 | 提示先选择租户 | 待测 |
| INVITE-003 | 选择租户后点击「添加」 | 打开生成邀请码弹窗 | 待测 |
| INVITE-004 | 生成弹窗点击「取消」 | 弹窗关闭,列表不变化 | 待测 |
| INVITE-005 | 生成弹窗清空到期时间后点击「确认」 | 表单校验提示请选择到期时间 | 待测 |
| INVITE-006 | 填写到期时间、备注后点击「确认」 | 生成成功,列表刷新并出现新邀请码 | 待测 |
| INVITE-007 | 点击「批量生成」 | 打开批量生成弹窗,包含生成数量、到期时间、备注 | 待测 |
| INVITE-008 | 批量生成数量小于 1 或大于 1000 | 表单校验拦截 | 待测 |
| INVITE-009 | 输入合法数量后确认 | 批量生成成功,列表出现同一批次的邀请码 | 待测 |
| INVITE-010 | 按邀请码、状态、批次 ID、创建人、创建时间、到期时间查询 | 列表按条件刷新 | 待测 |
| INVITE-011 | 点击行操作「批次」 | 打开批次详情弹窗,展示总数、有效、撤销、过期、使用次数和明细 | 待测 |
| INVITE-012 | 点击有效邀请码行操作「撤销」再取消 | 确认框正常,取消后状态不变 | 待测 |
| INVITE-013 | 点击有效邀请码行操作「撤销」并确认 | 撤销成功,状态变为已撤销 | 待测 |
| INVITE-014 | 点击有效批次行操作「撤销批次」再取消 | 确认框正常,取消后批次状态不变 | 待测 |
| INVITE-015 | 点击有效批次行操作「撤销批次」并确认 | 批次内有效邀请码变为已撤销 | 待测 |
| INVITE-016 | 点击「导出」 | 下载 CSV 文件,内容与当前筛选条件一致 | 待测 |
## 7. 文件策略
菜单路径:`IM管理 / 文件策略`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| FILE-POLICY-001 | 打开页面 | 策略列表正常展示,包含用途、大小上限、保留天数、自动清理、病毒扫描、MIME 白名单等字段 | 待测 |
| FILE-POLICY-002 | 按租户、用途、自动清理、病毒扫描查询 | 列表按条件刷新 | 待测 |
| FILE-POLICY-003 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
| FILE-POLICY-004 | 编辑弹窗点击「取消」 | 弹窗关闭,数据不变化 | 待测 |
| FILE-POLICY-005 | 修改大小上限、保留天数、开关、MIME 白名单后保存 | 保存成功,列表刷新并展示新值 | 待测 |
| FILE-POLICY-006 | 输入非法大小或保留天数后保存 | 表单校验或后端错误提示清晰 | 待测 |
## 8. 通知模板
菜单路径:`IM管理 / 通知模板`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| NOTIFY-001 | 打开页面 | 模板列表正常展示,包含场景、状态、站内、Push、短信、标题模板、内容模板等字段 | 待测 |
| NOTIFY-002 | 按租户、场景、状态查询 | 列表按条件刷新 | 待测 |
| NOTIFY-003 | 点击行操作「编辑」 | 打开编辑弹窗,模板字段正确回填 | 待测 |
| NOTIFY-004 | 编辑弹窗点击「取消」 | 弹窗关闭,数据不变化 | 待测 |
| NOTIFY-005 | 修改标题模板、内容模板、站内/Push/短信开关后保存 | 保存成功,列表刷新并展示新值 | 待测 |
| NOTIFY-006 | 清空必填字段后保存 | 表单校验或后端错误提示清晰 | 待测 |
## 9. 工作台配置
菜单路径:`IM管理 / 工作台配置`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| WORKBENCH-001 | 打开页面 | 工作台模块列表正常展示,包含编码、名称、路由、排序、图标 ID、角标 Key、状态、权限等字段 | 待测 |
| WORKBENCH-002 | 按租户、编码、名称、状态查询 | 列表按条件刷新 | 待测 |
| WORKBENCH-003 | 点击「新增模块」 | 打开模块编辑弹窗 | 待测 |
| WORKBENCH-004 | 新增弹窗点击「取消」 | 弹窗关闭,列表不变化 | 待测 |
| WORKBENCH-005 | 新增弹窗直接保存 | 必填项校验正常展示 | 待测 |
| WORKBENCH-006 | 填写编码、名称、路由、排序、状态等信息后保存 | 保存成功,列表刷新 | 待测 |
| WORKBENCH-007 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
| WORKBENCH-008 | 修改模块信息后保存 | 保存成功,列表展示新值 | 待测 |
| WORKBENCH-009 | 点击行操作「禁用」再取消 | 确认框正常,取消后状态不变 | 待测 |
| WORKBENCH-010 | 点击行操作「禁用」并确认 | 禁用成功,列表刷新后状态正确 | 待测 |
| WORKBENCH-011 | 点击「保存排序」 | 排序保存成功;列表刷新后顺序保持一致 | 待测 |
## 10. 外部服务配置
菜单路径:`IM管理 / 外部服务配置`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| EXT-CONFIG-001 | 打开页面 | 配置列表正常展示,包含服务、服务商、端点、启用、配置状态、敏感配置、检查结果等字段 | 待测 |
| EXT-CONFIG-002 | 按租户、服务、服务商、启用状态查询 | 列表按条件刷新 | 待测 |
| EXT-CONFIG-003 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
| EXT-CONFIG-004 | 编辑弹窗切换「启用」开关 | 开关状态可变更,保存前不应提前提交 | 待测 |
| EXT-CONFIG-005 | 修改服务商、端点、配置内容后保存 | 保存成功,列表刷新并展示新值 | 待测 |
| EXT-CONFIG-006 | 点击行操作「检查」再取消 | 确认框正常,取消后不触发检查 | 待测 |
| EXT-CONFIG-007 | 点击行操作「检查」并确认 | 检查请求成功,检查结果和检查时间刷新 | 待测 |
## 11. 版本更新
菜单路径:`IM管理 / 版本更新`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| RELEASE-001 | 打开页面 | 版本策略列表正常展示,包含平台、渠道、最新版本、最低支持、更新类型、下载地址等字段 | 待测 |
| RELEASE-002 | 按租户、平台、渠道查询 | 列表按条件刷新 | 待测 |
| RELEASE-003 | 点击「新增策略」 | 打开策略编辑弹窗 | 待测 |
| RELEASE-004 | 新增弹窗直接保存 | 必填项校验正常展示 | 待测 |
| RELEASE-005 | 填写平台、渠道、版本号、更新类型、地址、发布时间后保存 | 保存成功,列表刷新 | 待测 |
| RELEASE-006 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
| RELEASE-007 | 修改版本策略后保存 | 保存成功,列表展示新值 | 待测 |
| RELEASE-008 | 输入非法版本号或 URL 后保存 | 表单校验或后端错误提示清晰 | 待测 |
## 12. 举报原因
菜单路径:`IM管理 / 举报原因`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| ABUSE-001 | 打开页面 | 原因列表正常展示,包含对象、编码、文案、要求描述、允许证据、排序、状态等字段 | 待测 |
| ABUSE-002 | 按租户、对象、编码、状态查询 | 列表按条件刷新 | 待测 |
| ABUSE-003 | 点击「新增原因」 | 打开原因编辑弹窗 | 待测 |
| ABUSE-004 | 新增弹窗直接保存 | 必填项校验正常展示 | 待测 |
| ABUSE-005 | 填写对象、编码、文案、排序、状态等信息后保存 | 保存成功,列表刷新 | 待测 |
| ABUSE-006 | 点击行操作「编辑」 | 打开编辑弹窗,字段正确回填 | 待测 |
| ABUSE-007 | 修改原因配置后保存 | 保存成功,列表展示新值 | 待测 |
| ABUSE-008 | 点击行操作「禁用」再取消 | 确认框正常,取消后状态不变 | 待测 |
| ABUSE-009 | 点击行操作「禁用」并确认 | 禁用成功,列表刷新后状态正确 | 待测 |
## 13. 总经理信箱配置
菜单路径:`IM管理 / 总经理信箱配置`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| MAILBOX-001 | 打开页面 | 配置表单正常展示,租户选择框可用 | 待测 |
| MAILBOX-002 | 选择租户 | 自动加载该租户信箱配置 | 待测 |
| MAILBOX-003 | 点击「刷新」 | 当前租户配置重新加载,表单无异常 | 待测 |
| MAILBOX-004 | 未选择租户时观察「保存」 | 按钮禁用或提示先选择租户 | 待测 |
| MAILBOX-005 | 修改启用状态、接收人、处理策略等配置后点击「保存」 | 保存成功,再次刷新后配置保持一致 | 待测 |
| MAILBOX-006 | 输入非法配置后保存 | 表单校验或后端错误提示清晰 | 待测 |
## 14. 意见反馈配置
菜单路径:`IM管理 / 意见反馈配置`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| FEEDBACK-001 | 打开页面 | 配置表单正常展示,租户选择框可用 | 待测 |
| FEEDBACK-002 | 选择租户 | 自动加载该租户反馈配置 | 待测 |
| FEEDBACK-003 | 点击「刷新」 | 当前租户配置重新加载,表单无异常 | 待测 |
| FEEDBACK-004 | 未选择租户时观察「保存」 | 按钮禁用或提示先选择租户 | 待测 |
| FEEDBACK-005 | 修改启用状态、分类、处理人、限制策略等配置后点击「保存」 | 保存成功,再次刷新后配置保持一致 | 待测 |
| FEEDBACK-006 | 输入非法配置后保存 | 表单校验或后端错误提示清晰 | 待测 |
## 15. 任务看板
菜单路径:`IM管理 / 任务看板`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| TASK-001 | 打开页面 | 看板列表正常展示,包含租户、企业码、状态、DB、业务待投递、业务失败、群待投递、群失败、孤儿文件、待物理清理等字段 | 待测 |
| TASK-002 | 按租户、状态查询 | 列表按条件刷新 | 待测 |
| TASK-003 | 查看建议动作字段 | 异常租户展示可理解的建议动作;正常租户展示正常或为空 | 待测 |
| TASK-004 | 刷新表格 | 统计数据重新加载,无接口报错 | 待测 |
## 16. Kafka 命令
菜单路径:`IM管理 / Kafka命令`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| KAFKA-CMD-001 | 打开页面 | 命令列表正常展示,包含租户、命令 ID、事件类型、业务类型、业务 ID、状态、错误码、消息等字段 | 待测 |
| KAFKA-CMD-002 | 按租户、命令 ID、事件 ID、事件类型、业务 ID、状态、创建时间查询 | 列表按条件刷新 | 待测 |
| KAFKA-CMD-003 | 点击「发送缓存失效测试」 | 请求成功,新增一条缓存失效命令或页面提示成功 | 待测 |
| KAFKA-CMD-004 | 点击「发送工作台测试事件」 | 请求成功,新增一条工作台测试事件命令或页面提示成功 | 待测 |
| KAFKA-CMD-005 | 点击行操作「详情」 | 打开详情弹窗,展示命令 payload、结果、错误原因等信息 | 待测 |
| KAFKA-CMD-006 | 详情弹窗点击「关闭」 | 弹窗正常关闭 | 待测 |
| KAFKA-CMD-007 | 发送测试事件后等待消费结果 | 状态从待发送/已发送变为成功或失败,失败时错误信息可读 | 待测 |
## 17. Kafka 死信
菜单路径:`IM管理 / Kafka死信`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| DEAD-001 | 打开页面 | 死信列表正常展示,包含死信 ID、事件 ID、来源 Topic、消费组、事件类型、业务类型、业务 ID、状态等字段 | 待测 |
| DEAD-002 | 按租户、状态、事件类型、业务类型、业务 ID 查询 | 列表按条件刷新 | 待测 |
| DEAD-003 | 点击行操作「详情」 | 打开详情弹窗,展示死信 payload、错误原因、处理信息 | 待测 |
| DEAD-004 | 详情弹窗点击「关闭」 | 弹窗正常关闭 | 待测 |
| DEAD-005 | 详情弹窗点击「忽略」 | 操作成功,状态变为已忽略或列表刷新后状态正确 | 待测 |
| DEAD-006 | 详情弹窗点击「标记已处理」 | 操作成功,状态变为已处理 | 待测 |
| DEAD-007 | 详情弹窗点击「重放」 | 操作成功,重放次数增加,状态或处理结果正确更新 | 待测 |
| DEAD-008 | 对已处理、已忽略死信重复操作 | 系统应拦截或给出清晰提示,不出现脏状态 | 待测 |
## 18. Kafka 审计事件
菜单路径:`IM管理 / Kafka审计事件`
| 编号 | 操作 | 预期结果 | 结果 |
| --- | --- | --- | --- |
| KAFKA-AUDIT-001 | 打开页面 | 审计事件列表正常展示,包含租户、事件 ID、事件类型、动作、资源、资源 ID、操作者、结果等字段 | 待测 |
| KAFKA-AUDIT-002 | 按租户编码、事件 ID、事件类型、动作、资源、资源 ID、结果、接收时间查询 | 列表按条件刷新 | 待测 |
| KAFKA-AUDIT-003 | 点击行操作「详情」 | 打开详情弹窗,展示事件详情、请求上下文、结果信息 | 待测 |
| KAFKA-AUDIT-004 | 详情弹窗点击「关闭」 | 弹窗正常关闭 | 待测 |
## 19. 测试数据建议
| 数据类型 | 建议准备 |
| --- | --- |
| 租户 | 至少 1 个正常租户,具备独立业务库 |
| 组织架构 | 至少 1 个根部门、1 个子部门、2 个用户成员 |
| 文件治理 | 至少 1 个正常文件、1 个可隔离文件、1 个孤儿文件 |
| 注册邀请码 | 至少 1 个有效邀请码、1 个过期邀请码、1 个批次邀请码 |
| Kafka | Kafka、消费者、IM Server 正常运行;准备 1 条可产生成功结果的命令和 1 条可产生死信的命令 |
| 配置类 | 文件策略、通知模板、工作台、外部服务、版本更新、举报原因均有初始数据 |
## 20. 缺陷记录模板
| 缺陷编号 | 菜单 | 操作步骤 | 实际结果 | 预期结果 | Console/Network 信息 | 严重级别 | 状态 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| BUG-001 | | | | | | | 待处理 |
## 21. 第一轮执行记录
执行时间:2026-09-03 15:16 - 15:22
执行环境:
| 项目 | 值 |
| --- | --- |
| 前端 | `http://127.0.0.1:8001/` |
| 后端 | Docker 容器 `im-local-admin`,端口 `8000` |
| MySQL | Docker 容器 `relaxed_cori`,库 `hotgo`、`tinode`、`tinode_test` |
| Redis | Docker 容器 `brave_blackburn` |
| Kafka | Docker 容器 `kafka` |
本轮说明:
1. 从左侧菜单「组织架构」开始,按菜单顺序逐页点击。
2. 对查询、重置、展开、刷新、新增、编辑、详情等非破坏性按钮进行了点击验证。
3. 对删除、隔离、撤销、禁用、重放等有副作用的按钮,本轮只验证入口和取消流程,不确认执行。
4. 「收起」按钮在自动化脚本里先于「展开」检测,产生过误报,本轮不作为缺陷记录。
5. 「注册邀请码」菜单点击正常,真实路由为 `/#/im/registrationInvite`;直接访问 `/#/im/registrationInvite/index` 会进入 ErrorPage,测试文档已修正。
### 21.1 页面通过项
| 菜单 | 本轮结果 |
| --- | --- |
| 组织架构 | 页面可打开,查询/重置/展开/新增部门可点击;添加成员因未先选择部门处于禁用,符合预期 |
| 文件治理 | 页面可打开,查询/重置/展开/详情可点击;列表有数据 |
| 注册邀请码 | 通过左侧菜单可打开,查询/重置/展开/添加/批量生成/导出可见;列表有 2 条历史数据 |
| 工作台配置 | 页面可打开,查询/重置/展开/新增模块/编辑可点击;禁用弹窗可打开并取消 |
| 版本更新 | 页面可打开,查询/重置/展开/新增策略/编辑可点击;列表有数据 |
| 举报原因 | 页面可打开,查询/重置/展开/新增原因/编辑可点击;禁用弹窗可打开并取消 |
| 总经理信箱配置 | 页面可打开,保存/刷新按钮可见,刷新可点击 |
| 意见反馈配置 | 页面可打开,保存/刷新按钮可见,刷新可点击 |
| 任务看板 | 页面可打开,查询/重置/展开可点击 |
### 21.2 本轮发现的问题
| 缺陷编号 | 菜单 | 操作步骤 | 实际结果 | 预期结果 | 证据 | 严重级别 | 状态 |
| --- | --- | --- | --- | --- | --- | --- | --- |
| BUG-IM-001 | 文件策略 | 点击「IM管理 / 文件策略」,或点击查询/重置 | 页面 Console 报 `检查IM文件策略失败`,列表无法加载,行操作「编辑」不可测 | 文件策略列表应正常加载,缺省策略应自动初始化 | 后端日志:`SELECT COUNT(1) FROM im_file_policy ... Error 1146: Table 'hotgo.im_file_policy' doesn't exist`;代码位置:`server/internal/logic/sys/im_file_policy.go:128` | P0 | 待处理 |
| BUG-IM-002 | 通知模板 | 点击「IM管理 / 通知模板」,或点击查询/重置 | 页面 Console 报 `检查IM通知模板失败,请稍后重试!`,列表无法加载,行操作「编辑」不可测 | 通知模板列表应正常加载,缺省模板应自动初始化 | 后端日志:`SELECT COUNT(1) FROM im_notification_template ... Error 1146: Table 'hotgo.im_notification_template' doesn't exist`;代码位置:`server/internal/logic/sys/im_notification.go:123` | P0 | 待处理 |
| BUG-IM-003 | 外部服务配置 | 点击「IM管理 / 外部服务配置」,或点击查询/重置 | 页面 Console 报 `初始化IM外部服务配置失败`,列表无法加载,行操作「编辑」「检查」不可测 | 外部服务配置列表应正常加载,缺省服务配置应自动初始化 | 后端日志:`SELECT COUNT(1) FROM im_external_config ... Error 1146: Table 'hotgo.im_external_config' doesn't exist`;代码位置:`server/internal/logic/sys/im_external_config.go:194` | P0 | 待处理 |
| BUG-IM-004 | Kafka命令 | 点击「IM管理 / Kafka命令」后点击「发送缓存失效测试」 | 页面 toast 显示 `读取IM命令Outbox失败`,Kafka 命令列表仍为 0 条 | 应写入 Outbox 并投递 Kafka,页面能看到新事件或明确状态 | 后端日志:`读取IM命令Outbox失败: element of parameter "pointer" for function Scan should type of struct/*struct/[]struct/[]*struct`;代码位置:`server/internal/library/imkafka/command.go:103` | P0 | 待处理 |
| BUG-IM-005 | Kafka命令 | 点击「发送缓存失效测试」「发送工作台测试事件」后查看数据库 | 正确表 `hotgo.im_admin_outbox` 为 0 条;带前缀表 `hotgo.hg_im_admin_outbox` 新增 3 条 pending 记录 | Admin 新增 IM 表和代码查询应统一使用无前缀 `im_*` 表,避免 `hg_im_*` 和 `im_*` 双表分裂 | MySQL:`SHOW TABLES LIKE 'hg_im_%'` 可见 `hg_im_admin_outbox`、`hg_im_admin_commands`、`hg_im_audit_events`;`hg_im_admin_outbox` 有 pending 数据,`im_admin_outbox` 为空 | P0 | 待处理 |
| BUG-IM-006 | Kafka死信 | 点击「IM管理 / Kafka死信」 | 页面可打开,但列表无数据,行操作「详情」「忽略」「标记已处理」「重放」无法覆盖 | 联调测试库应准备至少 1 条死信数据,才能覆盖处置按钮 | MySQL:`hotgo.im_kafka_dead_letter_events` 计数为 0 | P1 | 待补测试数据 |
| BUG-IM-007 | Kafka审计事件 | 点击「IM管理 / Kafka审计事件」 | 页面可打开,但列表无数据,行操作「详情」无法覆盖 | 联调测试库应准备至少 1 条审计事件数据,才能覆盖详情按钮 | MySQL:`hotgo.im_audit_events` 计数为 0 | P1 | 待补测试数据 |
### 21.3 数据库核查结果
| 库 | 表 | 结果 |
| --- | --- | --- |
| `hotgo` | `im_file_policy` | 不存在 |
| `hotgo` | `im_notification_template` | 不存在 |
| `hotgo` | `im_external_config` | 不存在 |
| `hotgo` | `im_admin_commands` | 存在,0 条 |
| `hotgo` | `im_admin_outbox` | 存在,0 条 |
| `hotgo` | `hg_im_admin_outbox` | 存在,3 条 pending |
| `hotgo` | `im_kafka_dead_letter_events` | 存在,0 条 |
| `hotgo` | `im_audit_events` | 存在,0 条 |
### 21.4 下一步建议
1. 先导入缺失表 SQL:`server/storage/data/generate/im_file_policy_menu.sql`、`server/storage/data/generate/im_notification_menu.sql`、`server/storage/data/generate/im_external_config_menu.sql`。
2. 修复 Kafka Outbox 表名前缀分裂问题,确保所有 Kafka Admin 表统一访问 `im_*`,不要落到 `hg_im_*`。
3. 修复 `DispatchOutboxByEvent` 中 `g.DB().GetOne` 触发的 `Scan` 参数类型错误。
4. 准备 Kafka 死信和 Kafka 审计事件测试数据后,重新覆盖「详情」「忽略」「标记已处理」「重放」等按钮。
5. 上述 P0 处理后,重新执行本测试文档第 4 到第 18 章。
## 22. 第二轮修复记录
执行时间:2026-09-03 15:37 - 15:50
### 22.1 已处理项
| 缺陷编号 | 处理结果 | 复测结果 |
| --- | --- | --- |
| BUG-IM-001 | 已补齐 `hotgo.im_file_policy` 表;访问文件策略列表时可自动初始化默认策略 | `/admin/imFilePolicy/list?tenantId=1` 返回 `code=0`,列表 6 条 |
| BUG-IM-002 | 已补齐 `hotgo.im_notification_template` 表;访问通知模板列表时可自动初始化默认模板 | `/admin/imNotification/templateList` 返回 `code=0`,列表 7 条 |
| BUG-IM-003 | 已补齐 `hotgo.im_external_config` 表;访问外部服务配置列表时可自动初始化默认配置 | `/admin/imExternalConfig/list?tenantId=1` 返回 `code=0`,列表 5 条 |
| BUG-IM-004 | 已修复 Kafka Admin 命令/Outbox 读写 DB,统一改为无前缀 `imRegistry` 连接 | 点击等价接口 `/admin/imKafkaCommand/cacheInvalidate` 返回 `code=0`,不再报 `读取IM命令Outbox失败` |
| BUG-IM-005 | 已修复 `im_admin_commands`、`im_admin_outbox`、`im_audit_events`、`im_kafka_dead_letter_events` 读写前缀分裂 | 新增 Kafka 测试事件写入 `hotgo.im_admin_outbox`,状态为 `sent`;没有继续写入 `hg_im_admin_outbox` |
### 22.2 额外修复
| 问题 | 处理结果 | 复测结果 |
| --- | --- | --- |
| Docker 本地 admin 缺少运行时 MySQL/Redis 环境变量导致启动 panic | 本地容器启动时显式传入 `MYSQL_DSN`、`REDIS_ADDR`、`TOKEN_SECRET_KEY` 等变量 | `im-local-admin` 正常启动,`http://127.0.0.1:8000/admin/` 可访问 |
| admin 容器内 `im-server:6060` 解析到不可用地址,死信内部查询 EOF | `config.template`、`entrypoint.sh` 增加 IM API 环境变量;本地容器使用 `IM_INTERNAL_API_BASE_URL=http://im-local-im:6060` | `/admin/imDeadLetter/list?tenantId=1` 返回 `code=0`,列表 18 条 |
| 测试库 IM 菜单中文标题乱码 | 已在测试库用 UTF-8 连接修复 `hg_admin_menu.title`;新增 `im_menu_title_repair.sql` 可重复修复 | `imManager`、`imOrgIndex`、`imFilePolicyIndex`、`imKafkaCommandIndex` 等标题查询显示正常中文 |
| 部分新增菜单 SQL 使用旧版 `hg_admin_menu` 字段 `route/is_cache/created_by/updated_by` | 已转换相关 `*_menu.sql` 菜单插入字段为当前 HotGo 表结构 | 相关 SQL 在本地 `hotgo` 库重复执行通过,无 `Unknown column 'route'` |
### 22.3 当前剩余注意项
1. 当前浏览器页签仍保存旧 token,页面上可能继续弹「登录身份已失效」;刷新并重新登录后再继续 UI 点击测试。
2. `hg_im_admin_outbox` 里还保留第一轮测试误写入的 3 条旧数据,仅用于证明旧问题,不影响新链路;后续可按运维需要清理。
3. Kafka 审计事件列表当前仍无数据,需要 IM Server 产生或导入审计事件后才能覆盖详情弹窗。
+358
View File
@@ -0,0 +1,358 @@
# IM 未实现增强项
> 更新时间:2026-08-22
> 范围:HotGo IM 后台、IM 服务端配合项、客户端动态配置来源
本文用于记录当前后台已识别但尚未完成的增强项,作为后续开发排期和验收依据。当前多租户方案为“一个 HotGo 控制库 + 每租户一个独立 IM 业务库”,租户业务表不带 `tenant_id`,后台必须通过登录态解析租户并选择租户库。
## 已完成基线
1. 邀请码管理:单个生成、批量生成、列表、批次详情、撤销、批量撤销、导出、注册短信开关。
2. 租户管理:列表、详情、编辑、状态、用量/健康、删除前检查、进入删除流程、恢复。
3. 用户管理:列表、详情、设备、重置密码、撤销会话、账号状态停用/恢复。
4. 审计日志:列表、详情、导出。
5. Outbox 运维:列表、详情、重试、标记死信,覆盖 `im_business_events_outbox` 和 `im_group_events_outbox`。
6. 租户边界:用户、审计、群、实名、工单、Outbox 等后台接口已按登录态解析租户范围。
7. 群治理处置:治理配置更新、解散群、成员禁言、解除禁言、入群审批、管理员设置/取消、群主转让、群文件删除。
## 剩余 P0
### P0-1 Bootstrap 动态配置来源
状态:未实现。
目标:
1. 后台提供 `bootstrap` 所需配置来源,包括 `features`、`message_policy`、`limits`、`settings_menu`、`workbench.modules`。
2. IM 服务端从后台配置或控制库读取后,下发给客户端。
3. 客户端不硬编码功能开关、消息策略、设置菜单和工作台基础入口。
建议实现:
1. 扩展 `im_tenant_config` 或新增独立配置表。
2. 使用 GoFrame CLI 重新生成 DAO/entity/do,禁止手改生成文件。
3. 后台提供租户配置页面,敏感字段脱敏回显。
4. IM 服务端实现配置读取和缓存,缓存失效策略单独定义。
依赖:
1. 控制库迁移 SQL。
2. IM 服务端 `bootstrap` 接口对接。
3. 客户端按返回配置裁剪 UI。
### P0-2 完整租户配置与配额
状态:部分实现。
已实现:
1. `max_users`
2. `max_groups`
3. `max_group_members`
4. 租户状态、企业码、名称、DB 路由
未实现:
1. 品牌 Logo、品牌色、登录页展示配置。
2. HTTP API、WebSocket、文件服务等终端地址。
3. 可用登录方式。
4. 密码策略。
5. 文件存储配额。
6. 功能开关配置。
7. App 更新策略基础配置。
建议实现:
1. 将基础配额、品牌、终端地址、登录方式、密码策略拆成清晰字段或 JSON 配置。
2. 对 `db_dsn`、密钥类字段只保存不明文回显。
3. 平台管理员可编辑,租户管理员按权限只读或申请变更。
### P0-3 日志脱敏统一治理
状态:未实现。
目标:
1. 访问日志、错误日志、审计摘要、导出任务日志不得泄露敏感字段。
2. 覆盖邀请码、验证码、密码、Token、服务密钥、数据库 DSN、实名证件信息。
至少覆盖字段:
1. `inviteCode`
2. `invite_code`
3. `code`
4. `verification_code`
5. `password`
6. `secret`
7. `serviceSecret`
8. `service_secret`
9. `authorization`
10. `db_dsn`
11. `id_number`
12. `token`
建议实现:
1. 在统一响应/日志中间件或日志写入前增加脱敏工具。
2. 对业务审计 `detail_json` 做字段级脱敏。
3. 补充单元测试,覆盖嵌套 JSON、大小写字段、数组对象。
### P0-4 邀请码明文查看与精确查询审计拆分
状态:部分实现。
已实现:
1. 邀请码生成、批量生成、列表、批次详情、撤销、批量撤销、导出。
2. 基础菜单权限。
待确认/增强:
1. 明文查看是否需要独立权限点。
2. 精确查询邀请码是否需要独立权限点。
3. 导出明文邀请码是否需要二次确认和独立审计。
4. 查询/查看/导出操作是否全部写入业务审计。
建议实现:
1. 增加 `imInvitePlainView` 或等价权限。
2. 列表默认脱敏,只有具备权限时显示明文。
3. 精确查询、明文查看、导出记录审计,审计中不写邀请码明文。
## 后续 P1
### P1-0 组织架构管理
状态:部分实现。
已实现:
1. 部门列表。
2. 部门新增、编辑、移动。
3. 部门删除前校验子部门和成员。
4. 部门移动防环校验。
5. 部门成员列表。
6. 成员添加、编辑、调岗、移除。
7. 组织搜索基础接口,覆盖部门和成员。
8. 组织操作写入 IM 后台审计。
9. 部门导出和成员导出。
待实现/配合:
1. 组织数据权限:当前后台按租户选库隔离,部门级 `org_admin` 数据范围尚未建模。
2. 组织导入:需要定义 Excel 字段、冲突覆盖规则、失败明细和幂等更新策略。
3. 离职用户处理策略:当前支持 `employment_state` 和 `contactable`,客户端/IM 服务端需按该状态裁剪通讯录和可联系入口。
4. 组织版本增量同步:当前维护部门/成员版本号,客户端增量同步协议需由 IM 服务端继续承接。
### P1-1 群治理处置动作
状态:已完成基础后台闭环。
已实现:
1. 群列表、详情。
2. 成员列表。
3. 治理配置查看。
4. 入群申请、邀请、公告、禁言、管理日志列表。
5. 治理配置更新。
6. 全员禁言开关。
7. 单人禁言/解禁。
8. 解散群。
9. 入群审批通过/拒绝。
10. 设置/取消管理员。
11. 转让群主。
12. 群文件列表和删除处置。
待 IM 服务端配合确认:
1. 后台写入 `im_group_events_outbox` 后,IM 服务端消费者需同步在线会话、群资料缓存和成员 ACL。
2. 群文件删除当前为业务索引软删除,底层对象文件物理清理归入 P1-2 文件治理。
### P1-2 文件治理
状态:部分实现。
已实现:
1. 文件列表。
2. 文件用途、大小、状态查询。
3. 隔离、删除。
4. 文件详情查看审计。
5. 孤儿文件手动清理。
6. 孤儿文件定时任务 `imFileCleanupOrphan`,参数为保留天数、单租户处理上限。
7. 租户级文件用途策略配置:大小上限、保留天数、MIME 白名单、自动清理开关、病毒扫描开关。
待实现/配合:
1. 下载审计:需要 IM 服务端文件下载链路或后台下载代理在实际下载时写入审计。
2. 底层对象物理删除:当前后台将 `im_fileuploads.status` 标记为已删除,物理对象清理需由文件服务或 IM 服务端清理器消费 `location`。
3. 过期文件执行:后台已提供策略配置,IM 服务端/文件清理器需读取 `im_file_policy` 后执行过期扫描、状态更新和物理清理。
4. 文件用途识别目前基于 `im_filemsglinks`/群文件索引做基础归类,头像、举报证据、反馈附件等用途需要 IM 服务端补齐绑定关系或独立索引。
5. 病毒扫描执行:后台已提供开关,实际扫描任务、扫描结果回写和用户通知需要 IM 服务端或文件服务承接。
### P1-3 外部服务配置
状态:部分实现。
已实现:
1. SMS 服务商配置。
2. Push/APNS/FCM 配置。
3. TURN/STUN 配置。
4. 对象存储配置。
5. 审核插件配置。
6. 敏感字段 `secret_json` 只写入,不明文回显,仅返回 `hasSecret`。
7. 基础配置完整性检查,写入 `status`、`checked_at`、`check_result`。
8. 配置变更和检查写入 IM 后台审计。
待实现/配合:
1. 真实连通性检查:需要各服务商 SDK 或 IM 服务端提供探测接口。
2. IM 服务端 `bootstrap`、短信、Push、TURN、对象存储、审核链路需要读取 `im_external_config`。
3. `im_external_config` 为新增控制库表,需要执行 `server/storage/data/generate/im_external_config_menu.sql`。
### P1-4 任务看板
状态:部分实现。
已实现:
1. 只读任务看板,按租户汇总 IM 业务库健康状态。
2. Outbox 巡检:统计业务事件和群事件的待投递、失败数量。
3. 文件清理巡检:统计孤儿文件和待物理清理文件数量。
4. 展示最新业务事件、最新群事件、最新文件更新时间。
5. 根据失败/待处理/待清理数量给出基础建议动作。
待实现/配合:
1. 主动执行任务:当前看板只读,不直接触发修复或调度。
2. Push 重试:需要 IM 服务端暴露 Push 失败队列、重试入口或统一任务表。
3. 搜索索引重建:需要 IM 服务端定义索引任务入口、进度查询和失败重试模型。
4. 邀请码投影补偿:需要 IM 服务端或后台定义投影任务表、幂等键和重放策略。
5. 文件物理清理任务详情:需要文件服务或 IM 服务端回写物理删除结果。
6. 跨租户任务执行审计:执行型任务落地后需补充独立权限点和审计明细。
### P1-5 系统通知模板
状态:部分实现。
已实现:
1. 租户级通知模板列表。
2. 账号安全、好友申请、群事件、文件事件、会议提醒、日报月报提醒、工单处理等默认场景初始化。
3. 站内通知、Push、短信渠道开关。
4. 标题模板、内容模板、扩展 JSON 配置。
5. 模板启用/禁用。
6. 模板变更写入 IM 后台审计。
待实现/配合:
1. 实际投递:需要 IM 服务端 Outbox 消费器或系统 Topic 桥接读取模板并发送。
2. 投递记录:需要 IM 服务端回写站内、Push、短信投递状态和失败原因。
3. 模板变量校验:当前仅校验非空和 JSON 格式,变量白名单、缺失变量检查需要按具体场景定义。
4. 多语言模板:当前为单语言模板,后续如客户端支持多语言需扩展语言维度。
## 后续 P2
### P2-1 工作台模块配置
状态:已完成基础后台闭环。
已实现:
1. 租户工作台模块列表、搜索。
2. 模块新增、编辑、启用/禁用。
3. 模块排序接口和后台入口。
4. 默认初始化组织架构、会议通知、日报、月报、总经理信箱五个入口。
5. 路由选项对齐 IM 服务端当前白名单,避免配置后客户端接口丢弃。
6. 工作台配置变更写入 IM 后台审计。
待 IM 服务端/客户端配合确认:
1. `GET /v1/im/workbench` 当前读取 `im_workbench_apps` 已具备,需在实际环境确认租户库连接与后台修改后的缓存刷新策略。
2. 用户或角色级工作台权限表 `im_workbench_user_apps` 尚未接入后台,当前只覆盖租户级模块配置。
3. 未读角标实时事件 `workbench.badge.updated` 的聚合和增量推送仍由 IM 服务端承接。
4. 新增自定义入口若超出当前五个路由,需要先扩展 IM 服务端路由白名单和客户端页面。
### P2-2 App 版本更新策略
状态:已完成基础后台闭环。
已实现:
1. 租户级客户端版本策略列表、搜索。
2. 按 `platform + channel` 配置最新版本、最低支持版本、更新类型、商店地址、安装包地址、校验值和再次提醒时间。
3. 默认初始化 iOS/Android stable 策略。
4. 配置变更写入 IM 后台审计。
待 IM 服务端/客户端配合确认:
1. `GET /v1/im/config/app-version` 已有 IM 服务端读取链路,需确认实际环境租户库数据和客户端版本比较规则。
2. 灰度比例、分渠道灰度名单、强更弹窗样式尚未建模。
### P2-3 举报原因配置
状态:已完成基础后台闭环。
已实现:
1. 按用户、群组、消息三类对象配置举报原因。
2. 支持原因新增、编辑、禁用、排序、是否要求描述、是否允许上传证据。
3. 默认初始化诈骗、骚扰、垃圾广告、违法违规等基础原因。
4. 配置变更写入 IM 后台审计。
待实现/配合:
1. 举报工单 SLA、合并策略、批量导出仍未实现。
2. 举报工单处置结果与客户端通知联动仍由后续工单模块增强。
### P2-4 总经理信箱配置
状态:已完成基础后台闭环。
已实现:
1. 租户级总经理信箱启用/禁用。
2. 匿名提交开关。
3. 必填字段、最大字数、附件策略配置。
4. 告知标题和告知内容配置,更新时同步刷新 `notice_updated_at`。
5. 配置变更写入 IM 后台审计。
待实现/配合:
1. 信箱提交列表、处理状态、回复闭环尚未实现。
2. 受理人/部门、SLA、匿名身份展示范围尚未建模。
### P2-5 意见反馈配置
状态:已完成基础后台闭环。
已实现:
1. 租户级意见反馈分类配置。
2. 最大字数、附件策略、联系方式开关、诊断信息提示配置。
3. 配置变更写入 IM 后台审计。
待实现/配合:
1. 反馈提交列表、合并、回复、导出尚未实现。
2. 客户端分类文案如需多语言,需要扩展分类结构。
### P2 待实现
1. 会议。
2. 日报/月报。
3. 总经理信箱提交处理闭环。
4. 青少年模式。
5. 反诈与内容审核策略。
6. 举报/反馈 SLA、合并、导出。
7. 实名审核策略配置。
## 验收提示
1. 所有后台租户业务操作必须通过登录态解析租户,不能信任浏览器传入的 `tenantId`。
2. 平台管理员跨租户操作必须选择租户并写审计。
3. 租户管理员隐藏租户选择器,后端使用绑定租户。
4. 所有新增按钮必须有独立菜单权限。
5. 前端字典、状态、下拉选项必须从后端字典加载。
6. 敏感字段不得在日志、审计、导出文件中明文出现。
File diff suppressed because it is too large Load Diff