SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; USE hyapp_wallet; -- 装备幂等表是 append-only 小行写入;所有重放只按主键 (app_code,command_id) 点查, -- 不修改 user_resource_equipment 主键,也不会为上线迁移扫描用户装备或 entitlement 大表。 CREATE TABLE IF NOT EXISTS user_resource_equipment_commands ( app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', command_id VARCHAR(128) NOT NULL COMMENT '装备业务幂等 ID', request_hash CHAR(64) NOT NULL COMMENT '用户、资源和指定 entitlement 的请求语义哈希', user_id BIGINT NOT NULL COMMENT '操作用户 ID', resource_id BIGINT NOT NULL COMMENT '目标资源 ID', requested_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '客户端指定的 entitlement;空表示服务端选择', result_entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首个成功命中的 entitlement', resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '首个成功结果的资源类型', response_json JSON NULL COMMENT '首个成功 UserResourceEntitlement 回包快照', 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='用户资源装备命令幂等事实表'; -- 配置矩阵很小,历史表按 admin config_version 追加;装备和 appearance 只按资源覆盖索引点查。 -- 记录 program_type,避免 legacy_timed App 的普通资源被 tiered VIP 门禁误伤。 CREATE TABLE IF NOT EXISTS vip_benefit_resource_binding_history ( app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离', history_id CHAR(64) NOT NULL COMMENT '配置版本、阶段、等级和权益的稳定哈希', config_version BIGINT NOT NULL COMMENT '写入该快照的 VIP 配置版本', change_phase VARCHAR(16) NOT NULL COMMENT 'baseline/before/after', program_type VARCHAR(32) NOT NULL COMMENT '快照时的 VIP program 类型', level INT NOT NULL COMMENT 'VIP 等级', benefit_code VARCHAR(64) NOT NULL COMMENT '权益编码', benefit_status VARCHAR(32) NOT NULL COMMENT '权益快照状态', trial_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '体验卡是否可用快照', resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '当时绑定资源 ID;0 表示未绑定', resource_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '当时要求的资源类型', auto_equip BOOLEAN NOT NULL DEFAULT FALSE COMMENT '当时自动装备策略', recorded_at_ms BIGINT NOT NULL COMMENT '记录时间,UTC epoch ms', PRIMARY KEY (app_code, history_id), UNIQUE KEY uk_vip_binding_history_snapshot (app_code, config_version, change_phase, level, benefit_code), KEY idx_vip_binding_history_resource (app_code, resource_id, program_type, benefit_code, level) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 权益资源绑定不可变历史'; -- 装备门禁按单 App、单 resource_id 查询 active 绑定。该配置表规模远小于业务流水, -- 仍显式补覆盖索引并使用在线 DDL,避免每次装备扫描完整权益矩阵。 SET @ddl := IF( (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_level_benefits' AND INDEX_NAME = 'idx_vip_benefit_resource') = 0, 'ALTER TABLE vip_level_benefits ADD INDEX idx_vip_benefit_resource (app_code, resource_id, status, benefit_code, level), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1' ); PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; SET @vip_resource_contract_now_ms := CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED); -- 在任何修正规则执行前保存旧映射;后续 admin 更新还会为每个新版本分别追加 before/after。 INSERT IGNORE INTO vip_benefit_resource_binding_history ( app_code, history_id, config_version, change_phase, program_type, level, benefit_code, benefit_status, trial_enabled, resource_id, resource_type, auto_equip, recorded_at_ms ) SELECT b.app_code, SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, @vip_resource_contract_now_ms FROM vip_level_benefits AS b JOIN vip_program_configs AS p ON p.app_code = b.app_code; -- tiered Fami 的房间视觉权益使用稳定映射:mic_skin -> mic_seat_animation、 -- room_border -> room_border、colored_room_name -> room_name_style。三项都属于 decoration; -- disabled 可以无素材,但若已有 resource_id 也不能保留错误类型或下架资源。 UPDATE vip_level_benefits SET benefit_type = 'decoration', resource_type = CASE benefit_code WHEN 'mic_skin' THEN 'mic_seat_animation' WHEN 'room_border' THEN 'room_border' WHEN 'colored_room_name' THEN 'room_name_style' END, updated_at_ms = @vip_resource_contract_now_ms WHERE app_code = 'fami' AND benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') AND ( benefit_type <> 'decoration' OR resource_type <> CASE benefit_code WHEN 'mic_skin' THEN 'mic_seat_animation' WHEN 'room_border' THEN 'room_border' WHEN 'colored_room_name' THEN 'room_name_style' END ); SET @vip_decoration_normalize_count := ROW_COUNT(); UPDATE vip_level_benefits AS benefit LEFT JOIN resources AS resource ON resource.app_code = benefit.app_code AND resource.resource_id = benefit.resource_id AND resource.status = 'active' AND resource.resource_type = CASE benefit.benefit_code WHEN 'mic_skin' THEN 'mic_seat_animation' WHEN 'room_border' THEN 'room_border' WHEN 'colored_room_name' THEN 'room_name_style' END SET benefit.resource_id = 0, benefit.updated_at_ms = @vip_resource_contract_now_ms WHERE benefit.app_code = 'fami' AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') AND benefit.resource_id > 0 AND resource.resource_id IS NULL; SET @vip_invalid_resource_clear_count := ROW_COUNT(); -- 本地现有 Fami VIP4..VIP8 资源组各自只有一个可核实的 active mic_seat_animation。 -- 这里按稳定 group_code + 类型解析 ID,并用 HAVING COUNT(DISTINCT)=1 防止运营新增候选后误绑; -- VIP9、room_border、room_name_style 当前没有真实目录资源,必须保持未配置,不能伪造 ID/URL。 UPDATE vip_level_benefits AS benefit JOIN ( SELECT resource_group.group_code, MIN(resource.resource_id) AS resource_id FROM resource_groups AS resource_group JOIN resource_group_items AS group_item ON group_item.app_code = resource_group.app_code AND group_item.group_id = resource_group.group_id AND group_item.item_type = 'resource' JOIN resources AS resource ON resource.app_code = group_item.app_code AND resource.resource_id = group_item.resource_id AND resource.resource_type = 'mic_seat_animation' AND resource.status = 'active' WHERE resource_group.app_code = 'fami' AND resource_group.status = 'active' AND resource_group.group_code IN ('VIP4', 'VIP5', 'VIP6', 'VIP7', 'VIP8') GROUP BY resource_group.group_code HAVING COUNT(DISTINCT resource.resource_id) = 1 ) AS mapping ON mapping.group_code = CONCAT('VIP', benefit.level) SET benefit.resource_id = mapping.resource_id, benefit.resource_type = 'mic_seat_animation', benefit.updated_at_ms = @vip_resource_contract_now_ms WHERE benefit.app_code = 'fami' AND benefit.level BETWEEN 4 AND 8 AND benefit.benefit_code = 'mic_skin' AND benefit.status = 'active' AND benefit.resource_id = 0; SET @vip_mic_skin_bind_count := ROW_COUNT(); -- 只给“已经被 active mic_skin 绑定”的真实麦座动画补显式 format:先信任后台已有的 -- 合法 animation_format,否则只从实际 animation/asset URL 的明确后缀推导。这个更新 -- 通过权益配置小表圈定 resource_id,再按 resources 主键点查,不会扫描或修改未绑定资源。 UPDATE vip_level_benefits AS benefit JOIN resources AS resource FORCE INDEX (PRIMARY) ON resource.resource_id = benefit.resource_id AND resource.app_code = benefit.app_code AND resource.resource_type = 'mic_seat_animation' AND resource.status = 'active' SET resource.metadata_json = JSON_SET( COALESCE(resource.metadata_json, JSON_OBJECT()), '$.format', CASE WHEN LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') THEN LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) WHEN LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' THEN CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) WHEN 'jpg' THEN 'jpeg' ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1), '.', -1)) END ELSE CASE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) WHEN 'jpg' THEN 'jpeg' ELSE LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1), '.', -1)) END END ), resource.updated_at_ms = @vip_resource_contract_now_ms WHERE benefit.app_code = 'fami' AND benefit.benefit_code = 'mic_skin' AND benefit.status = 'active' AND benefit.resource_type = 'mic_seat_animation' AND benefit.resource_id > 0 AND (TRIM(resource.asset_url) <> '' OR TRIM(resource.animation_url) <> '') AND TRIM(resource.preview_url) <> '' AND LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') AND ( LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.animation_url), '?', 1), '#', 1)) REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' OR LOWER(SUBSTRING_INDEX(SUBSTRING_INDEX(TRIM(resource.asset_url), '?', 1), '#', 1)) REGEXP '\\.(png|jpeg|jpg|webp|gif|svga|pag|mp4)$' ); SET @vip_mic_format_backfill_count := ROW_COUNT(); -- active 必须能联结到同 App、active、精确类型的真实资源。麦座动画和房间边框还必须 -- 同时具备素材、preview、合法 format,且可选 animation_format 一旦配置就必须一致。 -- 缺少真实配置时只禁用权益,不创建占位资源,也不猜测素材 URL。 UPDATE vip_level_benefits AS benefit LEFT JOIN resources AS resource ON resource.app_code = benefit.app_code AND resource.resource_id = benefit.resource_id AND resource.status = 'active' AND resource.resource_type = benefit.resource_type SET benefit.status = 'disabled', benefit.updated_at_ms = @vip_resource_contract_now_ms WHERE benefit.app_code = 'fami' AND benefit.benefit_code IN ('mic_skin', 'room_border', 'colored_room_name') AND benefit.status = 'active' AND ( resource.resource_id IS NULL OR ( benefit.resource_type IN ('mic_seat_animation', 'room_border') AND ( (TRIM(resource.asset_url) = '' AND TRIM(resource.animation_url) = '') OR TRIM(resource.preview_url) = '' OR LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')), ''))) NOT IN ('png', 'jpeg', 'webp', 'gif', 'svga', 'pag', 'mp4') OR ( LOWER(TRIM(COALESCE(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')), ''))) <> '' AND LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.animation_format')))) <> LOWER(TRIM(JSON_UNQUOTE(JSON_EXTRACT(resource.metadata_json, '$.format')))) ) ) ) ); SET @vip_decoration_disable_count := ROW_COUNT(); -- config_version 只在首次真实回填时递增;迁移重复执行不会制造无意义版本变化。 UPDATE vip_program_configs SET config_version = config_version + 1, updated_at_ms = @vip_resource_contract_now_ms WHERE app_code = 'fami' AND ( @vip_decoration_normalize_count + @vip_invalid_resource_clear_count + @vip_mic_skin_bind_count + @vip_mic_format_backfill_count + @vip_decoration_disable_count ) > 0; -- 本次迁移若提升了 config_version,立即写入新版本的最终 baseline。这样下一次重放开头的 -- baseline INSERT 会命中唯一键,不会仅因为上次版本递增再追加一轮完全相同的历史快照。 INSERT IGNORE INTO vip_benefit_resource_binding_history ( app_code, history_id, config_version, change_phase, program_type, level, benefit_code, benefit_status, trial_enabled, resource_id, resource_type, auto_equip, recorded_at_ms ) SELECT b.app_code, SHA2(CONCAT_WS('|', b.app_code, p.config_version, 'baseline', b.level, b.benefit_code), 256), p.config_version, 'baseline', p.program_type, b.level, b.benefit_code, b.status, b.trial_enabled, b.resource_id, b.resource_type, b.auto_equip, @vip_resource_contract_now_ms FROM vip_level_benefits AS b JOIN vip_program_configs AS p ON p.app_code = b.app_code;