259 lines
13 KiB
SQL
259 lines
13 KiB
SQL
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;
|