feat: add Aslan game king and gift challenge activities
This commit is contained in:
parent
c064ae8568
commit
5c06ad41fa
310
.deploy/prod-deploy/sql/20260717_aslan_game_king.sql
Normal file
310
.deploy/prod-deploy/sql/20260717_aslan_game_king.sql
Normal file
@ -0,0 +1,310 @@
|
||||
-- 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;
|
||||
427
.deploy/prod-deploy/sql/20260718_aslan_gift_challenge.sql
Normal file
427
.deploy/prod-deploy/sql/20260718_aslan_gift_challenge.sql
Normal file
@ -0,0 +1,427 @@
|
||||
-- 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;
|
||||
310
.deploy/test-deploy/sql/20260717_aslan_game_king.sql
Normal file
310
.deploy/test-deploy/sql/20260717_aslan_game_king.sql
Normal file
@ -0,0 +1,310 @@
|
||||
-- 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;
|
||||
427
.deploy/test-deploy/sql/20260718_aslan_gift_challenge.sql
Normal file
427
.deploy/test-deploy/sql/20260718_aslan_gift_challenge.sql
Normal file
@ -0,0 +1,427 @@
|
||||
-- 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;
|
||||
@ -0,0 +1,10 @@
|
||||
package com.red.circle.other.inner.endpoint.activity;
|
||||
|
||||
import com.red.circle.other.inner.endpoint.activity.api.AslanGameKingManageClientApi;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
|
||||
/** Aslan 游戏王管理 Feign Client。 */
|
||||
@FeignClient(name = "aslanGameKingManageClient", url = "${feign.other.url}"
|
||||
+ AslanGameKingManageClientApi.API_PREFIX)
|
||||
public interface AslanGameKingManageClient extends AslanGameKingManageClientApi {
|
||||
}
|
||||
@ -0,0 +1,10 @@
|
||||
package com.red.circle.other.inner.endpoint.activity;
|
||||
|
||||
import com.red.circle.other.inner.endpoint.activity.api.AslanGiftChallengeManageClientApi;
|
||||
import org.springframework.cloud.openfeign.FeignClient;
|
||||
|
||||
/** Aslan 礼物挑战管理 Feign Client。 */
|
||||
@FeignClient(name = "aslanGiftChallengeManageClient", url = "${feign.other.url}"
|
||||
+ AslanGiftChallengeManageClientApi.API_PREFIX)
|
||||
public interface AslanGiftChallengeManageClient extends AslanGiftChallengeManageClientApi {
|
||||
}
|
||||
@ -0,0 +1,82 @@
|
||||
package com.red.circle.other.inner.endpoint.activity.api;
|
||||
|
||||
import com.red.circle.framework.dto.ResultResponse;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.BackfillResult;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Prize;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.PrizeSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SettlementRecord;
|
||||
import java.util.List;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/** Aslan 游戏王 webconsole 跨服务管理 API。 */
|
||||
public interface AslanGameKingManageClientApi {
|
||||
|
||||
String API_PREFIX = "/aslan-game-king-manage/client";
|
||||
|
||||
@GetMapping("/list")
|
||||
ResultResponse<List<Activity>> list();
|
||||
|
||||
@GetMapping("/detail/{id}")
|
||||
ResultResponse<Detail> detail(@PathVariable("id") Long id);
|
||||
|
||||
@PostMapping("/save")
|
||||
ResultResponse<Activity> save(@RequestBody SaveCommand cmd);
|
||||
|
||||
@PutMapping("/enable/{id}")
|
||||
ResultResponse<Void> enable(@PathVariable("id") Long id,
|
||||
@RequestParam("enabled") boolean enabled);
|
||||
|
||||
@GetMapping("/prizes/{id}")
|
||||
ResultResponse<List<Prize>> prizes(@PathVariable("id") Long id);
|
||||
|
||||
@PutMapping("/prizes/{id}")
|
||||
ResultResponse<Void> savePrizes(@PathVariable("id") Long id,
|
||||
@RequestBody PrizeSaveCommand cmd);
|
||||
|
||||
@GetMapping("/rank-rewards/{id}")
|
||||
ResultResponse<List<RankReward>> rankRewards(@PathVariable("id") Long id);
|
||||
|
||||
@PutMapping("/rank-rewards/{id}")
|
||||
ResultResponse<Void> saveRankRewards(@PathVariable("id") Long id,
|
||||
@RequestBody RankRewardSaveCommand cmd);
|
||||
|
||||
@GetMapping("/settlement-records/{id}")
|
||||
ResultResponse<List<SettlementRecord>> settlementRecords(@PathVariable("id") Long id);
|
||||
|
||||
@GetMapping("/draw-records/{id}")
|
||||
ResultResponse<List<DrawRecord>> drawRecords(@PathVariable("id") Long id,
|
||||
@RequestParam(value = "deliveryStatus", required = false) String deliveryStatus,
|
||||
@RequestParam(value = "limit", required = false) Integer limit);
|
||||
|
||||
@PostMapping("/draw/retry/{recordId}")
|
||||
ResultResponse<Void> retryDraw(@PathVariable("recordId") Long recordId);
|
||||
|
||||
@PostMapping("/settlement/{id}")
|
||||
ResultResponse<Void> settle(@PathVariable("id") Long id);
|
||||
|
||||
@PostMapping("/settlement/retry/{recordId}")
|
||||
ResultResponse<Void> retrySettlement(@PathVariable("recordId") Long recordId);
|
||||
|
||||
/** UNKNOWN 必须先核对下游账目;delivered=false 才会解锁并补发该 item。 */
|
||||
@PostMapping("/delivery-item/resolve/{itemId}")
|
||||
ResultResponse<Void> resolveUnknownDeliveryItem(@PathVariable("itemId") Long itemId,
|
||||
@RequestParam("delivered") boolean delivered);
|
||||
|
||||
@PostMapping("/backfill/{id}")
|
||||
ResultResponse<BackfillResult> backfill(@PathVariable("id") Long id,
|
||||
@RequestParam("eventType") String eventType,
|
||||
@RequestParam("receiptType") Integer receiptType,
|
||||
@RequestParam(value = "lastId", required = false) Long lastId,
|
||||
@RequestParam(value = "limit", required = false) Integer limit);
|
||||
}
|
||||
@ -0,0 +1,73 @@
|
||||
package com.red.circle.other.inner.endpoint.activity.api;
|
||||
|
||||
import com.red.circle.framework.dto.ResultResponse;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Ranking;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankingQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Task;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.TaskSaveCommand;
|
||||
import java.util.List;
|
||||
import org.springframework.cloud.openfeign.SpringQueryMap;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
|
||||
/** Aslan 礼物挑战 webconsole 跨服务管理 API。 */
|
||||
public interface AslanGiftChallengeManageClientApi {
|
||||
|
||||
String API_PREFIX = "/aslan-gift-challenge-manage/client";
|
||||
|
||||
@GetMapping("/list")
|
||||
ResultResponse<List<Activity>> list();
|
||||
|
||||
@GetMapping("/detail/{id}")
|
||||
ResultResponse<Detail> detail(@PathVariable("id") Long id);
|
||||
|
||||
@PostMapping("/save")
|
||||
ResultResponse<Activity> save(@RequestBody SaveCommand cmd);
|
||||
|
||||
@PutMapping("/enable/{id}")
|
||||
ResultResponse<Void> enable(@PathVariable("id") Long id,
|
||||
@RequestParam("enabled") boolean enabled);
|
||||
|
||||
@GetMapping("/tasks/{id}")
|
||||
ResultResponse<List<Task>> tasks(@PathVariable("id") Long id);
|
||||
|
||||
@PutMapping("/tasks/{id}")
|
||||
ResultResponse<Void> saveTasks(@PathVariable("id") Long id,
|
||||
@RequestBody TaskSaveCommand cmd);
|
||||
|
||||
@GetMapping("/rank-rewards/{id}")
|
||||
ResultResponse<List<RankReward>> rankRewards(@PathVariable("id") Long id);
|
||||
|
||||
@PutMapping("/rank-rewards/{id}")
|
||||
ResultResponse<Void> saveRankRewards(@PathVariable("id") Long id,
|
||||
@RequestBody RankRewardSaveCommand cmd);
|
||||
|
||||
@GetMapping("/ranking/{id}")
|
||||
ResultResponse<Ranking> ranking(@PathVariable("id") Long id,
|
||||
@SpringQueryMap RankingQuery query);
|
||||
|
||||
@GetMapping("/settlement-records/{id}")
|
||||
ResultResponse<List<SettlementRecord>> settlementRecords(@PathVariable("id") Long id,
|
||||
@SpringQueryMap SettlementQuery query);
|
||||
|
||||
@PostMapping("/settlement/{id}")
|
||||
ResultResponse<Void> settle(@PathVariable("id") Long id,
|
||||
@RequestParam("period") String period,
|
||||
@RequestParam(value = "statDate", required = false) String statDate);
|
||||
|
||||
/** UNKNOWN 必须由运营核账;delivered=false 才会把单项重新开放为待发。 */
|
||||
@PostMapping("/delivery-item/resolve/{itemId}")
|
||||
ResultResponse<Void> resolveUnknownDeliveryItem(@PathVariable("itemId") Long itemId,
|
||||
@RequestParam("delivered") boolean delivered);
|
||||
}
|
||||
@ -0,0 +1,385 @@
|
||||
package com.red.circle.other.inner.model.activity.aslan;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Positive;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Aslan 游戏王活动的跨服务契约。
|
||||
*
|
||||
* <p>活动使用独立模型,避免旧版赛事制 GameKing 与通用 Lottery 的状态、票券和周榜语义
|
||||
* 渗入“游戏消费换抽奖次数”这条业务链。所有 Long ID 都按字符串序列化,防止 H5
|
||||
* JavaScript 丢失 Snowflake ID 精度。</p>
|
||||
*/
|
||||
public final class AslanGameKingModels {
|
||||
|
||||
private AslanGameKingModels() {
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Activity implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
private String activityCode;
|
||||
private String activityName;
|
||||
private String activityDesc;
|
||||
private String sysOrigin;
|
||||
private String timeZone;
|
||||
private Long startTime;
|
||||
private Long endTime;
|
||||
private Integer settlementDelayMinutes;
|
||||
private Long settlementTime;
|
||||
private Long coinPerDraw;
|
||||
private List<String> eventTypes = new ArrayList<>();
|
||||
private String rankingType;
|
||||
private String rankingPeriod;
|
||||
private Boolean enabled;
|
||||
private String settlementStatus;
|
||||
private Long createTime;
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Prize implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
private String prizeName;
|
||||
private String prizeImage;
|
||||
private Long weight;
|
||||
/** -1 表示不限库存,0 表示已耗尽,正数表示剩余可抽数量。 */
|
||||
private Long stock = -1L;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private Boolean enabled;
|
||||
private Integer sortOrder;
|
||||
private List<RewardItem> rewardItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankReward implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
private Integer startRank;
|
||||
private Integer endRank;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private String rewardName;
|
||||
private List<RewardItem> rewardItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
/** 排名奖励组中允许 H5 展示的最小资源视图,不暴露后台发放实现字段。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RewardItem implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
private String type;
|
||||
private String detailType;
|
||||
private String content;
|
||||
private Integer quantity;
|
||||
private String cover;
|
||||
private String sourceUrl;
|
||||
private String name;
|
||||
private Integer sort;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Detail implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Activity activity;
|
||||
private List<Prize> prizes = new ArrayList<>();
|
||||
private List<RankReward> rankRewards = new ArrayList<>();
|
||||
/** Figma 排行页真实支持的两个维度,机会与结算只使用 TYCOON/OVERALL。 */
|
||||
private List<String> supportedRankingTypes = List.of("TYCOON", "VICTORIOUS");
|
||||
private List<String> supportedRankingPeriods = List.of("DAILY", "OVERALL");
|
||||
private String activityStatus;
|
||||
private Long serverTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class UserState implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
private BigDecimal totalConsumed = BigDecimal.ZERO;
|
||||
private Long earnedChances = 0L;
|
||||
private Long usedChances = 0L;
|
||||
private Long availableChances = 0L;
|
||||
private BigDecimal nextChanceRemaining = BigDecimal.ZERO;
|
||||
private Integer rank;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class DrawRecord implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
private String requestId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long prizeId;
|
||||
private String prizeName;
|
||||
private String prizeImage;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private String deliveryStatus;
|
||||
private String failureReason;
|
||||
private Long drawTime;
|
||||
private Long deliverTime;
|
||||
/** 仅管理接口填充,便于运营确认逐项发放结果后再决定是否重试。 */
|
||||
private List<DeliveryItem> deliveryItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class DrawResult implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private DrawRecord record;
|
||||
private Prize prize;
|
||||
private Long remainingChances;
|
||||
private Boolean idempotentReplay;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankEntry implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer rank;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
private String account;
|
||||
private String nickname;
|
||||
private String avatar;
|
||||
/** 当前排行榜维度的统一分值,避免 H5 猜测消费榜或胜利榜字段。 */
|
||||
private BigDecimal score;
|
||||
private BigDecimal totalConsumed;
|
||||
private BigDecimal totalWon;
|
||||
private Boolean me;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Ranking implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String rankingType;
|
||||
private String period;
|
||||
/** DAILY 时为活动时区的 yyyy-MM-dd;OVERALL 时为空。 */
|
||||
private String statDate;
|
||||
private List<RankEntry> entries = new ArrayList<>();
|
||||
private RankEntry my;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class SettlementRecord implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
private Integer rank;
|
||||
private BigDecimal totalConsumed;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long rankRewardId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private String businessNo;
|
||||
private String deliveryStatus;
|
||||
private Integer retryCount;
|
||||
private String failureReason;
|
||||
private Long createTime;
|
||||
private Long deliverTime;
|
||||
/** 仅管理接口填充,避免整组奖励部分成功时把已成功项再次盲发。 */
|
||||
private List<DeliveryItem> deliveryItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
/** 奖励组拆分后的单项发放快照与状态,仅供 webconsole 排障。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class DeliveryItem implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long rewardConfigId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private String rewardType;
|
||||
private String detailType;
|
||||
private String content;
|
||||
private Integer quantity;
|
||||
private Integer sortOrder;
|
||||
private String remark;
|
||||
private String deliveryStatus;
|
||||
private Integer retryCount;
|
||||
private String failureReason;
|
||||
private Long deliverTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class SaveCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "activityCode required.")
|
||||
@Size(max = 64, message = "activityCode max length is 64.")
|
||||
private String activityCode;
|
||||
|
||||
@NotBlank(message = "activityName required.")
|
||||
@Size(max = 128, message = "activityName max length is 128.")
|
||||
private String activityName;
|
||||
private String activityDesc;
|
||||
|
||||
@NotBlank(message = "sysOrigin required.")
|
||||
private String sysOrigin;
|
||||
|
||||
/** 活动配置页面未传时沿用 Aslan 的业务时区。 */
|
||||
@NotBlank(message = "timeZone required.")
|
||||
private String timeZone = "Asia/Riyadh";
|
||||
|
||||
@NotNull(message = "startTime required.")
|
||||
private Long startTime;
|
||||
@NotNull(message = "endTime required.")
|
||||
private Long endTime;
|
||||
|
||||
/** 给异步钱包事件留出到达窗口,避免活动结束瞬间冻结遗漏有效流水。 */
|
||||
@Min(value = 0, message = "settlementDelayMinutes must be at least 0.")
|
||||
@Max(value = 1440, message = "settlementDelayMinutes max is 1440.")
|
||||
private Integer settlementDelayMinutes = 30;
|
||||
|
||||
@NotNull(message = "coinPerDraw required.")
|
||||
@Positive(message = "coinPerDraw must be greater than zero.")
|
||||
private Long coinPerDraw;
|
||||
|
||||
@NotNull(message = "eventTypes required.")
|
||||
@Size(min = 1, max = 30, message = "eventTypes size must be between 1 and 30.")
|
||||
private List<@NotBlank String> eventTypes;
|
||||
|
||||
private String rankingType = "TYCOON";
|
||||
private String rankingPeriod = "OVERALL";
|
||||
private Boolean enabled = Boolean.FALSE;
|
||||
|
||||
@Valid
|
||||
private List<Prize> prizes;
|
||||
@Valid
|
||||
private List<RankReward> rankRewards;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class PrizeSaveCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull(message = "activityId required.")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
@NotNull(message = "prizes required.")
|
||||
@Valid
|
||||
private List<Prize> prizes;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankRewardSaveCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull(message = "activityId required.")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
@NotNull(message = "rankRewards required.")
|
||||
@Valid
|
||||
private List<RankReward> rankRewards;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class DrawCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
|
||||
@NotBlank(message = "requestId required.")
|
||||
@Size(max = 64, message = "requestId max length is 64.")
|
||||
private String requestId;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class BackfillResult implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String eventType;
|
||||
/** 0=收入(VICTORIOUS),1=支出(TYCOON 与抽奖次数)。 */
|
||||
private Integer receiptType;
|
||||
private Integer scanned;
|
||||
private Integer accepted;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long nextLastId;
|
||||
private Boolean finished;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,446 @@
|
||||
package com.red.circle.other.inner.model.activity.aslan;
|
||||
|
||||
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
|
||||
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
|
||||
import jakarta.validation.Valid;
|
||||
import jakarta.validation.constraints.DecimalMin;
|
||||
import jakarta.validation.constraints.Max;
|
||||
import jakarta.validation.constraints.Min;
|
||||
import jakarta.validation.constraints.NotBlank;
|
||||
import jakarta.validation.constraints.NotNull;
|
||||
import jakarta.validation.constraints.Pattern;
|
||||
import jakarta.validation.constraints.Size;
|
||||
import java.io.Serial;
|
||||
import java.io.Serializable;
|
||||
import java.math.BigDecimal;
|
||||
import java.util.ArrayList;
|
||||
import java.util.List;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Aslan 礼物挑战活动的 H5 与 webconsole 共享契约。
|
||||
*
|
||||
* <p>活动独立于旧通用 Mongo 排行:用户分值、日榜、任务快照和结算状态都带 activityId,
|
||||
* 防止下一期活动复用类型后串入历史数据。所有 Long ID 按字符串序列化,避免 JavaScript
|
||||
* 对 Snowflake ID 发生精度丢失。</p>
|
||||
*/
|
||||
public final class AslanGiftChallengeModels {
|
||||
|
||||
public static final String PERIOD_DAILY = "DAILY";
|
||||
public static final String PERIOD_OVERALL = "OVERALL";
|
||||
public static final String TASK_ENTER_PAGE = "ENTER_PAGE";
|
||||
public static final String TASK_SEND_GIFT_GOLD = "SEND_GIFT_GOLD";
|
||||
/** taskCode 会直接进入 H5 path variable,只允许无需 URL 转义的稳定路径段字符。 */
|
||||
public static final String TASK_CODE_PATTERN = "^[A-Za-z0-9_-]{1,64}$";
|
||||
|
||||
private AslanGiftChallengeModels() {
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Activity implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
private String activityCode;
|
||||
private String activityName;
|
||||
private String activityDesc;
|
||||
private String sysOrigin;
|
||||
private String timeZone;
|
||||
private Long startTime;
|
||||
private Long endTime;
|
||||
private Integer dailySettlementDelayMinutes;
|
||||
private Integer overallSettlementDelayMinutes;
|
||||
private Long overallSettlementTime;
|
||||
private Integer displayTopN;
|
||||
private Boolean enabled;
|
||||
private String overallSettlementStatus;
|
||||
private Integer version;
|
||||
private Long createTime;
|
||||
private Long updateTime;
|
||||
}
|
||||
|
||||
/** 固定任务定义;三个槽位必须由一个进入页面任务和两个累计金币送礼任务组成。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Task implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
|
||||
@NotBlank(message = "taskCode required.")
|
||||
@Size(max = 64, message = "taskCode max length is 64.")
|
||||
@Pattern(regexp = TASK_CODE_PATTERN,
|
||||
message = "taskCode may contain only letters, digits, underscores, and hyphens.")
|
||||
private String taskCode;
|
||||
|
||||
@NotBlank(message = "taskType required.")
|
||||
private String taskType;
|
||||
|
||||
@NotBlank(message = "taskTitle required.")
|
||||
@Size(max = 128, message = "taskTitle max length is 128.")
|
||||
private String taskTitle;
|
||||
|
||||
@Size(max = 500, message = "taskDesc max length is 500.")
|
||||
private String taskDesc;
|
||||
|
||||
@NotNull(message = "targetValue required.")
|
||||
@DecimalMin(value = "0.01", message = "targetValue must be greater than zero.")
|
||||
private BigDecimal targetValue;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private Boolean enabled = Boolean.TRUE;
|
||||
|
||||
@NotNull(message = "sortOrder required.")
|
||||
@Min(value = 1, message = "sortOrder must be at least 1.")
|
||||
private Integer sortOrder;
|
||||
|
||||
private List<RewardItem> rewardItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankReward implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
|
||||
@NotBlank(message = "periodType required.")
|
||||
private String periodType;
|
||||
|
||||
@NotNull(message = "startRank required.")
|
||||
@Min(value = 1, message = "startRank must be at least 1.")
|
||||
private Integer startRank;
|
||||
|
||||
@NotNull(message = "endRank required.")
|
||||
@Min(value = 1, message = "endRank must be at least 1.")
|
||||
private Integer endRank;
|
||||
|
||||
@NotNull(message = "resourceGroupId required.")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
|
||||
@Size(max = 128, message = "rewardName max length is 128.")
|
||||
private String rewardName;
|
||||
private List<RewardItem> rewardItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
/** H5 只需展示奖励资源的稳定字段,不暴露后台发放实现和库存信息。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RewardItem implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
private String type;
|
||||
private String detailType;
|
||||
private String content;
|
||||
private Integer quantity;
|
||||
private String cover;
|
||||
private String sourceUrl;
|
||||
private String name;
|
||||
private Integer sort;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Detail implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Activity activity;
|
||||
private List<Task> tasks = new ArrayList<>();
|
||||
private List<RankReward> rankRewards = new ArrayList<>();
|
||||
private List<String> supportedPeriods = List.of(PERIOD_DAILY, PERIOD_OVERALL);
|
||||
private List<String> supportedTaskTypes = List.of(TASK_ENTER_PAGE, TASK_SEND_GIFT_GOLD);
|
||||
private String activityStatus;
|
||||
private Long serverTime;
|
||||
}
|
||||
|
||||
/** 当日任务使用配置快照,避免运营修改目标后追溯改变用户已经完成的进度。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class UserTask implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long taskConfigId;
|
||||
private String statDate;
|
||||
private String taskCode;
|
||||
private String taskType;
|
||||
private String taskTitle;
|
||||
private Integer sortOrder;
|
||||
private BigDecimal targetValue = BigDecimal.ZERO;
|
||||
private BigDecimal progressValue = BigDecimal.ZERO;
|
||||
private Boolean completed;
|
||||
private Long completedTime;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private String deliveryStatus;
|
||||
private Long claimedTime;
|
||||
private Long deliverTime;
|
||||
private List<RewardItem> rewardItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class UserState implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
private String statDate;
|
||||
private BigDecimal totalScore = BigDecimal.ZERO;
|
||||
private BigDecimal dailyScore = BigDecimal.ZERO;
|
||||
private Integer overallRank;
|
||||
private Integer dailyRank;
|
||||
private List<UserTask> tasks = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankEntry implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private Integer rank;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
private String account;
|
||||
private String nickname;
|
||||
private String avatar;
|
||||
private BigDecimal score = BigDecimal.ZERO;
|
||||
private Boolean me;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class Ranking implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String periodType;
|
||||
/** DAILY 为活动时区 yyyy-MM-dd,OVERALL 为空。 */
|
||||
private String statDate;
|
||||
private Boolean settled;
|
||||
private List<RankEntry> entries = new ArrayList<>();
|
||||
private RankEntry my;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class SettlementRecord implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
private String periodType;
|
||||
private String periodKey;
|
||||
private String statDate;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long userId;
|
||||
private Integer rank;
|
||||
private BigDecimal score;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long rankRewardId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private String businessNo;
|
||||
private String deliveryStatus;
|
||||
private Integer retryCount;
|
||||
private String failureReason;
|
||||
private Long createTime;
|
||||
private Long deliverTime;
|
||||
/** 奖励组拆项后单独记录状态,避免部分成功时重发已经成功的资源。 */
|
||||
private List<DeliveryItem> deliveryItems = new ArrayList<>();
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class DeliveryItem implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
private String ownerType;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long ownerId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long rewardConfigId;
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long resourceGroupId;
|
||||
private String rewardType;
|
||||
private String detailType;
|
||||
private String content;
|
||||
private Integer quantity;
|
||||
private Integer sortOrder;
|
||||
private String remark;
|
||||
private String deliveryStatus;
|
||||
private Integer retryCount;
|
||||
private String failureReason;
|
||||
private Long deliverTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class SaveCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long id;
|
||||
|
||||
@NotBlank(message = "activityCode required.")
|
||||
@Size(max = 64, message = "activityCode max length is 64.")
|
||||
private String activityCode;
|
||||
|
||||
@NotBlank(message = "activityName required.")
|
||||
@Size(max = 128, message = "activityName max length is 128.")
|
||||
private String activityName;
|
||||
|
||||
@Size(max = 1000, message = "activityDesc max length is 1000.")
|
||||
private String activityDesc;
|
||||
|
||||
@NotBlank(message = "sysOrigin required.")
|
||||
private String sysOrigin;
|
||||
|
||||
@NotBlank(message = "timeZone required.")
|
||||
private String timeZone = "Asia/Riyadh";
|
||||
|
||||
@NotNull(message = "startTime required.")
|
||||
private Long startTime;
|
||||
@NotNull(message = "endTime required.")
|
||||
private Long endTime;
|
||||
|
||||
@Min(value = 0, message = "dailySettlementDelayMinutes must be at least 0.")
|
||||
@Max(value = 1440, message = "dailySettlementDelayMinutes max is 1440.")
|
||||
private Integer dailySettlementDelayMinutes = 30;
|
||||
|
||||
@Min(value = 0, message = "overallSettlementDelayMinutes must be at least 0.")
|
||||
@Max(value = 1440, message = "overallSettlementDelayMinutes max is 1440.")
|
||||
private Integer overallSettlementDelayMinutes = 30;
|
||||
|
||||
@Min(value = 1, message = "displayTopN must be at least 1.")
|
||||
@Max(value = 500, message = "displayTopN max is 500.")
|
||||
private Integer displayTopN = 100;
|
||||
|
||||
private Boolean enabled = Boolean.FALSE;
|
||||
|
||||
/** 更新时做乐观锁;新增时允许为空。 */
|
||||
@Min(value = 0, message = "version must be at least 0.")
|
||||
private Integer version;
|
||||
|
||||
@Valid
|
||||
private List<Task> tasks;
|
||||
@Valid
|
||||
private List<RankReward> rankRewards;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class TaskSaveCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull(message = "activityId required.")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
|
||||
/** 产品固定三个位置,具体 code/type 组合由核心服务做集合校验。 */
|
||||
@NotNull(message = "tasks required.")
|
||||
@Size(min = 3, max = 3, message = "exactly three tasks required.")
|
||||
@Valid
|
||||
private List<Task> tasks;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankRewardSaveCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull(message = "activityId required.")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
@NotNull(message = "rankRewards required.")
|
||||
@Valid
|
||||
private List<RankReward> rankRewards;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankingQuery implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotBlank(message = "periodType required.")
|
||||
private String periodType;
|
||||
private String statDate;
|
||||
@Min(value = 1, message = "limit must be at least 1.")
|
||||
@Max(value = 500, message = "limit max is 500.")
|
||||
private Integer limit = 100;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class SettlementQuery implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
private String periodType;
|
||||
private String statDate;
|
||||
private String deliveryStatus;
|
||||
@Min(value = 1, message = "limit must be at least 1.")
|
||||
@Max(value = 500, message = "limit max is 500.")
|
||||
private Integer limit = 100;
|
||||
}
|
||||
|
||||
/** H5 进入页面命令;用户身份和统计日由服务端上下文决定。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class EnterCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
}
|
||||
|
||||
/** H5 任务领取命令;requestId 仅用于审计,任务日状态是最终幂等边界。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class TaskClaimCommand implements Serializable {
|
||||
@Serial
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@NotNull(message = "activityId required.")
|
||||
@JsonSerialize(using = ToStringSerializer.class)
|
||||
private Long activityId;
|
||||
|
||||
@NotBlank(message = "requestId required.")
|
||||
@Size(max = 128, message = "requestId max length is 128.")
|
||||
private String requestId;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,130 @@
|
||||
package com.red.circle.console.adapter.app.activity;
|
||||
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.web.controller.BaseController;
|
||||
import com.red.circle.other.inner.endpoint.activity.AslanGameKingManageClient;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.BackfillResult;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Prize;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.PrizeSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SettlementRecord;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** Aslan 游戏王 webconsole 管理接口。 */
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/activity/game-king")
|
||||
public class AslanGameKingBackRestController extends BaseController {
|
||||
|
||||
private final AslanGameKingManageClient client;
|
||||
|
||||
@GetMapping("/list")
|
||||
public List<Activity> list() {
|
||||
return client.list().getBody();
|
||||
}
|
||||
|
||||
@GetMapping("/detail/{id}")
|
||||
public Detail detail(@PathVariable Long id) {
|
||||
return client.detail(id).getBody();
|
||||
}
|
||||
|
||||
@PostMapping("/save")
|
||||
public Activity save(@RequestBody @Validated SaveCommand cmd) {
|
||||
return client.save(cmd).getBody();
|
||||
}
|
||||
|
||||
@PutMapping("/enable/{id}")
|
||||
public void enable(@PathVariable Long id, @RequestParam boolean enabled) {
|
||||
client.enable(id, enabled);
|
||||
}
|
||||
|
||||
@GetMapping("/prizes/{id}")
|
||||
public List<Prize> prizes(@PathVariable Long id) {
|
||||
return client.prizes(id).getBody();
|
||||
}
|
||||
|
||||
@PutMapping("/prizes/{id}")
|
||||
public void savePrizes(@PathVariable Long id,
|
||||
@RequestBody @Validated PrizeSaveCommand cmd) {
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
id.equals(cmd.getActivityId()));
|
||||
client.savePrizes(id, cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/rank-rewards/{id}")
|
||||
public List<RankReward> rankRewards(@PathVariable Long id) {
|
||||
return client.rankRewards(id).getBody();
|
||||
}
|
||||
|
||||
@PutMapping("/rank-rewards/{id}")
|
||||
public void saveRankRewards(@PathVariable Long id,
|
||||
@RequestBody @Validated RankRewardSaveCommand cmd) {
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
id.equals(cmd.getActivityId()));
|
||||
client.saveRankRewards(id, cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/settlement-records/{id}")
|
||||
public List<SettlementRecord> settlementRecords(@PathVariable Long id) {
|
||||
return client.settlementRecords(id).getBody();
|
||||
}
|
||||
|
||||
@GetMapping("/draw-records/{id}")
|
||||
public List<DrawRecord> drawRecords(@PathVariable Long id,
|
||||
@RequestParam(required = false) String deliveryStatus,
|
||||
@RequestParam(required = false) Integer limit) {
|
||||
return client.drawRecords(id, deliveryStatus, limit).getBody();
|
||||
}
|
||||
|
||||
@PostMapping("/draw/retry/{recordId}")
|
||||
public void retryDraw(@PathVariable Long recordId) {
|
||||
client.retryDraw(recordId);
|
||||
}
|
||||
|
||||
@PostMapping("/settlement/{id}")
|
||||
public void settle(@PathVariable Long id) {
|
||||
client.settle(id);
|
||||
}
|
||||
|
||||
@PostMapping("/settlement/retry/{recordId}")
|
||||
public void retrySettlement(@PathVariable Long recordId) {
|
||||
client.retrySettlement(recordId);
|
||||
}
|
||||
|
||||
/**
|
||||
* 仅用于 UNKNOWN 单项:运营先核账,再传 delivered=true 确认已到账,或 false 明确解锁补发。
|
||||
* 普通 retry 接口不会处理 UNKNOWN,避免超时但实际到账的金币/钻石被重复发放。
|
||||
*/
|
||||
@PostMapping("/delivery-item/resolve/{itemId}")
|
||||
public void resolveUnknownDeliveryItem(@PathVariable Long itemId,
|
||||
@RequestParam boolean delivered) {
|
||||
client.resolveUnknownDeliveryItem(itemId, delivered);
|
||||
}
|
||||
|
||||
/**
|
||||
* 按精确 eventType + receiptType 分页补数;两种收支类型各自维护 lastId,避免合并游标跳流水。
|
||||
*/
|
||||
@PostMapping("/backfill/{id}")
|
||||
public BackfillResult backfill(@PathVariable Long id, @RequestParam String eventType,
|
||||
@RequestParam Integer receiptType,
|
||||
@RequestParam(required = false) Long lastId,
|
||||
@RequestParam(required = false) Integer limit) {
|
||||
return client.backfill(id, eventType, receiptType, lastId, limit).getBody();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,150 @@
|
||||
package com.red.circle.console.adapter.app.activity;
|
||||
|
||||
import com.red.circle.console.app.service.admin.UserAccountService;
|
||||
import com.red.circle.console.infra.annotations.OpsOperationReqLog;
|
||||
import com.red.circle.console.inner.error.ConsoleErrorCode;
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.web.controller.BaseController;
|
||||
import com.red.circle.other.inner.endpoint.activity.AslanGiftChallengeManageClient;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Ranking;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankingQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Task;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.TaskSaveCommand;
|
||||
import java.util.List;
|
||||
import java.util.Objects;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.PutMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** Aslan 礼物挑战 webconsole 管理接口。 */
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping("/activity/gift-challenge")
|
||||
public class AslanGiftChallengeBackRestController extends BaseController {
|
||||
|
||||
private static final String VIEW_PERMISSION = "ActivityGiftChallenge";
|
||||
private static final String EDIT_PERMISSION = "activity:gift-challenge:edit";
|
||||
private static final String ENABLE_PERMISSION = "activity:gift-challenge:enable";
|
||||
private static final String SETTLE_PERMISSION = "activity:gift-challenge:settle";
|
||||
private static final String RECONCILE_PERMISSION = "activity:gift-challenge:reconcile";
|
||||
|
||||
private final AslanGiftChallengeManageClient client;
|
||||
private final UserAccountService userAccountService;
|
||||
|
||||
@GetMapping("/list")
|
||||
public List<Activity> list() {
|
||||
requirePermission(VIEW_PERMISSION);
|
||||
return client.list().getBody();
|
||||
}
|
||||
|
||||
@GetMapping("/detail/{id}")
|
||||
public Detail detail(@PathVariable Long id) {
|
||||
requirePermission(VIEW_PERMISSION);
|
||||
return client.detail(id).getBody();
|
||||
}
|
||||
|
||||
@OpsOperationReqLog("Aslan礼物挑战-保存活动配置")
|
||||
@PostMapping("/save")
|
||||
public Activity save(@RequestBody @Validated SaveCommand cmd) {
|
||||
requirePermission(EDIT_PERMISSION);
|
||||
return client.save(cmd).getBody();
|
||||
}
|
||||
|
||||
@OpsOperationReqLog("Aslan礼物挑战-启停活动")
|
||||
@PutMapping("/enable/{id}")
|
||||
public void enable(@PathVariable Long id, @RequestParam boolean enabled) {
|
||||
requirePermission(ENABLE_PERMISSION);
|
||||
client.enable(id, enabled);
|
||||
}
|
||||
|
||||
@GetMapping("/tasks/{id}")
|
||||
public List<Task> tasks(@PathVariable Long id) {
|
||||
requirePermission(VIEW_PERMISSION);
|
||||
return client.tasks(id).getBody();
|
||||
}
|
||||
|
||||
@OpsOperationReqLog("Aslan礼物挑战-保存任务配置")
|
||||
@PutMapping("/tasks/{id}")
|
||||
public void saveTasks(@PathVariable Long id,
|
||||
@RequestBody @Validated TaskSaveCommand cmd) {
|
||||
requirePermission(EDIT_PERMISSION);
|
||||
// 路径 ID 与请求体 ID 必须一致,避免 webconsole 切换活动时把三个固定任务写入另一活动。
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
id.equals(cmd.getActivityId()));
|
||||
client.saveTasks(id, cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/rank-rewards/{id}")
|
||||
public List<RankReward> rankRewards(@PathVariable Long id) {
|
||||
requirePermission(VIEW_PERMISSION);
|
||||
return client.rankRewards(id).getBody();
|
||||
}
|
||||
|
||||
@OpsOperationReqLog("Aslan礼物挑战-保存排行奖励")
|
||||
@PutMapping("/rank-rewards/{id}")
|
||||
public void saveRankRewards(@PathVariable Long id,
|
||||
@RequestBody @Validated RankRewardSaveCommand cmd) {
|
||||
requirePermission(EDIT_PERMISSION);
|
||||
// 双重归属校验防止日榜/总榜配置被错误覆盖到 URL 指向的其他活动。
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
id.equals(cmd.getActivityId()));
|
||||
client.saveRankRewards(id, cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/ranking/{id}")
|
||||
public Ranking ranking(@PathVariable Long id, @Validated RankingQuery query) {
|
||||
requirePermission(VIEW_PERMISSION);
|
||||
return client.ranking(id, query).getBody();
|
||||
}
|
||||
|
||||
@GetMapping("/settlement-records/{id}")
|
||||
public List<SettlementRecord> settlementRecords(@PathVariable Long id,
|
||||
@Validated SettlementQuery query) {
|
||||
requirePermission(VIEW_PERMISSION);
|
||||
return client.settlementRecords(id, query).getBody();
|
||||
}
|
||||
|
||||
@OpsOperationReqLog("Aslan礼物挑战-手动结算")
|
||||
@PostMapping("/settlement/{id}")
|
||||
public void settle(@PathVariable Long id, @RequestParam String period,
|
||||
@RequestParam(required = false) String statDate) {
|
||||
requirePermission(SETTLE_PERMISSION);
|
||||
// DAILY 必须由内层按活动时区解释 statDate;console 仅透传,避免服务器默认时区改变结算日。
|
||||
client.settle(id, period, statDate);
|
||||
}
|
||||
|
||||
/**
|
||||
* UNKNOWN 可能是下游超时但实际到账,运营核账后只能显式确认已到账或解锁补发;
|
||||
* 该入口使用独立核账权限,避免把“结果未知”误当作可直接重试的失败。
|
||||
*/
|
||||
@OpsOperationReqLog("Aslan礼物挑战-确认未知发奖结果")
|
||||
@PostMapping("/delivery-item/resolve/{itemId}")
|
||||
public void resolveUnknownDeliveryItem(@PathVariable Long itemId,
|
||||
@RequestParam boolean delivered) {
|
||||
requirePermission(RECONCILE_PERMISSION);
|
||||
client.resolveUnknownDeliveryItem(itemId, delivered);
|
||||
}
|
||||
|
||||
private void requirePermission(String alias) {
|
||||
Long reqUserId = getReqUserId();
|
||||
// 前端菜单/按钮隐藏不是安全边界;读取和变更入口都在 controller 再校验对应菜单/按钮别名。
|
||||
boolean permitted = Objects.nonNull(reqUserId)
|
||||
&& userAccountService.hasButtonsAlias(reqUserId.intValue(), alias);
|
||||
ResponseAssert.isTrue(ConsoleErrorCode.ACCOUNT_NOT_PERMISSIONS, permitted);
|
||||
}
|
||||
}
|
||||
190
rc-service/rc-service-other/docs/Aslan游戏王活动接口.md
Normal file
190
rc-service/rc-service-other/docs/Aslan游戏王活动接口.md
Normal file
@ -0,0 +1,190 @@
|
||||
# Aslan 游戏王活动接口
|
||||
|
||||
本文对应 H5 `activity/game-king/aslan.html` 与 webconsole 游戏王配置页。接口契约以当前仓库的 `AslanGameKingRestController`、`AslanGameKingBackRestController`、`AslanGameKingModels` 和 `AslanGameKingWalletListener` 为准。
|
||||
|
||||
## 通用约定
|
||||
|
||||
- 下列地址均为网关或 webconsole 域名后的相对路径;H5 通过现有 `common_aslan` 请求层调用。
|
||||
- Controller 返回值会由框架包装为 `ResultResponse`:成功时核心结构为 `{"status":true,"errorCode":0,"time":<毫秒时间戳>,"body":...}`;业务数据均在 `body`。
|
||||
- DTO 中的 Long ID 在响应 JSON 中按字符串输出,H5 应保留字符串,不要转成 JavaScript Number。
|
||||
- 时间字段 `startTime`、`endTime`、`settlementTime`、`serverTime`、`drawTime`、`deliverTime` 均为 Unix 毫秒时间戳。
|
||||
|
||||
### H5 鉴权头
|
||||
|
||||
H5 五个接口都使用网关鉴权,不接受客户端提交 `userId` 或 `sysOrigin` 业务字段。
|
||||
|
||||
| Header | 必填 | 精确格式/说明 |
|
||||
| --- | --- | --- |
|
||||
| `Authorization` | 是 | `Bearer <app access token>`;网关从已校验 token 取得用户 ID,再通过内部头注入 `AppExtCommand.reqUserId`。 |
|
||||
| `Req-Sys-Origin` | 是 | `origin=<活动所属系统>;originChild=<子系统>;`。`origin` 必须与后台活动的 `sysOrigin` 完全相同;未传 `originChild` 时框架回落为 `origin`。 |
|
||||
| `Req-Client` | 是 | H5 固定传 `H5`。 |
|
||||
| `Req-App-Intel` | 是 | 分号分隔,例如 `version=<版本>;build=<正整数>;channel=<渠道>;`;`version`、正数 `build`、`channel` 都由网关校验。 |
|
||||
|
||||
`Req-Inner-Internal` 是网关生成的内部头,H5 不应自行构造。抽奖 JSON 即使额外带入 `reqUserId`、`reqSysOrigin`,Controller 也不会使用这些字段。
|
||||
|
||||
### Webconsole 鉴权
|
||||
|
||||
webconsole 接口使用 `Authorization: Bearer <console JWT>`。迁移 SQL 同时登记了 `activity:game-king:*` 资源与 `ActivityGameKing` 菜单;账号需拥有相应后台菜单/资源权限。
|
||||
|
||||
## H5 接口
|
||||
|
||||
基础路径:`/activity/game/king/aslan`
|
||||
|
||||
### 1. 活动详情
|
||||
|
||||
`GET /activity/game/king/aslan/detail`
|
||||
|
||||
Query:
|
||||
|
||||
| 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `activityId` | 否 | 活动 ID。省略时,按当前 `Req-Sys-Origin` 依次选择进行中、最近将开始、最近结束的已启用活动。 |
|
||||
|
||||
`body` 为 `Detail`:
|
||||
|
||||
- `activity`:活动配置,主要字段为 `id`、`activityCode`、`activityName`、`activityDesc`、`sysOrigin`、`timeZone`、`startTime`、`endTime`、`settlementDelayMinutes`、`settlementTime`、`coinPerDraw`、`eventTypes`、`rankingType`、`rankingPeriod`、`enabled`、`settlementStatus`。
|
||||
- `prizes`:7 个奖项;主要字段为 `id`、`prizeName`、`prizeImage`、`weight`、`stock`、`resourceGroupId`、`sortOrder`、`rewardItems`。
|
||||
- `rankRewards`:固定档位 `1`、`2`、`3`、`4-7`、`8-10`、`11-30` 及其 `resourceGroupId`、`rewardName`、`rewardItems`。
|
||||
- `supportedRankingTypes`:固定为 `TYCOON`、`VICTORIOUS`。
|
||||
- `supportedRankingPeriods`:固定为 `DAILY`、`OVERALL`。
|
||||
- `activityStatus`:`UPCOMING`、`ACTIVE`、`ENDED` 或 `DISABLED`。
|
||||
- `serverTime`:服务端当前毫秒时间戳。
|
||||
|
||||
### 2. 我的活动状态
|
||||
|
||||
`GET /activity/game/king/aslan/me`
|
||||
|
||||
Query:可选 `activityId`,选择规则同详情接口。
|
||||
|
||||
`body` 为 `UserState`:`activityId`、`totalConsumed`、`earnedChances`、`usedChances`、`availableChances`、`nextChanceRemaining`、`rank`。其中抽奖机会只由支出流水累计,`earnedChances = floor(totalConsumed / coinPerDraw)`。
|
||||
|
||||
### 3. 抽奖
|
||||
|
||||
`POST /activity/game/king/aslan/draw`
|
||||
|
||||
Body:
|
||||
|
||||
```json
|
||||
{
|
||||
"activityId": "可选活动ID",
|
||||
"requestId": "本次点击生成且重试时保持不变的唯一值"
|
||||
}
|
||||
```
|
||||
|
||||
- `requestId` 必填、去除首尾空格后非空、最长 64 字符。
|
||||
- 幂等范围为“活动 + 当前用户 + `requestId`”。网络重试必须复用原值;新的一次抽奖必须生成新值。
|
||||
- 仅 `ACTIVE` 活动且 `availableChances > 0` 时可抽。
|
||||
|
||||
`body` 为 `DrawResult`:
|
||||
|
||||
- `record`:`id`、`activityId`、`userId`、`requestId`、`prizeId`、`prizeName`、`prizeImage`、`resourceGroupId`、`deliveryStatus`、`failureReason`、`drawTime`、`deliverTime`。
|
||||
- `prize`:命中奖项及展示用 `rewardItems`。
|
||||
- `remainingChances`:本次扣减后的剩余机会。
|
||||
- `idempotentReplay`:是否返回同一 `requestId` 的已有结果。
|
||||
|
||||
### 4. 排行榜
|
||||
|
||||
`GET /activity/game/king/aslan/ranking`
|
||||
|
||||
| Query 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `activityId` | 否 | 活动 ID。 |
|
||||
| `rankingType` | 否 | `TYCOON`(游戏支出)或 `VICTORIOUS`(游戏收入);省略时使用活动配置。 |
|
||||
| `period` | 否 | `DAILY` 或 `OVERALL`;省略时使用活动配置。 |
|
||||
| `limit` | 否 | 默认 30,服务端限制到 1~100。 |
|
||||
|
||||
`body` 为 `Ranking`:`rankingType`、`period`、`statDate`、`entries`、`my`。`DAILY` 的 `statDate` 是活动 `timeZone` 下当天的 `yyyy-MM-dd`,`OVERALL` 时为空。每个 `RankEntry` 包含 `rank`、`userId`、`account`、`nickname`、`avatar`、`score`、`totalConsumed`、`totalWon`、`me`。
|
||||
|
||||
同分排序固定为:分值降序、首次到达该分值时间升序、`userId` 升序。
|
||||
|
||||
### 5. 我的抽奖记录
|
||||
|
||||
`GET /activity/game/king/aslan/draw-records`
|
||||
|
||||
| Query 参数 | 必填 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| `activityId` | 否 | 活动 ID。 |
|
||||
| `limit` | 否 | 默认 20,服务端限制到 1~100。 |
|
||||
|
||||
`body` 为 `DrawRecord[]`,字段同抽奖结果中的 `record`。逐项发奖明细 `deliveryItems` 只在管理端填充。
|
||||
|
||||
## Webconsole 管理接口
|
||||
|
||||
基础路径:`/activity/game-king`
|
||||
|
||||
| 方法与路径 | 关键入参 | `body`/作用 |
|
||||
| --- | --- | --- |
|
||||
| `GET /list` | 无 | `Activity[]`。 |
|
||||
| `GET /detail/{id}` | 活动 ID | `Detail`。 |
|
||||
| `POST /save` | `SaveCommand` JSON | 新增或修改活动,返回 `Activity`。 |
|
||||
| `PUT /enable/{id}` | Query `enabled=true/false` | 启停活动。活动开始后不能再改变启用状态。 |
|
||||
| `GET /prizes/{id}` | 活动 ID | `Prize[]`。 |
|
||||
| `PUT /prizes/{id}` | `{"activityId":"同路径ID","prizes":[...]}` | 覆盖保存 7 个奖项;活动开始后禁止修改。 |
|
||||
| `GET /rank-rewards/{id}` | 活动 ID | `RankReward[]`。 |
|
||||
| `PUT /rank-rewards/{id}` | `{"activityId":"同路径ID","rankRewards":[...]}` | 覆盖保存 6 个固定排名奖励档;活动开始后禁止修改。 |
|
||||
| `GET /draw-records/{id}` | 可选 `deliveryStatus`、`limit` | 抽奖记录及 `deliveryItems`;状态可为 `PENDING/PROCESSING/SUCCESS/FAILED/UNKNOWN`,`limit` 默认/最大均为 100。 |
|
||||
| `POST /draw/retry/{recordId}` | 抽奖记录 ID | 仅重试已核实可重试的 `FAILED` 记录,不处理 `UNKNOWN`。 |
|
||||
| `GET /settlement-records/{id}` | 活动 ID | Top 30 冻结结算记录及逐项 `deliveryItems`。 |
|
||||
| `POST /settlement/{id}` | 活动 ID | 到达 `settlementTime` 后冻结 TYCOON/OVERALL Top 30 并发奖。 |
|
||||
| `POST /settlement/retry/{recordId}` | 结算记录 ID | 仅重试已核实可重试的 `FAILED` 记录,不处理 `UNKNOWN`。 |
|
||||
| `POST /delivery-item/resolve/{itemId}` | Query `delivered=true/false` | 只处理 `UNKNOWN` 单项;语义见下文。 |
|
||||
| `POST /backfill/{id}` | `eventType`、`receiptType` 必填;`lastId`、`limit` 可选 | 分页补算钱包流水,返回 `BackfillResult`。 |
|
||||
|
||||
### `SaveCommand` 关键字段与约束
|
||||
|
||||
| 字段 | 约束 |
|
||||
| --- | --- |
|
||||
| `id` | 修改时传;新增不传。 |
|
||||
| `activityCode` | 必填,最长 64,活动间唯一。 |
|
||||
| `activityName` | 必填,最长 128。 |
|
||||
| `activityDesc` | 可选,最长 1000。 |
|
||||
| `sysOrigin` | 必填,必须是服务端支持的 `SysOriginPlatformEnum` 值,并与 H5 `Req-Sys-Origin.origin`、钱包事件 `sysOrigin` 一致。 |
|
||||
| `timeZone` | 必填;默认 `Asia/Riyadh`,必须是合法 IANA 时区。 |
|
||||
| `startTime`、`endTime` | 必填毫秒时间戳,且 `startTime < endTime`。 |
|
||||
| `settlementDelayMinutes` | 0~1440,默认 30;`settlementTime = endTime + settlementDelayMinutes`。 |
|
||||
| `coinPerDraw` | 必填,正整数金币数。 |
|
||||
| `eventTypes` | 必填,1~30 个非空字符串;按钱包 `eventType` 精确匹配,单值不能含逗号。 |
|
||||
| `rankingType` | `TYCOON` 或 `VICTORIOUS`,默认 `TYCOON`。 |
|
||||
| `rankingPeriod` | `DAILY` 或 `OVERALL`,默认 `OVERALL`。 |
|
||||
| `enabled` | 默认 `false`。首次以 `true` 保存时,`startTime` 必须晚于当前时间。 |
|
||||
| `prizes`、`rankRewards` | 可随活动一起保存,也可通过独立接口保存。启用前两者都必须完整。 |
|
||||
|
||||
同一 `sysOrigin` 的已启用活动时间窗不得重叠。活动开始后,统计时间、系统、时区、结算延时、`coinPerDraw`、启用状态和 `eventTypes` 等统计口径会锁定。
|
||||
|
||||
奖项必须恰好 7 个且全部 `enabled=true`,`sortOrder` 为不重复的 1~7,`prizeName`/`prizeImage` 非空,`weight > 0`,`stock >= -1`,并配置 `resourceGroupId`。`stock=-1` 表示无限库存,0 表示耗尽。
|
||||
|
||||
排名奖励必须恰好为 `1-1`、`2-2`、`3-3`、`4-7`、`8-10`、`11-30` 六档。奖项和排名奖励引用的资源组必须已上架、`sysOrigin` 与活动一致且至少包含一个奖励配置。
|
||||
|
||||
### 补数返回
|
||||
|
||||
`POST /backfill/{id}` 的 `receiptType`:`0=INCOME/VICTORIOUS`,`1=EXPENDITURE/TYCOON 与抽奖机会`。`limit` 默认 200、最大 500;每个 `eventType + receiptType` 必须分别维护自己的 `lastId`,直到返回 `finished=true`。
|
||||
|
||||
`BackfillResult` 字段:`eventType`、`receiptType`、`scanned`、`accepted`、`nextLastId`、`finished`。补数查询边界同实时统计,为 `[startTime,endTime)`;调用钱包历史接口时实现为 `endTime - 1ms`。
|
||||
|
||||
## 钱包事件统计契约
|
||||
|
||||
- RocketMQ topic:逻辑 topic `RC_DEFAULT_APP_ORDINARY`,运行时由 `${rocketmq.topics.RC_DEFAULT_APP_ORDINARY}` 解析。
|
||||
- tag:`wallet_sync_gold_v2`。
|
||||
- 独立 consumer group:`ASLAN_GAME_KING_WALLET_SYNC_V1`。不得和钱包落库消费者共用 group,否则会竞争消费。
|
||||
- 事件模型:`WalletReceiptSyncEvent`。使用字段 `assetRecordId`、`userId`、`sysOrigin`、`eventType`、`eventId`、`receiptType`、`bizType`、`amount`、`createTime`。
|
||||
- 仅接收 `bizType=1`、金额非 0、`sysOrigin` 与活动相同、`eventType` 在活动白名单中、`createTime` 位于 `[startTime,endTime)` 的事件。
|
||||
- `receiptType=EXPENDITURE(1)` 累计 TYCOON 与抽奖机会;`receiptType=INCOME(0)` 累计 VICTORIOUS。
|
||||
- 实时 MQ、MQ 重投和后台补数共用唯一幂等键 `(activity_id, asset_record_id)`。
|
||||
- DAILY 按活动 `timeZone` 落日表;OVERALL 使用全活动累计。活动结算奖励固定按 TYCOON/OVERALL,而不是当前 H5 正在查看的榜单维度。
|
||||
|
||||
## 结算截点与发奖核账
|
||||
|
||||
- 默认在 `endTime + 30 分钟` 到达结算截点;后台可通过 `settlementDelayMinutes` 配置 0~1440 分钟。定时任务每 5 分钟扫描一次到期活动。
|
||||
- 统计有效时间仍只到 `endTime`(不含);30 分钟只是给异步消息到达留出的缓冲,不代表 RocketMQ 已提供“无更早消息”的 watermark。要求严格完整时,应在截点前按全部配置的 `eventType × receiptType` 完成补数,再执行结算。
|
||||
- 首次结算会在活动行排他锁内冻结 TYCOON/OVERALL Top 30;已进入统计事务会先完成,冻结后到达的 MQ 或补数会被拒绝,排名快照不会再漂移。
|
||||
- 每个奖励组拆成 `deliveryItems` 逐项发放。普通失败可标为 `FAILED` 并在运营核实后走 retry;外部适配器抛错,或 `PROCESSING` 超过 10 分钟租期时,结果不能判定是否已到账,因此标为 `UNKNOWN`。
|
||||
- `UNKNOWN` 永远不会被定时任务或普通 retry 盲目补发。运营核对下游账目后调用 `/delivery-item/resolve/{itemId}`:`delivered=true` 表示确认已到账,仅把该项记为成功且不再次发送;`delivered=false` 表示确认未到账,原子解锁父记录与该项后再补发。
|
||||
- 结算中存在 `FAILED` 或 `UNKNOWN` 时活动聚合状态为 `PARTIAL_FAILED`;全部成功后为 `COMPLETED`。
|
||||
|
||||
## 数据库迁移
|
||||
|
||||
- 测试环境:`.deploy/test-deploy/sql/20260717_aslan_game_king.sql`
|
||||
- 生产环境:`.deploy/prod-deploy/sql/20260717_aslan_game_king.sql`
|
||||
- 两份 SQL 内容应保持字节一致。迁移创建 9 张独立活动表,并登记 webconsole 菜单/资源。
|
||||
- SQL 文件开头已记录性能评估:实时写路径使用唯一键/主键聚合,榜单直接读取维度索引,恢复与到期扫描均有状态时间索引;迁移不回填钱包流水,也不扫描更新既有业务大表。
|
||||
- 当前交付只新增了迁移文件,**尚未在测试或生产数据库执行**。执行前仍需按环境发布流程复核目标库、变更窗口和执行计划。
|
||||
110
rc-service/rc-service-other/docs/Aslan礼物挑战活动接口.md
Normal file
110
rc-service/rc-service-other/docs/Aslan礼物挑战活动接口.md
Normal file
@ -0,0 +1,110 @@
|
||||
# Aslan 礼物挑战活动接口
|
||||
|
||||
## 接入约定
|
||||
|
||||
- H5 基础路径:`/activity/gift-challenge/aslan`
|
||||
- WebConsole 基础路径:`/activity/gift-challenge`
|
||||
- H5 请求必须经过网关并携带 `Authorization: Bearer ...`、`Req-Sys-Origin: ATYOU`、
|
||||
`Req-Client` 和 `Req-App-Intel`。用户 ID 与来源由网关注入,接口不接收客户端 userId。
|
||||
- 时间均为毫秒时间戳;日榜日期是活动 `timeZone` 下的 `yyyy-MM-dd`。
|
||||
- 单个活动最多覆盖 366 个活动时区自然日;首次启用会批量幂等预建全部 DAILY 结算门闩,
|
||||
因此没有任何参与者的日期也会按时冻结为空榜。
|
||||
- 启用会冻结任务、排名区间所引用资源组的完整奖励配置(发放字段和 H5 展示字段),最多
|
||||
5000 个奖励项。活动开始前修改任务、排名奖励或活动时间会先清理旧的 `NOT_STARTED`
|
||||
门闩和旧奖励快照再整体重建;停用也会清理这些预建数据。
|
||||
- `dailySettlementDelayMinutes` 与 `overallSettlementDelayMinutes` 是有界迟到窗口,不是无限
|
||||
追溯。结算门闩开始后对应榜单不再接受迟到写入;执行自动或人工结算前必须监控
|
||||
`ASLAN_GIFT_CHALLENGE_GIFT` consumer group lag,确认落后量已进入可接受范围。
|
||||
- 积分直接消费原始送礼事件:仅 `ATYOU + GOLD + 非背包 + amount > 0`,强制排除
|
||||
`LUCKY_GIFT` 与 `MAGIC`,并且事件时间满足 `[startTime, endTime)`。金额固定使用
|
||||
`GiveAwayGiftBatchEvent.countConsumAmountTotal()`,与礼物主流水口径一致。
|
||||
|
||||
## H5 接口
|
||||
|
||||
### 1. 活动详情
|
||||
|
||||
`GET /activity/gift-challenge/aslan/detail?activityId={id}`
|
||||
|
||||
- `activityId` 可选;缺省时返回当前进行中、最近待开始或最近结束的已启用活动。
|
||||
- 返回 `activity`、三个任务配置、`DAILY/OVERALL` 排名奖励、活动状态和服务端时间。
|
||||
- 本接口不修改任务;H5 打开页面后必须调用 `/enter`。
|
||||
|
||||
### 2. 每日进入并读取用户状态
|
||||
|
||||
`POST /activity/gift-challenge/aslan/enter`
|
||||
|
||||
```json
|
||||
{
|
||||
"activityId": "1234567890123456789"
|
||||
}
|
||||
```
|
||||
|
||||
- 服务端按活动时区创建当天三个任务快照,并幂等完成所有 `ENTER_PAGE` 任务。
|
||||
- 返回 `activityId`、`statDate`、总积分、今日积分、日榜/总榜名次以及三个任务状态。
|
||||
- 重复进入不会重复累计,也不会改变 `SEND_GIFT_GOLD` 任务进度。
|
||||
|
||||
### 3. 排行榜
|
||||
|
||||
`GET /activity/gift-challenge/aslan/ranking?activityId={id}&period={DAILY|OVERALL}&statDate={yyyy-MM-dd}&limit=100`
|
||||
|
||||
- `period` 缺省为 `DAILY`。
|
||||
- `statDate` 只对 `DAILY` 生效;缺省时自动选择活动窗口内的当前、首日或末日。
|
||||
- `limit` 最终受活动 `displayTopN` 和服务端最大值 500 双重限制。
|
||||
- 返回 `entries`、`my`、`settled`;排序固定为积分降序、达到当前积分时间升序、userId 升序。
|
||||
- 神秘人能力用户不出现在其他用户看到的公开榜单中,但本人仍可看到自己的排名。
|
||||
|
||||
### 4. 领取每日任务奖励
|
||||
|
||||
`POST /activity/gift-challenge/aslan/tasks/{taskCode}/claim`
|
||||
|
||||
```json
|
||||
{
|
||||
"activityId": "1234567890123456789",
|
||||
"requestId": "h5-generated-request-id"
|
||||
}
|
||||
```
|
||||
|
||||
- 任务必须属于当前活动日并且已完成。
|
||||
- `taskCode` 只允许 `A-Z/a-z/0-9/_/-`,长度 1 到 64,保证它可安全作为 URL 路径段。
|
||||
- 活动日任务行和逐奖励项 delivery item 是幂等边界;重复请求不会重复发放成功项。
|
||||
- 首次成功抢占领取状态的 `requestId` 会写入任务日快照作为审计号;它不替代服务端任务行
|
||||
和 delivery item 的发奖幂等边界。
|
||||
- 发奖结果不确定时任务进入 `UNKNOWN`,H5 不会自动重发,需运营在后台核账。
|
||||
- 返回最新的完整用户状态。
|
||||
|
||||
## WebConsole 接口
|
||||
|
||||
| 方法 | 路径 | 说明 |
|
||||
| --- | --- | --- |
|
||||
| GET | `/activity/gift-challenge/list` | 活动列表 |
|
||||
| GET | `/activity/gift-challenge/detail/{id}` | 活动、任务、排名奖励详情 |
|
||||
| POST | `/activity/gift-challenge/save` | 新增或按 version 乐观锁更新活动 |
|
||||
| PUT | `/activity/gift-challenge/enable/{id}?enabled=true` | 启停活动 |
|
||||
| GET/PUT | `/activity/gift-challenge/tasks/{id}` | 查询或保存恰好三个任务槽 |
|
||||
| GET/PUT | `/activity/gift-challenge/rank-rewards/{id}` | 查询或保存日榜/总榜奖励区间 |
|
||||
| GET | `/activity/gift-challenge/ranking/{id}` | 查询实时或冻结榜单 |
|
||||
| GET | `/activity/gift-challenge/settlement-records/{id}` | 查询结算及逐项发奖状态 |
|
||||
| POST | `/activity/gift-challenge/settlement/{id}` | 手动执行到期 DAILY/OVERALL 结算;period 必填,DAILY 还必须传 statDate |
|
||||
| POST | `/activity/gift-challenge/delivery-item/resolve/{itemId}` | 核账确认 UNKNOWN 已到账或未到账 |
|
||||
|
||||
任务配置必须恰好三条:一条 `ENTER_PAGE` 和两条 `SEND_GIFT_GOLD`;两档送礼任务可分别
|
||||
配置金币门槛和奖励,并且金币门槛必须按 `sortOrder` 严格递增。活动启用前必须同时配置 DAILY 和 OVERALL 奖励,
|
||||
同一周期排名区间不得重叠。启用后的 H5 奖励展示、每日任务发奖和榜单结算发奖均只读取
|
||||
该活动自己的冻结快照;共享资源组之后改名、改奖励或下架都不会改变本期用户所得。
|
||||
|
||||
## 礼物事件与发奖
|
||||
|
||||
活动使用独立 consumer group `ASLAN_GIFT_CHALLENGE_GIFT` 直接订阅原始
|
||||
`GiveGiftSink.TAG`,与主礼物消费者形成 RocketMQ fan-out,不依赖主消费者完成 Mongo
|
||||
落库后再派生消息。对于 broker 已接收的消息,本 consumer 依靠 RocketMQ 重投和业务
|
||||
唯一键实现 at-least-once;原 tag 发送失败沿用既有 `GiftMqRetryTask` 全局补偿,活动内
|
||||
不再新增第二套 retry tag。活动服务会再次校验完整资格,并通过
|
||||
`(activity_id, source_event_track_id)` 唯一键保证 RocketMQ 重投不会重复加分。
|
||||
|
||||
本活动不新增 IM 消息。奖励复用活动资源组发送器;启用时先形成活动级不可变奖励快照,
|
||||
实际领取和结算再把快照中的每个资源配置物化为独立 delivery item 并抢占。超时或
|
||||
进程中断统一转为 `UNKNOWN`,避免在下游到账结果不明时盲目重发。系统不设普通失败
|
||||
重试分支;运营核账确认未到账后,该单项才会回到 `PENDING` 并补发。排行榜冻结与奖励
|
||||
物化分属两个提交事务:门闩和获奖 parent 先提交,奖励项物化或发送短暂失败只会让
|
||||
parent 保持 `PENDING`、周期保持 `PROCESSING`;调度器会从活动快照幂等物化并继续发放,
|
||||
不会读取已经变化的共享资源组,也不会把已经冻结的榜单重新开放。
|
||||
@ -0,0 +1,72 @@
|
||||
package com.red.circle.other.adapter.app.activity;
|
||||
|
||||
import com.red.circle.common.business.dto.cmd.AppExtCommand;
|
||||
import com.red.circle.framework.web.controller.BaseController;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGameKingService;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawResult;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Ranking;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.UserState;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Aslan 游戏王 H5 接口。
|
||||
*
|
||||
* <p>用户身份和系统来源由网关写入 AppExtCommand;任何接口均不接收 userId。</p>
|
||||
*
|
||||
* @eo.api-type http
|
||||
* @eo.groupName 活动服务.Aslan游戏王
|
||||
* @eo.path /activity/game/king/aslan
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(value = "/activity/game/king/aslan", produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class AslanGameKingRestController extends BaseController {
|
||||
|
||||
private final AslanGameKingService aslanGameKingService;
|
||||
|
||||
@GetMapping("/detail")
|
||||
public Detail detail(AppExtCommand cmd,
|
||||
@RequestParam(value = "activityId", required = false) Long activityId) {
|
||||
return aslanGameKingService.detail(cmd, activityId);
|
||||
}
|
||||
|
||||
@GetMapping("/me")
|
||||
public UserState me(AppExtCommand cmd,
|
||||
@RequestParam(value = "activityId", required = false) Long activityId) {
|
||||
return aslanGameKingService.me(cmd, activityId);
|
||||
}
|
||||
|
||||
@PostMapping("/draw")
|
||||
public DrawResult draw(AppExtCommand identity, @RequestBody @Validated DrawCommand cmd) {
|
||||
// 网关上下文与 JSON body 分离,客户端即使伪造 reqUserId/sysOrigin 字段也不会参与抽奖。
|
||||
return aslanGameKingService.draw(identity, cmd);
|
||||
}
|
||||
|
||||
@GetMapping("/ranking")
|
||||
public Ranking ranking(AppExtCommand cmd,
|
||||
@RequestParam(value = "activityId", required = false) Long activityId,
|
||||
@RequestParam(value = "rankingType", required = false) String rankingType,
|
||||
@RequestParam(value = "period", required = false) String period,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
return aslanGameKingService.ranking(cmd, activityId, rankingType, period, limit);
|
||||
}
|
||||
|
||||
@GetMapping("/draw-records")
|
||||
public List<DrawRecord> drawRecords(AppExtCommand cmd,
|
||||
@RequestParam(value = "activityId", required = false) Long activityId,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
return aslanGameKingService.drawRecords(cmd, activityId, limit);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,67 @@
|
||||
package com.red.circle.other.adapter.app.activity;
|
||||
|
||||
import com.red.circle.common.business.dto.cmd.AppExtCommand;
|
||||
import com.red.circle.framework.web.controller.BaseController;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGiftChallengeService;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.EnterCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Ranking;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.TaskClaimCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.UserState;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.GetMapping;
|
||||
import org.springframework.web.bind.annotation.PathVariable;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RequestParam;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* Aslan Gift Challenge H5 接口。
|
||||
*
|
||||
* <p>所有用户身份与 sysOrigin 都来自网关注入的 AppExtCommand;body 不允许携带 userId、
|
||||
* 积分、任务进度或统计日期,从入口切断客户端代领和伪造积分。</p>
|
||||
*
|
||||
* @eo.api-type http
|
||||
* @eo.groupName 活动服务.AslanGiftChallenge
|
||||
* @eo.path /activity/gift-challenge/aslan
|
||||
*/
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(value = "/activity/gift-challenge/aslan",
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class AslanGiftChallengeRestController extends BaseController {
|
||||
|
||||
private final AslanGiftChallengeService service;
|
||||
|
||||
@GetMapping("/detail")
|
||||
public Detail detail(AppExtCommand cmd,
|
||||
@RequestParam(value = "activityId", required = false) Long activityId) {
|
||||
return service.detail(cmd, activityId);
|
||||
}
|
||||
|
||||
@PostMapping("/enter")
|
||||
public UserState enter(AppExtCommand cmd,
|
||||
@RequestBody(required = false) @Validated EnterCommand command) {
|
||||
return service.enter(cmd, command == null ? null : command.getActivityId());
|
||||
}
|
||||
|
||||
@GetMapping("/ranking")
|
||||
public Ranking ranking(AppExtCommand cmd,
|
||||
@RequestParam(value = "activityId", required = false) Long activityId,
|
||||
@RequestParam(value = "period", required = false) String period,
|
||||
@RequestParam(value = "statDate", required = false) String statDate,
|
||||
@RequestParam(value = "limit", required = false) Integer limit) {
|
||||
return service.ranking(cmd, activityId, period, statDate, limit);
|
||||
}
|
||||
|
||||
@PostMapping("/tasks/{taskCode}/claim")
|
||||
public UserState claimTask(AppExtCommand cmd, @PathVariable("taskCode") String taskCode,
|
||||
@RequestBody @Validated TaskClaimCommand command) {
|
||||
// requestId 只用于同一次领取的审计;真正幂等边界是活动日任务行和奖励 delivery item。
|
||||
return service.claimTask(cmd, command.getActivityId(), taskCode, command.getRequestId());
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,44 @@
|
||||
package com.red.circle.other.app.listener;
|
||||
|
||||
import com.red.circle.component.mq.MessageEventProcess;
|
||||
import com.red.circle.component.mq.MessageEventProcessDescribe;
|
||||
import com.red.circle.component.mq.config.RocketMqMessageListener;
|
||||
import com.red.circle.component.mq.service.Action;
|
||||
import com.red.circle.component.mq.service.ConsumerMessage;
|
||||
import com.red.circle.component.mq.service.MessageListener;
|
||||
import com.red.circle.mq.business.model.event.wallet.WalletReceiptSyncEvent;
|
||||
import com.red.circle.mq.rocket.business.streams.WalletAssetGoldSyncSinkV2;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGameKingServiceImpl;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Aslan 游戏王钱包收支监听器。
|
||||
*
|
||||
* <p>使用独立 consumer group 订阅 wallet_sync_gold_v2,不能与钱包流水落库消费者共用
|
||||
* group;否则两者会竞争消息,导致活动漏统计。业务层仍以活动+资产流水唯一键兜底 MQ 重投。</p>
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RocketMqMessageListener(
|
||||
groupId = "ASLAN_GAME_KING_WALLET_SYNC_V1",
|
||||
tag = WalletAssetGoldSyncSinkV2.TAG
|
||||
)
|
||||
public class AslanGameKingWalletListener implements MessageListener {
|
||||
|
||||
private final MessageEventProcess messageEventProcess;
|
||||
private final AslanGameKingServiceImpl aslanGameKingService;
|
||||
|
||||
@Override
|
||||
public Action consume(ConsumerMessage message) {
|
||||
return messageEventProcess.consume(
|
||||
MessageEventProcessDescribe.builder()
|
||||
.logTag("Aslan游戏王收支统计")
|
||||
.consumeMsgTimeoutMinute(60)
|
||||
.repeatConsumeMinute(30)
|
||||
.repeatConsumeTag("AslanGameKingWalletSyncV1")
|
||||
.checkConsumeTag(WalletAssetGoldSyncSinkV2.TAG)
|
||||
.message(message)
|
||||
.build(),
|
||||
WalletReceiptSyncEvent.class,
|
||||
aslanGameKingService::processWalletEvent);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,48 @@
|
||||
package com.red.circle.other.app.listener.gift;
|
||||
|
||||
import com.red.circle.component.mq.MessageEventProcess;
|
||||
import com.red.circle.component.mq.MessageEventProcessDescribe;
|
||||
import com.red.circle.component.mq.config.RocketMqMessageListener;
|
||||
import com.red.circle.component.mq.service.Action;
|
||||
import com.red.circle.component.mq.service.ConsumerMessage;
|
||||
import com.red.circle.component.mq.service.MessageListener;
|
||||
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftBatchEvent;
|
||||
import com.red.circle.mq.rocket.business.streams.GiveGiftSink;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGiftChallengeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
|
||||
/**
|
||||
* Aslan Gift Challenge 原始送礼消费者。
|
||||
*
|
||||
* <p>必须使用独立 consumer group 订阅现有 {@link GiveGiftSink#TAG},否则会与礼物主流程
|
||||
* 竞争消息并漏计活动积分。本 listener 对 broker 已接收的消息依靠 RocketMQ 重投和业务
|
||||
* 唯一键实现 at-least-once;原 tag 发送失败仍沿用 GiftMqRetryTask 的全局补偿,活动内
|
||||
* 不再新增 tag 或 Mongo 落库后的二次派生链。</p>
|
||||
*/
|
||||
@RequiredArgsConstructor
|
||||
@RocketMqMessageListener(
|
||||
groupId = "ASLAN_GIFT_CHALLENGE_GIFT",
|
||||
tag = GiveGiftSink.TAG
|
||||
)
|
||||
public class AslanGiftChallengeGiftListener implements MessageListener {
|
||||
|
||||
private final MessageEventProcess messageEventProcess;
|
||||
private final AslanGiftChallengeService aslanGiftChallengeService;
|
||||
|
||||
@Override
|
||||
public Action consume(ConsumerMessage message) {
|
||||
return messageEventProcess.consume(
|
||||
MessageEventProcessDescribe.builder()
|
||||
.logTag("Aslan Gift Challenge 送礼积分")
|
||||
// 原始礼物消息由业务账本的 trackId 唯一键幂等;这里不使用时间窗口吞掉必要重投。
|
||||
.consumeMsgTimeoutMinute(0)
|
||||
.repeatConsumeMinute(0)
|
||||
.repeatConsumeTag("AslanGiftChallengeGift")
|
||||
.checkConsumeTag(GiveGiftSink.TAG)
|
||||
.message(message)
|
||||
.build(),
|
||||
GiveAwayGiftBatchEvent.class,
|
||||
// 不捕获异常:业务处理抛错必须由 MessageEventProcess 转为 FAILURE,交给 MQ 重投。
|
||||
aslanGiftChallengeService::recordGift);
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,27 @@
|
||||
package com.red.circle.other.app.scheduler;
|
||||
|
||||
import com.red.circle.component.redis.annotation.TaskCacheLock;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGameKingService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** 到点自动冻结排行榜并结算 Aslan 游戏王前 30 名。 */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AslanGameKingSettlementTask {
|
||||
|
||||
private final AslanGameKingService aslanGameKingService;
|
||||
|
||||
/**
|
||||
* 五分钟扫描一次;分布式锁保证多实例不并行建快照,记录级状态抢占继续兜底人工并发。
|
||||
*/
|
||||
@Scheduled(cron = "15 */5 * * * ?")
|
||||
@TaskCacheLock(key = "ASLAN_GAME_KING_SETTLEMENT_TASK", expireSecond = 270)
|
||||
public void settleEndedActivities() {
|
||||
// 抽奖事务已提交但进程在发奖期间退出时,父记录会停在 PENDING/PROCESSING;
|
||||
// 单项状态抢占保证恢复扫描只补尚未成功的奖励项。
|
||||
aslanGameKingService.recoverPendingDrawDeliveries();
|
||||
aslanGameKingService.settleDueActivities();
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,26 @@
|
||||
package com.red.circle.other.app.scheduler;
|
||||
|
||||
import com.red.circle.component.redis.annotation.TaskCacheLock;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGiftChallengeService;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/** Gift Challenge 日榜、活动总榜和未完成奖励的恢复调度。 */
|
||||
@Component
|
||||
@RequiredArgsConstructor
|
||||
public class AslanGiftChallengeSettlementTask {
|
||||
|
||||
private final AslanGiftChallengeService service;
|
||||
|
||||
/**
|
||||
* 每五分钟扫描一次。分布式锁降低多实例重复扫描,活动/记录状态条件更新仍作为并发兜底;
|
||||
* 发奖超时会进入 UNKNOWN 等待人工核账,调度器不会猜测失败后盲目重发。
|
||||
*/
|
||||
@Scheduled(cron = "45 */5 * * * ?")
|
||||
@TaskCacheLock(key = "ASLAN_GIFT_CHALLENGE_SETTLEMENT_TASK", expireSecond = 270)
|
||||
public void settleAndRecover() {
|
||||
service.recoverPendingDeliveries();
|
||||
service.settleDuePeriods();
|
||||
}
|
||||
}
|
||||
File diff suppressed because it is too large
Load Diff
File diff suppressed because it is too large
Load Diff
@ -15,4 +15,12 @@
|
||||
<relativePath>./../pom.xml</relativePath>
|
||||
</parent>
|
||||
|
||||
<dependencies>
|
||||
<!-- Gift Challenge consumes the original gift event at the application service boundary. -->
|
||||
<dependency>
|
||||
<groupId>com.red.circle</groupId>
|
||||
<artifactId>business-mq-model</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
</project>
|
||||
|
||||
@ -0,0 +1,68 @@
|
||||
package com.red.circle.other.app.service.activity.aslan;
|
||||
|
||||
import com.red.circle.common.business.dto.cmd.AppExtCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.BackfillResult;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawResult;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Prize;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.PrizeSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Ranking;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SettlementRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.UserState;
|
||||
import java.util.List;
|
||||
|
||||
/** Aslan 游戏王活动应用服务。 */
|
||||
public interface AslanGameKingService {
|
||||
|
||||
Detail detail(AppExtCommand cmd, Long activityId);
|
||||
|
||||
UserState me(AppExtCommand cmd, Long activityId);
|
||||
|
||||
DrawResult draw(AppExtCommand identity, DrawCommand cmd);
|
||||
|
||||
Ranking ranking(AppExtCommand cmd, Long activityId, String rankingType, String period,
|
||||
Integer limit);
|
||||
|
||||
List<DrawRecord> drawRecords(AppExtCommand cmd, Long activityId, Integer limit);
|
||||
|
||||
List<Activity> listActivities();
|
||||
|
||||
Detail manageDetail(Long activityId);
|
||||
|
||||
Long save(SaveCommand cmd);
|
||||
|
||||
void enable(Long activityId, boolean enabled);
|
||||
|
||||
List<Prize> listPrizes(Long activityId);
|
||||
|
||||
void savePrizes(PrizeSaveCommand cmd);
|
||||
|
||||
List<RankReward> listRankRewards(Long activityId);
|
||||
|
||||
void saveRankRewards(RankRewardSaveCommand cmd);
|
||||
|
||||
List<SettlementRecord> listSettlementRecords(Long activityId);
|
||||
|
||||
List<DrawRecord> listManageDrawRecords(Long activityId, String deliveryStatus, Integer limit);
|
||||
|
||||
void retryDraw(Long recordId);
|
||||
|
||||
void settle(Long activityId);
|
||||
|
||||
void settleDueActivities();
|
||||
|
||||
void recoverPendingDrawDeliveries();
|
||||
|
||||
void retrySettlement(Long recordId);
|
||||
|
||||
void resolveUnknownDeliveryItem(Long itemId, boolean delivered);
|
||||
|
||||
BackfillResult backfill(Long activityId, String eventType, Integer receiptType, Long lastId,
|
||||
Integer limit);
|
||||
}
|
||||
@ -0,0 +1,66 @@
|
||||
package com.red.circle.other.app.service.activity.aslan;
|
||||
|
||||
import com.red.circle.common.business.dto.cmd.AppExtCommand;
|
||||
import com.red.circle.mq.business.model.event.gift.GiveAwayGiftBatchEvent;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Ranking;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankingQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Task;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.TaskSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.UserState;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* Aslan Gift Challenge 应用服务。
|
||||
*
|
||||
* <p>H5 入口只接收网关注入身份;礼物积分直接消费原始送礼事件并以事件 trackId 幂等,
|
||||
* 管理端则通过独立 inner client 调用同一套校验和结算状态机,避免出现两套积分或发奖口径。</p>
|
||||
*/
|
||||
public interface AslanGiftChallengeService {
|
||||
|
||||
Detail detail(AppExtCommand cmd, Long activityId);
|
||||
|
||||
UserState enter(AppExtCommand cmd, Long activityId);
|
||||
|
||||
Ranking ranking(AppExtCommand cmd, Long activityId, String period, String statDate,
|
||||
Integer limit);
|
||||
|
||||
UserState claimTask(AppExtCommand cmd, Long activityId, String taskCode, String requestId);
|
||||
|
||||
/** 直接消费原始送礼事件;返回 false 表示不符合资格、无活动命中或幂等重复。 */
|
||||
boolean recordGift(GiveAwayGiftBatchEvent event);
|
||||
|
||||
List<Activity> listActivities();
|
||||
|
||||
Detail manageDetail(Long activityId);
|
||||
|
||||
Long save(SaveCommand cmd);
|
||||
|
||||
void enable(Long activityId, boolean enabled);
|
||||
|
||||
List<Task> listTasks(Long activityId);
|
||||
|
||||
void saveTasks(TaskSaveCommand cmd);
|
||||
|
||||
List<RankReward> listRankRewards(Long activityId);
|
||||
|
||||
void saveRankRewards(RankRewardSaveCommand cmd);
|
||||
|
||||
Ranking manageRanking(Long activityId, RankingQuery query);
|
||||
|
||||
List<SettlementRecord> listSettlementRecords(Long activityId, SettlementQuery query);
|
||||
|
||||
void settle(Long activityId, String period, String statDate);
|
||||
|
||||
void settleDuePeriods();
|
||||
|
||||
void recoverPendingDeliveries();
|
||||
|
||||
void resolveUnknownDeliveryItem(Long itemId, boolean delivered);
|
||||
}
|
||||
@ -0,0 +1,520 @@
|
||||
package com.red.circle.other.infra.database.rds.dao.activity.aslan;
|
||||
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGameKingRows.ActivityRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGameKingRows.DeliveryItemRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGameKingRows.RankingRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGameKingRows.UserRow;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Prize;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SettlementRecord;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* Aslan 游戏王活动的专用持久化入口。
|
||||
*
|
||||
* <p>高频链路只访问带唯一键或排行榜复合索引的表。消费事件先写唯一流水,再原子累加
|
||||
* 用户汇总;抽奖只更新一行用户汇总,避免扫描消费流水或在 JVM 内做机会扣减。</p>
|
||||
*/
|
||||
public interface AslanGameKingMapper {
|
||||
|
||||
String ACTIVITY_COLUMNS = "id, activity_code, activity_name, activity_desc, sys_origin, "
|
||||
+ "time_zone, start_time, end_time, settlement_delay_minutes, settlement_time,"
|
||||
+ " coin_per_draw, event_types, ranking_type, ranking_period, "
|
||||
+ "enabled, settlement_status, create_time, update_time";
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_game_king_activity ORDER BY start_time DESC, id DESC")
|
||||
List<ActivityRow> listActivities();
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_game_king_activity WHERE id = #{id} LIMIT 1")
|
||||
ActivityRow getActivity(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_game_king_activity WHERE id = #{id} LIMIT 1 FOR UPDATE")
|
||||
ActivityRow lockActivity(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 钱包入账事务的活动级共享门闩。共享锁之间可并行;结算的 FOR UPDATE 会等待所有已进入
|
||||
* 的流水事务完成,并阻止新流水越过快照边界。
|
||||
*/
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_game_king_activity WHERE id = #{id} LIMIT 1 LOCK IN SHARE MODE")
|
||||
ActivityRow lockActivityForWalletGate(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_game_king_activity"
|
||||
+ " WHERE sys_origin = #{sysOrigin} AND enabled = 1"
|
||||
+ " ORDER BY CASE WHEN start_time <= #{now} AND end_time > #{now} THEN 0"
|
||||
+ " WHEN start_time > #{now} THEN 1 ELSE 2 END ASC,"
|
||||
+ " CASE WHEN start_time <= #{now} AND end_time > #{now} THEN start_time END DESC,"
|
||||
+ " CASE WHEN start_time > #{now} THEN start_time END ASC, end_time DESC LIMIT 1")
|
||||
ActivityRow findDisplayActivity(@Param("sysOrigin") String sysOrigin,
|
||||
@Param("now") Timestamp now);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_game_king_activity"
|
||||
+ " WHERE sys_origin = #{sysOrigin} AND enabled = 1"
|
||||
+ " AND start_time <= #{eventTime} AND end_time > #{eventTime}"
|
||||
+ " ORDER BY start_time DESC")
|
||||
List<ActivityRow> findActiveActivities(@Param("sysOrigin") String sysOrigin,
|
||||
@Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Select("SELECT id FROM aslan_game_king_activity WHERE sys_origin = #{sysOrigin}"
|
||||
+ " AND enabled = 1 AND start_time < #{endTime} AND end_time > #{startTime}"
|
||||
+ " AND (#{excludeId} IS NULL OR id <> #{excludeId}) FOR UPDATE")
|
||||
List<Long> lockEnabledOverlaps(@Param("sysOrigin") String sysOrigin,
|
||||
@Param("startTime") Timestamp startTime, @Param("endTime") Timestamp endTime,
|
||||
@Param("excludeId") Long excludeId);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_game_king_activity WHERE settlement_time <= #{now}"
|
||||
+ " AND settlement_status IN ('NOT_STARTED', 'PROCESSING')"
|
||||
+ " ORDER BY end_time ASC LIMIT 20")
|
||||
List<ActivityRow> listDueSettlements(@Param("now") Timestamp now);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_game_king_activity"
|
||||
+ " WHERE activity_code = #{activityCode} AND (#{excludeId} IS NULL OR id <> #{excludeId})")
|
||||
int countActivityCode(@Param("activityCode") String activityCode,
|
||||
@Param("excludeId") Long excludeId);
|
||||
|
||||
@Insert("INSERT INTO aslan_game_king_activity"
|
||||
+ " (id, activity_code, activity_name, activity_desc, sys_origin, start_time, end_time,"
|
||||
+ " time_zone, settlement_delay_minutes, settlement_time, coin_per_draw, event_types,"
|
||||
+ " ranking_type, ranking_period, enabled, settlement_status,"
|
||||
+ " create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityCode}, #{activityName}, #{activityDesc}, #{sysOrigin}, #{startTime},"
|
||||
+ " #{endTime}, #{timeZone}, #{settlementDelayMinutes}, #{settlementTime}, #{coinPerDraw},"
|
||||
+ " #{eventTypes}, #{rankingType}, #{rankingPeriod}, #{enabled},"
|
||||
+ " #{settlementStatus}, NOW(), NOW())")
|
||||
int insertActivity(ActivityRow row);
|
||||
|
||||
@Update("UPDATE aslan_game_king_activity SET activity_code = #{activityCode},"
|
||||
+ " activity_name = #{activityName}, activity_desc = #{activityDesc}, sys_origin = #{sysOrigin},"
|
||||
+ " time_zone = #{timeZone}, start_time = #{startTime}, end_time = #{endTime},"
|
||||
+ " settlement_delay_minutes = #{settlementDelayMinutes}, settlement_time = #{settlementTime},"
|
||||
+ " coin_per_draw = #{coinPerDraw},"
|
||||
+ " event_types = #{eventTypes}, ranking_type = #{rankingType},"
|
||||
+ " ranking_period = #{rankingPeriod}, enabled = #{enabled}, update_time = NOW()"
|
||||
+ " WHERE id = #{id}")
|
||||
int updateActivity(ActivityRow row);
|
||||
|
||||
@Update("UPDATE aslan_game_king_activity SET enabled = #{enabled}, update_time = NOW()"
|
||||
+ " WHERE id = #{id}")
|
||||
int updateEnabled(@Param("id") Long id, @Param("enabled") boolean enabled);
|
||||
|
||||
@Update("UPDATE aslan_game_king_activity SET settlement_status = #{status}, update_time = NOW()"
|
||||
+ " WHERE id = #{id}")
|
||||
int updateSettlementStatus(@Param("id") Long id, @Param("status") String status);
|
||||
|
||||
@Select("SELECT id, activity_id, prize_name, prize_image, weight, stock, resource_group_id, enabled,"
|
||||
+ " sort_order FROM aslan_game_king_prize WHERE activity_id = #{activityId}"
|
||||
+ " ORDER BY sort_order ASC, id ASC")
|
||||
List<Prize> listPrizes(@Param("activityId") Long activityId);
|
||||
|
||||
@Delete("DELETE FROM aslan_game_king_prize WHERE activity_id = #{activityId}")
|
||||
int deletePrizes(@Param("activityId") Long activityId);
|
||||
|
||||
@Insert("INSERT INTO aslan_game_king_prize"
|
||||
+ " (id, activity_id, prize_name, prize_image, weight, resource_group_id, enabled, sort_order,"
|
||||
+ " stock,"
|
||||
+ " create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{prizeName}, #{prizeImage}, #{weight}, #{resourceGroupId},"
|
||||
+ " #{enabled}, #{sortOrder}, #{stock}, NOW(), NOW())")
|
||||
int insertPrize(Prize prize);
|
||||
|
||||
@Update("UPDATE aslan_game_king_prize SET stock = CASE WHEN stock = -1 THEN -1 ELSE stock - 1 END,"
|
||||
+ " update_time = NOW() WHERE id = #{prizeId} AND activity_id = #{activityId}"
|
||||
+ " AND enabled = 1 AND (stock = -1 OR stock > 0)")
|
||||
int claimPrizeStock(@Param("activityId") Long activityId, @Param("prizeId") Long prizeId);
|
||||
|
||||
@Select("SELECT id, activity_id, start_rank, end_rank, resource_group_id, reward_name"
|
||||
+ " FROM aslan_game_king_rank_reward WHERE activity_id = #{activityId}"
|
||||
+ " ORDER BY start_rank ASC")
|
||||
List<RankReward> listRankRewards(@Param("activityId") Long activityId);
|
||||
|
||||
@Delete("DELETE FROM aslan_game_king_rank_reward WHERE activity_id = #{activityId}")
|
||||
int deleteRankRewards(@Param("activityId") Long activityId);
|
||||
|
||||
@Insert("INSERT INTO aslan_game_king_rank_reward"
|
||||
+ " (id, activity_id, start_rank, end_rank, resource_group_id, reward_name, create_time, update_time)"
|
||||
+ " VALUES (#{id}, #{activityId}, #{startRank}, #{endRank}, #{resourceGroupId}, #{rewardName},"
|
||||
+ " NOW(), NOW())")
|
||||
int insertRankReward(RankReward reward);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_game_king_wallet_ledger"
|
||||
+ " (id, activity_id, asset_record_id, user_id, sys_origin, event_type, event_id, receipt_type, amount,"
|
||||
+ " event_time, create_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{assetRecordId}, #{userId}, #{sysOrigin}, #{eventType}, #{eventId},"
|
||||
+ " #{receiptType}, #{amount}, #{eventTime}, NOW())")
|
||||
int insertConsumeLedger(@Param("id") Long id, @Param("activityId") Long activityId,
|
||||
@Param("assetRecordId") Long assetRecordId, @Param("userId") Long userId,
|
||||
@Param("sysOrigin") String sysOrigin, @Param("eventType") String eventType,
|
||||
@Param("eventId") String eventId, @Param("receiptType") int receiptType,
|
||||
@Param("amount") BigDecimal amount,
|
||||
@Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_game_king_user"
|
||||
+ " (activity_id, user_id, total_consumed, total_won, used_chances, create_time, update_time)"
|
||||
+ " VALUES (#{activityId}, #{userId}, 0, 0, 0, NOW(), NOW())")
|
||||
int ensureUser(@Param("activityId") Long activityId, @Param("userId") Long userId);
|
||||
|
||||
@Update("UPDATE aslan_game_king_user SET total_consumed = total_consumed + #{amount},"
|
||||
+ " consume_reached_time = CASE WHEN consume_reached_time IS NULL THEN #{eventTime}"
|
||||
+ " ELSE GREATEST(consume_reached_time, #{eventTime}) END, update_time = NOW()"
|
||||
+ " WHERE activity_id = #{activityId} AND user_id = #{userId}")
|
||||
int incrementUserConsumption(@Param("activityId") Long activityId,
|
||||
@Param("userId") Long userId, @Param("amount") BigDecimal amount,
|
||||
@Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Update("UPDATE aslan_game_king_user SET total_won = total_won + #{amount},"
|
||||
+ " win_reached_time = CASE WHEN win_reached_time IS NULL THEN #{eventTime}"
|
||||
+ " ELSE GREATEST(win_reached_time, #{eventTime}) END, update_time = NOW()"
|
||||
+ " WHERE activity_id = #{activityId} AND user_id = #{userId}")
|
||||
int incrementUserWinning(@Param("activityId") Long activityId,
|
||||
@Param("userId") Long userId, @Param("amount") BigDecimal amount,
|
||||
@Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_game_king_user_daily"
|
||||
+ " (activity_id, stat_date, user_id, total_consumed, total_won, create_time, update_time)"
|
||||
+ " VALUES (#{activityId}, #{statDate}, #{userId}, 0, 0, NOW(), NOW())")
|
||||
int ensureDailyUser(@Param("activityId") Long activityId, @Param("statDate") Date statDate,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
@Update("UPDATE aslan_game_king_user_daily SET total_consumed = total_consumed + #{amount},"
|
||||
+ " consume_reached_time = CASE WHEN consume_reached_time IS NULL THEN #{eventTime}"
|
||||
+ " ELSE GREATEST(consume_reached_time, #{eventTime}) END, update_time = NOW()"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate} AND user_id = #{userId}")
|
||||
int incrementDailyConsumption(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId,
|
||||
@Param("amount") BigDecimal amount, @Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Update("UPDATE aslan_game_king_user_daily SET total_won = total_won + #{amount},"
|
||||
+ " win_reached_time = CASE WHEN win_reached_time IS NULL THEN #{eventTime}"
|
||||
+ " ELSE GREATEST(win_reached_time, #{eventTime}) END, update_time = NOW()"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate} AND user_id = #{userId}")
|
||||
int incrementDailyWinning(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId,
|
||||
@Param("amount") BigDecimal amount, @Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Select("SELECT u.activity_id, u.user_id, u.total_consumed, u.total_won,"
|
||||
+ " FLOOR(u.total_consumed / a.coin_per_draw) AS earned_chances, u.used_chances,"
|
||||
+ " u.consume_reached_time, u.win_reached_time, u.update_time FROM aslan_game_king_user u"
|
||||
+ " JOIN aslan_game_king_activity a ON a.id = u.activity_id"
|
||||
+ " WHERE u.activity_id = #{activityId} AND u.user_id = #{userId} LIMIT 1")
|
||||
UserRow getUser(@Param("activityId") Long activityId, @Param("userId") Long userId);
|
||||
|
||||
@Update("UPDATE aslan_game_king_user u JOIN aslan_game_king_activity a ON a.id = u.activity_id"
|
||||
+ " SET u.used_chances = u.used_chances + 1, u.update_time = NOW()"
|
||||
+ " WHERE u.activity_id = #{activityId} AND u.user_id = #{userId}"
|
||||
+ " AND FLOOR(u.total_consumed / a.coin_per_draw) > u.used_chances")
|
||||
int consumeOneChance(@Param("activityId") Long activityId, @Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT user_id, total_consumed AS score FROM aslan_game_king_user"
|
||||
+ " WHERE activity_id = #{activityId} AND total_consumed > 0"
|
||||
+ " ORDER BY total_consumed DESC, consume_reached_time ASC, user_id ASC LIMIT #{limit}")
|
||||
List<RankingRow> listTycoonOverall(@Param("activityId") Long activityId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Select("SELECT COUNT(1) + 1 FROM aslan_game_king_user target"
|
||||
+ " JOIN aslan_game_king_user current"
|
||||
+ " ON current.activity_id = target.activity_id AND current.user_id = #{userId}"
|
||||
+ " WHERE target.activity_id = #{activityId} AND target.total_consumed > 0 AND ("
|
||||
+ " target.total_consumed > current.total_consumed OR"
|
||||
+ " (target.total_consumed = current.total_consumed AND target.consume_reached_time < current.consume_reached_time) OR"
|
||||
+ " (target.total_consumed = current.total_consumed AND target.consume_reached_time = current.consume_reached_time"
|
||||
+ " AND target.user_id < current.user_id))")
|
||||
Integer getTycoonOverallRank(@Param("activityId") Long activityId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT user_id, total_won AS score FROM aslan_game_king_user"
|
||||
+ " WHERE activity_id = #{activityId} AND total_won > 0"
|
||||
+ " ORDER BY total_won DESC, win_reached_time ASC, user_id ASC LIMIT #{limit}")
|
||||
List<RankingRow> listVictoriousOverall(@Param("activityId") Long activityId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Select("SELECT COUNT(1) + 1 FROM aslan_game_king_user target"
|
||||
+ " JOIN aslan_game_king_user current"
|
||||
+ " ON current.activity_id = target.activity_id AND current.user_id = #{userId}"
|
||||
+ " WHERE target.activity_id = #{activityId} AND target.total_won > 0 AND ("
|
||||
+ " target.total_won > current.total_won OR"
|
||||
+ " (target.total_won = current.total_won AND target.win_reached_time < current.win_reached_time) OR"
|
||||
+ " (target.total_won = current.total_won AND target.win_reached_time = current.win_reached_time"
|
||||
+ " AND target.user_id < current.user_id))")
|
||||
Integer getVictoriousOverallRank(@Param("activityId") Long activityId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT user_id, total_consumed AS score FROM aslan_game_king_user_daily"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate} AND total_consumed > 0"
|
||||
+ " ORDER BY total_consumed DESC, consume_reached_time ASC, user_id ASC LIMIT #{limit}")
|
||||
List<RankingRow> listTycoonDaily(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT COUNT(1) + 1 FROM aslan_game_king_user_daily target"
|
||||
+ " JOIN aslan_game_king_user_daily current ON current.activity_id = target.activity_id"
|
||||
+ " AND current.stat_date = target.stat_date AND current.user_id = #{userId}"
|
||||
+ " WHERE target.activity_id = #{activityId} AND target.stat_date = #{statDate}"
|
||||
+ " AND target.total_consumed > 0 AND (target.total_consumed > current.total_consumed OR"
|
||||
+ " (target.total_consumed = current.total_consumed"
|
||||
+ " AND target.consume_reached_time < current.consume_reached_time) OR"
|
||||
+ " (target.total_consumed = current.total_consumed"
|
||||
+ " AND target.consume_reached_time = current.consume_reached_time"
|
||||
+ " AND target.user_id < current.user_id))")
|
||||
Integer getTycoonDailyRank(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT user_id, total_won AS score FROM aslan_game_king_user_daily"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate} AND total_won > 0"
|
||||
+ " ORDER BY total_won DESC, win_reached_time ASC, user_id ASC LIMIT #{limit}")
|
||||
List<RankingRow> listVictoriousDaily(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT COUNT(1) + 1 FROM aslan_game_king_user_daily target"
|
||||
+ " JOIN aslan_game_king_user_daily current ON current.activity_id = target.activity_id"
|
||||
+ " AND current.stat_date = target.stat_date AND current.user_id = #{userId}"
|
||||
+ " WHERE target.activity_id = #{activityId} AND target.stat_date = #{statDate}"
|
||||
+ " AND target.total_won > 0 AND (target.total_won > current.total_won OR"
|
||||
+ " (target.total_won = current.total_won AND target.win_reached_time < current.win_reached_time) OR"
|
||||
+ " (target.total_won = current.total_won AND target.win_reached_time = current.win_reached_time"
|
||||
+ " AND target.user_id < current.user_id))")
|
||||
Integer getVictoriousDailyRank(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT total_consumed FROM aslan_game_king_user_daily"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate}"
|
||||
+ " AND user_id = #{userId} LIMIT 1")
|
||||
BigDecimal getDailyTycoonScore(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT total_won FROM aslan_game_king_user_daily"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate}"
|
||||
+ " AND user_id = #{userId} LIMIT 1")
|
||||
BigDecimal getDailyVictoriousScore(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT id, activity_id, user_id, request_id, prize_id, prize_name, prize_image,"
|
||||
+ " resource_group_id, delivery_status, failure_reason,"
|
||||
+ " UNIX_TIMESTAMP(draw_time) * 1000 AS draw_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_game_king_draw_record"
|
||||
+ " WHERE activity_id = #{activityId} AND user_id = #{userId} AND request_id = #{requestId} LIMIT 1")
|
||||
DrawRecord getDrawByRequest(@Param("activityId") Long activityId,
|
||||
@Param("userId") Long userId, @Param("requestId") String requestId);
|
||||
|
||||
@Select("SELECT id, activity_id, user_id, request_id, prize_id, prize_name, prize_image,"
|
||||
+ " resource_group_id, delivery_status, failure_reason,"
|
||||
+ " UNIX_TIMESTAMP(draw_time) * 1000 AS draw_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_game_king_draw_record WHERE id = #{id} LIMIT 1")
|
||||
DrawRecord getDrawRecord(@Param("id") Long id);
|
||||
|
||||
@Insert("INSERT INTO aslan_game_king_draw_record"
|
||||
+ " (id, activity_id, user_id, request_id, prize_id, prize_name, prize_image, resource_group_id,"
|
||||
+ " delivery_status, draw_time, create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{userId}, #{requestId}, #{prizeId}, #{prizeName}, #{prizeImage},"
|
||||
+ " #{resourceGroupId}, #{deliveryStatus}, NOW(), NOW(), NOW())")
|
||||
int insertDrawRecord(DrawRecord record);
|
||||
|
||||
@Update("UPDATE aslan_game_king_draw_record SET delivery_status = 'PROCESSING',"
|
||||
+ " failure_reason = NULL, update_time = NOW() WHERE id = #{id}"
|
||||
+ " AND delivery_status = 'PENDING'")
|
||||
int claimDrawDelivery(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_draw_record SET delivery_status = 'PENDING',"
|
||||
+ " update_time = NOW() WHERE id = #{id} AND delivery_status = 'FAILED'")
|
||||
int prepareFailedDrawRetry(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_draw_record SET delivery_status = #{status},"
|
||||
+ " failure_reason = #{failureReason}, deliver_time = CASE WHEN #{status} = 'SUCCESS' THEN NOW()"
|
||||
+ " ELSE deliver_time END, update_time = NOW() WHERE id = #{id}")
|
||||
int finishDrawDelivery(@Param("id") Long id, @Param("status") String status,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Select("SELECT id, activity_id, user_id, request_id, prize_id, prize_name, prize_image,"
|
||||
+ " resource_group_id, delivery_status, failure_reason,"
|
||||
+ " UNIX_TIMESTAMP(draw_time) * 1000 AS draw_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_game_king_draw_record WHERE activity_id = #{activityId} AND user_id = #{userId}"
|
||||
+ " ORDER BY id DESC LIMIT #{limit}")
|
||||
List<DrawRecord> listDrawRecords(@Param("activityId") Long activityId,
|
||||
@Param("userId") Long userId, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT id, activity_id, user_id, request_id, prize_id, prize_name, prize_image,"
|
||||
+ " resource_group_id, delivery_status, failure_reason,"
|
||||
+ " UNIX_TIMESTAMP(draw_time) * 1000 AS draw_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_game_king_draw_record WHERE activity_id = #{activityId}"
|
||||
+ " AND (#{deliveryStatus} IS NULL OR delivery_status = #{deliveryStatus})"
|
||||
+ " ORDER BY id DESC LIMIT #{limit}")
|
||||
List<DrawRecord> listManageDrawRecords(@Param("activityId") Long activityId,
|
||||
@Param("deliveryStatus") String deliveryStatus, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT id, activity_id, user_id, request_id, prize_id, prize_name, prize_image,"
|
||||
+ " resource_group_id, delivery_status, failure_reason,"
|
||||
+ " UNIX_TIMESTAMP(draw_time) * 1000 AS draw_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_game_king_draw_record"
|
||||
+ " WHERE delivery_status = 'PENDING'"
|
||||
+ " OR (delivery_status = 'PROCESSING' AND update_time <= #{staleBefore})"
|
||||
+ " ORDER BY update_time ASC, id ASC LIMIT #{limit}")
|
||||
List<DrawRecord> listRecoverableDrawRecords(@Param("staleBefore") Timestamp staleBefore,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_game_king_settlement_record"
|
||||
+ " (id, activity_id, user_id, rank_no, total_consumed, rank_reward_id, resource_group_id,"
|
||||
+ " business_no, delivery_status, retry_count, create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{userId}, #{rank}, #{totalConsumed}, #{rankRewardId},"
|
||||
+ " #{resourceGroupId}, #{businessNo}, #{deliveryStatus}, 0, NOW(), NOW())")
|
||||
int insertSettlementRecord(SettlementRecord record);
|
||||
|
||||
@Select("SELECT id, activity_id, user_id, rank_no AS rank, total_consumed, rank_reward_id,"
|
||||
+ " resource_group_id, business_no, delivery_status, retry_count, failure_reason,"
|
||||
+ " UNIX_TIMESTAMP(create_time) * 1000 AS create_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_game_king_settlement_record WHERE activity_id = #{activityId}"
|
||||
+ " ORDER BY rank_no ASC")
|
||||
List<SettlementRecord> listSettlementRecords(@Param("activityId") Long activityId);
|
||||
|
||||
@Select("SELECT id, activity_id, user_id, rank_no AS rank, total_consumed, rank_reward_id,"
|
||||
+ " resource_group_id, business_no, delivery_status, retry_count, failure_reason,"
|
||||
+ " UNIX_TIMESTAMP(create_time) * 1000 AS create_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_game_king_settlement_record WHERE id = #{id} LIMIT 1")
|
||||
SettlementRecord getSettlementRecord(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_settlement_record SET"
|
||||
+ " delivery_status = 'PROCESSING', failure_reason = NULL, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'PENDING'")
|
||||
int claimSettlementDelivery(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_settlement_record SET delivery_status = 'PENDING',"
|
||||
+ " retry_count = retry_count + 1, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'FAILED'")
|
||||
int prepareFailedSettlementRetry(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_settlement_record SET delivery_status = #{status},"
|
||||
+ " failure_reason = #{failureReason}, deliver_time = CASE WHEN #{status} = 'SUCCESS' THEN NOW()"
|
||||
+ " ELSE deliver_time END, update_time = NOW() WHERE id = #{id}")
|
||||
int finishSettlementDelivery(@Param("id") Long id, @Param("status") String status,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_game_king_settlement_record"
|
||||
+ " WHERE activity_id = #{activityId} AND delivery_status <> 'SUCCESS'")
|
||||
int countUnfinishedSettlements(@Param("activityId") Long activityId);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_game_king_settlement_record"
|
||||
+ " WHERE activity_id = #{activityId} AND delivery_status IN ('FAILED', 'UNKNOWN')")
|
||||
int countFailedSettlements(@Param("activityId") Long activityId);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_game_king_delivery_item"
|
||||
+ " (id, owner_type, owner_id, activity_id, user_id, reward_config_id, resource_group_id,"
|
||||
+ " reward_type, detail_type, content, quantity, sort_order, remark, delivery_status,"
|
||||
+ " retry_count, create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{ownerType}, #{ownerId}, #{activityId}, #{userId}, #{rewardConfigId},"
|
||||
+ " #{resourceGroupId}, #{rewardType}, #{detailType}, #{content}, #{quantity}, #{sortOrder},"
|
||||
+ " #{remark}, #{deliveryStatus}, 0, NOW(), NOW())")
|
||||
int insertDeliveryItem(DeliveryItemRow item);
|
||||
|
||||
@Select("SELECT id, owner_type, owner_id, activity_id, user_id, reward_config_id,"
|
||||
+ " resource_group_id, reward_type, detail_type, content, quantity, sort_order, remark,"
|
||||
+ " delivery_status, retry_count, failure_reason, deliver_time"
|
||||
+ " FROM aslan_game_king_delivery_item WHERE owner_type = #{ownerType}"
|
||||
+ " AND owner_id = #{ownerId} ORDER BY sort_order ASC, id ASC")
|
||||
List<DeliveryItemRow> listDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT id, owner_type, owner_id, activity_id, user_id, reward_config_id,"
|
||||
+ " resource_group_id, reward_type, detail_type, content, quantity, sort_order, remark,"
|
||||
+ " delivery_status, retry_count, failure_reason, deliver_time"
|
||||
+ " FROM aslan_game_king_delivery_item WHERE id = #{id} LIMIT 1")
|
||||
DeliveryItemRow getDeliveryItem(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_delivery_item SET delivery_status = 'PROCESSING',"
|
||||
+ " failure_reason = NULL, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'PENDING'")
|
||||
int claimDeliveryItem(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_delivery_item SET delivery_status = 'SUCCESS',"
|
||||
+ " failure_reason = NULL, deliver_time = NOW(), update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'PROCESSING'")
|
||||
int finishDeliveryItemSuccess(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_delivery_item SET delivery_status = 'UNKNOWN',"
|
||||
+ " failure_reason = #{failureReason}, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'PROCESSING'")
|
||||
int finishProcessingDeliveryItemUnknown(@Param("id") Long id,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Update("UPDATE aslan_game_king_delivery_item SET delivery_status = 'UNKNOWN',"
|
||||
+ " failure_reason = #{failureReason}, update_time = NOW()"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status = 'PROCESSING' AND update_time <= #{staleBefore}")
|
||||
int expireProcessingDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId, @Param("staleBefore") Timestamp staleBefore,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Update("UPDATE aslan_game_king_delivery_item SET delivery_status = 'PENDING',"
|
||||
+ " retry_count = retry_count + 1, failure_reason = NULL, update_time = NOW()"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status = 'FAILED'")
|
||||
int prepareFailedDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT id, owner_type, owner_id, activity_id, user_id, reward_config_id,"
|
||||
+ " resource_group_id, reward_type, detail_type, content, quantity, sort_order, remark,"
|
||||
+ " delivery_status, retry_count, failure_reason, deliver_time"
|
||||
+ " FROM aslan_game_king_delivery_item WHERE id = #{id} LIMIT 1 FOR UPDATE")
|
||||
DeliveryItemRow lockDeliveryItem(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_delivery_item SET delivery_status = #{resolvedStatus},"
|
||||
+ " retry_count = retry_count + CASE WHEN #{resolvedStatus} = 'PENDING' THEN 1 ELSE 0 END,"
|
||||
+ " failure_reason = CASE WHEN #{resolvedStatus} = 'SUCCESS'"
|
||||
+ " THEN 'OPERATOR_CONFIRMED_DELIVERED' ELSE NULL END,"
|
||||
+ " deliver_time = CASE WHEN #{resolvedStatus} = 'SUCCESS'"
|
||||
+ " THEN COALESCE(deliver_time, NOW()) ELSE deliver_time END, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'UNKNOWN'")
|
||||
int resolveUnknownDeliveryItem(@Param("id") Long id,
|
||||
@Param("resolvedStatus") String resolvedStatus);
|
||||
|
||||
@Update("UPDATE aslan_game_king_draw_record SET delivery_status = 'PENDING',"
|
||||
+ " failure_reason = NULL, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'UNKNOWN'")
|
||||
int reopenUnknownDrawDelivery(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_game_king_settlement_record SET delivery_status = 'PENDING',"
|
||||
+ " retry_count = retry_count + 1, failure_reason = NULL, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'UNKNOWN'")
|
||||
int reopenUnknownSettlementDelivery(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_game_king_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}")
|
||||
int countDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_game_king_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status <> 'SUCCESS'")
|
||||
int countUnfinishedDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_game_king_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status = 'FAILED'")
|
||||
int countFailedDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_game_king_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status = 'UNKNOWN'")
|
||||
int countUnknownDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
}
|
||||
@ -0,0 +1,589 @@
|
||||
package com.red.circle.other.infra.database.rds.dao.activity.aslan;
|
||||
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.ActivityRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.DeliveryItemRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.GiftLedgerRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.PeriodSettlementRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.RankingRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.RewardSnapshotRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.TaskDailyRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.UserDailyScoreRow;
|
||||
import com.red.circle.other.infra.database.rds.model.activity.aslan.AslanGiftChallengeRows.UserScoreRow;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Task;
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import java.util.List;
|
||||
import org.apache.ibatis.annotations.Delete;
|
||||
import org.apache.ibatis.annotations.Insert;
|
||||
import org.apache.ibatis.annotations.Param;
|
||||
import org.apache.ibatis.annotations.Select;
|
||||
import org.apache.ibatis.annotations.Update;
|
||||
|
||||
/**
|
||||
* Aslan 礼物挑战专用持久化入口。
|
||||
*
|
||||
* <p>送礼热链路只写唯一流水及按主键定位的总榜、日榜、任务行。所有榜单读取都从物化
|
||||
* 聚合表走活动前缀复合索引;禁止在请求或结算时扫描礼物事实账本做临时聚合。</p>
|
||||
*/
|
||||
public interface AslanGiftChallengeMapper {
|
||||
|
||||
String ACTIVITY_COLUMNS = "id, activity_code, activity_name, activity_desc, sys_origin, "
|
||||
+ "time_zone, start_time, end_time, daily_settlement_delay_minutes, "
|
||||
+ "overall_settlement_delay_minutes, overall_settlement_time, display_top_n, enabled, "
|
||||
+ "overall_settlement_status, version, create_time, update_time";
|
||||
|
||||
String TASK_DAILY_COLUMNS = "id, activity_id, stat_date, user_id, task_config_id, task_code, "
|
||||
+ "task_type, task_title, sort_order, target_value, progress_value, completed_time, "
|
||||
+ "resource_group_id, business_no, claim_request_id, delivery_status, retry_count, failure_reason, "
|
||||
+ "claimed_time, deliver_time, update_time";
|
||||
|
||||
String PERIOD_COLUMNS = "id, activity_id, period_type, period_key, stat_date, "
|
||||
+ "snapshot_due_time, status, create_time, update_time";
|
||||
|
||||
String DELIVERY_COLUMNS = "id, owner_type, owner_id, activity_id, user_id, reward_config_id, "
|
||||
+ "resource_group_id, reward_type, detail_type, content, quantity, sort_order, remark, "
|
||||
+ "delivery_status, retry_count, failure_reason, deliver_time, update_time";
|
||||
|
||||
String REWARD_SNAPSHOT_COLUMNS = "id, activity_id, resource_group_id, reward_config_id, "
|
||||
+ "reward_type, detail_type, content, quantity, sort_order, remark, cover, source_url, "
|
||||
+ "display_name";
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_activity ORDER BY start_time DESC, id DESC")
|
||||
List<ActivityRow> listActivities();
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_activity WHERE id = #{id} LIMIT 1")
|
||||
ActivityRow getActivity(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_activity WHERE id = #{id} LIMIT 1 FOR UPDATE")
|
||||
ActivityRow lockActivity(@Param("id") Long id);
|
||||
|
||||
/**
|
||||
* 礼物事务共享锁与 OVERALL 结算独占锁形成快照门闩;DAILY 同时锁对应 period header。
|
||||
*/
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_activity WHERE id = #{id} LIMIT 1 LOCK IN SHARE MODE")
|
||||
ActivityRow lockActivityForGiftGate(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_activity"
|
||||
+ " WHERE sys_origin = #{sysOrigin} AND enabled = 1"
|
||||
+ " ORDER BY CASE WHEN start_time <= #{now} AND end_time > #{now} THEN 0"
|
||||
+ " WHEN start_time > #{now} THEN 1 ELSE 2 END ASC,"
|
||||
+ " CASE WHEN start_time <= #{now} AND end_time > #{now} THEN start_time END DESC,"
|
||||
+ " CASE WHEN start_time > #{now} THEN start_time END ASC, end_time DESC LIMIT 1")
|
||||
ActivityRow findDisplayActivity(@Param("sysOrigin") String sysOrigin,
|
||||
@Param("now") Timestamp now);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_activity"
|
||||
+ " WHERE sys_origin = #{sysOrigin} AND enabled = 1"
|
||||
+ " AND start_time <= #{eventTime} AND end_time > #{eventTime}"
|
||||
+ " ORDER BY start_time DESC")
|
||||
List<ActivityRow> findActiveActivities(@Param("sysOrigin") String sysOrigin,
|
||||
@Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Select("SELECT id FROM aslan_gift_challenge_activity WHERE sys_origin = #{sysOrigin}"
|
||||
+ " AND enabled = 1 AND start_time < #{endTime} AND end_time > #{startTime}"
|
||||
+ " AND (#{excludeId} IS NULL OR id <> #{excludeId}) FOR UPDATE")
|
||||
List<Long> lockEnabledOverlaps(@Param("sysOrigin") String sysOrigin,
|
||||
@Param("startTime") Timestamp startTime, @Param("endTime") Timestamp endTime,
|
||||
@Param("excludeId") Long excludeId);
|
||||
|
||||
@Select("SELECT " + ACTIVITY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_activity"
|
||||
+ " WHERE overall_settlement_time <= #{now}"
|
||||
+ " AND overall_settlement_status IN ('NOT_STARTED', 'PROCESSING')"
|
||||
+ " ORDER BY overall_settlement_time ASC, id ASC LIMIT #{limit}")
|
||||
List<ActivityRow> listDueOverallActivities(@Param("now") Timestamp now,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_gift_challenge_activity"
|
||||
+ " WHERE activity_code = #{activityCode} AND (#{excludeId} IS NULL OR id <> #{excludeId})")
|
||||
int countActivityCode(@Param("activityCode") String activityCode,
|
||||
@Param("excludeId") Long excludeId);
|
||||
|
||||
@Insert("INSERT INTO aslan_gift_challenge_activity"
|
||||
+ " (id, activity_code, activity_name, activity_desc, sys_origin, time_zone, start_time,"
|
||||
+ " end_time, daily_settlement_delay_minutes, overall_settlement_delay_minutes,"
|
||||
+ " overall_settlement_time, display_top_n, enabled, overall_settlement_status, version,"
|
||||
+ " create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityCode}, #{activityName}, #{activityDesc}, #{sysOrigin}, #{timeZone},"
|
||||
+ " #{startTime}, #{endTime}, #{dailySettlementDelayMinutes},"
|
||||
+ " #{overallSettlementDelayMinutes}, #{overallSettlementTime}, #{displayTopN}, #{enabled},"
|
||||
+ " #{overallSettlementStatus}, 0, NOW(), NOW())")
|
||||
int insertActivity(ActivityRow row);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_activity SET activity_code = #{activityCode},"
|
||||
+ " activity_name = #{activityName}, activity_desc = #{activityDesc},"
|
||||
+ " sys_origin = #{sysOrigin}, time_zone = #{timeZone}, start_time = #{startTime},"
|
||||
+ " end_time = #{endTime}, daily_settlement_delay_minutes = #{dailySettlementDelayMinutes},"
|
||||
+ " overall_settlement_delay_minutes = #{overallSettlementDelayMinutes},"
|
||||
+ " overall_settlement_time = #{overallSettlementTime}, display_top_n = #{displayTopN},"
|
||||
+ " enabled = #{enabled}, version = version + 1, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND version = #{version}")
|
||||
int updateActivity(ActivityRow row);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_activity SET enabled = #{enabled}, version = version + 1,"
|
||||
+ " update_time = NOW() WHERE id = #{id}")
|
||||
int updateEnabled(@Param("id") Long id, @Param("enabled") boolean enabled);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_activity SET overall_settlement_status = #{status},"
|
||||
+ " update_time = NOW() WHERE id = #{id}")
|
||||
int updateOverallSettlementStatus(@Param("id") Long id, @Param("status") String status);
|
||||
|
||||
@Select("SELECT id, activity_id, task_code, task_type, task_title, task_desc, target_value,"
|
||||
+ " resource_group_id, enabled, sort_order FROM aslan_gift_challenge_task_config"
|
||||
+ " WHERE activity_id = #{activityId} ORDER BY sort_order ASC, id ASC")
|
||||
List<Task> listTasks(@Param("activityId") Long activityId);
|
||||
|
||||
@Delete("DELETE FROM aslan_gift_challenge_task_config WHERE activity_id = #{activityId}")
|
||||
int deleteTasks(@Param("activityId") Long activityId);
|
||||
|
||||
@Insert("INSERT INTO aslan_gift_challenge_task_config"
|
||||
+ " (id, activity_id, task_code, task_type, task_title, task_desc, target_value,"
|
||||
+ " resource_group_id, enabled, sort_order, create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{taskCode}, #{taskType}, #{taskTitle}, #{taskDesc},"
|
||||
+ " #{targetValue}, #{resourceGroupId}, #{enabled}, #{sortOrder}, NOW(), NOW())")
|
||||
int insertTask(Task task);
|
||||
|
||||
@Select("SELECT id, activity_id, period_type, start_rank, end_rank, resource_group_id,"
|
||||
+ " reward_name FROM aslan_gift_challenge_rank_reward WHERE activity_id = #{activityId}"
|
||||
+ " ORDER BY CASE period_type WHEN 'DAILY' THEN 0 ELSE 1 END, start_rank ASC, id ASC")
|
||||
List<RankReward> listRankRewards(@Param("activityId") Long activityId);
|
||||
|
||||
@Select("SELECT id, activity_id, period_type, start_rank, end_rank, resource_group_id,"
|
||||
+ " reward_name FROM aslan_gift_challenge_rank_reward"
|
||||
+ " WHERE activity_id = #{activityId} AND period_type = #{periodType}"
|
||||
+ " AND start_rank <= #{rank} AND end_rank >= #{rank} ORDER BY start_rank DESC LIMIT 1")
|
||||
RankReward findRankReward(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("rank") int rank);
|
||||
|
||||
@Delete("DELETE FROM aslan_gift_challenge_rank_reward WHERE activity_id = #{activityId}")
|
||||
int deleteRankRewards(@Param("activityId") Long activityId);
|
||||
|
||||
@Insert("INSERT INTO aslan_gift_challenge_rank_reward"
|
||||
+ " (id, activity_id, period_type, start_rank, end_rank, resource_group_id, reward_name,"
|
||||
+ " create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{periodType}, #{startRank}, #{endRank}, #{resourceGroupId},"
|
||||
+ " #{rewardName}, NOW(), NOW())")
|
||||
int insertRankReward(RankReward reward);
|
||||
|
||||
@Select("SELECT " + REWARD_SNAPSHOT_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_reward_snapshot WHERE activity_id = #{activityId}"
|
||||
+ " ORDER BY resource_group_id ASC, sort_order ASC, reward_config_id ASC")
|
||||
List<RewardSnapshotRow> listRewardSnapshots(@Param("activityId") Long activityId);
|
||||
|
||||
@Select("SELECT " + REWARD_SNAPSHOT_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_reward_snapshot WHERE activity_id = #{activityId}"
|
||||
+ " AND resource_group_id = #{resourceGroupId}"
|
||||
+ " ORDER BY sort_order ASC, reward_config_id ASC")
|
||||
List<RewardSnapshotRow> listRewardSnapshotsByGroup(@Param("activityId") Long activityId,
|
||||
@Param("resourceGroupId") Long resourceGroupId);
|
||||
|
||||
@Delete("DELETE FROM aslan_gift_challenge_reward_snapshot WHERE activity_id = #{activityId}")
|
||||
int deleteRewardSnapshots(@Param("activityId") Long activityId);
|
||||
|
||||
@Insert({
|
||||
"<script>",
|
||||
"INSERT INTO aslan_gift_challenge_reward_snapshot",
|
||||
" (id, activity_id, resource_group_id, reward_config_id, reward_type, detail_type, content,",
|
||||
" quantity, sort_order, remark, cover, source_url, display_name, create_time) VALUES",
|
||||
"<foreach collection='rows' item='row' separator=','>",
|
||||
" (#{row.id}, #{row.activityId}, #{row.resourceGroupId}, #{row.rewardConfigId},",
|
||||
" #{row.rewardType}, #{row.detailType}, #{row.content}, #{row.quantity}, #{row.sortOrder},",
|
||||
" #{row.remark}, #{row.cover}, #{row.sourceUrl}, #{row.displayName}, NOW())",
|
||||
"</foreach>",
|
||||
"</script>"
|
||||
})
|
||||
int insertRewardSnapshots(@Param("rows") List<RewardSnapshotRow> rows);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_gift_challenge_reward_snapshot"
|
||||
+ " WHERE activity_id = #{activityId}")
|
||||
int countRewardSnapshots(@Param("activityId") Long activityId);
|
||||
|
||||
/** 返回 1 才能继续累加;0 表示同一原始送礼事件已经完整处理过。 */
|
||||
@Insert("INSERT IGNORE INTO aslan_gift_challenge_gift_ledger"
|
||||
+ " (id, activity_id, source_event_track_id, user_id, gift_id, gift_tab, amount,"
|
||||
+ " event_time, stat_date, create_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{sourceEventTrackId}, #{userId}, #{giftId}, #{giftTab},"
|
||||
+ " #{amount}, #{eventTime}, #{statDate}, NOW())")
|
||||
int insertGiftLedger(GiftLedgerRow row);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_gift_challenge_user_score"
|
||||
+ " (activity_id, user_id, score, create_time, update_time)"
|
||||
+ " VALUES (#{activityId}, #{userId}, 0, NOW(), NOW())")
|
||||
int ensureUserScore(@Param("activityId") Long activityId, @Param("userId") Long userId);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_score SET score = score + #{amount},"
|
||||
+ " score_reached_time = CASE WHEN score_reached_time IS NULL THEN #{eventTime}"
|
||||
+ " ELSE GREATEST(score_reached_time, #{eventTime}) END, update_time = NOW()"
|
||||
+ " WHERE activity_id = #{activityId} AND user_id = #{userId}")
|
||||
int incrementUserScore(@Param("activityId") Long activityId, @Param("userId") Long userId,
|
||||
@Param("amount") BigDecimal amount, @Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Select("SELECT activity_id, user_id, score, score_reached_time, create_time, update_time"
|
||||
+ " FROM aslan_gift_challenge_user_score"
|
||||
+ " WHERE activity_id = #{activityId} AND user_id = #{userId} LIMIT 1")
|
||||
UserScoreRow getUserScore(@Param("activityId") Long activityId,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT user_id, score, score_reached_time FROM aslan_gift_challenge_user_score"
|
||||
+ " WHERE activity_id = #{activityId} AND score > 0"
|
||||
+ " ORDER BY score DESC, score_reached_time ASC, user_id ASC LIMIT #{limit}")
|
||||
List<RankingRow> listOverallTop(@Param("activityId") Long activityId,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Select("SELECT (SELECT COUNT(1) + 1 FROM aslan_gift_challenge_user_score target"
|
||||
+ " WHERE target.activity_id = current.activity_id AND target.score > 0 AND ("
|
||||
+ " target.score > current.score OR"
|
||||
+ " (target.score = current.score AND target.score_reached_time < current.score_reached_time) OR"
|
||||
+ " (target.score = current.score AND target.score_reached_time = current.score_reached_time"
|
||||
+ " AND target.user_id < current.user_id)))"
|
||||
+ " FROM aslan_gift_challenge_user_score current"
|
||||
+ " WHERE current.activity_id = #{activityId} AND current.user_id = #{userId}"
|
||||
+ " AND current.score > 0 LIMIT 1")
|
||||
Integer getOverallRank(@Param("activityId") Long activityId, @Param("userId") Long userId);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_gift_challenge_user_daily_score"
|
||||
+ " (activity_id, stat_date, user_id, score, create_time, update_time)"
|
||||
+ " VALUES (#{activityId}, #{statDate}, #{userId}, 0, NOW(), NOW())")
|
||||
int ensureUserDailyScore(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_daily_score SET score = score + #{amount},"
|
||||
+ " score_reached_time = CASE WHEN score_reached_time IS NULL THEN #{eventTime}"
|
||||
+ " ELSE GREATEST(score_reached_time, #{eventTime}) END, update_time = NOW()"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate} AND user_id = #{userId}")
|
||||
int incrementUserDailyScore(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId,
|
||||
@Param("amount") BigDecimal amount, @Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Select("SELECT activity_id, stat_date, user_id, score, score_reached_time, create_time, update_time"
|
||||
+ " FROM aslan_gift_challenge_user_daily_score WHERE activity_id = #{activityId}"
|
||||
+ " AND stat_date = #{statDate} AND user_id = #{userId} LIMIT 1")
|
||||
UserDailyScoreRow getUserDailyScore(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT user_id, score, score_reached_time"
|
||||
+ " FROM aslan_gift_challenge_user_daily_score"
|
||||
+ " WHERE activity_id = #{activityId} AND stat_date = #{statDate} AND score > 0"
|
||||
+ " ORDER BY score DESC, score_reached_time ASC, user_id ASC LIMIT #{limit}")
|
||||
List<RankingRow> listDailyTop(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT (SELECT COUNT(1) + 1 FROM aslan_gift_challenge_user_daily_score target"
|
||||
+ " WHERE target.activity_id = current.activity_id AND target.stat_date = current.stat_date"
|
||||
+ " AND target.score > 0 AND (target.score > current.score OR"
|
||||
+ " (target.score = current.score AND target.score_reached_time < current.score_reached_time) OR"
|
||||
+ " (target.score = current.score AND target.score_reached_time = current.score_reached_time"
|
||||
+ " AND target.user_id < current.user_id)))"
|
||||
+ " FROM aslan_gift_challenge_user_daily_score current"
|
||||
+ " WHERE current.activity_id = #{activityId} AND current.stat_date = #{statDate}"
|
||||
+ " AND current.user_id = #{userId} AND current.score > 0 LIMIT 1")
|
||||
Integer getDailyRank(@Param("activityId") Long activityId, @Param("statDate") Date statDate,
|
||||
@Param("userId") Long userId);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_gift_challenge_user_task_daily"
|
||||
+ " (id, activity_id, stat_date, user_id, task_config_id, task_code, task_type, task_title,"
|
||||
+ " sort_order, target_value, progress_value, resource_group_id, business_no,"
|
||||
+ " delivery_status, retry_count, create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{statDate}, #{userId}, #{taskConfigId}, #{taskCode}, #{taskType},"
|
||||
+ " #{taskTitle}, #{sortOrder}, #{targetValue}, 0, #{resourceGroupId}, #{businessNo},"
|
||||
+ " 'NOT_CLAIMED', 0, NOW(), NOW())")
|
||||
int insertTaskDailySnapshot(TaskDailyRow row);
|
||||
|
||||
@Select("SELECT " + TASK_DAILY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_user_task_daily WHERE activity_id = #{activityId}"
|
||||
+ " AND stat_date = #{statDate} AND user_id = #{userId}"
|
||||
+ " ORDER BY sort_order ASC, id ASC")
|
||||
List<TaskDailyRow> listUserTaskDaily(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId);
|
||||
|
||||
@Select("SELECT " + TASK_DAILY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_user_task_daily WHERE activity_id = #{activityId}"
|
||||
+ " AND stat_date = #{statDate} AND user_id = #{userId} AND task_code = #{taskCode} LIMIT 1")
|
||||
TaskDailyRow getTaskDaily(@Param("activityId") Long activityId,
|
||||
@Param("statDate") Date statDate, @Param("userId") Long userId,
|
||||
@Param("taskCode") String taskCode);
|
||||
|
||||
@Select("SELECT " + TASK_DAILY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_user_task_daily WHERE id = #{id} LIMIT 1 FOR UPDATE")
|
||||
TaskDailyRow lockTaskDaily(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_task_daily SET"
|
||||
+ " progress_value = LEAST(target_value, progress_value + #{amount}),"
|
||||
+ " completed_time = CASE WHEN completed_time IS NULL"
|
||||
+ " AND progress_value + #{amount} >= target_value THEN #{eventTime} ELSE completed_time END,"
|
||||
+ " update_time = NOW() WHERE id = #{id}")
|
||||
int incrementTaskProgress(@Param("id") Long id, @Param("amount") BigDecimal amount,
|
||||
@Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_task_daily SET progress_value = target_value,"
|
||||
+ " completed_time = COALESCE(completed_time, #{eventTime}), update_time = NOW()"
|
||||
+ " WHERE id = #{id}")
|
||||
int completeTask(@Param("id") Long id, @Param("eventTime") Timestamp eventTime);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_task_daily SET delivery_status = 'PENDING',"
|
||||
+ " claim_request_id = #{requestId}, claimed_time = NOW(), failure_reason = NULL,"
|
||||
+ " update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND completed_time IS NOT NULL AND resource_group_id IS NOT NULL"
|
||||
+ " AND delivery_status = 'NOT_CLAIMED'")
|
||||
int claimTaskReward(@Param("id") Long id, @Param("requestId") String requestId);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_task_daily SET delivery_status = 'PROCESSING',"
|
||||
+ " failure_reason = NULL, update_time = NOW() WHERE id = #{id}"
|
||||
+ " AND delivery_status = 'PENDING'")
|
||||
int claimTaskDelivery(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_task_daily SET delivery_status = #{status},"
|
||||
+ " failure_reason = #{failureReason}, deliver_time = CASE WHEN #{status} = 'SUCCESS'"
|
||||
+ " THEN NOW() ELSE deliver_time END, update_time = NOW() WHERE id = #{id}")
|
||||
int finishTaskDelivery(@Param("id") Long id, @Param("status") String status,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Select("SELECT " + TASK_DAILY_COLUMNS
|
||||
+ " FROM aslan_gift_challenge_user_task_daily WHERE delivery_status = 'PENDING'"
|
||||
+ " OR (delivery_status = 'PROCESSING' AND update_time <= #{staleBefore})"
|
||||
+ " ORDER BY update_time ASC, id ASC LIMIT #{limit}")
|
||||
List<TaskDailyRow> listRecoverableTaskDeliveries(@Param("staleBefore") Timestamp staleBefore,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_gift_challenge_period_settlement"
|
||||
+ " (id, activity_id, period_type, period_key, stat_date, snapshot_due_time, status,"
|
||||
+ " create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{periodType}, #{periodKey}, #{statDate}, #{snapshotDueTime},"
|
||||
+ " #{status}, NOW(), NOW())")
|
||||
int ensurePeriodSettlement(PeriodSettlementRow row);
|
||||
|
||||
/** 启用时批量预建所有活动日门闩,避免逐日往返且使真正空榜也能按 due time 调度。 */
|
||||
@Insert({
|
||||
"<script>",
|
||||
"INSERT IGNORE INTO aslan_gift_challenge_period_settlement",
|
||||
" (id, activity_id, period_type, period_key, stat_date, snapshot_due_time, status,",
|
||||
" create_time, update_time) VALUES",
|
||||
"<foreach collection='rows' item='row' separator=','>",
|
||||
" (#{row.id}, #{row.activityId}, #{row.periodType}, #{row.periodKey}, #{row.statDate},",
|
||||
" #{row.snapshotDueTime}, #{row.status}, NOW(), NOW())",
|
||||
"</foreach>",
|
||||
"</script>"
|
||||
})
|
||||
int ensurePeriodSettlements(@Param("rows") List<PeriodSettlementRow> rows);
|
||||
|
||||
@Delete("DELETE FROM aslan_gift_challenge_period_settlement"
|
||||
+ " WHERE activity_id = #{activityId} AND period_type = 'DAILY'"
|
||||
+ " AND status = 'NOT_STARTED'")
|
||||
int deleteNotStartedDailyPeriods(@Param("activityId") Long activityId);
|
||||
|
||||
@Select("SELECT " + PERIOD_COLUMNS + " FROM aslan_gift_challenge_period_settlement"
|
||||
+ " WHERE activity_id = #{activityId} AND period_type = #{periodType}"
|
||||
+ " AND period_key = #{periodKey} LIMIT 1")
|
||||
PeriodSettlementRow getPeriodSettlement(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("periodKey") String periodKey);
|
||||
|
||||
@Select("SELECT " + PERIOD_COLUMNS + " FROM aslan_gift_challenge_period_settlement"
|
||||
+ " WHERE activity_id = #{activityId} AND period_type = #{periodType}"
|
||||
+ " AND period_key = #{periodKey} LIMIT 1 FOR UPDATE")
|
||||
PeriodSettlementRow lockPeriodSettlement(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("periodKey") String periodKey);
|
||||
|
||||
/**
|
||||
* 礼物热链路只持有共享门闩,允许同一天的不同用户并发累计;结算使用上面的
|
||||
* FOR UPDATE 独占锁等待已进入的礼物事务完成后再冻结日榜。
|
||||
*/
|
||||
@Select("SELECT " + PERIOD_COLUMNS + " FROM aslan_gift_challenge_period_settlement"
|
||||
+ " WHERE activity_id = #{activityId} AND period_type = #{periodType}"
|
||||
+ " AND period_key = #{periodKey} LIMIT 1 LOCK IN SHARE MODE")
|
||||
PeriodSettlementRow lockPeriodSettlementForGiftGate(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("periodKey") String periodKey);
|
||||
|
||||
@Select("SELECT " + PERIOD_COLUMNS + " FROM aslan_gift_challenge_period_settlement"
|
||||
+ " WHERE snapshot_due_time <= #{now}"
|
||||
+ " AND status = 'NOT_STARTED'"
|
||||
+ " ORDER BY snapshot_due_time ASC, id ASC LIMIT #{limit}")
|
||||
List<PeriodSettlementRow> listDuePeriodSettlements(@Param("now") Timestamp now,
|
||||
@Param("limit") int limit);
|
||||
|
||||
/**
|
||||
* 恢复“所有 parent 已成功、进程却在刷新 header 前退出”的窗口。和待快照查询拆开,
|
||||
* 两条语句都使用单一 status 等值条件,保持命中 status/due/id 索引且不扫描 UNKNOWN。
|
||||
*/
|
||||
@Select("SELECT " + PERIOD_COLUMNS + " FROM aslan_gift_challenge_period_settlement"
|
||||
+ " WHERE snapshot_due_time <= #{now} AND status = 'PROCESSING'"
|
||||
+ " ORDER BY snapshot_due_time ASC, id ASC LIMIT #{limit}")
|
||||
List<PeriodSettlementRow> listProcessingPeriodSettlements(@Param("now") Timestamp now,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_period_settlement SET status = #{status},"
|
||||
+ " update_time = NOW() WHERE id = #{id}")
|
||||
int updatePeriodSettlementStatus(@Param("id") Long id, @Param("status") String status);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_gift_challenge_settlement"
|
||||
+ " (id, activity_id, period_type, period_key, stat_date, user_id, rank_no, score,"
|
||||
+ " rank_reward_id, resource_group_id, business_no, delivery_status, retry_count,"
|
||||
+ " create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{activityId}, #{periodType}, #{periodKey}, #{statDate}, #{userId}, #{rank},"
|
||||
+ " #{score}, #{rankRewardId}, #{resourceGroupId}, #{businessNo}, #{deliveryStatus}, 0,"
|
||||
+ " NOW(), NOW())")
|
||||
int insertSettlement(SettlementRecord record);
|
||||
|
||||
@Select("SELECT id, activity_id, period_type, period_key, stat_date, user_id, rank_no AS rank,"
|
||||
+ " score, rank_reward_id, resource_group_id, business_no, delivery_status, retry_count,"
|
||||
+ " failure_reason, UNIX_TIMESTAMP(create_time) * 1000 AS create_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_gift_challenge_settlement WHERE activity_id = #{activityId}"
|
||||
+ " AND (#{periodType} IS NULL OR period_type = #{periodType})"
|
||||
+ " AND (#{statDate} IS NULL OR stat_date = #{statDate})"
|
||||
+ " AND (#{deliveryStatus} IS NULL OR delivery_status = #{deliveryStatus})"
|
||||
+ " ORDER BY period_key DESC, rank_no ASC LIMIT #{limit}")
|
||||
List<SettlementRecord> listSettlements(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("statDate") String statDate,
|
||||
@Param("deliveryStatus") String deliveryStatus, @Param("limit") int limit);
|
||||
|
||||
@Select("SELECT id, activity_id, period_type, period_key, stat_date, user_id, rank_no AS rank,"
|
||||
+ " score, rank_reward_id, resource_group_id, business_no, delivery_status, retry_count,"
|
||||
+ " failure_reason, UNIX_TIMESTAMP(create_time) * 1000 AS create_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_gift_challenge_settlement WHERE id = #{id} LIMIT 1")
|
||||
SettlementRecord getSettlement(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT id, activity_id, period_type, period_key, stat_date, user_id, rank_no AS rank,"
|
||||
+ " score, rank_reward_id, resource_group_id, business_no, delivery_status, retry_count,"
|
||||
+ " failure_reason, UNIX_TIMESTAMP(create_time) * 1000 AS create_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_gift_challenge_settlement WHERE id = #{id} LIMIT 1 FOR UPDATE")
|
||||
SettlementRecord lockSettlement(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT id, activity_id, period_type, period_key, stat_date, user_id, rank_no AS rank,"
|
||||
+ " score, rank_reward_id, resource_group_id, business_no, delivery_status, retry_count,"
|
||||
+ " failure_reason, UNIX_TIMESTAMP(create_time) * 1000 AS create_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_gift_challenge_settlement WHERE activity_id = #{activityId}"
|
||||
+ " AND period_type = #{periodType} AND period_key = #{periodKey}"
|
||||
+ " ORDER BY rank_no ASC LIMIT #{limit}")
|
||||
List<SettlementRecord> listPeriodSettlements(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("periodKey") String periodKey,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_settlement SET delivery_status = 'PROCESSING',"
|
||||
+ " failure_reason = NULL, update_time = NOW() WHERE id = #{id}"
|
||||
+ " AND delivery_status = 'PENDING'")
|
||||
int claimSettlementDelivery(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_settlement SET delivery_status = #{status},"
|
||||
+ " failure_reason = #{failureReason}, deliver_time = CASE WHEN #{status} = 'SUCCESS'"
|
||||
+ " THEN NOW() ELSE deliver_time END, update_time = NOW() WHERE id = #{id}")
|
||||
int finishSettlementDelivery(@Param("id") Long id, @Param("status") String status,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Select("SELECT id, activity_id, period_type, period_key, stat_date, user_id, rank_no AS rank,"
|
||||
+ " score, rank_reward_id, resource_group_id, business_no, delivery_status, retry_count,"
|
||||
+ " failure_reason, UNIX_TIMESTAMP(create_time) * 1000 AS create_time,"
|
||||
+ " UNIX_TIMESTAMP(deliver_time) * 1000 AS deliver_time"
|
||||
+ " FROM aslan_gift_challenge_settlement WHERE delivery_status = 'PENDING'"
|
||||
+ " OR (delivery_status = 'PROCESSING' AND update_time <= #{staleBefore})"
|
||||
+ " ORDER BY update_time ASC, id ASC LIMIT #{limit}")
|
||||
List<SettlementRecord> listRecoverableSettlements(@Param("staleBefore") Timestamp staleBefore,
|
||||
@Param("limit") int limit);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_gift_challenge_settlement"
|
||||
+ " WHERE activity_id = #{activityId} AND period_type = #{periodType}"
|
||||
+ " AND period_key = #{periodKey} AND delivery_status <> 'SUCCESS'")
|
||||
int countUnfinishedSettlements(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("periodKey") String periodKey);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_gift_challenge_settlement"
|
||||
+ " WHERE activity_id = #{activityId} AND period_type = #{periodType}"
|
||||
+ " AND period_key = #{periodKey} AND delivery_status = 'UNKNOWN'")
|
||||
int countUnknownSettlements(@Param("activityId") Long activityId,
|
||||
@Param("periodType") String periodType, @Param("periodKey") String periodKey);
|
||||
|
||||
@Insert("INSERT IGNORE INTO aslan_gift_challenge_delivery_item"
|
||||
+ " (id, owner_type, owner_id, activity_id, user_id, reward_config_id, resource_group_id,"
|
||||
+ " reward_type, detail_type, content, quantity, sort_order, remark, delivery_status,"
|
||||
+ " retry_count, create_time, update_time) VALUES"
|
||||
+ " (#{id}, #{ownerType}, #{ownerId}, #{activityId}, #{userId}, #{rewardConfigId},"
|
||||
+ " #{resourceGroupId}, #{rewardType}, #{detailType}, #{content}, #{quantity}, #{sortOrder},"
|
||||
+ " #{remark}, #{deliveryStatus}, 0, NOW(), NOW())")
|
||||
int insertDeliveryItem(DeliveryItemRow item);
|
||||
|
||||
@Select("SELECT " + DELIVERY_COLUMNS + " FROM aslan_gift_challenge_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " ORDER BY sort_order ASC, id ASC")
|
||||
List<DeliveryItemRow> listDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT " + DELIVERY_COLUMNS + " FROM aslan_gift_challenge_delivery_item"
|
||||
+ " WHERE id = #{id} LIMIT 1")
|
||||
DeliveryItemRow getDeliveryItem(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT " + DELIVERY_COLUMNS + " FROM aslan_gift_challenge_delivery_item"
|
||||
+ " WHERE id = #{id} LIMIT 1 FOR UPDATE")
|
||||
DeliveryItemRow lockDeliveryItem(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_delivery_item SET delivery_status = 'PROCESSING',"
|
||||
+ " failure_reason = NULL, update_time = NOW() WHERE id = #{id}"
|
||||
+ " AND delivery_status = 'PENDING'")
|
||||
int claimDeliveryItem(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_delivery_item SET delivery_status = 'SUCCESS',"
|
||||
+ " failure_reason = NULL, deliver_time = NOW(), update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'PROCESSING'")
|
||||
int finishDeliveryItemSuccess(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_delivery_item SET delivery_status = 'UNKNOWN',"
|
||||
+ " failure_reason = #{failureReason}, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'PROCESSING'")
|
||||
int finishProcessingDeliveryItemUnknown(@Param("id") Long id,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_delivery_item SET delivery_status = 'UNKNOWN',"
|
||||
+ " failure_reason = #{failureReason}, update_time = NOW()"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status = 'PROCESSING' AND update_time <= #{staleBefore}")
|
||||
int expireProcessingDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId, @Param("staleBefore") Timestamp staleBefore,
|
||||
@Param("failureReason") String failureReason);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_delivery_item SET delivery_status = #{resolvedStatus},"
|
||||
+ " retry_count = retry_count + CASE WHEN #{resolvedStatus} = 'PENDING' THEN 1 ELSE 0 END,"
|
||||
+ " failure_reason = CASE WHEN #{resolvedStatus} = 'SUCCESS'"
|
||||
+ " THEN 'OPERATOR_CONFIRMED_DELIVERED' ELSE NULL END,"
|
||||
+ " deliver_time = CASE WHEN #{resolvedStatus} = 'SUCCESS'"
|
||||
+ " THEN COALESCE(deliver_time, NOW()) ELSE deliver_time END, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status = 'UNKNOWN'")
|
||||
int resolveUnknownDeliveryItem(@Param("id") Long id,
|
||||
@Param("resolvedStatus") String resolvedStatus);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_settlement SET delivery_status = 'PENDING',"
|
||||
+ " retry_count = retry_count + 1, failure_reason = NULL, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status IN ('UNKNOWN', 'PROCESSING', 'PENDING')")
|
||||
int reopenUnknownSettlementDelivery(@Param("id") Long id);
|
||||
|
||||
@Update("UPDATE aslan_gift_challenge_user_task_daily SET delivery_status = 'PENDING',"
|
||||
+ " retry_count = retry_count + 1, failure_reason = NULL, update_time = NOW()"
|
||||
+ " WHERE id = #{id} AND delivery_status IN ('UNKNOWN', 'PROCESSING', 'PENDING')")
|
||||
int reopenUnknownTaskDelivery(@Param("id") Long id);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_gift_challenge_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}")
|
||||
int countDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_gift_challenge_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status <> 'SUCCESS'")
|
||||
int countUnfinishedDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
|
||||
@Select("SELECT COUNT(1) FROM aslan_gift_challenge_delivery_item"
|
||||
+ " WHERE owner_type = #{ownerType} AND owner_id = #{ownerId}"
|
||||
+ " AND delivery_status = 'UNKNOWN'")
|
||||
int countUnknownDeliveryItems(@Param("ownerType") String ownerType,
|
||||
@Param("ownerId") Long ownerId);
|
||||
}
|
||||
@ -0,0 +1,85 @@
|
||||
package com.red.circle.other.infra.database.rds.model.activity.aslan;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Timestamp;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Aslan 游戏王活动数据库行模型。
|
||||
*
|
||||
* <p>这些类型只在持久层使用,尤其把事件白名单保留为规范化 CSV;对外契约仍使用
|
||||
* List,防止存储格式泄漏到 H5 与 webconsole。</p>
|
||||
*/
|
||||
public final class AslanGameKingRows {
|
||||
|
||||
private AslanGameKingRows() {
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class ActivityRow {
|
||||
private Long id;
|
||||
private String activityCode;
|
||||
private String activityName;
|
||||
private String activityDesc;
|
||||
private String sysOrigin;
|
||||
private String timeZone;
|
||||
private Timestamp startTime;
|
||||
private Timestamp endTime;
|
||||
private Integer settlementDelayMinutes;
|
||||
private Timestamp settlementTime;
|
||||
private Long coinPerDraw;
|
||||
private String eventTypes;
|
||||
private String rankingType;
|
||||
private String rankingPeriod;
|
||||
private Boolean enabled;
|
||||
private String settlementStatus;
|
||||
private Timestamp createTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class UserRow {
|
||||
private Long activityId;
|
||||
private Long userId;
|
||||
private BigDecimal totalConsumed;
|
||||
private BigDecimal totalWon;
|
||||
private Long earnedChances;
|
||||
private Long usedChances;
|
||||
private Timestamp consumeReachedTime;
|
||||
private Timestamp winReachedTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankingRow {
|
||||
private Long userId;
|
||||
private BigDecimal score;
|
||||
}
|
||||
|
||||
/** 单个奖励配置的事务型发放项;owner 为抽奖记录或排名结算记录。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class DeliveryItemRow {
|
||||
private Long id;
|
||||
private String ownerType;
|
||||
private Long ownerId;
|
||||
private Long activityId;
|
||||
private Long userId;
|
||||
private Long rewardConfigId;
|
||||
private Long resourceGroupId;
|
||||
private String rewardType;
|
||||
private String detailType;
|
||||
private String content;
|
||||
private Integer quantity;
|
||||
private Integer sortOrder;
|
||||
private String remark;
|
||||
private String deliveryStatus;
|
||||
private Integer retryCount;
|
||||
private String failureReason;
|
||||
private Timestamp deliverTime;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,173 @@
|
||||
package com.red.circle.other.infra.database.rds.model.activity.aslan;
|
||||
|
||||
import java.math.BigDecimal;
|
||||
import java.sql.Date;
|
||||
import java.sql.Timestamp;
|
||||
import lombok.Data;
|
||||
import lombok.experimental.Accessors;
|
||||
|
||||
/**
|
||||
* Aslan 礼物挑战专用数据库行模型。
|
||||
*
|
||||
* <p>活动时区日期在写入事件时即物化为 {@link Date},避免榜单查询依赖数据库会话时区。
|
||||
* 对外契约仍使用毫秒时间戳和 yyyy-MM-dd,存储类型不会泄漏到 H5 或 webconsole。</p>
|
||||
*/
|
||||
public final class AslanGiftChallengeRows {
|
||||
|
||||
private AslanGiftChallengeRows() {
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class ActivityRow {
|
||||
private Long id;
|
||||
private String activityCode;
|
||||
private String activityName;
|
||||
private String activityDesc;
|
||||
private String sysOrigin;
|
||||
private String timeZone;
|
||||
private Timestamp startTime;
|
||||
private Timestamp endTime;
|
||||
private Integer dailySettlementDelayMinutes;
|
||||
private Integer overallSettlementDelayMinutes;
|
||||
private Timestamp overallSettlementTime;
|
||||
private Integer displayTopN;
|
||||
private Boolean enabled;
|
||||
private String overallSettlementStatus;
|
||||
private Integer version;
|
||||
private Timestamp createTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class GiftLedgerRow {
|
||||
private Long id;
|
||||
private Long activityId;
|
||||
/** 原始 GiveAwayGiftBatchEvent.trackId;不是 Mongo 礼物流水 ID。 */
|
||||
private Long sourceEventTrackId;
|
||||
private Long userId;
|
||||
private Long giftId;
|
||||
private String giftTab;
|
||||
private BigDecimal amount;
|
||||
private Timestamp eventTime;
|
||||
private Date statDate;
|
||||
}
|
||||
|
||||
/** 活动首次启用时冻结的奖励配置;后续资源组修改不会改变本期展示与实际发放内容。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RewardSnapshotRow {
|
||||
private Long id;
|
||||
private Long activityId;
|
||||
private Long resourceGroupId;
|
||||
private Long rewardConfigId;
|
||||
private String rewardType;
|
||||
private String detailType;
|
||||
private String content;
|
||||
private Integer quantity;
|
||||
private Integer sortOrder;
|
||||
private String remark;
|
||||
private String cover;
|
||||
private String sourceUrl;
|
||||
private String displayName;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class UserScoreRow {
|
||||
private Long activityId;
|
||||
private Long userId;
|
||||
private BigDecimal score;
|
||||
private Timestamp scoreReachedTime;
|
||||
private Timestamp createTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class UserDailyScoreRow {
|
||||
private Long activityId;
|
||||
private Date statDate;
|
||||
private Long userId;
|
||||
private BigDecimal score;
|
||||
private Timestamp scoreReachedTime;
|
||||
private Timestamp createTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class RankingRow {
|
||||
private Long userId;
|
||||
private BigDecimal score;
|
||||
private Timestamp scoreReachedTime;
|
||||
}
|
||||
|
||||
/** 任务定义的当日快照;后续修改配置不会改变这一行的目标或资源组。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class TaskDailyRow {
|
||||
private Long id;
|
||||
private Long activityId;
|
||||
private Date statDate;
|
||||
private Long userId;
|
||||
private Long taskConfigId;
|
||||
private String taskCode;
|
||||
private String taskType;
|
||||
private String taskTitle;
|
||||
private Integer sortOrder;
|
||||
private BigDecimal targetValue;
|
||||
private BigDecimal progressValue;
|
||||
private Timestamp completedTime;
|
||||
private Long resourceGroupId;
|
||||
private String businessNo;
|
||||
/** 首次成功抢占领取状态的客户端请求号,保留作稳定审计证据。 */
|
||||
private String claimRequestId;
|
||||
private String deliveryStatus;
|
||||
private Integer retryCount;
|
||||
private String failureReason;
|
||||
private Timestamp claimedTime;
|
||||
private Timestamp deliverTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
|
||||
/** DAILY 和 OVERALL 的快照门闩;空榜也能留下 COMPLETED 状态。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class PeriodSettlementRow {
|
||||
private Long id;
|
||||
private Long activityId;
|
||||
private String periodType;
|
||||
private String periodKey;
|
||||
private Date statDate;
|
||||
private Timestamp snapshotDueTime;
|
||||
private String status;
|
||||
private Timestamp createTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
|
||||
/** 结算或任务奖励组拆分后的单项发放状态。 */
|
||||
@Data
|
||||
@Accessors(chain = true)
|
||||
public static class DeliveryItemRow {
|
||||
private Long id;
|
||||
private String ownerType;
|
||||
private Long ownerId;
|
||||
private Long activityId;
|
||||
private Long userId;
|
||||
private Long rewardConfigId;
|
||||
private Long resourceGroupId;
|
||||
private String rewardType;
|
||||
private String detailType;
|
||||
private String content;
|
||||
private Integer quantity;
|
||||
private Integer sortOrder;
|
||||
private String remark;
|
||||
private String deliveryStatus;
|
||||
private Integer retryCount;
|
||||
private String failureReason;
|
||||
private Timestamp deliverTime;
|
||||
private Timestamp updateTime;
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,127 @@
|
||||
package com.red.circle.other.app.inner.endpoint.activity;
|
||||
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.dto.ResultResponse;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGameKingService;
|
||||
import com.red.circle.other.inner.endpoint.activity.api.AslanGameKingManageClientApi;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.BackfillResult;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.DrawRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.Prize;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.PrizeSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGameKingModels.SettlementRecord;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** Aslan 游戏王跨服务管理端点。 */
|
||||
@Validated
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(value = AslanGameKingManageClientApi.API_PREFIX,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class AslanGameKingManageClientEndpoint implements AslanGameKingManageClientApi {
|
||||
|
||||
private final AslanGameKingService aslanGameKingService;
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<Activity>> list() {
|
||||
return ResultResponse.success(aslanGameKingService.listActivities());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Detail> detail(Long id) {
|
||||
return ResultResponse.success(aslanGameKingService.manageDetail(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Activity> save(@Validated SaveCommand cmd) {
|
||||
Long id = aslanGameKingService.save(cmd);
|
||||
return ResultResponse.success(aslanGameKingService.manageDetail(id).getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> enable(Long id, boolean enabled) {
|
||||
aslanGameKingService.enable(id, enabled);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<Prize>> prizes(Long id) {
|
||||
return ResultResponse.success(aslanGameKingService.listPrizes(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> savePrizes(Long id, @Validated PrizeSaveCommand cmd) {
|
||||
// 路径 ID 是权限与审计定位依据,禁止 body 指向另一活动。
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
cmd != null && id.equals(cmd.getActivityId()));
|
||||
aslanGameKingService.savePrizes(cmd);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<RankReward>> rankRewards(Long id) {
|
||||
return ResultResponse.success(aslanGameKingService.listRankRewards(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> saveRankRewards(Long id,
|
||||
@Validated RankRewardSaveCommand cmd) {
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
cmd != null && id.equals(cmd.getActivityId()));
|
||||
aslanGameKingService.saveRankRewards(cmd);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<SettlementRecord>> settlementRecords(Long id) {
|
||||
return ResultResponse.success(aslanGameKingService.listSettlementRecords(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<DrawRecord>> drawRecords(Long id, String deliveryStatus,
|
||||
Integer limit) {
|
||||
return ResultResponse.success(
|
||||
aslanGameKingService.listManageDrawRecords(id, deliveryStatus, limit));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> retryDraw(Long recordId) {
|
||||
aslanGameKingService.retryDraw(recordId);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> settle(Long id) {
|
||||
aslanGameKingService.settle(id);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> retrySettlement(Long recordId) {
|
||||
aslanGameKingService.retrySettlement(recordId);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> resolveUnknownDeliveryItem(Long itemId, boolean delivered) {
|
||||
aslanGameKingService.resolveUnknownDeliveryItem(itemId, delivered);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<BackfillResult> backfill(Long id, String eventType, Integer receiptType,
|
||||
Long lastId, Integer limit) {
|
||||
return ResultResponse.success(
|
||||
aslanGameKingService.backfill(id, eventType, receiptType, lastId, limit));
|
||||
}
|
||||
}
|
||||
@ -0,0 +1,111 @@
|
||||
package com.red.circle.other.app.inner.endpoint.activity;
|
||||
|
||||
import com.red.circle.framework.core.asserts.ResponseAssert;
|
||||
import com.red.circle.framework.core.response.CommonErrorCode;
|
||||
import com.red.circle.framework.dto.ResultResponse;
|
||||
import com.red.circle.other.app.service.activity.aslan.AslanGiftChallengeService;
|
||||
import com.red.circle.other.inner.endpoint.activity.api.AslanGiftChallengeManageClientApi;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Activity;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Detail;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankReward;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankRewardSaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Ranking;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.RankingQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SaveCommand;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementQuery;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.SettlementRecord;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.Task;
|
||||
import com.red.circle.other.inner.model.activity.aslan.AslanGiftChallengeModels.TaskSaveCommand;
|
||||
import java.util.List;
|
||||
import lombok.RequiredArgsConstructor;
|
||||
import org.springframework.http.MediaType;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/** Aslan 礼物挑战跨服务管理端点。 */
|
||||
@Validated
|
||||
@RestController
|
||||
@RequiredArgsConstructor
|
||||
@RequestMapping(value = AslanGiftChallengeManageClientApi.API_PREFIX,
|
||||
produces = MediaType.APPLICATION_JSON_VALUE)
|
||||
public class AslanGiftChallengeManageClientEndpoint
|
||||
implements AslanGiftChallengeManageClientApi {
|
||||
|
||||
private final AslanGiftChallengeService aslanGiftChallengeService;
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<Activity>> list() {
|
||||
return ResultResponse.success(aslanGiftChallengeService.listActivities());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Detail> detail(Long id) {
|
||||
return ResultResponse.success(aslanGiftChallengeService.manageDetail(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Activity> save(@Validated SaveCommand cmd) {
|
||||
Long id = aslanGiftChallengeService.save(cmd);
|
||||
return ResultResponse.success(aslanGiftChallengeService.manageDetail(id).getActivity());
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> enable(Long id, boolean enabled) {
|
||||
aslanGiftChallengeService.enable(id, enabled);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<Task>> tasks(Long id) {
|
||||
return ResultResponse.success(aslanGiftChallengeService.listTasks(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> saveTasks(Long id, @Validated TaskSaveCommand cmd) {
|
||||
// URL 是权限和审计归属,body 不能把三个固定任务写入另一活动。
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
cmd != null && id.equals(cmd.getActivityId()));
|
||||
aslanGiftChallengeService.saveTasks(cmd);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<RankReward>> rankRewards(Long id) {
|
||||
return ResultResponse.success(aslanGiftChallengeService.listRankRewards(id));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> saveRankRewards(Long id,
|
||||
@Validated RankRewardSaveCommand cmd) {
|
||||
// DAILY/OVERALL 奖励属于 URL 活动;拒绝跨活动覆盖防止错发。
|
||||
ResponseAssert.isTrue(CommonErrorCode.INOPERABLE_WRONG_ATTRIBUTION,
|
||||
cmd != null && id.equals(cmd.getActivityId()));
|
||||
aslanGiftChallengeService.saveRankRewards(cmd);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Ranking> ranking(Long id, @Validated RankingQuery query) {
|
||||
return ResultResponse.success(aslanGiftChallengeService.manageRanking(id, query));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<List<SettlementRecord>> settlementRecords(Long id,
|
||||
@Validated SettlementQuery query) {
|
||||
return ResultResponse.success(
|
||||
aslanGiftChallengeService.listSettlementRecords(id, query));
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> settle(Long id, String period, String statDate) {
|
||||
aslanGiftChallengeService.settle(id, period, statDate);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
|
||||
@Override
|
||||
public ResultResponse<Void> resolveUnknownDeliveryItem(Long itemId, boolean delivered) {
|
||||
aslanGiftChallengeService.resolveUnknownDeliveryItem(itemId, delivered);
|
||||
return ResultResponse.success();
|
||||
}
|
||||
}
|
||||
@ -88,8 +88,8 @@ public class UserBankTransferGoldCmdExe {
|
||||
ArithmeticUtils.gtZero(cmd.getAmount()));
|
||||
|
||||
boolean freight = freightBalanceService.existsBalance(cmd.getAcceptUserId());
|
||||
// 工资可转入任意有效币商账户;付款人与币商为同一用户时也允许兑换。
|
||||
ResponseAssert.isTrue(WalletErrorCode.TARGET_USER_NOT_RECHARGE_AGENCY, freight);
|
||||
ResponseAssert.isTrue(WalletErrorCode.NOT_TRANSFER_YOURSELF, !Objects.equals(cmd.getAcceptUserId(), cmd.requiredReqUserId()));
|
||||
|
||||
// 上锁
|
||||
String key = "UBWTransferGold:" + cmd.requiredReqUserId();
|
||||
|
||||
@ -44,12 +44,6 @@ public class UserBankSearchUserProfileQryExe {
|
||||
return null;
|
||||
}
|
||||
|
||||
if (isTransfer(cmd)) {
|
||||
if (Objects.equals(cmd.getReqUserId(), userProfile.getId())) {
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
if (cmd.typeEqBD()) {
|
||||
return
|
||||
Objects.equals(ResponseAssert.requiredSuccess(bdTeamClient.check(userProfile.getId())),
|
||||
|
||||
@ -80,8 +80,8 @@ public class SalaryTransferGoldCmdExe {
|
||||
ArithmeticUtils.gtZero(cmd.getAmount()));
|
||||
|
||||
boolean freight = freightBalanceService.existsBalance(cmd.getAcceptUserId());
|
||||
// 工资可转入任意有效币商账户;付款人与币商为同一用户时也允许兑换。
|
||||
ResponseAssert.isTrue(WalletErrorCode.TARGET_USER_NOT_RECHARGE_AGENCY, freight);
|
||||
ResponseAssert.isTrue(WalletErrorCode.NOT_TRANSFER_YOURSELF, !Objects.equals(cmd.getAcceptUserId(), cmd.requiredReqUserId()));
|
||||
|
||||
// 上锁
|
||||
String key = "USTransferGold:" + cmd.requiredReqUserId();
|
||||
|
||||
@ -46,7 +46,8 @@ public class WalletGoldClientEndpoint implements WalletGoldClientApi {
|
||||
|
||||
@Override
|
||||
public ResultResponse<WalletReceiptResDTO> changeBalanceTest(TestWalletReceiptCmd cmd) {
|
||||
return ResultResponse.success(walletGoldClientService.changeBalanceTest(cmd));
|
||||
// 生产环境停用测试发金币入口,保留空成功响应以兼容已有测试调用方。
|
||||
return ResultResponse.success(null);
|
||||
}
|
||||
|
||||
@Override
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user