SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci;
SET time_zone = '+00:00';


-- 使用 utf8mb4 连接字符集，保证中文注释、中文名称和 emoji 在导入时不被降级。
CREATE DATABASE IF NOT EXISTS hyapp_wallet DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci;

USE hyapp_wallet;

CREATE TABLE IF NOT EXISTS wallet_accounts (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  asset_type VARCHAR(32) NOT NULL COMMENT '钱包资产类型',
  available_amount BIGINT NOT NULL COMMENT '可用数量',
  frozen_amount BIGINT NOT NULL COMMENT '冻结数量',
  version BIGINT 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, user_id, asset_type)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包账户余额表';

CREATE TABLE IF NOT EXISTS wallet_transactions (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
  biz_type VARCHAR(64) NOT NULL COMMENT '业务类型',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  request_hash VARCHAR(128) NOT NULL COMMENT '请求哈希值',
  external_ref VARCHAR(128) NOT NULL DEFAULT '' COMMENT '外部引用',
  metadata_json JSON NULL COMMENT '扩展元数据 JSON',
  gift_sender_user_id BIGINT GENERATED ALWAYS AS (
    CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.sender_user_id')) AS SIGNED)
  ) VIRTUAL COMMENT '送礼人内部用户 ID，仅用于 gift metadata 的索引检索',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, transaction_id),
  UNIQUE KEY uk_wallet_tx_command (app_code, command_id),
  KEY idx_wallet_tx_biz_time (app_code, biz_type, created_at_ms),
  KEY idx_wallet_tx_biz_status_time (app_code, biz_type, status, created_at_ms),
  KEY idx_wallet_tx_gift_record_page (app_code, biz_type, status, created_at_ms, transaction_id),
  KEY idx_wallet_tx_gift_sender_page (app_code, gift_sender_user_id, biz_type, status, created_at_ms, transaction_id),
  KEY idx_wallet_tx_external_ref (app_code, external_ref)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包交易主表';

