topfans/backend/scripts/recalc_exhibition_hours.sql
zerosaturation 878bd46399 fix(backend): financial correctness — settlement/hours/mint idempotency (batch 1)
- settlement(1.1): exhibition_revenue_records 加 UNIQUE(exhibition_id,cycle_start_time)
  + CreateRevenueRecord ON CONFLICT DO NOTHING; MQ 用 settled_at 替代复用 is_processed;
  恢复扫描器过滤改 settled_at IS NULL; created_at 统一毫秒; 删死代码 cleanup_worker.go。
- hours(1.2): 新增 exhibition_hours_log/asset_exhibition_hours_log(source_id 唯一)幂等表;
  fan_profile/assetLevel 的 AddExhibitionHours 按 sourceID 幂等(事务包裹); 存量重算脚本。
- mint(1.4/1.5/1.6): crystal_transaction_records (source_id,change_type) 部分唯一索引
  + UpdateCrystalBalance/CreateMintOrder 幂等; 保底改 crypto/rand; 下线伪 tx_hash;
  doMint Redis Lua 原子限流。
- migrations 001/002/003; 各服务单测(自包含, 缺 DB t.Skip)。

Co-Authored-By: Claude <noreply@anthropic.com>
2026-07-23 01:11:54 +08:00

173 lines
8.0 KiB
SQL

