SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; -- 这不是可直接混跑旧 admin 的普通滚动迁移:旧 admin 不读 operations_status,会把总体 status=pending 的新单直接财务终审。 -- 生产必须先摘流所有旧 admin,再执行 102 并部署新 admin,恢复 admin 流量后才能滚动 gateway。 -- checkpoint 在首次执行时固定迁移前的最大申请 ID。迁移中断后重放仍使用原边界, -- 不会把部署窗口内由旧 gateway 新建的单误标为 skipped。该表只保存小型迁移元数据,不承载业务状态。 CREATE TABLE IF NOT EXISTS schema_migration_checkpoints ( version VARCHAR(128) NOT NULL, checkpoint_value BIGINT UNSIGNED NOT NULL DEFAULT 0, created_at_ms BIGINT NOT NULL DEFAULT 0, PRIMARY KEY (version) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='可重放迁移的持久边界'; INSERT IGNORE INTO schema_migration_checkpoints (version, checkpoint_value, created_at_ms) SELECT '102_user_withdrawal_operations_review.sql', COALESCE(MAX(id), 0), CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED) FROM admin_user_withdrawal_applications; -- 新列的数据库默认必须是 pending:旧 gateway 不传新列,在滚动窗口内也会自动进入运营审核。 -- 单个 ALTER 的列变更具有原子性;信息查询和持久 checkpoint 使整个文件在中断后可安全重放。 SET @ddl := IF( (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_user_withdrawal_applications' AND COLUMN_NAME = 'operations_status') = 0, 'ALTER TABLE admin_user_withdrawal_applications ADD COLUMN operations_status VARCHAR(32) NOT NULL DEFAULT ''pending'' COMMENT ''运营审核状态:skipped/pending/rejecting/approved/rejected'' AFTER status, ADD COLUMN operations_reviewer_user_id BIGINT UNSIGNED NULL AFTER operations_status, ADD COLUMN operations_reviewer_name VARCHAR(64) NOT NULL DEFAULT '''' AFTER operations_reviewer_user_id, ADD COLUMN operations_audit_remark TEXT NULL AFTER operations_reviewer_name, ADD COLUMN operations_audit_command_id VARCHAR(128) NOT NULL DEFAULT '''' AFTER operations_audit_remark, ADD COLUMN operations_audit_transaction_id VARCHAR(128) NOT NULL DEFAULT '''' AFTER operations_audit_command_id, ADD COLUMN operations_reviewed_at_ms BIGINT NULL AFTER operations_audit_transaction_id, ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1' ); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 兼容早期局部执行过 default=skipped 的开发库;修正默认值是元数据操作,不回写全表。 SET @ddl := IF( (SELECT COLUMN_DEFAULT FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_user_withdrawal_applications' AND COLUMN_NAME = 'operations_status') <> 'pending', 'ALTER TABLE admin_user_withdrawal_applications ALTER COLUMN operations_status SET DEFAULT ''pending'', ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1' ); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; -- 只把迁移前已经终审的历史行标为 skipped,保留 status=pending 的真实待审申请进入运营初审; -- 按主键范围回填低频 admin 申请,不扫描 wallet/user 流水。大量存量时在低峰执行,并监控行锁和复制延迟。 UPDATE admin_user_withdrawal_applications application JOIN schema_migration_checkpoints checkpoint ON checkpoint.version = '102_user_withdrawal_operations_review.sql' SET application.operations_status = 'skipped' WHERE application.id <= checkpoint.checkpoint_value AND application.status IN ('approved', 'rejected') AND application.operations_status = 'pending'; -- 索引在回填后构建,避免回填期间逐行维护新索引。建索引会扫描该 admin 表并产生额外 IO/磁盘占用, -- 必须在低峰确认磁盘余量,并监控 metadata lock 和复制延迟。 SET @ddl := IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'admin_user_withdrawal_applications' AND INDEX_NAME = 'idx_admin_withdrawal_app_operations_status_time') = 0, 'ALTER TABLE admin_user_withdrawal_applications ADD KEY idx_admin_withdrawal_app_operations_status_time (app_code, operations_status, created_at_ms, id), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1' ); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); -- 财务权限保留原 code,避免存量自建角色和 JWT 契约失效;仅收敛后台展示名称并新增独立运营权限。 INSERT INTO admin_permissions (name, code, kind, description, created_at_ms, updated_at_ms) VALUES ('用户提现财务审核', 'finance-withdrawal:view', 'menu', '查看已通过运营审核或历史兼容的用户提现申请', @now_ms, @now_ms), ('用户提现财务审核操作', 'finance-withdrawal:audit', 'button', '财务终审用户提现并扣减或释放冻结余额', @now_ms, @now_ms), ('用户提现运营审核', 'operations-withdrawal:view', 'menu', '查看需要运营初审的用户提现申请', @now_ms, @now_ms), ('用户提现运营审核操作', 'operations-withdrawal:audit', 'button', '运营初审用户提现;通过后转交财务,拒绝时释放冻结余额', @now_ms, @now_ms) ON DUPLICATE KEY UPDATE name = VALUES(name), kind = VALUES(kind), description = VALUES(description), updated_at_ms = @now_ms; -- 固定运营岗位不能继续持有财务终审权限,否则可绕过运营初审直接结算 frozen;只清理两个精确 role code 和两个精确 permission code。 DELETE role_permission FROM admin_role_permissions role_permission JOIN admin_roles role ON role.id = role_permission.role_id JOIN admin_permissions permission ON permission.id = role_permission.permission_id WHERE role.code IN ('ops-admin', 'operations-specialist') AND permission.code IN ('finance-withdrawal:view', 'finance-withdrawal:audit'); INSERT IGNORE INTO admin_role_permissions (role_id, permission_id) SELECT role.id, permission.id FROM admin_roles role JOIN admin_permissions permission WHERE role.code IN ('platform-admin', 'ops-admin', 'operations-specialist') AND permission.code IN ('operations-withdrawal:view', 'operations-withdrawal:audit');