361 lines
22 KiB
SQL
361 lines
22 KiB
SQL
SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
|
||
|
||
USE hyapp_wallet;
|
||
|
||
-- 本迁移只建立按 App 可配置的 VIP 规则和体验卡事实;不会为 Fami 猜测价格、有效期或资源组。
|
||
CREATE TABLE IF NOT EXISTS vip_program_configs (
|
||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||
program_type VARCHAR(32) NOT NULL COMMENT '体系类型:legacy_timed/tiered_privilege_v1',
|
||
level_count INT NOT NULL COMMENT '该 App 可配置等级数量',
|
||
same_level_expiry_policy VARCHAR(32) NOT NULL COMMENT '同级续费有效期策略:extend_remaining',
|
||
upgrade_expiry_policy VARCHAR(32) NOT NULL COMMENT '升级有效期策略:extend_remaining/replace_from_now',
|
||
downgrade_purchase_policy VARCHAR(32) NOT NULL COMMENT '降级购买策略:reject',
|
||
benefit_inheritance_policy VARCHAR(32) NOT NULL COMMENT '权益矩阵策略:当前固定 target_only,每级保存完整集合',
|
||
grant_mode VARCHAR(32) NOT NULL COMMENT '后台赠送语义:direct_membership/trial_card',
|
||
trial_card_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否允许体验卡背包和佩戴流程',
|
||
status VARCHAR(32) NOT NULL COMMENT '业务状态:active/disabled',
|
||
config_version BIGINT NOT NULL DEFAULT 1 COMMENT '配置版本;订单和会员状态持久化该快照版本',
|
||
updated_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '最后更新管理员 ID',
|
||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code),
|
||
KEY idx_vip_program_status (status, updated_at_ms)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='按 App 配置的 VIP 体系规则';
|
||
|
||
CREATE TABLE IF NOT EXISTS vip_level_benefits (
|
||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||
level INT NOT NULL COMMENT '拥有该权益的具体 VIP 等级',
|
||
benefit_code VARCHAR(96) NOT NULL COMMENT '稳定权益编码,跨服务权限判断只使用该值',
|
||
name VARCHAR(128) NOT NULL COMMENT '权益展示名称',
|
||
benefit_type VARCHAR(32) NOT NULL COMMENT '权益类型:decoration/function',
|
||
unlock_level INT NOT NULL COMMENT 'P1 初始解锁等级,仅用于展示来源,不参与运行时继承',
|
||
status VARCHAR(32) NOT NULL COMMENT '业务状态:active/disabled',
|
||
trial_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '体验卡是否可获得;金币返现必须为 false',
|
||
resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '绑定资源 ID;0 表示尚未绑定或纯功能权益',
|
||
resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '绑定资源类型;空表示纯功能或待运营配置',
|
||
execution_scope VARCHAR(32) NOT NULL COMMENT '实际执行方:wallet/room/user/notice',
|
||
auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '资源下发后是否自动佩戴最高等级样式',
|
||
sort_order INT NOT NULL DEFAULT 0 COMMENT '权益展示顺序',
|
||
metadata_json JSON NULL COMMENT '执行方扩展配置;初始 P1 权益不猜测参数',
|
||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code, level, benefit_code),
|
||
KEY idx_vip_benefit_code (app_code, benefit_code, status, level),
|
||
KEY idx_vip_benefit_level_sort (app_code, level, status, sort_order)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 每等级完整权益矩阵';
|
||
|
||
-- 体验卡元数据和通用背包事实按 entitlement_id 一一对应;佩戴切换不更新卡片绝对过期时间。
|
||
CREATE TABLE IF NOT EXISTS user_vip_trial_cards (
|
||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||
trial_card_id VARCHAR(96) NOT NULL COMMENT '体验卡实例 ID',
|
||
command_id VARCHAR(128) NOT NULL COMMENT '发卡命令幂等 ID',
|
||
request_hash VARCHAR(128) NOT NULL COMMENT '发卡请求语义哈希',
|
||
user_id BIGINT NOT NULL COMMENT '持有用户 ID',
|
||
entitlement_id VARCHAR(96) NOT NULL COMMENT '通用背包权益实例 ID,也是佩戴入口标识',
|
||
resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '可选资源 ID;0 表示直接按等级和时长发卡',
|
||
level INT NOT NULL COMMENT '体验卡 VIP 等级快照',
|
||
name VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'VIP 等级名称快照',
|
||
status VARCHAR(32) NOT NULL COMMENT '卡片业务状态:active/revoked',
|
||
duration_ms BIGINT NOT NULL COMMENT '发卡时长快照;佩戴切换不改变该值',
|
||
effective_at_ms BIGINT NOT NULL COMMENT '卡片开始计时,UTC epoch ms',
|
||
expires_at_ms BIGINT NOT NULL COMMENT '卡片绝对过期时间,UTC epoch ms',
|
||
grant_source VARCHAR(32) NOT NULL COMMENT '发放来源',
|
||
source_grant_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '通用资源发放 ID',
|
||
operator_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '发放操作人用户 ID',
|
||
reason VARCHAR(256) NOT NULL DEFAULT '' COMMENT '发放原因',
|
||
program_type VARCHAR(32) NOT NULL COMMENT '发卡时 VIP 体系快照',
|
||
config_version BIGINT NOT NULL COMMENT '发卡时 VIP 配置版本',
|
||
created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms',
|
||
updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code, trial_card_id),
|
||
UNIQUE KEY uk_vip_trial_card_command (app_code, command_id),
|
||
UNIQUE KEY uk_vip_trial_card_entitlement (app_code, entitlement_id),
|
||
KEY idx_vip_trial_card_user (app_code, user_id, status, expires_at_ms),
|
||
KEY idx_vip_trial_card_resource (app_code, resource_id, status)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 体验卡背包元数据';
|
||
|
||
-- purchase、直接会员赠送与体验卡发放共享同一命令守卫,先串行幂等键再获取业务行锁。
|
||
CREATE TABLE IF NOT EXISTS vip_command_locks (
|
||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||
command_id VARCHAR(128) NOT NULL COMMENT 'VIP 写命令幂等 ID',
|
||
biz_type VARCHAR(64) NOT NULL COMMENT '命令类型:vip_purchase/vip_grant/vip_trial_card_grant',
|
||
request_hash VARCHAR(128) NOT NULL COMMENT '请求语义哈希',
|
||
created_at_ms BIGINT NOT NULL COMMENT '首次命令时间,UTC epoch ms',
|
||
updated_at_ms BIGINT NOT NULL COMMENT '最后写入时间,UTC epoch ms',
|
||
PRIMARY KEY (app_code, command_id)
|
||
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 写命令串行化守卫';
|
||
|
||
-- 旧库按列逐项补齐;所有 ALTER 都可重复执行,且默认值保持 Lalu 的历史语义。
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_memberships' AND COLUMN_NAME = 'program_type') = 0,
|
||
'ALTER TABLE user_vip_memberships ADD COLUMN program_type VARCHAR(32) NOT NULL DEFAULT ''legacy_timed'' COMMENT ''激活时使用的 VIP 体系快照'' AFTER status',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
-- 历史成功命令必须在上线前进入同一守卫空间,否则升级后一段时间内仍可能走旧唯一键竞争路径。
|
||
-- wallet_transactions 的业务索引以 app_code 为首列;这里显式限定当前已注册 App,避免生产迁移
|
||
-- 因只按 biz_type 过滤退化为全表扫描。新 App 上线后的命令会直接经过运行时 command guard。
|
||
INSERT IGNORE INTO vip_command_locks (
|
||
app_code, command_id, biz_type, request_hash, created_at_ms, updated_at_ms
|
||
)
|
||
SELECT app_code, command_id, biz_type, request_hash, created_at_ms, updated_at_ms
|
||
FROM wallet_transactions
|
||
WHERE app_code IN ('lalu', 'fami', 'huwaa', 'yumi')
|
||
AND biz_type IN ('vip_purchase', 'vip_grant');
|
||
|
||
INSERT IGNORE INTO vip_command_locks (
|
||
app_code, command_id, biz_type, request_hash, created_at_ms, updated_at_ms
|
||
)
|
||
SELECT app_code, command_id, 'vip_trial_card_grant', request_hash, created_at_ms, updated_at_ms
|
||
FROM user_vip_trial_cards;
|
||
|
||
-- Lalu 显式保持现状;Fami 使用替换升级、体验卡赠送和每级完整权益集合。
|
||
INSERT IGNORE INTO vip_program_configs (
|
||
app_code, program_type, level_count, same_level_expiry_policy, upgrade_expiry_policy,
|
||
downgrade_purchase_policy, benefit_inheritance_policy, grant_mode, trial_card_enabled,
|
||
status, config_version, updated_by_admin_id, created_at_ms, updated_at_ms
|
||
) VALUES
|
||
('lalu', 'legacy_timed', 10, 'extend_remaining', 'extend_remaining',
|
||
'reject', 'target_only', 'direct_membership', FALSE, 'active', 1, 0, 0, 0),
|
||
('fami', 'tiered_privilege_v1', 9, 'extend_remaining', 'replace_from_now',
|
||
'reject', 'target_only', 'trial_card', TRUE, 'active', 1, 0, 0, 0);
|
||
|
||
-- Fami 等级初始均不可购买;30 天只满足配置校验,价格与资源组保持 0,须由运营确认后启用。
|
||
INSERT IGNORE INTO vip_levels (
|
||
app_code, level, name, status, price_coin, duration_ms, reward_resource_group_id,
|
||
required_recharge_coin_amount, sort_order, created_at_ms, updated_at_ms
|
||
) VALUES
|
||
('fami', 1, 'VIP1', 'disabled', 0, 2592000000, 0, 0, 10, 0, 0),
|
||
('fami', 2, 'VIP2', 'disabled', 0, 2592000000, 0, 0, 20, 0, 0),
|
||
('fami', 3, 'VIP3', 'disabled', 0, 2592000000, 0, 0, 30, 0, 0),
|
||
('fami', 4, 'VIP4', 'disabled', 0, 2592000000, 0, 0, 40, 0, 0),
|
||
('fami', 5, 'VIP5', 'disabled', 0, 2592000000, 0, 0, 50, 0, 0),
|
||
('fami', 6, 'VIP6', 'disabled', 0, 2592000000, 0, 0, 60, 0, 0),
|
||
('fami', 7, 'VIP7', 'disabled', 0, 2592000000, 0, 0, 70, 0, 0),
|
||
('fami', 8, 'VIP8', 'disabled', 0, 2592000000, 0, 0, 80, 0, 0),
|
||
('fami', 9, 'VIP9', 'disabled', 0, 2592000000, 0, 0, 90, 0, 0);
|
||
|
||
-- 每级直接持久化最终权益集合,不在运行时根据 unlock_level 自动继承。
|
||
INSERT IGNORE INTO vip_level_benefits (
|
||
app_code, level, benefit_code, name, benefit_type, unlock_level, status, trial_enabled,
|
||
resource_id, resource_type, execution_scope, auto_equip, sort_order, metadata_json,
|
||
created_at_ms, updated_at_ms
|
||
)
|
||
SELECT
|
||
'fami', levels.level, benefits.benefit_code, benefits.name, benefits.benefit_type,
|
||
benefits.unlock_level, 'active', benefits.trial_enabled, 0, '', benefits.execution_scope,
|
||
benefits.auto_equip, benefits.sort_order,
|
||
CASE
|
||
WHEN benefits.benefit_code = 'online_global_notice'
|
||
THEN JSON_OBJECT('message', '欢迎进入Fami,祝你有美好的一天')
|
||
ELSE NULL
|
||
END,
|
||
0, 0
|
||
FROM (
|
||
SELECT 1 AS level UNION ALL SELECT 2 UNION ALL SELECT 3 UNION ALL SELECT 4 UNION ALL SELECT 5
|
||
UNION ALL SELECT 6 UNION ALL SELECT 7 UNION ALL SELECT 8 UNION ALL SELECT 9
|
||
) AS levels
|
||
INNER JOIN (
|
||
SELECT 'vip_badge_identity' AS benefit_code, 'VIP标识' AS name, 'function' AS benefit_type, 1 AS unlock_level, TRUE AS trial_enabled, 'user' AS execution_scope, FALSE AS auto_equip, 10 AS sort_order
|
||
UNION ALL SELECT 'vip_medal', 'VIP勋章', 'decoration', 1, TRUE, 'user', TRUE, 20
|
||
UNION ALL SELECT 'avatar_frame', 'VIP头像框', 'decoration', 1, TRUE, 'user', TRUE, 30
|
||
UNION ALL SELECT 'vip_title', 'VIP称号', 'function', 1, TRUE, 'user', FALSE, 40
|
||
UNION ALL SELECT 'chat_bubble', 'VIP聊天气泡', 'decoration', 2, TRUE, 'room', TRUE, 50
|
||
UNION ALL SELECT 'vip_gift', 'VIP礼物', 'function', 2, TRUE, 'room', FALSE, 60
|
||
UNION ALL SELECT 'entry_effect', 'VIP进场通知', 'function', 2, TRUE, 'room', FALSE, 70
|
||
UNION ALL SELECT 'visitor_history', '查看访客', 'function', 2, TRUE, 'user', FALSE, 80
|
||
UNION ALL SELECT 'voice_wave', 'VIP声波纹', 'decoration', 3, TRUE, 'room', TRUE, 90
|
||
UNION ALL SELECT 'profile_card', 'VIP资料卡皮肤', 'decoration', 3, TRUE, 'user', TRUE, 100
|
||
UNION ALL SELECT 'custom_room_background', '自定义房间背景', 'function', 3, TRUE, 'room', FALSE, 110
|
||
UNION ALL SELECT 'room_image_message', '房间发图', 'function', 3, TRUE, 'room', FALSE, 120
|
||
UNION ALL SELECT 'animated_avatar', '动态头像', 'function', 4, TRUE, 'user', FALSE, 130
|
||
UNION ALL SELECT 'animated_room_cover', '动态房间封面', 'function', 4, TRUE, 'room', FALSE, 140
|
||
UNION ALL SELECT 'mic_skin', '麦克风皮肤', 'decoration', 4, TRUE, 'room', TRUE, 150
|
||
UNION ALL SELECT 'room_border', '房间边框', 'decoration', 4, TRUE, 'room', TRUE, 160
|
||
UNION ALL SELECT 'vehicle', 'VIP座驾', 'decoration', 4, TRUE, 'room', TRUE, 170
|
||
UNION ALL SELECT 'colored_room_name', 'VIP彩色房间名称', 'function', 5, TRUE, 'room', FALSE, 180
|
||
UNION ALL SELECT 'colored_nickname', 'VIP彩色昵称', 'function', 5, TRUE, 'user', FALSE, 190
|
||
UNION ALL SELECT 'colored_id', 'VIP彩色ID', 'function', 5, TRUE, 'user', FALSE, 200
|
||
UNION ALL SELECT 'gift_tray_skin', '送礼托盘皮肤', 'function', 5, TRUE, 'room', FALSE, 210
|
||
UNION ALL SELECT 'hide_profile_data', '隐藏个人数据', 'function', 6, TRUE, 'user', FALSE, 220
|
||
UNION ALL SELECT 'custom_avatar_frame', '定制头框', 'function', 6, TRUE, 'user', FALSE, 230
|
||
UNION ALL SELECT 'leaderboard_invisible', '榜单隐身', 'function', 6, TRUE, 'user', FALSE, 240
|
||
UNION ALL SELECT 'daily_coin_rebate', '金币返现', 'function', 6, FALSE, 'wallet', FALSE, 250
|
||
UNION ALL SELECT 'custom_profile_card', '定制资料卡', 'function', 7, TRUE, 'user', FALSE, 260
|
||
UNION ALL SELECT 'anonymous_profile_visit', '匿名访问主页', 'function', 7, TRUE, 'user', FALSE, 270
|
||
UNION ALL SELECT 'custom_gift', '定制礼物', 'function', 7, TRUE, 'room', FALSE, 280
|
||
UNION ALL SELECT 'room_entry_notice', '进房高亮通知', 'function', 8, TRUE, 'room', FALSE, 290
|
||
UNION ALL SELECT 'custom_pretty_id', '定制靓号', 'function', 8, TRUE, 'user', FALSE, 300
|
||
UNION ALL SELECT 'custom_vehicle', '定制座驾', 'function', 8, TRUE, 'room', FALSE, 310
|
||
UNION ALL SELECT 'anti_kick', '防踢', 'function', 9, TRUE, 'room', FALSE, 320
|
||
UNION ALL SELECT 'anti_mute', '防禁言', 'function', 9, TRUE, 'room', FALSE, 330
|
||
UNION ALL SELECT 'online_global_notice', '上线全服通知', 'function', 9, TRUE, 'notice', FALSE, 340
|
||
) AS benefits ON benefits.unlock_level <= levels.level;
|
||
|
||
-- 早期执行过本迁移的环境已有权益行;只给空元数据补默认文案,不覆盖运营已配置的 App 文案。
|
||
UPDATE vip_level_benefits
|
||
SET metadata_json = JSON_OBJECT('message', '欢迎进入Fami,祝你有美好的一天')
|
||
WHERE app_code = 'fami'
|
||
AND benefit_code = 'online_global_notice'
|
||
AND COALESCE(JSON_LENGTH(metadata_json), 0) = 0;
|
||
|
||
-- resource_id 使用 resources 自增主键;空素材和仅含 vip_level 的 metadata 明确等待运营补齐视觉资产。
|
||
INSERT IGNORE INTO resources (
|
||
app_code, resource_code, resource_type, name, status, grantable, manager_grant_enabled,
|
||
grant_strategy, wallet_asset_type, wallet_asset_amount, price_type, coin_price, gift_point_amount,
|
||
usage_scope_json, asset_url, preview_url, animation_url, metadata_json, sort_order,
|
||
created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
|
||
) VALUES
|
||
('fami', 'vip_trial_card_1', 'vip_trial_card', 'VIP1体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":1}', 10, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_2', 'vip_trial_card', 'VIP2体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":2}', 20, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_3', 'vip_trial_card', 'VIP3体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":3}', 30, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_4', 'vip_trial_card', 'VIP4体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":4}', 40, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_5', 'vip_trial_card', 'VIP5体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":5}', 50, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_6', 'vip_trial_card', 'VIP6体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":6}', 60, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_7', 'vip_trial_card', 'VIP7体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":7}', 70, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_8', 'vip_trial_card', 'VIP8体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":8}', 80, 0, 0, 0, 0),
|
||
('fami', 'vip_trial_card_9', 'vip_trial_card', 'VIP9体验卡', 'active', TRUE, TRUE, 'new_entitlement', '', 0, 'free', 0, 0, '{}', '', '', '', '{"vip_level":9}', 90, 0, 0, 0, 0);
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_memberships' AND COLUMN_NAME = 'config_version') = 0,
|
||
'ALTER TABLE user_vip_memberships ADD COLUMN config_version BIGINT NOT NULL DEFAULT 1 COMMENT ''激活时使用的 VIP 配置版本'' AFTER program_type',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_purchase_orders' AND COLUMN_NAME = 'previous_expires_at_ms') = 0,
|
||
'ALTER TABLE vip_purchase_orders ADD COLUMN previous_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''购买前会员过期时间快照,UTC epoch ms'' AFTER previous_level',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_purchase_orders' AND COLUMN_NAME = 'new_expires_at_ms') = 0,
|
||
'ALTER TABLE vip_purchase_orders ADD COLUMN new_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''购买后会员过期时间快照,UTC epoch ms'' AFTER previous_expires_at_ms',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_purchase_orders' AND COLUMN_NAME = 'duration_ms') = 0,
|
||
'ALTER TABLE vip_purchase_orders ADD COLUMN duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''购买等级的有效期配置快照'' AFTER price_coin',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_purchase_orders' AND COLUMN_NAME = 'program_type') = 0,
|
||
'ALTER TABLE vip_purchase_orders ADD COLUMN program_type VARCHAR(32) NOT NULL DEFAULT ''legacy_timed'' COMMENT ''订单执行的 VIP 体系快照'' AFTER duration_ms',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_purchase_orders' AND COLUMN_NAME = 'config_version') = 0,
|
||
'ALTER TABLE vip_purchase_orders ADD COLUMN config_version BIGINT NOT NULL DEFAULT 1 COMMENT ''订单执行的 VIP 配置版本'' AFTER program_type',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_purchase_orders' AND COLUMN_NAME = 'expiry_policy') = 0,
|
||
'ALTER TABLE vip_purchase_orders ADD COLUMN expiry_policy VARCHAR(32) NOT NULL DEFAULT ''extend_remaining'' COMMENT ''本单实际采用的有效期策略'' AFTER config_version',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_history' AND COLUMN_NAME = 'previous_expires_at_ms') = 0,
|
||
'ALTER TABLE user_vip_history ADD COLUMN previous_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''变更前过期时间快照,UTC epoch ms'' AFTER new_level',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_history' AND COLUMN_NAME = 'new_expires_at_ms') = 0,
|
||
'ALTER TABLE user_vip_history ADD COLUMN new_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''变更后过期时间快照,UTC epoch ms'' AFTER previous_expires_at_ms',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_history' AND COLUMN_NAME = 'program_type') = 0,
|
||
'ALTER TABLE user_vip_history ADD COLUMN program_type VARCHAR(32) NOT NULL DEFAULT ''legacy_timed'' COMMENT ''变更使用的 VIP 体系快照'' AFTER new_expires_at_ms',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
SET @ddl := IF(
|
||
(SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'user_vip_history' AND COLUMN_NAME = 'config_version') = 0,
|
||
'ALTER TABLE user_vip_history ADD COLUMN config_version BIGINT NOT NULL DEFAULT 1 COMMENT ''变更使用的 VIP 配置版本'' AFTER program_type',
|
||
'SELECT 1'
|
||
);
|
||
PREPARE stmt FROM @ddl;
|
||
EXECUTE stmt;
|
||
DEALLOCATE PREPARE stmt;
|
||
|
||
-- 老库 history 在上述有效期列刚增加时只能得到 0,不能据此否定存量会员的日切资格。
|
||
-- 仅对“完全没有新格式有效期快照”的当前会员写入一条迁移基线;previous_* 保持 0
|
||
-- 明确表示迁移不猜测基线前状态。稳定 order_id 与 NOT EXISTS 使脚本重复执行不增行,
|
||
-- 已有完整新格式 history 的用户也不会被覆盖或改写。
|
||
SET @vip_history_baseline_ms := CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);
|
||
|
||
INSERT INTO user_vip_history (
|
||
app_code, user_id, action, previous_level, new_level, previous_expires_at_ms,
|
||
new_expires_at_ms, program_type, config_version, order_id, created_at_ms
|
||
)
|
||
SELECT
|
||
memberships.app_code,
|
||
memberships.user_id,
|
||
'migration_baseline',
|
||
0,
|
||
memberships.level,
|
||
0,
|
||
memberships.expires_at_ms,
|
||
memberships.program_type,
|
||
memberships.config_version,
|
||
'vip_configurable_060_baseline',
|
||
@vip_history_baseline_ms
|
||
FROM user_vip_memberships AS memberships
|
||
WHERE memberships.level > 0
|
||
AND memberships.expires_at_ms > 0
|
||
AND NOT EXISTS (
|
||
SELECT 1
|
||
FROM user_vip_history AS complete_history
|
||
WHERE complete_history.app_code = memberships.app_code
|
||
AND complete_history.user_id = memberships.user_id
|
||
AND complete_history.new_level > 0
|
||
AND complete_history.new_expires_at_ms > 0
|
||
)
|
||
AND NOT EXISTS (
|
||
SELECT 1
|
||
FROM user_vip_history AS existing_baseline
|
||
WHERE existing_baseline.app_code = memberships.app_code
|
||
AND existing_baseline.user_id = memberships.user_id
|
||
AND existing_baseline.order_id = 'vip_configurable_060_baseline'
|
||
);
|