-- recalc_exhibition_hours.sql
-- 批次1.2 存量校正(DRY-RUN): 输出"按 distinct exhibition 重算"前后的差异
-- ⚠️ 本文件仅 SELECT,禁止任何 DDL/DML。
--
-- 列名适配说明:
-- * exhibitions.slot_owner_uid -> 实际为 host_profile_id
-- * 收益归属(revenue_service.go L356): user_id=SlotOwnerUID(=host_profile_id), star_id=OccupierStarID
-- * level_up_bonus 触发: AddExhibitionHours(userID=host_profile_id, starID=occupier_star_id)
\echo '==== A. 用户级:当前 vs 重算 (差值 > 0 即虚高) ===='
WITH per_user_star AS (
SELECT e.host_profile_id AS user_id,
e.occupier_star_id AS star_id,
SUM((e.expire_at - e.start_time) / 3600000)::bigint AS recalc_hours
FROM exhibitions e
WHERE e.deleted_at IS NULL
AND e.host_profile_id IS NOT NULL
AND e.occupier_star_id IS NOT NULL
GROUP BY e.host_profile_id, e.occupier_star_id
),
current_ueh AS (
SELECT user_id, star_id, total_exhibition_hours AS current_hours
FROM user_exhibition_hours
)
SELECT COALESCE(c.user_id, r.user_id) AS user_id,
COALESCE(c.star_id, r.star_id) AS star_id,
COALESCE(c.current_hours, 0) AS current_hours,
COALESCE(r.recalc_hours, 0) AS recalc_hours,
(COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) AS diff_hours
FROM current_ueh c
FULL OUTER JOIN per_user_star r USING (user_id, star_id)
WHERE COALESCE(c.current_hours, 0) <> COALESCE(r.recalc_hours, 0)
ORDER BY ABS(COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) DESC
LIMIT 50;
\echo '==== B. 用户级 diff 汇总 (待下修/虚高,无 DML) ===='
WITH per_user_star AS (
SELECT e.host_profile_id AS user_id,
e.occupier_star_id AS star_id,
SUM((e.expire_at - e.start_time) / 3600000)::bigint AS recalc_hours
FROM exhibitions e
WHERE e.deleted_at IS NULL
AND e.host_profile_id IS NOT NULL
AND e.occupier_star_id IS NOT NULL
GROUP BY e.host_profile_id, e.occupier_star_id
)
SELECT count(*) FILTER (WHERE (COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) > 0) AS user_star_pairs_overstated,
count(*) FILTER (WHERE (COALESCE(c.current_hours, 0) - COALESCE(c.current_hours, 0)) > 0) AS placeholder_no_overcount,
count(*) FILTER (WHERE (COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) < 0) AS user_star_pairs_understated,
count(*) FILTER (WHERE c.user_id IS NULL) AS user_star_pairs_in_exhibition_only,
count(*) FILTER (WHERE r.user_id IS NULL) AS user_star_pairs_in_ueh_only,
COALESCE(sum(GREATEST(COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0), 0)), 0) AS total_overstated_hours,
COALESCE(sum(GREATEST(COALESCE(r.recalc_hours, 0) - COALESCE(c.current_hours, 0), 0)), 0) AS total_understated_hours
FROM (SELECT user_id, star_id, total_exhibition_hours AS current_hours FROM user_exhibition_hours) c
FULL OUTER JOIN per_user_star r USING (user_id, star_id);
\echo '==== C. 资产级:当前 vs 重算 (差值 > 0 即虚高) ===='
WITH per_asset AS (
SELECT e.asset_id,
SUM((e.expire_at - e.start_time) / 3600000)::bigint AS recalc_hours
FROM exhibitions e
WHERE e.deleted_at IS NULL AND e.asset_id > 0
GROUP BY e.asset_id
)
SELECT COALESCE(c.asset_id, r.asset_id) AS asset_id,
COALESCE(c.current_hours, 0) AS current_hours,
COALESCE(r.recalc_hours, 0) AS recalc_hours,
(COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) AS diff_hours
FROM (
SELECT asset_id, season_exhibition_hours AS current_hours
FROM asset_level_records WHERE season_exhibition_hours > 0
) c
FULL OUTER JOIN per_asset r USING (asset_id)
WHERE COALESCE(c.current_hours, 0) <> COALESCE(r.recalc_hours, 0)
ORDER BY ABS(COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) DESC
LIMIT 50;
\echo '==== D. 资产级 diff 汇总 ===='
WITH per_asset AS (
SELECT e.asset_id,
SUM((e.expire_at - e.start_time) / 3600000)::bigint AS recalc_hours
FROM exhibitions e
WHERE e.deleted_at IS NULL AND e.asset_id > 0
GROUP BY e.asset_id
)
SELECT count(*) FILTER (WHERE (COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) > 0) AS assets_overstated,
count(*) FILTER (WHERE (COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0)) < 0) AS assets_understated,
count(*) FILTER (WHERE c.asset_id IS NULL) AS assets_in_exhibition_only,
count(*) FILTER (WHERE r.asset_id IS NULL) AS assets_in_alr_only,
COALESCE(sum(GREATEST(COALESCE(c.current_hours, 0) - COALESCE(r.recalc_hours, 0), 0)), 0) AS total_overstated_asset_hours,
COALESCE(sum(GREATEST(COALESCE(r.recalc_hours, 0) - COALESCE(c.current_hours, 0), 0)), 0) AS total_understated_asset_hours
FROM (SELECT asset_id, season_exhibition_hours AS current_hours FROM asset_level_records WHERE season_exhibition_hours > 0) c
FULL OUTER JOIN per_asset r USING (asset_id);
\echo '==== E. 已发升级奖励水晶总额 (change_type=level_up_bonus, 仅统计) ===='
SELECT count(*) AS level_up_bonus_records,
COALESCE(sum(delta), 0) AS total_crystal
FROM crystal_transaction_records
WHERE change_type = 'level_up_bonus';
\echo '==== F. level_up_bonus 按 source_id 是否形如 exhibition_<id> 拆分 ===='
SELECT
count(*) FILTER (WHERE source_id ~ '^exhibition_[0-9]+$') AS looks_like_exhibition_source,
count(*) FILTER (WHERE source_id IS NULL OR source_id = '') AS no_source_id,
count(*) FILTER (WHERE source_id IS NOT NULL AND source_id <> '' AND source_id !~ '^exhibition_[0-9]+$') AS other_source,
COALESCE(sum(delta) FILTER (WHERE source_id ~ '^exhibition_[0-9]+$'), 0) AS exhibition_source_crystal,
COALESCE(sum(delta) FILTER (WHERE source_id IS NULL OR source_id = ''), 0) AS no_source_crystal
FROM crystal_transaction_records
WHERE change_type = 'level_up_bonus';
\echo '==== G. 误升用户数: fan_profiles 当前 level vs 按 recalc_hours 应得 level ===='
WITH per_user_star AS (
SELECT e.host_profile_id AS user_id,
e.occupier_star_id AS star_id,
SUM((e.expire_at - e.start_time) / 3600000)::bigint AS recalc_hours
FROM exhibitions e
WHERE e.deleted_at IS NULL
AND e.host_profile_id IS NOT NULL
AND e.occupier_star_id IS NOT NULL
GROUP BY e.host_profile_id, e.occupier_star_id
),
expected_level AS (
SELECT lt.level AS expected_level, r.user_id, r.star_id, r.recalc_hours
FROM per_user_star r
JOIN LATERAL (
SELECT level FROM level_thresholds
WHERE max_exhibition_hours <= r.recalc_hours
ORDER BY level DESC LIMIT 1
) lt ON true
)
SELECT count(*) FILTER (WHERE fp.level > el.expected_level) AS profiles_will_downgrade,
count(*) FILTER (WHERE fp.level < el.expected_level) AS profiles_will_upgrade,
count(*) FILTER (WHERE fp.level = el.expected_level) AS profiles_match,
COALESCE(sum(fp.level - el.expected_level) FILTER (WHERE fp.level > el.expected_level), 0) AS total_level_reduction
FROM fan_profiles fp
JOIN expected_level el
ON fp.user_id = el.user_id AND fp.star_id = el.star_id;
\echo '==== H. 误升用户关联的 level_up_bonus 总额 (若 fan_profile 当前 level > recalc 应得 level) ===='
WITH per_user_star AS (
SELECT e.host_profile_id AS user_id,
e.occupier_star_id AS star_id,
SUM((e.expire_at - e.start_time) / 3600000)::bigint AS recalc_hours
FROM exhibitions e
WHERE e.deleted_at IS NULL
AND e.host_profile_id IS NOT NULL
AND e.occupier_star_id IS NOT NULL
GROUP BY e.host_profile_id, e.occupier_star_id
),
expected_level AS (
SELECT r.user_id, r.star_id, lt.level AS expected_level
FROM per_user_star r
JOIN LATERAL (
SELECT level FROM level_thresholds
WHERE max_exhibition_hours <= r.recalc_hours
ORDER BY level DESC LIMIT 1
) lt ON true
),
mis_upgraded_users AS (
SELECT fp.user_id, fp.star_id
FROM fan_profiles fp
JOIN expected_level el
ON fp.user_id = el.user_id AND fp.star_id = el.star_id
WHERE fp.level > el.expected_level
)
SELECT count(*) AS mis_upgrade_crystal_records,
COALESCE(sum(delta), 0) AS mis_upgrade_total_crystal
FROM crystal_transaction_records ctr
WHERE ctr.change_type = 'level_up_bonus'
AND EXISTS (
SELECT 1 FROM mis_upgraded_users m
WHERE m.user_id = ctr.user_id AND m.star_id = ctr.star_id
);