-- Aslan Gift Challenge: non-lucky paid-gift ranking, daily tasks and reward settlement. -- -- Performance review (DDL only; run through the normal deployment migration process): -- 1. The gift hot path inserts one row through uq_aslan_gc_ledger_source_event, then updates one -- overall PK row, one daily PK row and at most three daily-task rows. Every lookup is bounded -- by activity/user/date keys and is O(log N); ranking never GROUP BYs the append-only ledger. -- 2. Overall and daily Top-N queries bind activity_id (and stat_date for DAILY) before reading -- score order from covering indexes. The same score/reached-time/user-id order supports a -- deterministic rank query without sorting the entire activity in memory. -- 3. Campaign lookup uses idx_aslan_gc_activity_window. start_time is the range column and -- end_time is an index-covered residual filter; enabled campaigns per sys_origin are expected -- to remain few and overlapping enabled windows are rejected by the application transaction. -- 4. Period headers make an empty DAILY ranking settle exactly once and gate late writes after a -- snapshot starts. Due headers and task-delivery recovery use status/time indexes; reward -- reconciliation reads only one frozen owner and its configured group through unique keys. -- 5. No UPDATE or DELETE below uses an unindexed predicate. This migration creates empty campaign -- tables and idempotent RBAC rows only; it does not scan or backfill gift_give_running_water. -- 6. Before production rollout, run EXPLAIN for display-activity, Top-N, my-rank, due-settlement -- and recovery queries against production-like cardinality. Any gift-history backfill must use -- a checked Mongo index and an id cursor; OFFSET/full collection scans are explicitly excluded. -- 7. Enabling a campaign inserts at most 366 DAILY headers in one batched primary/unique-key write. -- claim_request_id adds one nullable audit index updated only on the first successful task claim. -- 8. Enabling also freezes at most 5000 reward rows in batches of 500. Reads always bind -- activity_id + resource_group_id and follow idx_aslan_gc_reward_snapshot_group; pre-start -- rebuild/delete binds activity_id and uses the same leftmost index prefix, so no table scan is -- introduced when a shared reward group is edited later. SET NAMES utf8mb4; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_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, daily_settlement_delay_minutes INT NOT NULL DEFAULT 30, overall_settlement_delay_minutes INT NOT NULL DEFAULT 30, overall_settlement_time DATETIME(3) NOT NULL, display_top_n SMALLINT NOT NULL DEFAULT 100, enabled TINYINT(1) NOT NULL DEFAULT 0, overall_settlement_status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED', version INT NOT NULL DEFAULT 0, 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_gc_activity_code (activity_code), KEY idx_aslan_gc_activity_window (sys_origin, enabled, start_time, end_time), KEY idx_aslan_gc_activity_settle ( overall_settlement_status, overall_settlement_time, end_time, id ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Aslan paid non-lucky gift challenge campaign'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_task_config ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, task_code VARCHAR(64) NOT NULL, task_type VARCHAR(32) NOT NULL COMMENT 'ENTER_PAGE or SEND_GIFT_GOLD', task_title VARCHAR(128) NOT NULL, task_desc VARCHAR(500) NULL, target_value DECIMAL(24,2) NOT NULL, resource_group_id BIGINT NULL, enabled TINYINT(1) NOT NULL DEFAULT 1, sort_order SMALLINT 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_gc_task_code (activity_id, task_code), UNIQUE KEY uq_aslan_gc_task_sort (activity_id, sort_order), KEY idx_aslan_gc_task_enabled (activity_id, enabled, sort_order) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Exactly three activity-scoped daily task definitions'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_rank_reward ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, period_type VARCHAR(16) NOT NULL COMMENT 'DAILY or OVERALL', 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_gc_rank_range (activity_id, period_type, start_rank, end_rank), KEY idx_aslan_gc_rank_start (activity_id, period_type, start_rank, end_rank) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Configurable DAILY and OVERALL rank reward ranges'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_reward_snapshot ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, resource_group_id BIGINT NOT NULL, reward_config_id BIGINT NOT NULL, reward_type VARCHAR(32) NOT NULL, detail_type VARCHAR(64) NULL, content VARCHAR(500) NULL, quantity INT NULL, sort_order INT NULL, remark VARCHAR(500) NULL, cover TEXT NULL, source_url TEXT NULL, display_name VARCHAR(500) NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gc_reward_snapshot ( activity_id, resource_group_id, reward_config_id ), KEY idx_aslan_gc_reward_snapshot_group ( activity_id, resource_group_id, sort_order, reward_config_id ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Immutable per-activity reward item templates captured before start'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_gift_ledger ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, source_event_track_id BIGINT NOT NULL, user_id BIGINT NOT NULL, gift_id BIGINT NOT NULL, gift_tab VARCHAR(32) NULL, amount DECIMAL(24,2) NOT NULL, event_time DATETIME(3) NOT NULL, stat_date DATE NOT NULL, create_time DATETIME(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3), PRIMARY KEY (id), UNIQUE KEY uq_aslan_gc_ledger_source_event (activity_id, source_event_track_id), KEY idx_aslan_gc_ledger_user (activity_id, user_id, event_time), KEY idx_aslan_gc_ledger_date (activity_id, stat_date, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Idempotent eligible paid-gift fact ledger'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_user_score ( activity_id BIGINT NOT NULL, user_id BIGINT NOT NULL, score DECIMAL(24,2) NOT NULL DEFAULT 0, score_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_gc_user_rank ( activity_id, score DESC, score_reached_time ASC, user_id ASC ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Activity-wide user score aggregate'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_user_daily_score ( activity_id BIGINT NOT NULL, stat_date DATE NOT NULL, user_id BIGINT NOT NULL, score DECIMAL(24,2) NOT NULL DEFAULT 0, score_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_gc_daily_rank ( activity_id, stat_date, score DESC, score_reached_time ASC, user_id ASC ) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Activity-timezone daily user score aggregate'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_user_task_daily ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, stat_date DATE NOT NULL, user_id BIGINT NOT NULL, task_config_id BIGINT NOT NULL, task_code VARCHAR(64) NOT NULL, task_type VARCHAR(32) NOT NULL, task_title VARCHAR(128) NOT NULL, sort_order SMALLINT NOT NULL, target_value DECIMAL(24,2) NOT NULL, progress_value DECIMAL(24,2) NOT NULL DEFAULT 0, completed_time DATETIME(3) NULL, resource_group_id BIGINT NULL, business_no VARCHAR(160) NOT NULL, claim_request_id VARCHAR(128) NULL COMMENT 'First successful H5 claim request audit id', delivery_status VARCHAR(32) NOT NULL DEFAULT 'NOT_CLAIMED' COMMENT 'NOT_CLAIMED, PENDING, PROCESSING, SUCCESS or UNKNOWN', retry_count INT NOT NULL DEFAULT 0, failure_reason VARCHAR(500) NULL, claimed_time DATETIME(3) 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_gc_task_daily (activity_id, stat_date, user_id, task_code), UNIQUE KEY uq_aslan_gc_task_business (business_no), KEY idx_aslan_gc_task_user (activity_id, user_id, stat_date, sort_order), KEY idx_aslan_gc_task_claim_request (claim_request_id), KEY idx_aslan_gc_task_recover (delivery_status, update_time, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Daily task target/reward snapshot, progress and claim state'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_period_settlement ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, period_type VARCHAR(16) NOT NULL COMMENT 'DAILY or OVERALL', period_key VARCHAR(16) NOT NULL COMMENT 'yyyy-MM-dd for DAILY, OVERALL otherwise', stat_date DATE NULL, snapshot_due_time DATETIME(3) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'NOT_STARTED' COMMENT 'NOT_STARTED, PROCESSING, COMPLETED or RECONCILIATION_REQUIRED', 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_gc_period (activity_id, period_type, period_key), KEY idx_aslan_gc_period_due (status, snapshot_due_time, id), KEY idx_aslan_gc_period_activity (activity_id, period_type, status, period_key) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Per-period snapshot gate including empty DAILY rankings'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_settlement ( id BIGINT NOT NULL, activity_id BIGINT NOT NULL, period_type VARCHAR(16) NOT NULL COMMENT 'DAILY or OVERALL', period_key VARCHAR(16) NOT NULL COMMENT 'yyyy-MM-dd for DAILY, OVERALL otherwise', stat_date DATE NULL, user_id BIGINT NOT NULL, rank_no SMALLINT NOT NULL, score DECIMAL(24,2) NOT NULL, rank_reward_id BIGINT NOT NULL, resource_group_id BIGINT NOT NULL, business_no VARCHAR(160) NOT NULL, delivery_status VARCHAR(32) NOT NULL COMMENT 'PENDING, PROCESSING, SUCCESS or UNKNOWN', 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_gc_settlement_user ( activity_id, period_type, period_key, user_id ), UNIQUE KEY uq_aslan_gc_settlement_rank ( activity_id, period_type, period_key, rank_no ), UNIQUE KEY uq_aslan_gc_settlement_business (business_no), KEY idx_aslan_gc_settlement_status ( activity_id, period_type, period_key, delivery_status, rank_no ), KEY idx_aslan_gc_settlement_recover (delivery_status, update_time, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Frozen DAILY/OVERALL rank snapshots and delivery state'; CREATE TABLE IF NOT EXISTS aslan_gift_challenge_delivery_item ( id BIGINT NOT NULL, owner_type VARCHAR(20) NOT NULL COMMENT 'TASK 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(32) NOT NULL, detail_type VARCHAR(64) NULL, content VARCHAR(500) NULL, quantity INT NULL, sort_order INT NULL, remark VARCHAR(500) NULL, delivery_status VARCHAR(32) NOT NULL COMMENT 'PENDING, PROCESSING, SUCCESS or UNKNOWN', 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_gc_delivery_config (owner_type, owner_id, reward_config_id), KEY idx_aslan_gc_delivery_owner (owner_type, owner_id, delivery_status), KEY idx_aslan_gc_delivery_activity (activity_id, delivery_status, id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='Per reward-config transactional delivery outbox'; -- RBAC is idempotent. The page is inherited only by roles that already own Operations. INSERT INTO sys_resource (id, resource_name, mapping, method, auth_type, perm, update_time) VALUES ('activity:gift-challenge:list', '礼物挑战活动列表', '/activity/gift-challenge/list', 'GET', 3, 'activity:gift-challenge:list', NOW()), ('activity:gift-challenge:detail', '礼物挑战活动详情', '/activity/gift-challenge/detail/**', 'GET', 3, 'activity:gift-challenge:detail', NOW()), ('activity:gift-challenge:save', '保存礼物挑战活动', '/activity/gift-challenge/save', 'POST', 3, 'activity:gift-challenge:save', NOW()), ('activity:gift-challenge:enable', '启停礼物挑战活动', '/activity/gift-challenge/enable/**', 'PUT', 3, 'activity:gift-challenge:enable', NOW()), ('activity:gift-challenge:tasks:get', '礼物挑战任务列表', '/activity/gift-challenge/tasks/**', 'GET', 3, 'activity:gift-challenge:tasks:get', NOW()), ('activity:gift-challenge:tasks:save', '保存礼物挑战任务', '/activity/gift-challenge/tasks/**', 'PUT', 3, 'activity:gift-challenge:tasks:save', NOW()), ('activity:gift-challenge:rank:get', '礼物挑战排名奖励', '/activity/gift-challenge/rank-rewards/**', 'GET', 3, 'activity:gift-challenge:rank:get', NOW()), ('activity:gift-challenge:rank:save', '保存礼物挑战排名奖励', '/activity/gift-challenge/rank-rewards/**', 'PUT', 3, 'activity:gift-challenge:rank:save', NOW()), ('activity:gift-challenge:ranking', '查看礼物挑战榜单', '/activity/gift-challenge/ranking/**', 'GET', 3, 'activity:gift-challenge:ranking', NOW()), ('activity:gift-challenge:settlement:get', '礼物挑战结算记录', '/activity/gift-challenge/settlement-records/**', 'GET', 3, 'activity:gift-challenge:settlement:get', NOW()), ('activity:gift-challenge:settlement:run', '执行礼物挑战结算', '/activity/gift-challenge/settlement/**', 'POST', 3, 'activity:gift-challenge:settlement:run', NOW()), ('activity:gift-challenge:delivery:resolve', '核账处理礼物挑战未知奖励', '/activity/gift-challenge/delivery-item/resolve/**', 'POST', 3, 'activity:gift-challenge:delivery:resolve', 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/gift-challenge/index', 'activity/gift-challenge', 2, 'gift', 'ActivityGiftChallenge', 89, 0, '0', '0', NOW(), NOW() FROM sys_menu parent LEFT JOIN sys_menu existing ON existing.alias = 'ActivityGiftChallenge' 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/gift-challenge/index', campaign_menu.router = 'activity/gift-challenge', campaign_menu.menu_type = 2, campaign_menu.icon = 'gift', campaign_menu.sort = 89, campaign_menu.status = 0, campaign_menu.update_user = '0', campaign_menu.update_time = NOW() WHERE campaign_menu.alias = 'ActivityGiftChallenge'; 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:gift-challenge:%' LEFT JOIN sys_menu_resource existing ON existing.menu_id = menu.id AND existing.resource_id = resource.id WHERE menu.alias = 'ActivityGiftChallenge' 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 = 'ActivityGiftChallenge' 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; -- Buttons are real menu_type=3 rows because controller-side hasButtonsAlias checks sys_menu.alias, -- not sys_resource.perm. Existing page owners inherit all four buttons idempotently. 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 page.id, button.menu_name, '', '', 3, '', button.alias, button.sort_no, 0, '0', '0', NOW(), NOW() FROM sys_menu page JOIN ( SELECT '编辑礼物挑战配置' AS menu_name, 'activity:gift-challenge:edit' AS alias, 1 AS sort_no UNION ALL SELECT '启停礼物挑战活动', 'activity:gift-challenge:enable', 2 UNION ALL SELECT '执行礼物挑战结算', 'activity:gift-challenge:settle', 3 UNION ALL SELECT '核账礼物挑战未知奖励', 'activity:gift-challenge:reconcile', 4 ) button ON 1 = 1 LEFT JOIN sys_menu existing ON existing.alias = button.alias WHERE page.alias = 'ActivityGiftChallenge' AND existing.id IS NULL; UPDATE sys_menu button JOIN sys_menu page ON page.alias = 'ActivityGiftChallenge' SET button.parent_id = page.id, button.menu_type = 3, button.status = 0, button.update_user = '0', button.update_time = NOW() WHERE button.alias IN ( 'activity:gift-challenge:edit', 'activity:gift-challenge:enable', 'activity:gift-challenge:settle', 'activity:gift-challenge:reconcile' ); INSERT INTO sys_role_menu (role_id, menu_id) SELECT DISTINCT page_role.role_id, button.id FROM sys_role_menu page_role JOIN sys_menu page ON page.id = page_role.menu_id AND page.alias = 'ActivityGiftChallenge' JOIN sys_menu button ON button.parent_id = page.id AND button.menu_type = 3 LEFT JOIN sys_role_menu existing ON existing.role_id = page_role.role_id AND existing.menu_id = button.id WHERE button.alias IN ( 'activity:gift-challenge:edit', 'activity:gift-challenge:enable', 'activity:gift-challenge:settle', 'activity:gift-challenge:reconcile' ) AND existing.id IS NULL;