aslan-server/.deploy/prod-deploy/sql/20260717_aslan_game_king.sql

315 lines
15 KiB
SQL

-- Aslan Game King: fixed third-party game expenditure-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.
-- 8. Historical wallet backfill is not executed by this migration. Application queries bind
-- sys_origin + EXPENDITURE and four escaped left-anchored game prefixes, so the existing
-- wallet index (sys_origin,event_type,create_time) remains usable instead of scanning all spend.
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 DEFAULT 'GAME_EXPENDITURE'
COMMENT 'compatibility sentinel; event scope is fixed in service code',
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 'fixed 1: third-party game expenditure',
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 third-party game expenditure 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:gk:list', '游戏王活动列表', '/activity/game-king/list', 'GET', 3, 'activity:game-king:list', NOW()),
('activity:gk:detail', '游戏王活动详情', '/activity/game-king/detail/**', 'GET', 3, 'activity:game-king:detail', NOW()),
('activity:gk:save', '保存游戏王活动', '/activity/game-king/save', 'POST', 3, 'activity:game-king:save', NOW()),
('activity:gk:enable', '启停游戏王活动', '/activity/game-king/enable/**', 'PUT', 3, 'activity:game-king:enable', NOW()),
('activity:gk:prizes:get', '游戏王奖品列表', '/activity/game-king/prizes/**', 'GET', 3, 'activity:game-king:prizes:get', NOW()),
('activity:gk:prizes:save', '保存游戏王奖品', '/activity/game-king/prizes/**', 'PUT', 3, 'activity:game-king:prizes:save', NOW()),
('activity:gk:rank:get', '游戏王排名奖励', '/activity/game-king/rank-rewards/**', 'GET', 3, 'activity:game-king:rank:get', NOW()),
('activity:gk:rank:save', '保存游戏王排名奖励', '/activity/game-king/rank-rewards/**', 'PUT', 3, 'activity:game-king:rank:save', NOW()),
('activity:gk:draw:get', '游戏王抽奖记录', '/activity/game-king/draw-records/**', 'GET', 3, 'activity:game-king:draw:get', NOW()),
('activity:gk:draw:retry', '重试游戏王抽奖奖励', '/activity/game-king/draw/retry/**', 'POST', 3, 'activity:game-king:draw:retry', NOW()),
('activity:gk:delivery:resolve', '核账处理游戏王未知奖励', '/activity/game-king/delivery-item/resolve/**', 'POST', 3, 'activity:game-king:delivery:resolve', NOW()),
('activity:gk:settlement:get', '游戏王结算记录', '/activity/game-king/settlement-records/**', 'GET', 3, 'activity:game-king:settlement:get', NOW()),
('activity:gk:settlement:run', '执行游戏王结算', '/activity/game-king/settlement/**', 'POST', 3, 'activity:game-king:settlement:run', NOW()),
('activity:gk: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:gk:%'
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;