hyapp-server/scripts/mysql/069_vip_resource_equipment_contract.sql
2026-07-17 16:55:22 +08:00

259 lines
13 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;
-- 装备幂等表是 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 '当时绑定资源 ID0 表示未绑定',
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;