-- Aslan Game King: dedicated consume-to-draw campaign, ranking and settlement. -- -- Performance review (DDL only; this migration must not run ad-hoc against production): -- 1. Wallet hot path inserts one ledger row by uq_activity_asset, then updates one overall PK row -- and one daily PK row. All are O(log N); ranking never GROUP BYs the append-only ledger. -- 2. Overall/daily Tycoon and Victorious rankings read at most 30/100 rows through dimension -- indexes whose prefixes bind activity_id (and stat_date for daily rankings). -- 3. Current campaign lookup uses idx_aslan_gk_activity_window. start_time is the range column; -- end_time remains an index-covered residual filter, which is acceptable because concurrent -- enabled campaigns per sys_origin are expected to be very small. -- 4. Draw request and settlement retry paths use unique keys. The recovery scan uses -- idx_aslan_gk_draw_recover (delivery_status, update_time, id), so the five-minute task does -- not scan or filesort the full draw table as records accumulate. -- 5. Settlement due scans bind settlement_status first and range on settlement_time through -- idx_aslan_gk_activity_settle; end_time remains available for the bounded due-order scan. -- Per-reward delivery writes only the small configured group and reads it by owner/status. -- 6. No UPDATE/DELETE scans an unindexed predicate. The migration only creates empty tables and -- idempotent menu rows; it does not backfill wallet history or lock existing business tables. -- 7. Unlimited prize stock (-1) is never updated at draw time, avoiding a global hot-row lock; -- only finite stock uses the activity/prize primary-key update. SET NAMES utf8mb4; CREATE TABLE IF NOT EXISTS aslan_game_king_activity ( id BIGINT NOT NULL, activity_code VARCHAR(64) NOT NULL, activity_name VARCHAR(128) NOT NULL, activity_desc VARCHAR(1000) NULL, sys_origin VARCHAR(32) NOT NULL, time_zone VARCHAR(64) NOT NULL DEFAULT 'Asia/Riyadh', start_time DATETIME(3) NOT NULL, end_time DATETIME(3) NOT NULL, settlement_delay_minutes INT NOT NULL DEFAULT 30, settlement_time DATETIME(3) NOT NULL, coin_per_draw BIGINT NOT NULL, event_types VARCHAR(2000) NOT NULL, ranking_type VARCHAR(32) NOT NULL DEFAULT 'TYCOON', ranking_period VARCHAR(32) NOT NULL DEFAULT 'OVERALL', enabled TINYINT(1) NOT NULL DEFAULT 0, settlement_status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED', create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gk_activity_code (activity_code), KEY idx_aslan_gk_activity_window (sys_origin, enabled, start_time, end_time), KEY idx_aslan_gk_activity_settle (settlement_status, settlement_time, end_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Aslan Game King campaign'; CREATE TABLE IF NOT EXISTS aslan_game_king_prize ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, prize_name VARCHAR(128) NOT NULL, prize_image VARCHAR(500) NULL, weight BIGINT NOT NULL, stock BIGINT NOT NULL DEFAULT -1 COMMENT '-1 unlimited, 0 depleted, positive remaining', resource_group_id BIGINT NOT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, sort_order TINYINT NOT NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gk_prize_position (activity_id, sort_order), KEY idx_aslan_gk_prize_enabled (activity_id, enabled) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Seven configured prizes for Aslan Game King'; CREATE TABLE IF NOT EXISTS aslan_game_king_rank_reward ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, start_rank SMALLINT NOT NULL, end_rank SMALLINT NOT NULL, resource_group_id BIGINT NOT NULL, reward_name VARCHAR(128) NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gk_rank_range (activity_id, start_rank, end_rank), KEY idx_aslan_gk_rank_start (activity_id, start_rank) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Six exact end-settlement rank reward ranges'; CREATE TABLE IF NOT EXISTS aslan_game_king_wallet_ledger ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, asset_record_id BIGINT NOT NULL, user_id BIGINT NOT NULL, sys_origin VARCHAR(32) NOT NULL, event_type VARCHAR(64) NOT NULL, event_id VARCHAR(128) NULL, receipt_type TINYINT NOT NULL COMMENT '0 income/winning, 1 expenditure/consumption', amount DECIMAL(24,2) NOT NULL, event_time DATETIME(3) NOT NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gk_activity_asset (activity_id, asset_record_id), KEY idx_aslan_gk_ledger_user (activity_id, user_id, event_time) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Idempotent eligible game expenditure and winning ledger'; CREATE TABLE IF NOT EXISTS aslan_game_king_user ( activity_id BIGINT NOT NULL, user_id BIGINT NOT NULL, total_consumed DECIMAL(24,2) NOT NULL DEFAULT 0, total_won DECIMAL(24,2) NOT NULL DEFAULT 0, used_chances BIGINT NOT NULL DEFAULT 0, consume_reached_time DATETIME(3) NULL, win_reached_time DATETIME(3) NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (activity_id, user_id), KEY idx_aslan_gk_user_tycoon ( activity_id, total_consumed DESC, consume_reached_time ASC, user_id ASC ), KEY idx_aslan_gk_user_victorious ( activity_id, total_won DESC, win_reached_time ASC, user_id ASC ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Overall Tycoon/Victorious aggregates and used draw chances'; CREATE TABLE IF NOT EXISTS aslan_game_king_user_daily ( activity_id BIGINT NOT NULL, stat_date DATE NOT NULL, user_id BIGINT NOT NULL, total_consumed DECIMAL(24,2) NOT NULL DEFAULT 0, total_won DECIMAL(24,2) NOT NULL DEFAULT 0, consume_reached_time DATETIME(3) NULL, win_reached_time DATETIME(3) NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (activity_id, stat_date, user_id), KEY idx_aslan_gk_daily_tycoon ( activity_id, stat_date, total_consumed DESC, consume_reached_time ASC, user_id ASC ), KEY idx_aslan_gk_daily_victorious ( activity_id, stat_date, total_won DESC, win_reached_time ASC, user_id ASC ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Timezone-materialized daily Tycoon/Victorious aggregates'; CREATE TABLE IF NOT EXISTS aslan_game_king_draw_record ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, user_id BIGINT NOT NULL, request_id VARCHAR(64) NOT NULL, prize_id BIGINT NOT NULL, prize_name VARCHAR(128) NOT NULL, prize_image VARCHAR(500) NULL, resource_group_id BIGINT NULL, delivery_status VARCHAR(32) NOT NULL, failure_reason VARCHAR(500) NULL, draw_time DATETIME(3) NOT NULL, deliver_time DATETIME(3) NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gk_draw_request (activity_id, user_id, request_id), KEY idx_aslan_gk_draw_user (activity_id, user_id, id), KEY idx_aslan_gk_draw_manage (activity_id, delivery_status, id), KEY idx_aslan_gk_draw_recover (delivery_status, update_time, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Idempotent Aslan Game King draws and delivery state'; CREATE TABLE IF NOT EXISTS aslan_game_king_settlement_record ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, user_id BIGINT NOT NULL, rank_no SMALLINT NOT NULL, total_consumed DECIMAL(24,2) NOT NULL, rank_reward_id BIGINT NOT NULL, resource_group_id BIGINT NOT NULL, business_no VARCHAR(128) NOT NULL, delivery_status VARCHAR(32) NOT NULL, retry_count INT NOT NULL DEFAULT 0, failure_reason VARCHAR(500) NULL, deliver_time DATETIME(3) NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gk_settlement_user (activity_id, user_id), UNIQUE KEY uq_aslan_gk_settlement_rank (activity_id, rank_no), UNIQUE KEY uq_aslan_gk_settlement_business (business_no), KEY idx_aslan_gk_settlement_status (activity_id, delivery_status) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Frozen top-30 settlement snapshot and retry state'; CREATE TABLE IF NOT EXISTS aslan_game_king_delivery_item ( id BIGINT NOT NULL, owner_type VARCHAR(20) NOT NULL COMMENT 'DRAW or SETTLEMENT', owner_id BIGINT NOT NULL, activity_id BIGINT NOT NULL, user_id BIGINT NOT NULL, reward_config_id BIGINT NOT NULL, resource_group_id BIGINT NOT NULL, reward_type VARCHAR(64) NOT NULL, detail_type VARCHAR(128) NULL, content VARCHAR(2000) NULL, quantity INT NULL, sort_order INT NULL, remark VARCHAR(1000) NULL, delivery_status VARCHAR(32) NOT NULL, retry_count INT NOT NULL DEFAULT 0, failure_reason VARCHAR(500) NULL, deliver_time DATETIME(3) NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), update_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gk_delivery_config (owner_type, owner_id, reward_config_id), KEY idx_aslan_gk_delivery_owner (owner_type, owner_id, delivery_status), KEY idx_aslan_gk_delivery_activity (activity_id, delivery_status, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Per reward-config transactional delivery outbox'; -- The following menu/resource statements are idempotent and inherit only roles that already own -- the Operations directory. They do not grant the campaign page to unrelated roles. INSERT INTO sys_resource (id, resource_name, mapping, method, auth_type, perm, update_time) VALUES ('activity:game-king:list', '游戏王活动列表', '/activity/game-king/list', 'GET', 3, 'activity:game-king:list', NOW()), ('activity:game-king:detail', '游戏王活动详情', '/activity/game-king/detail/**', 'GET', 3, 'activity:game-king:detail', NOW()), ('activity:game-king:save', '保存游戏王活动', '/activity/game-king/save', 'POST', 3, 'activity:game-king:save', NOW()), ('activity:game-king:enable', '启停游戏王活动', '/activity/game-king/enable/**', 'PUT', 3, 'activity:game-king:enable', NOW()), ('activity:game-king:prizes:get', '游戏王奖品列表', '/activity/game-king/prizes/**', 'GET', 3, 'activity:game-king:prizes:get', NOW()), ('activity:game-king:prizes:save', '保存游戏王奖品', '/activity/game-king/prizes/**', 'PUT', 3, 'activity:game-king:prizes:save', NOW()), ('activity:game-king:rank:get', '游戏王排名奖励', '/activity/game-king/rank-rewards/**', 'GET', 3, 'activity:game-king:rank:get', NOW()), ('activity:game-king:rank:save', '保存游戏王排名奖励', '/activity/game-king/rank-rewards/**', 'PUT', 3, 'activity:game-king:rank:save', NOW()), ('activity:game-king:draw:get', '游戏王抽奖记录', '/activity/game-king/draw-records/**', 'GET', 3, 'activity:game-king:draw:get', NOW()), ('activity:game-king:draw:retry', '重试游戏王抽奖奖励', '/activity/game-king/draw/retry/**', 'POST', 3, 'activity:game-king:draw:retry', NOW()), ('activity:game-king:delivery:resolve', '核账处理游戏王未知奖励', '/activity/game-king/delivery-item/resolve/**', 'POST', 3, 'activity:game-king:delivery:resolve', NOW()), ('activity:game-king:settlement:get', '游戏王结算记录', '/activity/game-king/settlement-records/**', 'GET', 3, 'activity:game-king:settlement:get', NOW()), ('activity:game-king:settlement:run', '执行游戏王结算', '/activity/game-king/settlement/**', 'POST', 3, 'activity:game-king:settlement:run', NOW()), ('activity:game-king:backfill', '补算游戏王消费', '/activity/game-king/backfill/**', 'POST', 3, 'activity:game-king:backfill', NOW()) ON DUPLICATE KEY UPDATE resource_name = VALUES(resource_name), mapping = VALUES(mapping), method = VALUES(method), auth_type = VALUES(auth_type), perm = VALUES(perm), update_time = NOW(); INSERT INTO sys_menu ( parent_id, menu_name, path, router, menu_type, icon, alias, sort, status, create_user, update_user, create_time, update_time ) SELECT parent.id, '游戏王活动', 'activity/game-king/index', 'activity/game-king', 2, 'trophy', 'ActivityGameKing', 88, 0, '0', '0', NOW(), NOW() FROM sys_menu parent LEFT JOIN sys_menu existing ON existing.alias = 'ActivityGameKing' WHERE (parent.alias = 'OperationManager' OR parent.menu_name = '运营管理') AND parent.menu_type = 1 AND existing.id IS NULL LIMIT 1; UPDATE sys_menu campaign_menu JOIN ( SELECT id FROM sys_menu WHERE (alias = 'OperationManager' OR menu_name = '运营管理') AND menu_type = 1 LIMIT 1 ) parent ON 1 = 1 SET campaign_menu.parent_id = parent.id, campaign_menu.menu_name = '游戏王活动', campaign_menu.path = 'activity/game-king/index', campaign_menu.router = 'activity/game-king', campaign_menu.menu_type = 2, campaign_menu.icon = 'trophy', campaign_menu.sort = 88, campaign_menu.status = 0, campaign_menu.update_user = '0', campaign_menu.update_time = NOW() WHERE campaign_menu.alias = 'ActivityGameKing'; INSERT INTO sys_menu_resource (menu_id, resource_id) SELECT menu.id, resource.id FROM sys_menu menu JOIN sys_resource resource ON resource.id LIKE 'activity:game-king:%' LEFT JOIN sys_menu_resource existing ON existing.menu_id = menu.id AND existing.resource_id = resource.id WHERE menu.alias = 'ActivityGameKing' AND existing.id IS NULL; INSERT INTO sys_role_menu (role_id, menu_id) SELECT DISTINCT source_role.role_id, campaign_menu.id FROM sys_role_menu source_role JOIN sys_menu source_menu ON source_menu.id = source_role.menu_id JOIN sys_menu campaign_menu ON campaign_menu.alias = 'ActivityGameKing' LEFT JOIN sys_role_menu existing ON existing.role_id = source_role.role_id AND existing.menu_id = campaign_menu.id WHERE (source_menu.alias = 'OperationManager' OR source_menu.menu_name = '运营管理') AND source_menu.menu_type = 1 AND existing.id IS NULL;