-- 提现终局锁是按申请增长的小表，不在 10GB 级 wallet_transactions 上增建唯一生成列索引。
-- 首次 INSERT/ON DUPLICATE KEY 在主键上串行化 settle/release，避免两个“未找到终局”的 gap lock 同时通过后双向执行。
CREATE TABLE IF NOT EXISTS wallet_withdrawal_terminal_locks (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  withdrawal_application_id VARCHAR(64) NOT NULL 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, withdrawal_application_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='提现申请钱包终局串行锁';

-- App 用户送礼榜历史应急查询会按 app_code/biz_type/status/created_at_ms 过滤；
-- 老索引缺 status 时会在 10GB 级 wallet_transactions 上读取大量失败/非成功交易再做 JSON 聚合。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_transactions' AND INDEX_NAME = 'idx_wallet_tx_biz_status_time') = 0,
  'ALTER TABLE wallet_transactions ADD INDEX idx_wallet_tx_biz_status_time (app_code, biz_type, status, created_at_ms)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 送礼记录分页按 app/biz_type/status 过滤，并用 created_at_ms + transaction_id 做稳定倒序；
-- 把 transaction_id 放进索引末尾可直接覆盖同毫秒翻页边界，避免深页额外回表后再做不稳定排序。
-- 存量大表建索引会扫描 wallet_transactions 并产生额外 IO/磁盘占用，生产执行必须安排低峰窗口并先确认磁盘余量。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_transactions' AND INDEX_NAME = 'idx_wallet_tx_gift_record_page') = 0,
  'ALTER TABLE wallet_transactions ADD INDEX idx_wallet_tx_gift_record_page (app_code, biz_type, status, created_at_ms, transaction_id), ALGORITHM=INPLACE, LOCK=NONE',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 公共用户筛选先在 user owner 库解析内部 user_id，再按 metadata 中冻结的 sender_user_id 检索钱包事实；
-- 虚拟生成列不复制业务事实，历史交易会自动可查，也避免后台分页对 JSON_EXTRACT 做全表扫描。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_transactions' AND COLUMN_NAME = 'gift_sender_user_id') = 0,
  'ALTER TABLE wallet_transactions ADD COLUMN gift_sender_user_id BIGINT GENERATED ALWAYS AS (CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, ''$.sender_user_id'')) AS SIGNED)) VIRTUAL COMMENT ''送礼人内部用户 ID，仅用于 gift metadata 的索引检索''',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 组合索引覆盖 app + 送礼人 + 场景/状态 + 稳定时间翻页；存量大表建索引仍会扫描 wallet_transactions，
-- 上线执行前必须在低峰确认磁盘余量和在线 DDL 能力，执行中监控 IO、复制延迟与 metadata lock 等待。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_transactions' AND INDEX_NAME = 'idx_wallet_tx_gift_sender_page') = 0,
  'ALTER TABLE wallet_transactions ADD INDEX idx_wallet_tx_gift_sender_page (app_code, gift_sender_user_id, biz_type, status, created_at_ms, transaction_id), ALGORITHM=INPLACE, LOCK=NONE',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS wallet_entries (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  entry_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '流水 ID',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  asset_type VARCHAR(32) NOT NULL COMMENT '钱包资产类型',
  available_delta BIGINT NOT NULL COMMENT '可用变动',
  frozen_delta BIGINT NOT NULL COMMENT '冻结变动',
  available_after BIGINT NOT NULL COMMENT '可用后',
  frozen_after BIGINT NOT NULL COMMENT '冻结后',
  counterparty_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '交易对手用户 ID',
  room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  KEY idx_wallet_entries_user_time (app_code, user_id, created_at_ms),
  KEY idx_wallet_entries_tx (app_code, transaction_id),
  KEY idx_wallet_entries_asset_time (app_code, asset_type, created_at_ms),
  KEY idx_wallet_entries_asset_user_time (app_code, asset_type, user_id, created_at_ms, entry_id),
  KEY idx_wallet_entries_asset_counterparty_time (app_code, asset_type, counterparty_user_id, created_at_ms, entry_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包复式流水表';

-- 金币流水和统计回放会按 app/asset/user/time 顺序读取钱包分录；
-- 老索引只能按 asset/time 或 user/time 命中，百万级 COIN 分录回放时会退化成大范围扫描和排序。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_entries' AND INDEX_NAME = 'idx_wallet_entries_asset_user_time') = 0,
  'ALTER TABLE wallet_entries ADD INDEX idx_wallet_entries_asset_user_time (app_code, asset_type, user_id, created_at_ms, entry_id)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 工资转币商查询从币商侧分录反查来源工资用户；老表缺对手用户组合索引时，
-- source_user 关键词会在 COIN_SELLER_COIN 分录中做大范围过滤。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_entries' AND INDEX_NAME = 'idx_wallet_entries_asset_counterparty_time') = 0,
  'ALTER TABLE wallet_entries ADD INDEX idx_wallet_entries_asset_counterparty_time (app_code, asset_type, counterparty_user_id, created_at_ms, entry_id)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS wallet_outbox (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  event_id VARCHAR(128) NOT NULL COMMENT '事件幂等 ID',
  event_type VARCHAR(64) NOT NULL COMMENT '事件类型',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  asset_type VARCHAR(32) NOT NULL COMMENT '钱包资产类型',
  available_delta BIGINT NOT NULL COMMENT '可用变动',
  frozen_delta BIGINT NOT NULL COMMENT '冻结变动',
  payload JSON NOT NULL COMMENT '业务负载快照',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  worker_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'MQ 投递 worker ID',
  lock_until_ms BIGINT NULL COMMENT 'MQ 投递锁过期时间，UTC epoch ms',
  retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
  next_retry_at_ms BIGINT NULL COMMENT '下一次 MQ 投递重试时间，UTC epoch ms',
  last_error TEXT 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, event_id),
  KEY idx_wallet_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms, event_id),
  KEY idx_wallet_outbox_claim (app_code, status, lock_until_ms, created_at_ms, event_id),
  KEY idx_wallet_outbox_event_pending (app_code, status, event_type, next_retry_at_ms, created_at_ms, event_id),
  KEY idx_wallet_outbox_event_claim (app_code, status, event_type, lock_until_ms, created_at_ms, event_id),
  KEY idx_wallet_outbox_retention (app_code, status, updated_at_ms, event_id),
  KEY idx_wallet_outbox_event_created (app_code, event_type, created_at_ms, event_id),
  KEY idx_wallet_outbox_asset_event_created (app_code, asset_type, event_type, created_at_ms, event_id),
  KEY idx_wallet_outbox_tx (app_code, transaction_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包事件 outbox 表';

-- 只记录 data/manifest/_SUCCESS 均在私有 COS 读回校验成功的批次。
-- 当前表不授权删除 wallet_outbox；notice/statistics/gift-wall 等读者迁移前仅用于推进 archive-only 稳定游标。
CREATE TABLE IF NOT EXISTS wallet_outbox_archive_receipts (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  batch_id CHAR(64) NOT NULL COMMENT '归档批次内容指纹',
  state VARCHAR(32) NOT NULL COMMENT '只有 VERIFIED 可作为后续归档游标，不代表已恢复演练',
  data_object_key VARCHAR(512) NOT NULL,
  manifest_object_key VARCHAR(512) NOT NULL,
  success_object_key VARCHAR(512) NOT NULL,
  schema_version INT NOT NULL,
  row_count INT NOT NULL,
  first_updated_at_ms BIGINT NOT NULL,
  first_event_id VARCHAR(128) NOT NULL,
  last_updated_at_ms BIGINT NOT NULL,
  last_event_id VARCHAR(128) NOT NULL,
  min_created_at_ms BIGINT NOT NULL,
  max_created_at_ms BIGINT NOT NULL,
  available_delta_total VARCHAR(80) NOT NULL,
  frozen_delta_total VARCHAR(80) NOT NULL,
  ndjson_sha256 CHAR(64) NOT NULL,
  gzip_sha256 CHAR(64) NOT NULL,
  data_crc64 VARCHAR(32) NOT NULL,
  manifest_crc64 VARCHAR(32) NOT NULL,
  success_crc64 VARCHAR(32) NOT NULL,
  manifest_sha256 CHAR(64) NOT NULL,
  uncompressed_bytes BIGINT NOT NULL,
  compressed_bytes BIGINT NOT NULL,
  verified_at_ms BIGINT NOT NULL,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, batch_id),
  KEY idx_wallet_outbox_archive_cursor (app_code, state, last_updated_at_ms, last_event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='wallet outbox 外部归档读回验证回执';

-- wallet_outbox 自动清理只新增独立窄表，不 ALTER 热 outbox，也不会触发表重写；member 随未清理归档行增减。
-- 授权行必须由部署流程在隔离库完成 RESTORE_VERIFIED 并核对归档回执后手工写入；初始化脚本绝不生成清理授权。
CREATE TABLE IF NOT EXISTS wallet_outbox_archive_members (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  event_id VARCHAR(128) NOT NULL COMMENT '已归档 wallet_outbox 事件 ID',
  updated_at_ms BIGINT NOT NULL COMMENT '归档时 delivered 行的精确更新时间，UTC epoch ms',
  batch_id CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '包含该精确事件的归档批次 ID',
  manifest_sha256 CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '包含该事件的 manifest SHA-256',
  receipt_verified_at_ms BIGINT NOT NULL COMMENT '归档 receipt 完成 COS 读回验证的时间，UTC epoch ms',
  PRIMARY KEY (app_code, event_id),
  KEY idx_wallet_outbox_archive_member_purge (app_code, updated_at_ms, event_id),
  KEY idx_wallet_outbox_archive_member_batch (app_code, batch_id, event_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='wallet outbox 已验证归档精确成员';

CREATE TABLE IF NOT EXISTS wallet_outbox_archive_purge_authorizations (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  archive_batch_id CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '完成恢复演练的归档批次内容指纹',
  manifest_sha256 CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '恢复演练核对通过的 manifest SHA-256',
  schema_version INT NOT NULL COMMENT '恢复演练核对通过的归档 schema 版本',
  row_count INT NOT NULL COMMENT '恢复演练核对通过的精确行数',
  data_crc64 VARCHAR(32) NOT NULL COMMENT '恢复演练核对通过的 data COS CRC64',
  manifest_crc64 VARCHAR(32) NOT NULL COMMENT '恢复演练核对通过的 manifest COS CRC64',
  success_crc64 VARCHAR(32) NOT NULL COMMENT '恢复演练核对通过的 _SUCCESS COS CRC64',
  rehearsal_receipt_id VARCHAR(64) NOT NULL COMMENT '隔离库 RESTORE_VERIFIED 演练回执 ID',
  state VARCHAR(32) NOT NULL COMMENT '固定为 RESTORE_VERIFIED，其他状态不能授权清理',
  verified_at_ms BIGINT NOT NULL COMMENT '恢复演练验证完成时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '授权证据录入时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '授权证据更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, archive_batch_id, manifest_sha256),
  KEY idx_wallet_outbox_purge_auth_verified (app_code, state, verified_at_ms, archive_batch_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='wallet outbox 恢复演练清理授权证据';

-- 一行只对应一个已提交的精确 PK 删除批次；删除与回执写入同事务，失败或回滚时不会留下虚假清理证据。
CREATE TABLE IF NOT EXISTS wallet_outbox_archive_purge_receipts (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  purge_id CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '清理批次稳定指纹',
  state VARCHAR(32) NOT NULL COMMENT '固定为 PURGED',
  policy_version VARCHAR(32) NOT NULL COMMENT '正向安全事件白名单版本',
  cutoff_ms BIGINT NOT NULL COMMENT '本批固定的 delivered 保留截止时间，UTC epoch ms，删除严格小于该值',
  receipt_verified_before_ms BIGINT NOT NULL COMMENT '独立归档回执成熟截止时间，只有更早 VERIFIED receipt 可清理',
  candidate_sha256 CHAR(64) CHARACTER SET ascii COLLATE ascii_bin NOT NULL COMMENT '按游标排序的精确 (app_code,event_id) 候选集合 SHA-256',
  candidate_count INT NOT NULL COMMENT '事务内锁定的候选行数',
  deleted_count INT NOT NULL COMMENT '精确 PK 删除影响行数，必须与 candidate_count 一致',
  first_updated_at_ms BIGINT NOT NULL COMMENT '首条候选 delivered 游标时间',
  first_event_id VARCHAR(128) NOT NULL COMMENT '首条候选事件 ID',
  last_updated_at_ms BIGINT NOT NULL COMMENT '末条候选 delivered 游标时间',
  last_event_id VARCHAR(128) NOT NULL COMMENT '末条候选事件 ID',
  purged_at_ms BIGINT NOT NULL COMMENT '清理提交时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, purge_id),
  KEY idx_wallet_outbox_purge_receipt_time (app_code, purged_at_ms, purge_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='wallet outbox 精确 PK 清理审计回执';

-- 现有 POINT 提现和兑换继续读取原运行政策表；礼物收益只走 Host 政策，不能读取本表入 POINT。
CREATE TABLE IF NOT EXISTS wallet_policy_instances (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  instance_code VARCHAR(96) NOT NULL COMMENT '后台策略实例编码',
  template_code VARCHAR(96) NOT NULL COMMENT '模板编码快照',
  template_version VARCHAR(64) NOT NULL COMMENT '模板版本快照',
  region_id BIGINT NOT NULL DEFAULT 0 COMMENT '适用地区，0 表示 App 兜底',
  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
  effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'UTC epoch ms',
  effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '0 表示长期有效',
  host_point_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '兼容既有表结构；新礼物不读取',
  host_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '兼容既有表结构；新礼物不读取',
  agency_point_ratio_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '兼容既有表结构；新礼物不读取',
  points_per_usd BIGINT NOT NULL DEFAULT 100000 COMMENT 'POINT/USD 展示换算比例',
  withdraw_fee_bps INT NOT NULL DEFAULT 500 COMMENT '提现手续费 bps',
  rule_json JSON NOT NULL COMMENT '完整政策快照',
  published_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
  published_at_ms BIGINT NOT NULL DEFAULT 0,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, instance_code, region_id),
  KEY idx_wallet_policy_active (app_code, region_id, status, effective_from_ms, effective_to_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包运行侧政策实例表';

-- 三套 App 共享同一张配置表，但 app_code 是主键的一部分；新 App 只需配置数据，不再复制表和业务代码。
-- 未配置任何 active 行时 H5 币商提现入口显示为空，不能用伪造默认币商兜底。
CREATE TABLE IF NOT EXISTS point_withdrawal_coin_seller_configs (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  seller_user_id BIGINT NOT NULL COMMENT '币商用户 ID',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '展示顺序，越小越靠前',
  point_amount BIGINT NOT NULL COMMENT '兑换比例中的 POINT 分母',
  seller_coin_amount BIGINT NOT NULL COMMENT '兑换比例中的币商金币分子',
  service_country_codes JSON NOT NULL COMMENT '服务国家码数组；空数组表示不限国家',
  service_region_ids JSON NULL COMMENT '服务区域 ID 数组；与国家数组互斥，两个数组都为空表示全量',
  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT 'active/disabled',
  created_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
  updated_by_admin_id BIGINT UNSIGNED NOT NULL DEFAULT 0,
  created_at_ms BIGINT NOT NULL,
  updated_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, seller_user_id),
  KEY idx_point_withdraw_seller_list (app_code, status, sort_order, seller_user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT 提现币商白名单与兑换比例';

-- Agency 分成独立流水同时承担归属快照和审计依据；不能按当前成员关系反推历史收益。
CREATE TABLE IF NOT EXISTS agency_point_share_entries (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  transaction_id VARCHAR(96) NOT NULL COMMENT '礼物钱包交易 ID',
  host_user_id BIGINT NOT NULL COMMENT '收礼主播用户 ID 快照',
  agency_owner_user_id BIGINT NOT NULL COMMENT '送礼时 Agency owner 用户 ID 快照',
  sender_user_id BIGINT NOT NULL COMMENT '送礼用户 ID',
  room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID，私聊送礼为空',
  point_delta BIGINT NOT NULL COMMENT 'Agency owner 本次 POINT 分成',
  ratio_ppm BIGINT NOT NULL COMMENT 'Agency 分成比例快照，计算基数见 share_base_type',
  share_base_type VARCHAR(32) NOT NULL DEFAULT 'unknown' COMMENT '分成基数：unknown/charge_amount/host_income；unknown 表示旧写入方未提供快照',
  share_base_amount BIGINT NOT NULL DEFAULT 0 COMMENT '本次分成计算基数；share_base_type=unknown 时不可解释为真实 0',
  policy_instance_code VARCHAR(96) NOT NULL DEFAULT '' COMMENT '收益策略实例快照',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  PRIMARY KEY (app_code, transaction_id, agency_owner_user_id),
  KEY idx_agency_point_share_owner_time (app_code, agency_owner_user_id, created_at_ms),
  KEY idx_agency_point_share_host_time (app_code, host_user_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='旧 Agency POINT 流水结构；新送礼不读写';

-- 主播工资周期钻石账户：只记录政策结算用累计值，不作为用户可消费或可提现的钱包资产。
-- cycle_key 当前固定为 UTC yyyy-MM；后续日结/半月结不清空该账户，月末清算任务按策略处理。
CREATE TABLE IF NOT EXISTS host_period_diamond_accounts (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '主播用户 ID',
  cycle_key VARCHAR(16) NOT NULL COMMENT '工资周期键，UTC yyyy-MM',
  region_id BIGINT NOT NULL COMMENT '主播身份所属区域 ID，用于匹配区域工资政策',
  agency_owner_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '送礼入账时主播归属 Agency 的 owner 用户 ID，0 表示无代理收款人',
  total_diamonds BIGINT NOT NULL DEFAULT 0 COMMENT '周期累计钻石',
  gift_diamond_total BIGINT NOT NULL DEFAULT 0 COMMENT '送礼产生的周期钻石累计',
  version BIGINT NOT NULL DEFAULT 1 COMMENT '版本号',
  first_gift_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '本周期首次收礼时间，UTC epoch ms',
  last_gift_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '本周期最后收礼时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, user_id, cycle_key),
  KEY idx_host_period_diamond_region_cycle (app_code, region_id, cycle_key),
  KEY idx_host_period_diamond_cycle_total (app_code, cycle_key, total_diamonds),
  KEY idx_host_period_diamond_cycle_owner (app_code, cycle_key, agency_owner_user_id, total_diamonds)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播工资周期钻石账户表';

-- 经理中心团队工资统计按 (cycle_key, agency_owner_user_id) 圈定团队主播；
-- 存量环境缺该索引时聚合会退化为整周期扫描，这里幂等补建。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'host_period_diamond_accounts' AND INDEX_NAME = 'idx_host_period_diamond_cycle_owner') = 0,
  'ALTER TABLE host_period_diamond_accounts ADD INDEX idx_host_period_diamond_cycle_owner (app_code, cycle_key, agency_owner_user_id, total_diamonds)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 主播工资周期钻石流水：每笔成功送礼只在 target 为 active host 时写入，用于审计和补偿重建周期账户。
CREATE TABLE IF NOT EXISTS host_period_diamond_entries (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  entry_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '流水 ID',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
  user_id BIGINT NOT NULL COMMENT '主播用户 ID',
  cycle_key VARCHAR(16) NOT NULL COMMENT '工资周期键，UTC yyyy-MM',
  region_id BIGINT NOT NULL COMMENT '主播身份所属区域 ID',
  agency_owner_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '本次送礼入账使用的代理收款用户快照',
  diamond_delta BIGINT NOT NULL COMMENT '本次增加钻石',
  diamond_after BIGINT NOT NULL COMMENT '本次入账后周期累计钻石',
  gift_charge_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '礼物扣费资产类型快照',
  gift_charge_amount BIGINT NOT NULL DEFAULT 0 COMMENT '礼物扣费金额快照',
  gift_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '礼物 ID',
  gift_count INT NOT NULL DEFAULT 0 COMMENT '礼物数量',
  sender_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '送礼用户 ID',
  room_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '房间 ID',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  UNIQUE KEY uk_host_period_diamond_tx (app_code, transaction_id, user_id, cycle_key),
  KEY idx_host_period_diamond_entries_user_cycle (app_code, user_id, cycle_key, created_at_ms),
  KEY idx_host_period_diamond_entries_user_time (app_code, user_id, created_at_ms, entry_id),
  KEY idx_host_period_diamond_entries_region_cycle (app_code, region_id, cycle_key, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播工资周期钻石流水表';

-- POINT_DIAMOND 的钱包分录允许 Host 与 Agency 同用户时合并入账，不能承担角色统计。
-- 该表只由成功送礼事务写入，并保留两部分原始金额；任务奖励、兑换、提现和退回没有写入入口。
CREATE TABLE IF NOT EXISTS point_diamond_gift_income_entries (
  entry_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '投影流水 ID',
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码，必须显式写入以保证多租户隔离',
  transaction_id VARCHAR(96) NOT NULL COMMENT '送礼钱包交易 ID',
  host_user_id BIGINT NOT NULL COMMENT '收礼主播用户 ID',
  agency_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '本次代理分成收款用户 ID，0 表示无分成',
  sender_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '送礼用户 ID',
  host_base_amount BIGINT NOT NULL COMMENT '本次 Host 基础钻石积分，禁止包含 Agency 分成',
  agency_amount BIGINT NOT NULL DEFAULT 0 COMMENT '本次 Agency 钻石积分分成',
  created_at_ms BIGINT NOT NULL COMMENT '送礼成功时间，UTC epoch ms',
  UNIQUE KEY uk_point_diamond_gift_income_tx (app_code, transaction_id),
  KEY idx_point_diamond_gift_host_time (app_code, host_user_id, created_at_ms, entry_id),
  KEY idx_point_diamond_gift_agency_time (app_code, agency_user_id, created_at_ms, entry_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='POINT_DIAMOND 送礼收益角色拆分投影';

-- 运行侧工资政策表：字段与 admin_host_agency_salary_policies 对齐，wallet-service 结算只读取这里的已发布快照。
-- 后台配置仍由 admin-server 管理；同步/导入时必须整份写入 policy + levels，结算事务不直接读 admin 库。
CREATE TABLE IF NOT EXISTS host_agency_salary_policies (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  policy_id BIGINT UNSIGNED NOT NULL COMMENT '工资政策 ID，来自后台配置',
  cycle_key VARCHAR(7) NOT NULL COMMENT '政策归属工资周期，UTC yyyy-MM',
  policy_version BIGINT UNSIGNED NOT NULL COMMENT '不可变政策版本；首次等于后台政策 ID，修订后使用递增版本',
  name VARCHAR(120) NOT NULL DEFAULT '' COMMENT '政策名称',
  policy_type VARCHAR(32) NOT NULL DEFAULT 'SALARY_DIAMOND' COMMENT '政策类型：SALARY_DIAMOND/POINT_DIAMOND',
  region_id BIGINT NOT NULL COMMENT '适用区域 ID',
  status VARCHAR(24) NOT NULL DEFAULT 'disabled' COMMENT '状态：active/disabled',
  settlement_mode VARCHAR(24) NOT NULL DEFAULT 'daily' COMMENT '结算方式：daily/half_month',
  settlement_trigger_mode VARCHAR(24) NOT NULL DEFAULT 'automatic' COMMENT '结算触发方式：automatic/manual',
  gift_coin_to_diamond_ratio DECIMAL(18,6) NOT NULL DEFAULT 1.000000 COMMENT '周期钻石政策系数快照，1 表示不调整礼物类型倍率',
  point_diamonds_per_usd BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 每美元对应钻石积分',
  coins_per_usd BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 每美元对应金币',
  minimum_withdraw_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 最低提现美元美分',
  withdraw_fee_bps INT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 提现费率基点',
  agency_point_share_bps INT NOT NULL DEFAULT 0 COMMENT 'POINT_DIAMOND 代理分成占主播基础积分的基点',
  residual_diamond_to_usd_rate DECIMAL(24,12) NOT NULL DEFAULT 0.000000000000 COMMENT '月底剩余钻石转美元比例',
  coin_seller_withdrawal_limit_period VARCHAR(16) NOT NULL DEFAULT 'month' COMMENT '用户找币商提现限制周期：day/week/month',
  coin_seller_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找币商提现次数，0 表示不限',
  platform_withdrawal_limit_period VARCHAR(16) NOT NULL DEFAULT 'month' COMMENT '用户找平台提现限制周期：day/week/month',
  platform_withdrawal_limit_count BIGINT NOT NULL DEFAULT 0 COMMENT '用户找平台提现次数，0 表示不限',
  platform_withdrawal_allowed_days VARCHAR(96) NOT NULL DEFAULT '' COMMENT '用户找平台提现允许的 UTC 月内日，逗号分隔，空表示每天',
  effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间，UTC epoch ms',
  effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效结束时间，0 表示长期有效',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, policy_id, policy_version),
  KEY idx_host_agency_salary_policy_cycle (app_code, region_id, cycle_key, status, settlement_mode, settlement_trigger_mode)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播代理工资政策运行快照表';

-- 计数按完整主键点查/行锁，不读取钱包交易或提现申请流水，限制检查的成本与历史交易量无关。
CREATE TABLE IF NOT EXISTS point_withdrawal_policy_counters (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  source_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '提现来源资产；POINT 与 POINT_DIAMOND 独立计数',
  user_id BIGINT NOT NULL COMMENT '提现用户 ID',
  channel VARCHAR(24) NOT NULL COMMENT '提现渠道：coin_seller/platform',
  period VARCHAR(16) NOT NULL COMMENT '限制周期类型：day/week/month',
  period_key VARCHAR(10) NOT NULL COMMENT 'UTC 周期键：yyyy-MM-dd/yyyy-MM',
  withdrawal_count BIGINT UNSIGNED NOT NULL DEFAULT 0 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, source_asset_type, user_id, channel, period, period_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户自然周期提现次数计数表';

-- 平台提现只有申请创建失败回滚才释放 reservation；审核驳回仍保留已发起次数。
CREATE TABLE IF NOT EXISTS point_withdrawal_limit_reservations (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  source_asset_type VARCHAR(32) NOT NULL DEFAULT 'POINT' COMMENT '提现来源资产；避免不同积分钱包共享幂等域',
  withdrawal_ref VARCHAR(128) NOT NULL COMMENT 'gateway 生成的申请前置引用',
  freeze_command_id VARCHAR(128) NOT NULL COMMENT '首次冻结命令幂等 ID',
  user_id BIGINT NOT NULL COMMENT '提现用户 ID',
  channel VARCHAR(24) NOT NULL COMMENT '提现渠道，当前为 platform',
  policy_id BIGINT UNSIGNED NOT NULL COMMENT '命中的工资政策 ID',
  policy_version BIGINT UNSIGNED NOT NULL COMMENT '命中的不可变政策版本',
  period VARCHAR(16) NOT NULL COMMENT '命中的限制周期类型：day/week/month',
  period_key VARCHAR(10) NOT NULL COMMENT '命中的 UTC 限制周期键',
  counted BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否实际占用了非零次数限制',
  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '状态：active/released',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  released_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '创建失败回滚时间，UTC epoch ms',
  PRIMARY KEY (app_code, source_asset_type, withdrawal_ref),
  UNIQUE KEY uk_point_withdrawal_limit_reservation_command (app_code, source_asset_type, freeze_command_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='平台积分提现次数预留表';

-- 周期绑定按月唯一；历史月不更新，当前/未来月同一逻辑政策发布更高版本时只切换版本号。
CREATE TABLE IF NOT EXISTS host_salary_policy_cycle_bindings (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  region_id BIGINT NOT NULL COMMENT '适用区域 ID',
  cycle_key VARCHAR(7) NOT NULL COMMENT '工资周期键，UTC yyyy-MM',
  policy_id BIGINT UNSIGNED NOT NULL COMMENT '绑定的不可变政策 ID',
  policy_version BIGINT UNSIGNED NOT NULL COMMENT '绑定的不可变政策版本',
  source_cycle_key VARCHAR(7) NOT NULL DEFAULT '' COMMENT '来源月份；等于 cycle_key 表示显式发布，否则表示继承',
  created_at_ms BIGINT NOT NULL COMMENT '首次绑定时间，UTC epoch ms',
  PRIMARY KEY (app_code, region_id, cycle_key)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播工资周期不可变政策绑定表';

CREATE TABLE IF NOT EXISTS host_agency_salary_policy_levels (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  policy_id BIGINT UNSIGNED NOT NULL COMMENT '工资政策 ID',
  policy_version BIGINT UNSIGNED NOT NULL COMMENT '所属不可变政策版本',
  level_no INT NOT NULL COMMENT '等级',
  required_diamonds BIGINT NOT NULL COMMENT '达到该等级所需主播当月钻石',
  host_salary_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '主播累计美元工资，单位美分',
  host_coin_reward BIGINT NOT NULL DEFAULT 0 COMMENT '主播累计金币奖励',
  agency_salary_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '代理累计美元工资，单位美分',
  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '等级状态：active/disabled',
  sort_order INT NOT NULL DEFAULT 0 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, policy_id, policy_version, level_no),
  KEY idx_host_agency_salary_policy_level_required (app_code, policy_id, policy_version, status, required_diamonds)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播代理工资政策等级运行快照表';

-- 结算进度按主播+月份独立记录累计已发值；日结/半月结只更新累计，月底清算只打 cleared 标记。
-- 这里不覆盖 host_period_diamond_accounts.total_diamonds，历史累计钻石必须可审计和可重放。
CREATE TABLE IF NOT EXISTS host_salary_settlement_progress (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '主播用户 ID',
  cycle_key VARCHAR(16) NOT NULL COMMENT '工资周期键，UTC yyyy-MM',
  settled_level_no INT NOT NULL DEFAULT 0 COMMENT '已结算到的最高等级',
  settled_host_salary_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '已发主播美元工资累计，单位美分',
  settled_host_coin_reward BIGINT NOT NULL DEFAULT 0 COMMENT '已发主播金币奖励累计',
  settled_agency_salary_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '已发代理美元工资累计，单位美分',
  last_settled_total_diamonds BIGINT NOT NULL DEFAULT 0 COMMENT '上次结算任务已处理到的周期累计钻石，避免未升档时重复空跑',
  residual_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '月底剩余钻石已转主播美元金额，单位美分',
  month_end_cleared_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '月底清算完成时间，0 表示未清算',
  last_policy_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后一次使用的政策 ID',
  last_policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后一次使用的不可变政策版本',
  version BIGINT NOT NULL DEFAULT 1 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, user_id, cycle_key),
  KEY idx_host_salary_settlement_progress_cycle (app_code, cycle_key, month_end_cleared_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播工资周期结算进度表';

CREATE TABLE IF NOT EXISTS host_salary_settlement_records (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  settlement_id VARCHAR(128) NOT NULL COMMENT '结算记录 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '钱包交易幂等命令 ID',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  settlement_type VARCHAR(24) NOT NULL COMMENT '结算类型：daily/half_month/month_end',
  trigger_mode VARCHAR(24) NOT NULL DEFAULT 'automatic' COMMENT '触发方式：automatic/manual',
  settlement_role VARCHAR(24) NOT NULL DEFAULT 'all' COMMENT '结算身份：all/host/agency',
  user_id BIGINT NOT NULL COMMENT '主播用户 ID',
  agency_owner_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '代理收款用户 ID',
  cycle_key VARCHAR(16) NOT NULL COMMENT '工资周期键，UTC yyyy-MM',
  policy_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用的政策 ID',
  policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用的不可变政策版本',
  level_no INT NOT NULL DEFAULT 0 COMMENT '本次结算后的等级',
  total_diamonds BIGINT NOT NULL DEFAULT 0 COMMENT '结算时周期累计钻石',
  host_salary_usd_minor_delta BIGINT NOT NULL DEFAULT 0 COMMENT '本次主播美元工资增量，单位美分',
  host_coin_reward_delta BIGINT NOT NULL DEFAULT 0 COMMENT '本次主播金币奖励增量',
  agency_salary_usd_minor_delta BIGINT NOT NULL DEFAULT 0 COMMENT '本次代理美元工资增量，单位美分',
  residual_usd_minor_delta BIGINT NOT NULL DEFAULT 0 COMMENT '本次月底剩余钻石转美元增量，单位美分',
  status VARCHAR(24) NOT NULL DEFAULT 'succeeded' COMMENT '状态：succeeded/skipped/failed',
  reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '跳过或失败原因',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  PRIMARY KEY (app_code, settlement_id),
  UNIQUE KEY uk_host_salary_settlement_command (app_code, command_id),
  KEY idx_host_salary_settlement_user_cycle (app_code, user_id, cycle_key, created_at_ms),
  KEY idx_host_salary_settlement_agency_cycle (app_code, agency_owner_user_id, cycle_key, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='主播代理工资结算记录表';

CREATE TABLE IF NOT EXISTS team_salary_settlement_progress (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  policy_type VARCHAR(24) NOT NULL COMMENT '团队工资类型：bd/admin',
  user_id BIGINT NOT NULL COMMENT '收款用户 ID：BD 或 Admin',
  cycle_key VARCHAR(16) NOT NULL COMMENT '工资周期键，UTC yyyy-MM',
  region_id BIGINT NOT NULL COMMENT '结算时使用的区域 ID',
  settled_level_no INT NOT NULL DEFAULT 0 COMMENT '已结算到的最高等级',
  settled_salary_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '已发团队工资累计，单位美分',
  last_settled_income_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '上次已处理收入累计，单位美分，用于避免未升档重复空跑',
  last_policy_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '最后一次使用的政策 ID',
  month_closed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '完整月结算关闭时间，0 表示未关闭',
  version BIGINT NOT NULL DEFAULT 1 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, policy_type, user_id, cycle_key),
  KEY idx_team_salary_progress_cycle (app_code, policy_type, cycle_key, region_id, month_closed_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BD/Admin 工资周期结算进度表';

CREATE TABLE IF NOT EXISTS team_salary_settlement_records (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  settlement_id VARCHAR(128) NOT NULL COMMENT '结算记录 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '钱包交易幂等命令 ID',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  policy_type VARCHAR(24) NOT NULL COMMENT '团队工资类型：bd/admin',
  trigger_mode VARCHAR(24) NOT NULL DEFAULT 'manual' COMMENT '触发方式：automatic/manual',
  user_id BIGINT NOT NULL COMMENT '收款用户 ID：BD 或 Admin',
  cycle_key VARCHAR(16) NOT NULL COMMENT '工资周期键，UTC yyyy-MM',
  region_id BIGINT NOT NULL COMMENT '结算时使用的区域 ID',
  policy_id BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT '使用的政策 ID',
  level_no INT NOT NULL DEFAULT 0 COMMENT '本次结算后的等级',
  income_usd_minor BIGINT NOT NULL DEFAULT 0 COMMENT '结算时团队收入累计，单位美分',
  salary_usd_minor_delta BIGINT NOT NULL DEFAULT 0 COMMENT '本次团队美元工资增量，单位美分',
  status VARCHAR(24) NOT NULL DEFAULT 'succeeded' COMMENT '状态：succeeded/skipped/failed',
  reason VARCHAR(255) NOT NULL DEFAULT '' COMMENT '跳过或失败原因',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  PRIMARY KEY (app_code, settlement_id),
  UNIQUE KEY uk_team_salary_settlement_command (app_code, command_id),
  KEY idx_team_salary_records_user_cycle (app_code, policy_type, user_id, cycle_key, created_at_ms),
  KEY idx_team_salary_records_cycle (app_code, policy_type, cycle_key, region_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='BD/Admin 工资结算记录表';

-- 老环境已建过主播周期账户时，CREATE TABLE IF NOT EXISTS 不会补新字段；结算前必须有代理收款快照。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'host_period_diamond_accounts' AND COLUMN_NAME = 'agency_owner_user_id') = 0,
  'ALTER TABLE host_period_diamond_accounts ADD COLUMN agency_owner_user_id BIGINT NOT NULL DEFAULT 0 COMMENT ''送礼入账时主播归属 Agency 的 owner 用户 ID，0 表示无代理收款人'' AFTER region_id',
  '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 = 'host_period_diamond_entries' AND COLUMN_NAME = 'agency_owner_user_id') = 0,
  'ALTER TABLE host_period_diamond_entries ADD COLUMN agency_owner_user_id BIGINT NOT NULL DEFAULT 0 COMMENT ''本次送礼入账使用的代理收款用户快照'' AFTER region_id',
  '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 = 'host_agency_salary_policies' AND COLUMN_NAME = 'settlement_trigger_mode') = 0,
  'ALTER TABLE host_agency_salary_policies ADD COLUMN settlement_trigger_mode VARCHAR(24) NOT NULL DEFAULT ''automatic'' COMMENT ''结算触发方式：automatic/manual'' AFTER settlement_mode',
  '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 = 'host_agency_salary_policies' AND COLUMN_NAME = 'cycle_key') = 0,
  'ALTER TABLE host_agency_salary_policies ADD COLUMN cycle_key VARCHAR(7) NOT NULL DEFAULT '''' COMMENT ''政策归属工资周期，UTC yyyy-MM'' AFTER policy_id',
  '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 = 'host_agency_salary_policies' AND COLUMN_NAME = 'policy_version') = 0,
  'ALTER TABLE host_agency_salary_policies ADD COLUMN policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''不可变政策版本，等于后台政策 ID'' AFTER cycle_key',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 配置表行数很小；只回填未版本化旧政策，不扫描工资账户或结算流水。
UPDATE host_agency_salary_policies
SET cycle_key = DATE_FORMAT(FROM_UNIXTIME(COALESCE(NULLIF(effective_from_ms, 0), created_at_ms) / 1000), '%Y-%m'),
    policy_version = policy_id
WHERE cycle_key = '' OR policy_version = 0;

-- 旧库同一区域同月若有多条快照，选择生效时间和 policy_id 最大的一条作为一次性迁移绑定；之后主键禁止覆盖。
INSERT IGNORE INTO host_salary_policy_cycle_bindings (
  app_code, region_id, cycle_key, policy_id, policy_version, source_cycle_key, created_at_ms
)
SELECT policy.app_code, policy.region_id, policy.cycle_key, policy.policy_id, policy.policy_version, policy.cycle_key, policy.created_at_ms
FROM host_agency_salary_policies policy
WHERE policy.cycle_key <> ''
  AND policy.status = 'active'
  AND NOT EXISTS (
    SELECT 1
    FROM host_agency_salary_policies newer
    WHERE newer.app_code = policy.app_code
      AND newer.region_id = policy.region_id
      AND newer.cycle_key = policy.cycle_key
      AND newer.status = 'active'
      AND (
        newer.effective_from_ms > policy.effective_from_ms
        OR (newer.effective_from_ms = policy.effective_from_ms AND newer.policy_id > policy.policy_id)
      )
  );

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'host_agency_salary_policies' AND INDEX_NAME = 'idx_host_agency_salary_policy_cycle') = 0,
  'ALTER TABLE host_agency_salary_policies ADD INDEX idx_host_agency_salary_policy_cycle (app_code, region_id, cycle_key, status, settlement_mode, settlement_trigger_mode)',
  '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 = 'host_salary_settlement_progress' AND COLUMN_NAME = 'last_policy_version') = 0,
  'ALTER TABLE host_salary_settlement_progress ADD COLUMN last_policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''最后一次使用的不可变政策版本'' AFTER last_policy_id',
  '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 = 'host_salary_settlement_records' AND COLUMN_NAME = 'policy_version') = 0,
  'ALTER TABLE host_salary_settlement_records ADD COLUMN policy_version BIGINT UNSIGNED NOT NULL DEFAULT 0 COMMENT ''使用的不可变政策版本'' AFTER policy_id',
  '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 = 'host_salary_settlement_progress' AND COLUMN_NAME = 'last_settled_total_diamonds') = 0,
  'ALTER TABLE host_salary_settlement_progress ADD COLUMN last_settled_total_diamonds BIGINT NOT NULL DEFAULT 0 COMMENT ''上次结算任务已处理到的周期累计钻石，避免未升档时重复空跑'' AFTER settled_agency_salary_usd_minor',
  '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 = 'host_salary_settlement_records' AND COLUMN_NAME = 'trigger_mode') = 0,
  'ALTER TABLE host_salary_settlement_records ADD COLUMN trigger_mode VARCHAR(24) NOT NULL DEFAULT ''automatic'' COMMENT ''触发方式：automatic/manual'' AFTER settlement_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 = 'host_salary_settlement_records' AND COLUMN_NAME = 'settlement_role') = 0,
  'ALTER TABLE host_salary_settlement_records ADD COLUMN settlement_role VARCHAR(24) NOT NULL DEFAULT ''all'' COMMENT ''结算身份：all/host/agency'' AFTER trigger_mode',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 老环境可能已经存在 wallet_outbox，CREATE TABLE IF NOT EXISTS 不会补齐后续新增的投递锁字段。
-- 这里用 information_schema 生成幂等 ALTER，保证重复执行 initdb 时不会因重复列名失败。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND COLUMN_NAME = 'worker_id') = 0,
  'ALTER TABLE wallet_outbox ADD COLUMN worker_id VARCHAR(128) NOT NULL DEFAULT '''' COMMENT ''MQ 投递 worker ID'' AFTER status',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 投递锁过期时间用于多实例 claim outbox，老库缺这个列时会导致 worker 查询直接 1054。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND COLUMN_NAME = 'lock_until_ms') = 0,
  'ALTER TABLE wallet_outbox ADD COLUMN lock_until_ms BIGINT NULL COMMENT ''MQ 投递锁过期时间，UTC epoch ms'' AFTER worker_id',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 下一次重试时间让失败 outbox 可延迟重投；老库缺列时待投递扫描会直接失败。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND COLUMN_NAME = 'next_retry_at_ms') = 0,
  'ALTER TABLE wallet_outbox ADD COLUMN next_retry_at_ms BIGINT NULL COMMENT ''下一次 MQ 投递重试时间，UTC epoch ms'' AFTER retry_count',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- pending 索引的列顺序跟 outbox 扫描条件绑定；为兼容历史索引，存在就先删除再按当前结构重建。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_pending') > 0,
  'ALTER TABLE wallet_outbox DROP INDEX idx_wallet_outbox_pending',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := 'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_pending (app_code, status, next_retry_at_ms, created_at_ms, event_id)';
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- claim 索引用于查找锁过期的消息；列顺序也跟查询条件绑定，存在旧结构时重建。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_claim') > 0,
  'ALTER TABLE wallet_outbox DROP INDEX idx_wallet_outbox_claim',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := 'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_claim (app_code, status, lock_until_ms, created_at_ms, event_id)';
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- 实时红包/房间 UI 事件按 event_type 独立 claim；缺这个索引会在通用账务 backlog 上扫描，仍然可能延迟弹窗。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_event_pending') = 0,
  'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_event_pending (app_code, status, event_type, next_retry_at_ms, created_at_ms, event_id)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_event_claim') = 0,
  'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_event_claim (app_code, status, event_type, lock_until_ms, created_at_ms, event_id)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_retention') = 0,
  'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_retention (app_code, status, updated_at_ms, event_id)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

-- projection worker 只扫描单租户、单事件类型的 wallet_outbox；缺少该索引会退化成百万级全表扫描。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_event_created') = 0,
  'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_event_created (app_code, event_type, created_at_ms, event_id)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_outbox' AND INDEX_NAME = 'idx_wallet_outbox_asset_event_created') = 0,
  'ALTER TABLE wallet_outbox ADD INDEX idx_wallet_outbox_asset_event_created (app_code, asset_type, event_type, created_at_ms, event_id)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS wallet_gift_prices (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  gift_id VARCHAR(96) NOT NULL COMMENT '礼物 ID',
  price_version VARCHAR(64) NOT NULL COMMENT '价格版本',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  charge_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '扣费资产类型',
  coin_price BIGINT NOT NULL COMMENT '金币价格',
  gift_point_amount BIGINT NOT NULL COMMENT '历史字段：GIFT_POINT 已下线，新写入固定 0',
  heat_value BIGINT NOT NULL COMMENT '热度值',
  effective_at_ms BIGINT NOT NULL COMMENT '生效时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, gift_id, price_version),
  KEY idx_wallet_gift_prices_effective (app_code, gift_id, status, effective_at_ms),
  KEY idx_wallet_gift_prices_updated (app_code, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包礼物价格表';

CREATE TABLE IF NOT EXISTS wallet_recharge_policies (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  policy_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '策略 ID',
  region_id BIGINT NOT NULL COMMENT '区域 ID',
  policy_version VARCHAR(64) NOT NULL COMMENT '策略版本',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  currency_code VARCHAR(3) NOT NULL DEFAULT 'USD' COMMENT '币种编码',
  coin_amount BIGINT NOT NULL COMMENT '金币数量',
  usd_minor_amount BIGINT NOT NULL COMMENT '美元最小单位数量',
  effective_from_ms BIGINT NOT NULL COMMENT '生效开始时间，UTC epoch ms',
  effective_to_ms BIGINT NULL COMMENT '生效结束时间，UTC epoch ms',
  created_by_user_id BIGINT NULL COMMENT '创建人用户 ID',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  KEY idx_wallet_recharge_policies_active (app_code, region_id, status, effective_from_ms),
  UNIQUE KEY uk_wallet_recharge_policy_version (app_code, region_id, policy_version)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包充值策略表';

CREATE TABLE IF NOT EXISTS wallet_recharge_records (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  recharge_sequence BIGINT NOT NULL DEFAULT 0 COMMENT '该用户成功充值序号，从 1 开始',
  seller_user_id BIGINT NOT NULL COMMENT '商户用户 ID',
  seller_region_id BIGINT NOT NULL COMMENT '商户区域 ID',
  target_region_id BIGINT NOT NULL COMMENT '目标区域 ID',
  policy_id BIGINT NOT NULL COMMENT '策略 ID',
  policy_version VARCHAR(64) NOT NULL COMMENT '策略版本',
  currency_code VARCHAR(8) NOT NULL COMMENT '币种编码',
  coin_amount BIGINT NOT NULL COMMENT '金币数量',
  usd_minor_amount BIGINT NOT NULL COMMENT '美元最小单位数量',
  exchange_coin_amount BIGINT NOT NULL COMMENT '兑换金币数量',
  exchange_usd_minor_amount BIGINT NOT NULL COMMENT '兑换美元最小单位数量',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  PRIMARY KEY (app_code, transaction_id),
  KEY idx_wallet_recharge_records_user_time (app_code, user_id, created_at_ms),
  KEY idx_wallet_recharge_records_seller_time (app_code, seller_user_id, created_at_ms),
  KEY idx_wallet_recharge_records_region_time (app_code, target_region_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包充值记录表';

CREATE TABLE IF NOT EXISTS wallet_user_recharge_stats (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  recharge_count BIGINT NOT NULL DEFAULT 0 COMMENT '成功充值次数',
  first_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首笔充值交易 ID',
  first_recharged_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '首笔充值时间，UTC epoch ms',
  total_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '累计充值金币',
  total_usd_minor_amount BIGINT NOT NULL DEFAULT 0 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, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户充值统计表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_recharge_records' AND COLUMN_NAME = 'recharge_sequence') = 0,
  'ALTER TABLE wallet_recharge_records ADD COLUMN recharge_sequence BIGINT NOT NULL DEFAULT 0 COMMENT ''该用户成功充值序号，从 1 开始'' AFTER user_id',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS wallet_recharge_products (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  product_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '商品 ID',
  product_code VARCHAR(128) NULL COMMENT '商品编码',
  product_name VARCHAR(128) NOT NULL COMMENT '商品名称',
  description VARCHAR(512) NOT NULL DEFAULT '' COMMENT '描述信息',
  audience_type VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '购买对象：normal 普通用户，coin_seller 币商',
  platform VARCHAR(16) NOT NULL COMMENT '平台',
  channel VARCHAR(32) NOT NULL COMMENT '渠道',
  currency_code VARCHAR(8) NOT NULL DEFAULT 'USDT' COMMENT '币种编码',
  amount_micro BIGINT NOT NULL COMMENT '微单位金额',
  coin_amount BIGINT NOT NULL COMMENT '金币数量',
  resource_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '资源资产类型',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否在 App 普通充值列表展示；不影响 Google Play 验单',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重，数值越小越靠前',
  created_by_user_id BIGINT NULL COMMENT '创建人用户 ID',
  updated_by_user_id BIGINT NULL COMMENT '更新人用户 ID',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  UNIQUE KEY uk_wallet_recharge_products_code (app_code, product_code),
  KEY idx_wallet_recharge_products_scope (app_code, platform, audience_type, status, app_visible, amount_micro, product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包充值商品表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_recharge_products' AND COLUMN_NAME = 'audience_type') = 0,
  'ALTER TABLE wallet_recharge_products ADD COLUMN audience_type VARCHAR(32) NOT NULL DEFAULT ''normal'' COMMENT ''购买对象：normal 普通用户，coin_seller 币商'' AFTER description',
  '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 = 'wallet_recharge_products' AND COLUMN_NAME = 'app_visible') = 0,
  'ALTER TABLE wallet_recharge_products ADD COLUMN app_visible BOOLEAN NOT NULL DEFAULT TRUE COMMENT ''是否在 App 普通充值列表展示；不影响 Google Play 验单'' AFTER status',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS wallet_recharge_product_regions (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  product_id BIGINT NOT NULL COMMENT '商品 ID',
  region_id BIGINT NOT NULL COMMENT '区域 ID',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  PRIMARY KEY (app_code, product_id, region_id),
  KEY idx_wallet_recharge_product_regions_region (app_code, region_id, product_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包充值商品区域表';

CREATE TABLE IF NOT EXISTS third_party_payment_channels (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  provider_code VARCHAR(32) NOT NULL COMMENT '三方支付渠道编码',
  provider_name VARCHAR(64) NOT NULL COMMENT '三方支付渠道名称',
  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '渠道状态',
  sort_order INT NOT NULL DEFAULT 0 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, provider_code),
  KEY idx_third_party_payment_channels_status (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方支付渠道表';

CREATE TABLE IF NOT EXISTS third_party_payment_methods (
  method_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '支付方式 ID',
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  provider_code VARCHAR(32) NOT NULL COMMENT '三方支付渠道编码',
  country_code VARCHAR(8) NOT NULL COMMENT '国家码，GLOBAL 表示全球方式',
  country_name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '国家名称',
  currency_code VARCHAR(8) NOT NULL COMMENT '下单币种',
  pay_way VARCHAR(64) NOT NULL COMMENT 'MiFaPay payWay',
  pay_type VARCHAR(64) NOT NULL COMMENT 'MiFaPay payType',
  method_name VARCHAR(128) NOT NULL COMMENT '支付方式名称',
  logo_url VARCHAR(1024) NOT NULL DEFAULT '' COMMENT '支付方式图标',
  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '方式状态',
  usd_to_currency_rate DECIMAL(20,8) NOT NULL DEFAULT 1 COMMENT 'USD 到支付币种汇率',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  UNIQUE KEY uk_third_party_payment_methods_scope (app_code, provider_code, country_code, pay_way, pay_type),
  KEY idx_third_party_payment_methods_h5 (app_code, provider_code, country_code, status, sort_order),
  CONSTRAINT fk_third_party_payment_methods_channel FOREIGN KEY (app_code, provider_code) REFERENCES third_party_payment_channels(app_code, provider_code)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='三方支付国家方式配置表';

CREATE TABLE IF NOT EXISTS external_recharge_orders (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  order_id VARCHAR(96) NOT NULL COMMENT '外部充值订单号',
  command_id VARCHAR(128) NOT NULL COMMENT 'H5 幂等命令 ID',
  target_user_id BIGINT NOT NULL COMMENT '目标用户 ID',
  target_region_id BIGINT NOT NULL COMMENT '目标区域 ID',
  target_country_id BIGINT NOT NULL DEFAULT 0 COMMENT '目标国家 ID',
  target_country_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '目标国家码',
  audience_type VARCHAR(32) NOT NULL COMMENT '购买对象',
  product_id BIGINT NOT NULL COMMENT '商品 ID',
  product_code VARCHAR(128) NOT NULL COMMENT '商品编码快照',
  product_name VARCHAR(128) NOT NULL COMMENT '商品名称快照',
  coin_amount BIGINT NOT NULL COMMENT '到账金币数量',
  usd_minor_amount BIGINT NOT NULL COMMENT 'USD cent 金额快照',
  provider_code VARCHAR(32) NOT NULL COMMENT '支付渠道',
  payment_method_id BIGINT NOT NULL DEFAULT 0 COMMENT '三方支付方式 ID',
  country_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '支付方式国家码',
  currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '支付币种',
  provider_amount_minor BIGINT NOT NULL DEFAULT 0 COMMENT '三方支付币种最小单位金额',
  pay_way VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'MiFaPay payWay',
  pay_type VARCHAR(64) NOT NULL DEFAULT '' COMMENT 'MiFaPay payType',
  pay_url VARCHAR(2048) NOT NULL DEFAULT '' COMMENT '三方支付跳转 URL',
  provider_order_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '三方支付订单号',
  tx_hash VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'USDT tx hash',
  receive_address VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'USDT 收款地址',
  status VARCHAR(32) NOT NULL COMMENT '订单状态',
  failure_reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '失败原因',
  wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
  provider_payload JSON 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, order_id),
  UNIQUE KEY uk_external_recharge_orders_command (app_code, command_id),
  KEY idx_external_recharge_orders_user_time (app_code, target_user_id, created_at_ms),
  KEY idx_external_recharge_orders_provider_order (app_code, provider_code, provider_order_id),
  KEY idx_external_recharge_orders_reconcile (app_code, status, provider_code, updated_at_ms, created_at_ms),
  KEY idx_external_recharge_orders_wallet_tx (app_code, wallet_transaction_id),
  KEY idx_external_recharge_orders_tx (app_code, provider_code, tx_hash)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='H5 外部充值订单表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'external_recharge_orders' AND COLUMN_NAME = 'target_country_id') = 0,
  'ALTER TABLE external_recharge_orders ADD COLUMN target_country_id BIGINT NOT NULL DEFAULT 0 COMMENT ''目标国家 ID'' AFTER target_region_id',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @now_ms = CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);

INSERT IGNORE INTO third_party_payment_channels (app_code, provider_code, provider_name, status, sort_order, created_at_ms, updated_at_ms)
VALUES ('lalu', 'mifapay', 'MiFaPay', 'active', 10, @now_ms, @now_ms);

-- MiFaPay 是 H5 统一三方支付入口；真实商户号、商户标识、商户私钥、平台公钥只放运行环境配置。
-- 国家和支付方式默认打开，运营可在后台逐项关闭；默认汇率为 1，避免新环境缺配置时 H5 完全不可见。
INSERT IGNORE INTO third_party_payment_methods (
  app_code, provider_code, country_code, country_name, currency_code, pay_way, pay_type,
  method_name, logo_url, status, usd_to_currency_rate, sort_order, created_at_ms, updated_at_ms
) VALUES
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'BCA', 'BCA Bank', '', 'active', 1, 110, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'BNI', 'BNI Bank', '', 'active', 1, 120, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'BRI', 'BRI Bank', '', 'active', 1, 130, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'Mandiri', 'Mandiri Bank', '', 'active', 1, 140, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'Permata', 'Permata Bank', '', 'active', 1, 150, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'DANA', 'DANA', '', 'active', 1, 160, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'LinkAja', 'LinkAja', '', 'active', 1, 170, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'OVO', 'OVO', '', 'active', 1, 180, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'ShopeePay', 'ShopeePay', '', 'active', 1, 190, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'ID', 'Indonesia', 'IDR', 'QR', 'QRIS', 'QRIS', '', 'active', 1, 200, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'VN', 'Vietnam', 'VND', 'BankTransfer', 'VN_BankTransfer', 'Vietnam Bank Transfer', '', 'active', 1, 210, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'VN', 'Vietnam', 'VND', 'QR', 'VietQR', 'VietQR', '', 'active', 1, 220, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'BH', 'Bahrain', 'BHD', 'Card', 'CreditCard', 'Bahrain Credit Card', '', 'active', 1, 310, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'QA', 'Qatar', 'QAR', 'Card', 'CreditCard', 'Qatar Credit Card', '', 'active', 1, 410, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'OM', 'Oman', 'OMR', 'Card', 'CreditCard', 'Oman Credit Card', '', 'active', 1, 510, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'AE', 'United Arab Emirates', 'AED', 'Card', 'CreditCard', 'UAE Credit Card', '', 'active', 1, 610, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'KW', 'Kuwait', 'KWD', 'Card', 'Knet', 'KNET', '', 'active', 1, 710, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'SA', 'Saudi Arabia', 'SAR', 'Card', 'MADA', 'MADA', '', 'active', 1, 810, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'SA', 'Saudi Arabia', 'SAR', 'Ewallet', 'ApplePay', 'ApplePay', '', 'active', 1, 820, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'SA', 'Saudi Arabia', 'SAR', 'Ewallet', 'STCPay', 'STCPay', '', 'active', 1, 830, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'IN', 'India', 'INR', 'Ewallet', 'BHIM', 'BHIM', '', 'active', 1, 910, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'IN', 'India', 'INR', 'Ewallet', 'GooglePay', 'GooglePay', '', 'active', 1, 920, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'IN', 'India', 'INR', 'Ewallet', 'Mobikwik', 'Mobikwik', '', 'active', 1, 930, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'IN', 'India', 'INR', 'Ewallet', 'Paytm', 'Paytm', '', 'active', 1, 940, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'IN', 'India', 'INR', 'Ewallet', 'PhonePe', 'PhonePe', '', 'active', 1, 950, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'IN', 'India', 'INR', 'Ewallet', 'UPI', 'UPI', '', 'active', 1, 960, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'BD', 'Bangladesh', 'BDT', 'Ewallet', 'bKash', 'bKash', '', 'active', 1, 1010, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'PK', 'Pakistan', 'PKR', 'Ewallet', 'Easypaisa', 'Easypaisa', '', 'active', 1, 1110, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'PK', 'Pakistan', 'PKR', 'Ewallet', 'JazzCash', 'JazzCash', '', 'active', 1, 1120, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'EG', 'Egypt', 'EGP', 'Ewallet', 'FawryPay', 'FawryPay', '', 'active', 1, 1210, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'EG', 'Egypt', 'EGP', 'Ewallet', 'Meeza', 'Meeza', '', 'active', 1, 1220, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'EG', 'Egypt', 'EGP', 'Ewallet', 'Orange', 'Orange', '', 'active', 1, 1230, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'EG', 'Egypt', 'EGP', 'Ewallet', 'Vodafone', 'Vodafone', '', 'active', 1, 1240, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'Gcash', 'GCash', '', 'active', 1, 1310, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'PayMaya', 'PayMaya', '', 'active', 1, 1320, @now_ms, @now_ms),
  ('lalu', 'mifapay', 'PH', 'Philippines', 'PHP', 'QR', 'QRPH', 'QRPH', '', 'active', 1, 1330, @now_ms, @now_ms);

INSERT IGNORE INTO third_party_payment_channels (app_code, provider_code, provider_name, status, sort_order, created_at_ms, updated_at_ms)
VALUES ('lalu', 'v5pay', 'V5Pay', 'active', 20, @now_ms, @now_ms);

-- V5Pay 的 pay_type 直接保存 productType；运行时还会按 V5Pay productTypes 接口过滤当前 appKey 真实可用的产品。
INSERT IGNORE INTO third_party_payment_methods (
  app_code, provider_code, country_code, country_name, currency_code, pay_way, pay_type,
  method_name, logo_url, status, usd_to_currency_rate, sort_order, created_at_ms, updated_at_ms
) VALUES
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'UB_ONLINE', 'UnionBank Online', '', 'active', 1, 2010, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'INSTAPAY', 'InstaPay', '', 'active', 1, 2020, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'PESONET', 'PESONet', '', 'active', 1, 2030, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'GCASH_ONLINE', 'GCash Online', '', 'active', 1, 2040, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'COINS_ONLINE', 'Coins.ph Online', '', 'active', 1, 2050, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'BPI_ONLINE', 'BPI Online', '', 'active', 1, 2060, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'RCBC_ONLINE', 'RCBC Online', '', 'active', 1, 2070, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'UCPB_ONLINE', 'UCPB Online', '', 'active', 1, 2080, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'ROBINSONSBANK_ONLINE', 'Robinsons Bank Online', '', 'active', 1, 2090, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'PSBANK_ONLINE', 'PSBank Online', '', 'active', 1, 2100, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'METROBANK_ONLINE', 'Metrobank Online', '', 'active', 1, 2110, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'LANDBANK_ONLINE', 'Landbank Online', '', 'active', 1, 2120, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'CHINABANK_ONLINE', 'China Bank Online', '', 'active', 1, 2130, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'BDO_ONLINE', 'BDO Online', '', 'active', 1, 2140, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'BankTransfer', 'BOC_ONLINE', 'Bank of Commerce Online', '', 'active', 1, 2150, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'GRABPAY_ONLINE', 'GrabPay Online', '', 'active', 1, 2160, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'QR', 'INSTAPAY_QR', 'InstaPay QR', '', 'active', 1, 2170, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'QR', 'ALIPAY_QR', 'Alipay QR', '', 'active', 1, 2180, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'Ewallet', 'ALIPAY_PLUS', 'Alipay+', '', 'active', 1, 2190, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'Billing', 'BILL', 'Bills Payment', '', 'active', 1, 2200, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PH', 'Philippines', 'PHP', 'QR', 'QRPH_GCASH', 'QRPh GCash', '', 'active', 1, 2210, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'SA', 'Saudi Arabia', 'SAR', 'Ewallet', 'STCPAY', 'STC Pay', '', 'active', 1, 2310, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'SA', 'Saudi Arabia', 'SAR', 'Card', 'MADA', 'MADA', '', 'active', 1, 2320, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'SA', 'Saudi Arabia', 'SAR', 'Ewallet', 'APPLEPAY', 'Apple Pay', '', 'active', 1, 2330, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'EG', 'Egypt', 'EGP', 'Card', 'CARD', 'Egypt Card', '', 'active', 1, 2410, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'AE', 'United Arab Emirates', 'AED', 'Card', 'CARD', 'UAE Card', '', 'active', 1, 2510, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'BNI_VA', 'BNI Virtual Account', '', 'active', 1, 2610, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'BRI_VA', 'BRI Virtual Account', '', 'active', 1, 2620, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'CIMB_VA', 'CIMB Virtual Account', '', 'active', 1, 2630, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'MANDIRI_VA', 'Mandiri Virtual Account', '', 'active', 1, 2640, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'MAYBANK_VA', 'Maybank Virtual Account', '', 'active', 1, 2650, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'QR', 'NOBU_BANK_QRIS', 'Nobu Bank QRIS', '', 'active', 1, 2660, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'OVO', 'OVO', '', 'active', 1, 2670, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'BankTransfer', 'PERMATA_VA', 'Permata Virtual Account', '', 'active', 1, 2680, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'SHOPEEPAY_JUMPAPP', 'ShopeePay Jump App', '', 'active', 1, 2690, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'GOPAY', 'GoPay', '', 'active', 1, 2700, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'ID', 'Indonesia', 'IDR', 'Ewallet', 'ALIPAY_CN_ALIPAYPLUS', 'Alipay+ Indonesia', '', 'active', 1, 2710, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'IN', 'India', 'INR', 'BankTransfer', 'BANK_ONLINE', 'Bank Online', '', 'active', 1, 2810, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'IN', 'India', 'INR', 'UPI', 'UPI', 'UPI', '', 'active', 1, 2820, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'IN', 'India', 'INR', 'BankTransfer', 'ONLINE_BANKING', 'Online Banking', '', 'active', 1, 2830, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PK', 'Pakistan', 'PKR', 'Ewallet', 'EASYPAISA', 'Easypaisa', '', 'active', 1, 2910, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PK', 'Pakistan', 'PKR', 'Ewallet', 'JAZZCASH', 'JazzCash', '', 'active', 1, 2920, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PK', 'Pakistan', 'PKR', 'BankTransfer', 'ALFA', 'Bank Alfalah', '', 'active', 1, 2930, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PK', 'Pakistan', 'PKR', 'Ewallet', 'HBL_KONNECT', 'HBL Konnect', '', 'active', 1, 2940, @now_ms, @now_ms),
  ('lalu', 'v5pay', 'PK', 'Pakistan', 'PKR', 'Billing', 'BILL_PK', 'Pakistan Bill Payment', '', 'active', 1, 2950, @now_ms, @now_ms);

-- MiFaPay logo 使用已上传 COS 的固定对象地址；仅覆盖空值、早期占位图和 Apifox 临时预览图，避免误改运营手工替换的正式图。
UPDATE third_party_payment_methods m
JOIN (
  SELECT 'AE' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ae/card-creditcard.png' AS logo_url
  UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'Nagad' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-nagad.png' AS logo_url
  UNION ALL SELECT 'BD' AS country_code, 'Ewallet' AS pay_way, 'bKash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bd/ewallet-bkash.png' AS logo_url
  UNION ALL SELECT 'BH' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/bh/card-creditcard.png' AS logo_url
  UNION ALL SELECT 'EG' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/card-creditcard.png' AS logo_url
  UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'FawryPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-fawrypay.png' AS logo_url
  UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Meeza' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-meeza.png' AS logo_url
  UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Opay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-opay.png' AS logo_url
  UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Orange' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-orange.png' AS logo_url
  UNION ALL SELECT 'EG' AS country_code, 'Ewallet' AS pay_way, 'Vodafone' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/eg/ewallet-vodafone.png' AS logo_url
  UNION ALL SELECT 'GLOBAL' AS country_code, 'BankTransfer' AS pay_way, 'UnionPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/banktransfer-unionpay.png' AS logo_url
  UNION ALL SELECT 'GLOBAL' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/card-creditcard.png' AS logo_url
  UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-applepay.png' AS logo_url
  UNION ALL SELECT 'GLOBAL' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/global/ewallet-googlepay.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BCA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bca.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BNI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bni.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'BRI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-bri.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Mandiri' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-mandiri.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'Permata' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-permata.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'BankTransfer' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/banktransfer-qris.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'DANA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-dana.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'LinkAja' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-linkaja.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'OVO' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-ovo.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'Ewallet' AS pay_way, 'ShopeePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/ewallet-shopeepay.png' AS logo_url
  UNION ALL SELECT 'ID' AS country_code, 'QR' AS pay_way, 'QRIS' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/id/qr-qris.png' AS logo_url
  UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'BHIM' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-bhim.png' AS logo_url
  UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'GooglePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-googlepay.png' AS logo_url
  UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Mobikwik' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-mobikwik.png' AS logo_url
  UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'Paytm' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-paytm.png' AS logo_url
  UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'PhonePe' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-phonepe.png' AS logo_url
  UNION ALL SELECT 'IN' AS country_code, 'Ewallet' AS pay_way, 'UPI' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/in/ewallet-upi.png' AS logo_url
  UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'KENT' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-kent.png' AS logo_url
  UNION ALL SELECT 'KW' AS country_code, 'Card' AS pay_way, 'Knet' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/kw/card-knet.png' AS logo_url
  UNION ALL SELECT 'MY' AS country_code, 'BankTransfer' AS pay_way, 'FPX' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/banktransfer-fpx.png' AS logo_url
  UNION ALL SELECT 'MY' AS country_code, 'Ewallet' AS pay_way, 'TNG' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/my/ewallet-tng.png' AS logo_url
  UNION ALL SELECT 'OM' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/om/card-creditcard.png' AS logo_url
  UNION ALL SELECT 'PH' AS country_code, 'BankTransfer' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/banktransfer-qrph.png' AS logo_url
  UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'Gcash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-gcash.png' AS logo_url
  UNION ALL SELECT 'PH' AS country_code, 'Ewallet' AS pay_way, 'PayMaya' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/ewallet-paymaya.png' AS logo_url
  UNION ALL SELECT 'PH' AS country_code, 'QR' AS pay_way, 'QRPH' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/ph/qr-qrph.png' AS logo_url
  UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'Easypaisa' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-easypaisa.png' AS logo_url
  UNION ALL SELECT 'PK' AS country_code, 'Ewallet' AS pay_way, 'JazzCash' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/pk/ewallet-jazzcash.png' AS logo_url
  UNION ALL SELECT 'QA' AS country_code, 'Card' AS pay_way, 'CreditCard' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/qa/card-creditcard.png' AS logo_url
  UNION ALL SELECT 'SA' AS country_code, 'Card' AS pay_way, 'MADA' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/card-mada.png' AS logo_url
  UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'ApplePay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-applepay.png' AS logo_url
  UNION ALL SELECT 'SA' AS country_code, 'Ewallet' AS pay_way, 'STCPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/sa/ewallet-stcpay.png' AS logo_url
  UNION ALL SELECT 'TH' AS country_code, 'BankTransfer' AS pay_way, 'PromptPay' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/banktransfer-promptpay.png' AS logo_url
  UNION ALL SELECT 'TH' AS country_code, 'Ewallet' AS pay_way, 'TrueMoney' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/th/ewallet-truemoney.png' AS logo_url
  UNION ALL SELECT 'VN' AS country_code, 'BankTransfer' AS pay_way, 'VN_BankTransfer' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/banktransfer-vn-banktransfer.png' AS logo_url
  UNION ALL SELECT 'VN' AS country_code, 'QR' AS pay_way, 'VietQR' AS pay_type, 'https://media.haiyihy.com/admin/payment/mifapay/logos/vn/qr-vietqr.png' AS logo_url
) logos
  ON UPPER(m.country_code) = logos.country_code
 AND LOWER(m.pay_way) = LOWER(logos.pay_way)
 AND LOWER(m.pay_type) = LOWER(logos.pay_type)
SET m.logo_url = logos.logo_url,
    m.updated_at_ms = @now_ms
WHERE m.app_code = 'lalu'
  AND m.provider_code = 'mifapay'
  AND (m.logo_url = '' OR m.logo_url LIKE 'https://placehold.co/%' OR m.logo_url LIKE '%/image-preview');

CREATE TABLE IF NOT EXISTS payment_orders (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  payment_order_id VARCHAR(96) NOT NULL COMMENT '平台支付订单 ID',
  provider VARCHAR(32) NOT NULL COMMENT '支付提供方',
  channel VARCHAR(32) NOT NULL COMMENT '充值渠道',
  status VARCHAR(32) NOT NULL COMMENT '订单状态',
  command_id VARCHAR(128) NOT NULL COMMENT '客户端确认命令幂等 ID',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  product_id BIGINT NOT NULL COMMENT '后台充值商品 ID',
  product_code VARCHAR(128) NOT NULL COMMENT 'Google Play 商品 ID',
  package_name VARCHAR(256) NOT NULL DEFAULT '' COMMENT 'Google Play 包名',
  provider_order_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT 'Google Play orderId',
  purchase_token_hash VARCHAR(128) NOT NULL COMMENT 'Google purchase token SHA256，不落明文 token',
  purchase_state VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Google purchase state',
  consume_state VARCHAR(32) NOT NULL DEFAULT '' COMMENT 'Google consume 状态',
  wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
  coin_amount BIGINT NOT NULL COMMENT '到账金币数量',
  currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '商品币种编码',
  amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '商品金额微单位',
  paid_currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '用户实付币种，来自 Google Orders API',
  paid_amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '用户实付总额微单位（实付币种）',
  paid_tax_micro BIGINT NOT NULL DEFAULT 0 COMMENT '订单税额微单位（实付币种）',
  paid_net_micro BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 扣费后开发者净收入微单位（实付币种）',
  paid_synced_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '实付明细同步时间，UTC epoch ms；0 表示未同步',
  paid_sync_attempt_count INT NOT NULL DEFAULT 0 COMMENT 'Google 实付同步连续失败次数',
  paid_sync_last_error VARCHAR(512) NOT NULL DEFAULT '' COMMENT 'Google 实付同步最近错误',
  paid_sync_next_retry_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT 'Google 实付同步下次重试时间，UTC epoch ms',
  provider_payload JSON NULL COMMENT 'Google Play 校验返回快照',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, payment_order_id),
  UNIQUE KEY uk_payment_orders_token (app_code, provider, purchase_token_hash),
  UNIQUE KEY uk_payment_orders_command (app_code, command_id),
  KEY idx_payment_orders_user_time (app_code, user_id, created_at_ms),
  KEY idx_payment_orders_provider_order (app_code, provider, provider_order_id),
  KEY idx_payment_orders_wallet_tx (app_code, wallet_transaction_id),
  KEY idx_payment_orders_paid_sync (provider, paid_synced_at_ms, paid_sync_next_retry_at_ms, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='支付订单与 provider 审计表';

CREATE TABLE IF NOT EXISTS coin_seller_stock_records (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  stock_id VARCHAR(96) NOT NULL COMMENT '库存 ID',
  transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
  seller_user_id BIGINT NOT NULL COMMENT '商户用户 ID',
  stock_type VARCHAR(32) NOT NULL COMMENT '库存类型',
  counts_as_seller_recharge BOOLEAN NOT NULL COMMENT '数量as商户充值',
  coin_amount BIGINT NOT NULL COMMENT '金币数量',
  paid_currency_code VARCHAR(8) NOT NULL DEFAULT '' COMMENT '付款币种编码',
  paid_amount_micro BIGINT NOT NULL DEFAULT 0 COMMENT '付款数量微单位',
  payment_ref VARCHAR(128) NULL COMMENT '支付外部引用',
  evidence_ref VARCHAR(256) NOT NULL DEFAULT '' COMMENT '凭证外部引用',
  operator_user_id BIGINT NOT NULL COMMENT '操作人用户 ID',
  reason VARCHAR(512) NOT NULL DEFAULT '' COMMENT '原因',
  balance_after BIGINT NOT NULL COMMENT '余额后',
  metadata_json JSON NULL COMMENT '扩展元数据 JSON',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  PRIMARY KEY (app_code, stock_id),
  UNIQUE KEY uk_coin_seller_stock_command (app_code, command_id),
  UNIQUE KEY uk_coin_seller_stock_payment_ref (app_code, stock_type, payment_ref),
  KEY idx_coin_seller_stock_seller_time (app_code, seller_user_id, created_at_ms),
  KEY idx_coin_seller_stock_type_time (app_code, stock_type, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='金币商户库存记录表';

CREATE TABLE IF NOT EXISTS coin_seller_salary_exchange_rate_tiers (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  region_id BIGINT NOT NULL COMMENT '币商所属区域 ID',
  tier_id BIGINT NOT NULL AUTO_INCREMENT COMMENT '区间 ID',
  min_usd_minor BIGINT NOT NULL COMMENT '起始美元金额，单位美分，包含',
  max_usd_minor BIGINT NOT NULL COMMENT '结束美元金额，单位美分，包含',
  coin_per_usd BIGINT NOT NULL COMMENT '每 1 USD 工资可兑换的币商专用金币数量',
  status VARCHAR(24) NOT NULL DEFAULT 'active' COMMENT '状态：active/disabled',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
  created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
  updated_by_user_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 (tier_id),
  KEY idx_coin_seller_salary_rates_region (app_code, region_id, status, min_usd_minor, max_usd_minor),
  KEY idx_coin_seller_salary_rates_sort (app_code, region_id, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='币商工资兑换比例区间表';

CREATE TABLE IF NOT EXISTS resources (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  resource_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '资源 ID',
  resource_code VARCHAR(96) NOT NULL COMMENT '资源编码',
  resource_type VARCHAR(32) NOT NULL COMMENT '资源类型',
  name VARCHAR(128) NOT NULL COMMENT '名称',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  grantable BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否允许发放',
  manager_grant_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '管理端是否允许发放',
  grant_strategy VARCHAR(32) NOT NULL COMMENT '发放策略',
  wallet_asset_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '钱包资产类型',
  wallet_asset_amount BIGINT NOT NULL DEFAULT 0 COMMENT '钱包资产数量',
  price_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '资源价格类型：coin/free',
  coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '资源金币价格',
  gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT '历史字段：GIFT_POINT 已下线，新写入固定 0',
  usage_scope_json JSON NOT NULL COMMENT '使用范围 JSON 配置或快照',
  asset_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '资产 URL',
  preview_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '预览 URL',
  animation_url VARCHAR(512) NOT NULL DEFAULT '' COMMENT '动画 URL',
  metadata_json JSON NULL COMMENT '扩展元数据 JSON',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重，数值越小越靠前',
  created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
  updated_by_user_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',
  UNIQUE KEY uk_resources_code (app_code, resource_code),
  KEY idx_resources_type_status (app_code, resource_type, status, sort_order),
  KEY idx_resources_type_updated (app_code, resource_type, updated_at_ms),
  KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, grantable, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND COLUMN_NAME = 'manager_grant_enabled') = 0,
  'ALTER TABLE resources ADD COLUMN manager_grant_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT ''管理端是否允许发放'' AFTER grantable',
  '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 = 'resources' AND COLUMN_NAME = 'price_type') = 0,
  'ALTER TABLE resources ADD COLUMN price_type VARCHAR(32) NOT NULL DEFAULT '''' COMMENT ''资源价格类型：coin/free'' AFTER wallet_asset_amount',
  '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 = 'resources' AND COLUMN_NAME = 'coin_price') = 0,
  'ALTER TABLE resources ADD COLUMN coin_price BIGINT NOT NULL DEFAULT 0 COMMENT ''资源金币价格'' AFTER price_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 = 'resources' AND COLUMN_NAME = 'gift_point_amount') = 0,
  'ALTER TABLE resources ADD COLUMN gift_point_amount BIGINT NOT NULL DEFAULT 0 COMMENT ''历史字段：GIFT_POINT 已下线，新写入固定 0'' AFTER coin_price',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND INDEX_NAME = 'idx_resources_manager_grant') = 0,
  'ALTER TABLE resources ADD KEY idx_resources_manager_grant (app_code, manager_grant_enabled, status, grantable, sort_order)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS resource_groups (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  group_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '分组 ID',
  group_code VARCHAR(96) NOT NULL COMMENT '分组编码',
  name VARCHAR(128) NOT NULL COMMENT '名称',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  description VARCHAR(512) NOT NULL DEFAULT '' COMMENT '描述信息',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重，数值越小越靠前',
  created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
  updated_by_user_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',
  UNIQUE KEY uk_resource_groups_code (app_code, group_code),
  KEY idx_resource_groups_status (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源分组表';

CREATE TABLE IF NOT EXISTS resource_group_items (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  group_item_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '分组明细 ID',
  group_id BIGINT NOT NULL COMMENT '分组 ID',
  item_type VARCHAR(32) NOT NULL DEFAULT 'resource' COMMENT '明细类型',
  resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源 ID',
  wallet_asset_type VARCHAR(32) NOT NULL DEFAULT '' COMMENT '钱包资产类型',
  wallet_asset_amount BIGINT NOT NULL DEFAULT 0 COMMENT '钱包资产数量',
  quantity BIGINT NOT NULL DEFAULT 1 COMMENT '数量',
  duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '持续时长，毫秒',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重，数值越小越靠前',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  UNIQUE KEY uk_resource_group_items_member (app_code, group_id, item_type, resource_id, wallet_asset_type),
  KEY idx_resource_group_items_group (app_code, group_id, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源分组明细表';

CREATE TABLE IF NOT EXISTS resource_group_snapshots (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码',
  snapshot_id VARCHAR(96) NOT NULL COMMENT '不可变资源组快照 ID',
  pin_key VARCHAR(192) NOT NULL COMMENT '发布方业务版本幂等键',
  source_group_id BIGINT NOT NULL COMMENT '来源资源组 ID',
  version_no BIGINT NOT NULL COMMENT '来源资源组递增快照版本',
  snapshot_hash CHAR(64) NOT NULL COMMENT '完整快照 SHA-256',
  snapshot_json JSON NOT NULL COMMENT '含资源详情和钱包资产的完整不可变快照',
  source_group_updated_at_ms BIGINT NOT NULL COMMENT '锁定时来源资源组版本',
  source_content_hash CHAR(64) NOT NULL COMMENT '资源组、组项和嵌套资源的规范化内容哈希',
  created_by_user_id BIGINT NOT NULL,
  created_at_ms BIGINT NOT NULL,
  PRIMARY KEY (app_code, snapshot_id),
  UNIQUE KEY uk_resource_group_snapshot_pin (app_code, pin_key),
  UNIQUE KEY uk_resource_group_snapshot_version (app_code, source_group_id, version_no),
  KEY idx_resource_group_snapshot_source (app_code, source_group_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源组发布态不可变奖励快照';

CREATE TABLE IF NOT EXISTS resource_shop_items (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  shop_item_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '道具商店条目 ID',
  resource_id BIGINT NOT NULL COMMENT '售卖资源 ID',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  duration_days INT NOT NULL COMMENT '售卖天数，只允许 1/3/7',
  coin_price BIGINT NOT NULL DEFAULT 0 COMMENT '该售卖天数的独立金币价格；未自定义时复制资源列表价格',
  effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间，UTC epoch ms，0 表示立即生效',
  effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效结束时间，UTC epoch ms，0 表示长期有效',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重，数值越小越靠前',
  created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
  updated_by_user_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',
  UNIQUE KEY uk_resource_shop_items_resource_duration (app_code, resource_id, duration_days),
  KEY idx_resource_shop_items_status_sort (app_code, status, sort_order),
  KEY idx_resource_shop_items_effective (app_code, status, effective_from_ms, effective_to_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='道具商店售卖资源表';

CREATE TABLE IF NOT EXISTS resource_shop_purchase_orders (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  order_id VARCHAR(96) NOT NULL COMMENT '道具商店订单 ID',
  command_id VARCHAR(128) NOT NULL COMMENT 'App 购买命令幂等 ID',
  user_id BIGINT NOT NULL COMMENT '购买用户 ID',
  shop_item_id BIGINT NOT NULL COMMENT '道具商店条目 ID',
  resource_id BIGINT NOT NULL COMMENT '资源 ID',
  duration_days INT NOT NULL COMMENT '购买生效天数，订单快照',
  price_coin BIGINT NOT NULL COMMENT '实扣金币，订单快照',
  status VARCHAR(32) NOT NULL COMMENT '订单状态',
  wallet_transaction_id VARCHAR(96) NOT NULL COMMENT '扣费钱包交易 ID',
  resource_grant_id VARCHAR(96) NOT NULL COMMENT '资源发放 ID',
  entitlement_id VARCHAR(96) NOT NULL COMMENT '最终权益 ID',
  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, order_id),
  UNIQUE KEY uk_resource_shop_purchase_command (app_code, command_id),
  KEY idx_resource_shop_purchase_user_time (app_code, user_id, created_at_ms),
  KEY idx_resource_shop_purchase_tx (app_code, wallet_transaction_id),
  KEY idx_resource_shop_purchase_entitlement (app_code, entitlement_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='道具商店购买订单表';

CREATE TABLE IF NOT EXISTS gift_type_configs (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码，供 gift_configs 引用',
  name VARCHAR(64) NOT NULL COMMENT '后台展示名称',
  tab_key VARCHAR(32) NOT NULL COMMENT 'App 礼物面板 tab 展示 key',
  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT 'tab 状态，active 才在 App 面板展示',
  sort_order INT NOT NULL DEFAULT 0 COMMENT 'tab 排序权重，数值越小越靠前',
  created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
  updated_by_user_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, type_code),
  UNIQUE KEY uk_gift_type_configs_tab_key (app_code, tab_key),
  KEY idx_gift_type_configs_status_sort (app_code, status, sort_order),
  KEY idx_gift_type_configs_updated (app_code, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物类型和 App 面板 tab 配置表';

CREATE TABLE IF NOT EXISTS gift_configs (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  gift_id VARCHAR(96) NOT NULL COMMENT '礼物 ID',
  resource_id BIGINT NOT NULL COMMENT '资源 ID',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  name VARCHAR(128) NOT NULL COMMENT '名称',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重，数值越小越靠前',
  presentation_json JSON NULL COMMENT '展示配置 JSON 配置或快照',
  gift_type_code VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '礼物类型编码',
  vip_level INT NOT NULL DEFAULT 0 COMMENT 'VIP/贵族礼物对应的 VIP 等级，非 VIP 礼物为 0',
  effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效开始时间，UTC epoch ms',
  effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT '生效结束时间，UTC epoch ms',
  effect_types_json JSON NULL COMMENT '礼物特效类型 JSON 配置',
  created_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建人用户 ID',
  updated_by_user_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, gift_id),
  KEY idx_gift_configs_resource (app_code, resource_id),
  KEY idx_gift_configs_status_sort (app_code, status, sort_order),
  KEY idx_gift_configs_effective (app_code, status, effective_from_ms, effective_to_ms),
  KEY idx_gift_configs_updated (app_code, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物配置表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_gift_prices' AND COLUMN_NAME = 'charge_asset_type') = 0,
  'ALTER TABLE wallet_gift_prices ADD COLUMN charge_asset_type VARCHAR(32) NOT NULL DEFAULT ''COIN'' COMMENT ''扣费资产类型'' AFTER status',
  '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 = 'gift_configs' AND COLUMN_NAME = 'gift_type_code') = 0,
  'ALTER TABLE gift_configs ADD COLUMN gift_type_code VARCHAR(32) NOT NULL DEFAULT ''normal'' COMMENT ''礼物类型编码'' AFTER presentation_json',
  '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 = 'gift_configs' AND COLUMN_NAME = 'vip_level') = 0,
  'ALTER TABLE gift_configs ADD COLUMN vip_level INT NOT NULL DEFAULT 0 COMMENT ''VIP/贵族礼物对应的 VIP 等级，非 VIP 礼物为 0'' AFTER gift_type_code',
  '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 = 'gift_configs' AND COLUMN_NAME = 'effective_from_ms') = 0,
  'ALTER TABLE gift_configs ADD COLUMN effective_from_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''生效开始时间，UTC epoch ms'' AFTER gift_type_code',
  '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 = 'gift_configs' AND COLUMN_NAME = 'effective_to_ms') = 0,
  'ALTER TABLE gift_configs ADD COLUMN effective_to_ms BIGINT NOT NULL DEFAULT 0 COMMENT ''生效结束时间，UTC epoch ms'' AFTER effective_from_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 = 'gift_configs' AND COLUMN_NAME = 'effect_types_json') = 0,
  'ALTER TABLE gift_configs ADD COLUMN effect_types_json JSON NULL COMMENT ''礼物特效类型 JSON 配置'' AFTER effective_to_ms',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS gift_config_regions (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  gift_id VARCHAR(96) NOT NULL COMMENT '礼物 ID',
  region_id BIGINT NOT NULL 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, gift_id, region_id),
  KEY idx_gift_config_regions_region (app_code, region_id, gift_id),
  KEY idx_gift_config_regions_updated (app_code, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物配置区域表';

-- gateway 礼物面板每次请求只读取 owner 版本号；这些索引让版本探测保持为单租户索引最大值查询。
SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'wallet_gift_prices' AND INDEX_NAME = 'idx_wallet_gift_prices_updated') = 0,
  'ALTER TABLE wallet_gift_prices ADD INDEX idx_wallet_gift_prices_updated (app_code, updated_at_ms)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'resources' AND INDEX_NAME = 'idx_resources_type_updated') = 0,
  'ALTER TABLE resources ADD INDEX idx_resources_type_updated (app_code, resource_type, updated_at_ms)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gift_type_configs' AND INDEX_NAME = 'idx_gift_type_configs_updated') = 0,
  'ALTER TABLE gift_type_configs ADD INDEX idx_gift_type_configs_updated (app_code, updated_at_ms)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gift_configs' AND INDEX_NAME = 'idx_gift_configs_updated') = 0,
  'ALTER TABLE gift_configs ADD INDEX idx_gift_configs_updated (app_code, updated_at_ms)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gift_config_regions' AND INDEX_NAME = 'idx_gift_config_regions_updated') = 0,
  'ALTER TABLE gift_config_regions ADD INDEX idx_gift_config_regions_updated (app_code, updated_at_ms)',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS user_gift_wall (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  gift_id VARCHAR(96) NOT NULL COMMENT '礼物 ID',
  gift_name VARCHAR(128) NOT NULL COMMENT '礼物名称',
  resource_id BIGINT NOT NULL DEFAULT 0 COMMENT '资源 ID',
  resource_snapshot_json JSON NOT NULL COMMENT '资源快照 JSON',
  gift_type_code VARCHAR(32) NOT NULL DEFAULT 'normal' COMMENT '礼物类型编码',
  presentation_json JSON NULL COMMENT '展示配置 JSON 配置或快照',
  gift_count BIGINT NOT NULL DEFAULT 0 COMMENT '礼物数量',
  total_value BIGINT NOT NULL DEFAULT 0 COMMENT '总值',
  total_coin_value BIGINT NOT NULL DEFAULT 0 COMMENT '总金币值',
  total_diamond_value BIGINT NOT NULL DEFAULT 0 COMMENT '总钻石值',
  total_gift_point BIGINT NOT NULL DEFAULT 0 COMMENT '历史字段：GIFT_POINT 已下线，新送礼不再累加',
  total_heat_value BIGINT NOT NULL DEFAULT 0 COMMENT '总热度值',
  charge_asset_type VARCHAR(32) NOT NULL DEFAULT 'COIN' COMMENT '扣费资产类型',
  last_price_version VARCHAR(64) NOT NULL DEFAULT '' COMMENT '最后价格版本',
  first_received_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '首次收到时间，UTC epoch ms',
  last_received_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最后收到时间，UTC epoch ms',
  sort_order INT NOT NULL DEFAULT 0 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, user_id, gift_id),
  KEY idx_user_gift_wall_user_sort (app_code, user_id, sort_order, total_value, last_received_at_ms),
  KEY idx_user_gift_wall_gift (app_code, gift_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户礼物墙表';

CREATE TABLE IF NOT EXISTS wallet_projection_events (
  projection_name VARCHAR(64) NOT NULL COMMENT '投影名称',
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  event_id VARCHAR(128) NOT NULL COMMENT '事件幂等 ID',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  retry_count INT NOT NULL DEFAULT 0 COMMENT '重试次数',
  locked_by VARCHAR(96) NOT NULL DEFAULT '' COMMENT '当前锁定 worker',
  locked_until_ms BIGINT NOT NULL DEFAULT 0 COMMENT '锁定截止时间，UTC epoch ms',
  last_error TEXT NULL COMMENT '最后一次失败原因',
  processed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '处理时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (projection_name, app_code, event_id),
  KEY idx_wallet_projection_claim (projection_name, app_code, status, locked_until_ms, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包投影事件表';

CREATE TABLE IF NOT EXISTS resource_grants (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  grant_id VARCHAR(96) NOT NULL COMMENT '发放 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
  target_user_id BIGINT NOT NULL COMMENT '目标用户 ID',
  grant_source VARCHAR(32) NOT NULL COMMENT '发放来源',
  grant_subject_type VARCHAR(32) NOT NULL COMMENT '发放主体类型',
  grant_subject_id VARCHAR(96) NOT NULL COMMENT '发放主体 ID',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  request_hash VARCHAR(128) NOT NULL COMMENT '请求哈希值',
  group_snapshot_json JSON NULL COMMENT '资源分组快照 JSON',
  reason VARCHAR(256) NOT NULL COMMENT '原因',
  operator_user_id BIGINT NOT NULL COMMENT '操作人用户 ID',
  revoked_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '撤销时间，UTC epoch ms',
  revoked_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '撤销操作人用户 ID',
  revoke_reason VARCHAR(256) NOT NULL DEFAULT '' 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, grant_id),
  UNIQUE KEY uk_resource_grants_command (app_code, command_id),
  KEY idx_resource_grants_target_time (app_code, target_user_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源发放表';

CREATE TABLE IF NOT EXISTS resource_grant_items (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  grant_item_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '发放明细 ID',
  grant_id VARCHAR(96) NOT NULL COMMENT '发放 ID',
  resource_id BIGINT NOT NULL COMMENT '资源 ID',
  resource_snapshot_json JSON NOT NULL COMMENT '资源快照 JSON',
  quantity BIGINT NOT NULL DEFAULT 1 COMMENT '数量',
  duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '持续时长，毫秒',
  result_type VARCHAR(32) NOT NULL COMMENT '结果类型',
  wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '钱包交易 ID',
  entitlement_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '权益 ID',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  KEY idx_resource_grant_items_grant (app_code, grant_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='资源发放明细表';

CREATE TABLE IF NOT EXISTS user_resource_entitlements (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  entitlement_id VARCHAR(96) NOT NULL COMMENT '权益 ID',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  resource_id BIGINT NOT NULL COMMENT '资源 ID',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  quantity BIGINT NOT NULL DEFAULT 1 COMMENT '数量',
  remaining_quantity BIGINT NOT NULL DEFAULT 1 COMMENT '剩余数量',
  effective_at_ms BIGINT NOT NULL COMMENT '生效时间，UTC epoch ms',
  expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '过期时间，UTC epoch ms',
  source_grant_id VARCHAR(96) NOT NULL COMMENT '来源发放 ID',
  source_snapshot_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '不可变资源组快照 ID；空表示旧版可变目录发放',
  resource_snapshot_hash CHAR(64) NOT NULL DEFAULT '' COMMENT '权益资源语义 SHA-256；仅同哈希允许合并',
  resource_snapshot_json JSON NULL COMMENT '发放时完整不可变资源快照',
  gift_snapshot_hash CHAR(64) NOT NULL DEFAULT '' COMMENT '发放时礼物配置、价格、展示和区域语义 SHA-256',
  gift_snapshot_json JSON NULL COMMENT '钱包持有的不可变礼物语义快照；仅 pinned gift entitlement 使用',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, entitlement_id),
  KEY idx_user_resource_active (app_code, user_id, resource_id, status, expires_at_ms),
  KEY idx_user_resource_semantic (app_code, user_id, resource_id, resource_snapshot_hash, status, expires_at_ms),
  KEY idx_user_resource_user_type (app_code, user_id, status, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源权益表';

CREATE TABLE IF NOT EXISTS user_resource_equipment (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  resource_type VARCHAR(32) NOT NULL COMMENT '资源类型',
  entitlement_id VARCHAR(96) NOT NULL COMMENT '权益 ID',
  resource_id BIGINT NOT NULL COMMENT '资源 ID',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, user_id, resource_type, entitlement_id),
  KEY idx_user_resource_equipment_user (app_code, user_id, updated_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户资源装备表';

-- 装备命令以 (app_code,command_id) 点查并先于 entitlement/equipment 行加锁；不对装备表做历史扫描。
-- 记录首个成功回包快照，确保网络重试不会因资源随后到期或后台下架而改变业务结果。
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='用户资源装备命令幂等事实表';

CREATE TABLE IF NOT EXISTS wallet_diamond_exchange_rules (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  rule_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '规则 ID',
  exchange_type VARCHAR(32) NOT NULL COMMENT '兑换类型',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  from_asset_type VARCHAR(32) NOT NULL DEFAULT 'DIAMOND' COMMENT '源资产类型',
  to_asset_type VARCHAR(32) NOT NULL COMMENT '目标资产类型',
  from_amount BIGINT NOT NULL COMMENT '源数量',
  to_amount BIGINT NOT NULL COMMENT '目标数量',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重，数值越小越靠前',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  UNIQUE KEY uk_diamond_exchange_rule (app_code, exchange_type, from_amount, to_asset_type),
  KEY idx_diamond_exchange_active (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='钱包钻石兑换规则表';

CREATE TABLE IF NOT EXISTS gift_diamond_ratio_configs (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  region_id BIGINT NOT NULL DEFAULT 0 COMMENT '房间可见区域，0 表示全局默认',
  gift_type_code VARCHAR(32) NOT NULL COMMENT '礼物类型编码',
  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
  ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 100.00 COMMENT '房间贡献热度和主播周期钻石的礼物类型基础倍率，百分比',
  coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT '收礼人金币返还比例，百分比',
  created_by_admin_id BIGINT NOT NULL DEFAULT 0 COMMENT '创建后台用户 ID',
  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, region_id, gift_type_code),
  KEY idx_gift_diamond_ratio_region (app_code, region_id, status)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='礼物钻石和房间贡献比例配置表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'gift_diamond_ratio_configs' AND COLUMN_NAME = 'coin_return_ratio_percent') = 0,
  'ALTER TABLE gift_diamond_ratio_configs ADD COLUMN coin_return_ratio_percent DECIMAL(5,2) NOT NULL DEFAULT 0.00 COMMENT ''收礼人金币返还比例，百分比'' AFTER ratio_percent',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS red_packet_configs (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否启用红包',
  delayed_open_seconds INT NOT NULL DEFAULT 0 COMMENT '延迟红包打开秒数',
  expire_seconds INT NOT NULL DEFAULT 86400 COMMENT '红包过期秒数，房内红包固定 24 小时',
  daily_send_limit INT NOT NULL DEFAULT 0 COMMENT '每用户 UTC 日发送上限，0 表示禁止发送',
  rule_url VARCHAR(512) NOT NULL DEFAULT '' 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)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包配置主表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'red_packet_configs' AND COLUMN_NAME = 'rule_url') = 0,
  'ALTER TABLE red_packet_configs ADD COLUMN rule_url VARCHAR(512) NOT NULL DEFAULT '''' COMMENT ''红包规则说明链接'' AFTER daily_send_limit',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS red_packet_count_tiers (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  tier_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '数量档位 ID',
  packet_count INT NOT NULL COMMENT '红包数量',
  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  UNIQUE KEY uk_red_packet_count_tier (app_code, packet_count),
  KEY idx_red_packet_count_tier_status (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包数量档位表';

CREATE TABLE IF NOT EXISTS red_packet_amount_tiers (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  tier_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '金额档位 ID',
  total_amount BIGINT NOT NULL COMMENT '红包总金币金额',
  status VARCHAR(32) NOT NULL DEFAULT 'active' COMMENT '业务状态',
  sort_order INT NOT NULL DEFAULT 0 COMMENT '排序权重',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  UNIQUE KEY uk_red_packet_amount_tier (app_code, total_amount),
  KEY idx_red_packet_amount_tier_status (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包金额档位表';

CREATE TABLE IF NOT EXISTS red_packets (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  packet_id VARCHAR(96) NOT NULL COMMENT '红包 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '发红包命令幂等 ID',
  sender_user_id BIGINT NOT NULL COMMENT '发送者用户 ID',
  room_id VARCHAR(96) NOT NULL COMMENT '房间 ID',
  region_id BIGINT NOT NULL COMMENT '房间区域 ID',
  packet_type VARCHAR(32) NOT NULL COMMENT '红包类型：normal/delayed',
  total_amount BIGINT NOT NULL COMMENT '红包总金币金额',
  packet_count INT NOT NULL COMMENT '红包份数',
  remaining_amount BIGINT NOT NULL COMMENT '剩余未领取金币金额',
  remaining_count INT NOT NULL COMMENT '剩余未领取份数',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  open_at_ms BIGINT NOT NULL COMMENT '可领取时间，UTC epoch ms',
  expires_at_ms BIGINT NOT NULL COMMENT '过期时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, packet_id),
  UNIQUE KEY uk_red_packets_command (app_code, command_id),
  KEY idx_red_packets_room_time (app_code, room_id, created_at_ms),
  KEY idx_red_packets_sender_time (app_code, sender_user_id, created_at_ms),
  KEY idx_red_packets_expire (app_code, status, expires_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包主表';

CREATE TABLE IF NOT EXISTS red_packet_allocations (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  packet_id VARCHAR(96) NOT NULL COMMENT '红包 ID',
  allocation_no INT NOT NULL COMMENT '份额序号',
  amount BIGINT NOT NULL COMMENT '份额金币金额',
  status VARCHAR(32) NOT NULL COMMENT '份额状态',
  claimed_by_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '领取用户 ID',
  claim_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '领取记录 ID',
  claimed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '领取时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, packet_id, allocation_no),
  KEY idx_red_packet_alloc_claimable (app_code, packet_id, status, allocation_no),
  KEY idx_red_packet_alloc_user (app_code, claimed_by_user_id, claimed_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包随机份额表';

CREATE TABLE IF NOT EXISTS red_packet_claims (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  claim_id VARCHAR(96) NOT NULL COMMENT '领取记录 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '抢红包命令幂等 ID',
  packet_id VARCHAR(96) NOT NULL COMMENT '红包 ID',
  user_id BIGINT NOT NULL COMMENT '领取用户 ID',
  amount BIGINT NOT NULL COMMENT '领取金币金额',
  wallet_transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  failure_reason VARCHAR(64) NOT NULL DEFAULT '' 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, claim_id),
  UNIQUE KEY uk_red_packet_claim_command (app_code, command_id),
  UNIQUE KEY uk_red_packet_claim_user (app_code, packet_id, user_id),
  KEY idx_red_packet_claim_packet (app_code, packet_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包领取记录表';

CREATE TABLE IF NOT EXISTS red_packet_refunds (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  refund_id VARCHAR(96) NOT NULL COMMENT '退款记录 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '退款命令幂等 ID',
  packet_id VARCHAR(96) NOT NULL COMMENT '红包 ID',
  sender_user_id BIGINT NOT NULL COMMENT '发送者用户 ID',
  refund_amount BIGINT NOT NULL COMMENT '退款金币金额',
  wallet_transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  status VARCHAR(32) 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, refund_id),
  UNIQUE KEY uk_red_packet_refund_command (app_code, command_id),
  UNIQUE KEY uk_red_packet_refund_packet (app_code, packet_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包过期退款记录表';

CREATE TABLE IF NOT EXISTS red_packet_user_daily_counters (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  send_day CHAR(10) NOT NULL COMMENT 'UTC 日期，YYYY-MM-DD',
  sent_count INT 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, user_id, send_day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='红包用户每日发送计数表';

-- vip_program_configs 是 VIP 行为的唯一 App 级策略来源。购买、发放和权益查询必须读取该表，
-- 禁止在服务代码中用 app_code 分支复制 Lalu/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_levels (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  level INT NOT NULL COMMENT '等级',
  name VARCHAR(64) NOT NULL COMMENT '名称',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  price_coin BIGINT NOT NULL COMMENT 'VIP 价格金币数',
  duration_ms BIGINT NOT NULL COMMENT '持续时长，毫秒',
  reward_resource_group_id BIGINT NOT NULL DEFAULT 0 COMMENT '奖励资源分组 ID',
  required_recharge_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT '兼容旧 VIP 累充门槛字段，当前不参与购买资格',
  sort_order INT NOT NULL DEFAULT 0 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, level),
  KEY idx_vip_levels_status_sort (app_code, status, sort_order)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 等级表';

SET @ddl := IF(
  (SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'vip_levels' AND COLUMN_NAME = 'required_recharge_coin_amount') = 0,
  'ALTER TABLE vip_levels ADD COLUMN required_recharge_coin_amount BIGINT NOT NULL DEFAULT 0 COMMENT ''兼容旧 VIP 累充门槛字段，当前不参与购买资格'' AFTER reward_resource_group_id',
  'SELECT 1'
);
PREPARE stmt FROM @ddl;
EXECUTE stmt;
DEALLOCATE PREPARE stmt;

CREATE TABLE IF NOT EXISTS user_vip_memberships (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  level INT NOT NULL COMMENT '等级',
  name VARCHAR(64) NOT NULL DEFAULT '' COMMENT '名称',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  program_type VARCHAR(32) NOT NULL DEFAULT 'legacy_timed' COMMENT '激活时使用的 VIP 体系快照',
  config_version BIGINT NOT NULL DEFAULT 1 COMMENT '激活时使用的 VIP 配置版本',
  started_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '开始时间，UTC epoch ms',
  expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '过期时间，UTC epoch ms',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  PRIMARY KEY (app_code, user_id),
  KEY idx_user_vip_expiry (app_code, status, expires_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 VIP 成员关系表';

-- 用户偏好和 VIP 资格分离：没有记录时通知默认开启、三项隐私能力默认关闭；
-- 首次 PATCH 才物化，且未提交字段必须保留该默认。
-- 主键包含 app_code，确保同一 user_id 在 Fami/Lalu 可独立设置。
CREATE TABLE IF NOT EXISTS user_vip_settings (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码，用于多租户隔离',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  room_entry_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启进房通知',
  online_global_notice_enabled BOOLEAN NOT NULL DEFAULT TRUE COMMENT '是否开启上线全服通知',
  hide_profile_data_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启隐藏个人数据',
  anonymous_profile_visit_enabled BOOLEAN NOT NULL DEFAULT FALSE COMMENT '是否开启匿名访问主页',
  leaderboard_invisible_enabled BOOLEAN NOT NULL DEFAULT FALSE 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, user_id)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户按 App 隔离的 VIP 功能开关';

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;

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;

CREATE TABLE IF NOT EXISTS vip_purchase_orders (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  order_id VARCHAR(96) NOT NULL COMMENT '订单 ID',
  command_id VARCHAR(128) NOT NULL COMMENT '业务命令幂等 ID',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  target_level INT NOT NULL COMMENT '目标等级',
  previous_level INT NOT NULL COMMENT '原等级',
  previous_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '购买前会员过期时间快照，UTC epoch ms',
  new_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '购买后会员过期时间快照，UTC epoch ms',
  price_coin BIGINT NOT NULL COMMENT 'VIP 价格金币数',
  duration_ms BIGINT NOT NULL DEFAULT 0 COMMENT '购买等级的有效期配置快照',
  program_type VARCHAR(32) NOT NULL DEFAULT 'legacy_timed' COMMENT '订单执行的 VIP 体系快照',
  config_version BIGINT NOT NULL DEFAULT 1 COMMENT '订单执行的 VIP 配置版本',
  expiry_policy VARCHAR(32) NOT NULL DEFAULT 'extend_remaining' COMMENT '本单实际采用的有效期策略',
  status VARCHAR(32) NOT NULL COMMENT '业务状态',
  wallet_transaction_id VARCHAR(96) NOT NULL COMMENT '钱包交易 ID',
  resource_grant_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '资源发放 ID',
  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, order_id),
  UNIQUE KEY uk_vip_purchase_command (app_code, command_id),
  KEY idx_vip_purchase_user_time (app_code, user_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 购买订单表';

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;

CREATE TABLE IF NOT EXISTS user_vip_history (
  app_code VARCHAR(32) NOT NULL DEFAULT 'lalu' COMMENT '应用编码，用于多租户隔离',
  history_id BIGINT NOT NULL AUTO_INCREMENT PRIMARY KEY COMMENT '历史 ID',
  user_id BIGINT NOT NULL COMMENT '用户 ID',
  action VARCHAR(32) NOT NULL COMMENT '操作',
  previous_level INT NOT NULL COMMENT '原等级',
  new_level INT NOT NULL COMMENT '新等级',
  previous_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '变更前过期时间快照，UTC epoch ms',
  new_expires_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '变更后过期时间快照，UTC epoch ms',
  program_type VARCHAR(32) NOT NULL DEFAULT 'legacy_timed' COMMENT '变更使用的 VIP 体系快照',
  config_version BIGINT NOT NULL DEFAULT 1 COMMENT '变更使用的 VIP 配置版本',
  order_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '订单 ID',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  KEY idx_user_vip_history_user_time (app_code, user_id, created_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='用户 VIP 历史表';

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;

-- 重复执行 initdb 也可能面对旧 history 有效期快照为 0 的开发库。只为没有任何完整
-- 新格式 history 的当前会员补迁移基线；不 UPDATE 旧行，也不改写已有新格式历史语义。
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'
  );

-- vip_level_benefits 按“具体等级”保存完整权益集合。unlock_level 只保留 P1 来源信息，
-- 运行时不得靠它自动继承，以便后台将来可以显式调整任意等级的最终集合。
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),
  KEY idx_vip_benefit_resource (app_code, resource_id, status, benefit_code, level)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 每等级完整权益矩阵';

-- admin 每次更新都会在删除当前矩阵前后各写一份不可变快照。运行时只按资源索引读取这张
-- 小型配置历史，从而让改绑/删除/program disabled 后的旧 entitlement 继续失败关闭。
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 权益资源绑定不可变历史';

-- user_vip_trial_cards 只保存体验卡的 VIP 元数据；卡片库存与当前佩戴仍分别复用
-- user_resource_entitlements 和 user_resource_equipment，切换佩戴不会重置或作废其它卡片。
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 体验卡背包元数据';

-- VIP 购买、直发、体验卡和返现领取先锁定同一守卫键，再进入会员、账户或资源业务锁。守卫与业务事实同事务提交，
-- 既避免并发首发返回可变错误，也禁止 purchase/direct grant/trial grant 交叉复用 command_id。
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/vip_daily_coin_rebate',
  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 写命令串行化守卫';

-- vip_daily_coin_rebate_runs 固化单 App、单 UTC 日的金额矩阵和配置版本。分页只推进本表游标，
-- 不能在每页重新读取 vip_level_benefits，否则日切期间后台改价会让同一天用户拿到不同金额版本。
CREATE TABLE IF NOT EXISTS vip_daily_coin_rebate_runs (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码，用于多租户隔离',
  task_day CHAR(10) NOT NULL COMMENT 'UTC 日期，YYYY-MM-DD',
  run_id VARCHAR(96) NOT NULL COMMENT '稳定批任务 ID',
  day_start_ms BIGINT NOT NULL COMMENT 'UTC 当日开始，epoch ms',
  day_end_ms BIGINT NOT NULL COMMENT 'UTC 次日开始，排他结束 epoch ms',
  config_version BIGINT NOT NULL COMMENT '创建 run 时 VIP 配置版本',
  level_amounts_json JSON NOT NULL COMMENT '等级到返现金额的不可变快照',
  status VARCHAR(32) NOT NULL COMMENT 'running/completed',
  last_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '已扫描会员游标',
  scanned_count BIGINT NOT NULL DEFAULT 0 COMMENT '累计扫描会员数',
  created_count BIGINT NOT NULL DEFAULT 0 COMMENT '累计创建返现数',
  created_at_ms BIGINT NOT NULL COMMENT '创建时间，UTC epoch ms',
  updated_at_ms BIGINT NOT NULL COMMENT '更新时间，UTC epoch ms',
  completed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '完成时间，UTC epoch ms',
  PRIMARY KEY (app_code, task_day),
  UNIQUE KEY uk_vip_rebate_run_id (app_code, run_id),
  KEY idx_vip_rebate_run_status (app_code, status, task_day)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 每日金币返现运行快照';

-- vip_daily_coin_rebates 是付费 VIP 日切资格事实。体验卡不写入此表；领取只允许在
-- [available_at_ms, expires_at_ms) 内完成，并与 COIN 账户、分录和 outbox 同事务提交。
CREATE TABLE IF NOT EXISTS vip_daily_coin_rebates (
  app_code VARCHAR(32) NOT NULL COMMENT '应用编码，用于多租户隔离',
  rebate_id VARCHAR(96) NOT NULL COMMENT '返现事实 ID',
  run_id VARCHAR(96) NOT NULL COMMENT '来源日 run ID',
  task_day CHAR(10) NOT NULL COMMENT 'UTC 日期，YYYY-MM-DD',
  user_id BIGINT NOT NULL COMMENT '领取用户 ID',
  vip_level INT NOT NULL COMMENT 'UTC 日切时付费 VIP 等级快照',
  vip_name VARCHAR(64) NOT NULL COMMENT 'VIP 名称快照',
  coin_amount BIGINT NOT NULL COMMENT '返现金币正整数快照',
  status VARCHAR(32) NOT NULL COMMENT 'claimable/claimed；expired 由查询层投影',
  available_at_ms BIGINT NOT NULL COMMENT '领取开始，UTC epoch ms',
  expires_at_ms BIGINT NOT NULL COMMENT '领取排他结束，UTC epoch ms',
  config_version BIGINT NOT NULL COMMENT '来源 run 的 VIP 配置版本',
  claimed_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '实际领取时间，UTC epoch ms',
  wallet_transaction_id VARCHAR(96) NOT NULL DEFAULT '' 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, rebate_id),
  UNIQUE KEY uk_vip_rebate_user_day (app_code, task_day, user_id),
  KEY idx_vip_rebate_user_day (app_code, user_id, task_day, rebate_id),
  KEY idx_vip_rebate_expiry (app_code, status, expires_at_ms)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='VIP 每日金币返现领取事实';

-- initdb 也允许在保留数据的开发库重复执行，因此同步补齐守卫，不能只覆盖全新空库。
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 biz_type IN ('vip_purchase', 'vip_grant', 'vip_daily_coin_rebate');

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;

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);

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
  ('lalu', 1, 'VIP1', 'active', 1000, 604800000, 0, 0, 10, 0, 0),
  ('lalu', 2, 'VIP2', 'active', 3000, 604800000, 0, 0, 20, 0, 0),
  ('lalu', 3, 'VIP3', 'active', 8000, 604800000, 0, 0, 30, 0, 0),
  ('lalu', 4, 'VIP4', 'disabled', 0, 604800000, 0, 0, 40, 0, 0),
  ('lalu', 5, 'VIP5', 'disabled', 0, 604800000, 0, 0, 50, 0, 0),
  ('lalu', 6, 'VIP6', 'disabled', 0, 604800000, 0, 0, 60, 0, 0),
  ('lalu', 7, 'VIP7', 'disabled', 0, 604800000, 0, 0, 70, 0, 0),
  ('lalu', 8, 'VIP8', 'disabled', 0, 604800000, 0, 0, 80, 0, 0),
  ('lalu', 9, 'VIP9', 'disabled', 0, 604800000, 0, 0, 90, 0, 0),
  ('lalu', 10, 'VIP10', 'disabled', 0, 604800000, 0, 0, 100, 0, 0),
  -- Fami 的价格和资源组必须由运营后台配置；30 天仅满足后台正数校验且等级保持 disabled，
  -- 不能把 Lalu 的任何商业参数复制为可购买默认值。
  ('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);

-- P1 权益按每个等级展开成完整集合：例如 VIP4 会实际写入 VIP1..VIP4 的全部权益。
-- resource_id/resource_type 保持空，待运营绑定真实资源；体验卡仅排除每日金币返现。
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,
  CASE WHEN benefits.benefit_code = 'daily_coin_rebate' THEN 'disabled' ELSE 'active' END,
  benefits.trial_enabled, 0,
  CASE benefits.benefit_code
    WHEN 'mic_skin' THEN 'mic_seat_icon'
    WHEN 'room_border' THEN 'room_border'
    WHEN 'colored_room_name' THEN 'room_name_style'
    ELSE ''
  END,
  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彩色房间名称', 'decoration', 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;

-- 原型未给出各等级金额，不能猜默认值。未配置正 int64 金额前保持 disabled；运营填入
-- metadata_json.coin_amount 并启用后，后台校验和日 run 创建都会再次严格校验。
UPDATE vip_level_benefits
SET status = 'disabled', trial_enabled = FALSE
WHERE app_code = 'fami'
  AND benefit_code = 'daily_coin_rebate'
  AND (
    JSON_EXTRACT(metadata_json, '$.coin_amount') IS NULL
    OR JSON_TYPE(JSON_EXTRACT(metadata_json, '$.coin_amount')) <> 'INTEGER'
    OR CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_amount')) AS DECIMAL(65, 0)) <= 0
    OR CAST(JSON_UNQUOTE(JSON_EXTRACT(metadata_json, '$.coin_amount')) AS DECIMAL(65, 0)) > 9223372036854775807
  );

-- 老环境可能先执行过不含文案的 seed；这里只补空元数据，运营已配置的 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;

-- Fami 的九种体验卡只预置可发放的背包资源和等级元数据，不预置素材、不固定 resource_id、
-- 不猜测卡片时长；实际发放必须由 GrantVipTrialCard 显式提交 level+duration_ms。
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 @vip_resource_contract_now_ms := CAST(UNIX_TIMESTAMP(UTC_TIMESTAMP(3)) * 1000 AS UNSIGNED);

-- 三项房间视觉权益的 code -> resource_type 是跨服务稳定契约。老环境先收敛类型；
-- disabled 行可以没有素材，但不能保留指向下架资源或错误类型资源的脏 ID。
UPDATE vip_level_benefits
SET benefit_type = 'decoration',
    resource_type = CASE benefit_code
      WHEN 'mic_skin' THEN 'mic_seat_icon'
      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_icon'
      WHEN 'room_border' THEN 'room_border'
      WHEN 'colored_room_name' THEN 'room_name_style'
    END
  );

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_icon'
   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;

-- 只按已经存在的 active Fami VIP4..VIP8 资源组唯一匹配麦位中心图标；HAVING=1 防止多候选误绑。
-- VIP9、room_border、room_name_style 没有可核实目录资源时继续保持 resource_id=0，不生成占位素材。
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_icon'
   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_icon',
    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;

-- 只给 active mic_skin 已绑定的真实麦位中心图标补 format；优先采用合法 animation_format，
-- 否则只从实际 animation/asset URL 的明确后缀推导，jpg 统一写成 jpeg。
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_icon'
 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_icon'
  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)$'
  );

-- active 装扮必须能联结到同 App、active、精确类型的真实资源。麦位中心图标和房间边框还
-- 必须具备素材、preview、合法 format，并保持可选 animation_format 与 format 一致。
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_icon', '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'))))
        )
      )
    )
  );

-- fresh init 在资源映射收敛后建立 baseline；重放 initdb 由唯一键保持幂等。
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;

INSERT IGNORE INTO gift_type_configs (
  app_code, type_code, name, tab_key, status, sort_order,
  created_by_user_id, updated_by_user_id, created_at_ms, updated_at_ms
) VALUES
  ('lalu', 'normal', '普通礼物', 'Gift', 'active', 10, 0, 0, 0, 0),
  ('lalu', 'cp', 'CP礼物', 'CP', 'active', 20, 0, 0, 0, 0),
  ('lalu', 'lucky', '幸运礼物', 'Lucky', 'active', 30, 0, 0, 0, 0),
  ('lalu', 'super_lucky', '超级幸运礼物', 'Super Lucky', 'active', 40, 0, 0, 0, 0),
  ('lalu', 'exclusive', '专属礼物', 'Exclusive', 'active', 50, 0, 0, 0, 0),
  ('lalu', 'noble', '贵族礼物', 'Noble', 'active', 60, 0, 0, 0, 0),
  ('lalu', 'flag', '国旗礼物', 'Flag', 'active', 70, 0, 0, 0, 0),
  ('lalu', 'activity', '活动礼物', 'Activity', 'active', 80, 0, 0, 0, 0),
  ('lalu', 'magic', '魔法礼物', 'Magic', 'active', 90, 0, 0, 0, 0),
  ('lalu', 'custom', '定制礼物', 'Custom', 'active', 100, 0, 0, 0, 0);
