hyapp-server/scripts/mysql/060_configurable_vip_program.sql

361 lines
22 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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 '绑定资源 ID0 表示尚未绑定或纯功能权益',
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 '可选资源 ID0 表示直接按等级和时长发卡',
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'
);