hi-server/scripts/fix_split_multi_device_users.sql
shanshanzhong 7d5b4fcb84
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m13s
邮箱修复
2026-03-12 01:24:21 -07:00

284 lines
9.8 KiB
SQL
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

-- ============================================================
-- 修复脚本:将同一 user_id 下的多设备拆分为独立用户 + 家庭组
--
-- 问题:代码模型要求 每个设备 = 独立用户,多设备通过家庭组关联
-- 但旧数据中同一 user_id 下挂了多个 user_device
--
-- 修复策略:
-- 1. 每个用户保留第一个设备(最早创建的),作为 family owner
-- 2. 其余设备各创建一个新 user作为 family member
-- 3. 建立家庭组关系
--
-- ⚠️ 执行前请先备份!
-- ============================================================
-- ============================================================
-- Step 0: 诊断 - 查看受影响的用户和设备
-- ============================================================
SELECT
d.user_id,
COUNT(*) as device_count,
GROUP_CONCAT(d.id ORDER BY d.created_at ASC) as device_ids,
GROUP_CONCAT(d.identifier ORDER BY d.created_at ASC SEPARATOR ' | ') as identifiers
FROM user_device d
WHERE d.enabled = 1
GROUP BY d.user_id
HAVING device_count > 1
ORDER BY d.user_id;
-- ============================================================
-- Step 1: 创建临时表,标记需要拆分的设备
-- 每个用户保留最早的设备,其余标记为需要拆分
-- ============================================================
DROP TEMPORARY TABLE IF EXISTS tmp_devices_to_split;
CREATE TEMPORARY TABLE tmp_devices_to_split AS
SELECT
d.id as device_id,
d.user_id as original_user_id,
d.identifier,
d.ip,
d.user_agent,
d.created_at as device_created_at,
ROW_NUMBER() OVER (PARTITION BY d.user_id ORDER BY d.created_at ASC) as rn
FROM user_device d
WHERE d.enabled = 1
AND d.user_id IN (
SELECT user_id
FROM user_device
WHERE enabled = 1
GROUP BY user_id
HAVING COUNT(*) > 1
);
-- 确认rn=1 的保留在原用户rn>1 的需要创建新用户
SELECT * FROM tmp_devices_to_split ORDER BY original_user_id, rn;
-- ============================================================
-- Step 2: 为 rn>1 的设备创建新用户
-- 复制原用户的基本配置,生成新的 refer_code
-- ============================================================
-- 先看需要创建多少个新用户
SELECT COUNT(*) as new_users_needed FROM tmp_devices_to_split WHERE rn > 1;
-- 创建新用户(从原用户复制基本信息)
INSERT INTO `user` (
password, algo, salt, avatar, balance,
refer_code, referer_id, commission,
referral_percentage, only_first_purchase, gift_amount,
enable, is_admin,
enable_balance_notify, enable_login_notify,
enable_subscribe_notify, enable_trade_notify,
rules, member_status, remark,
created_at, updated_at
)
SELECT
u.password, u.algo, u.salt, '', 0,
'', -- refer_code 后面更新
u.referer_id, 0,
u.referral_percentage, u.only_first_purchase, 0,
u.enable, 0, -- is_admin = false
0, 0, 0, 0, -- 通知全关
'', '', CONCAT('split_from_user_', u.id),
NOW(), NOW()
FROM tmp_devices_to_split t
JOIN `user` u ON u.id = t.original_user_id
WHERE t.rn > 1;
-- ============================================================
-- Step 3: 映射新用户 ID 到设备
-- 因为 MySQL 不支持 INSERT ... RETURNING需要通过 remark 字段找到新创建的用户
-- ============================================================
DROP TEMPORARY TABLE IF EXISTS tmp_new_user_mapping;
CREATE TEMPORARY TABLE tmp_new_user_mapping AS
SELECT
u.id as new_user_id,
CAST(SUBSTRING(u.remark, LENGTH('split_from_user_') + 1) AS UNSIGNED) as original_user_id,
u.created_at
FROM `user` u
WHERE u.remark LIKE 'split_from_user_%'
AND u.deleted_at IS NULL
ORDER BY u.id ASC;
-- 验证映射关系
SELECT * FROM tmp_new_user_mapping;
-- 将新用户与待拆分设备匹配(按原用户分组内的顺序)
DROP TEMPORARY TABLE IF EXISTS tmp_device_user_mapping;
CREATE TEMPORARY TABLE tmp_device_user_mapping AS
SELECT
t.device_id,
t.original_user_id,
t.identifier,
m.new_user_id
FROM (
SELECT *, ROW_NUMBER() OVER (PARTITION BY original_user_id ORDER BY device_id ASC) as split_seq
FROM tmp_devices_to_split
WHERE rn > 1
) t
JOIN (
SELECT *, ROW_NUMBER() OVER (PARTITION BY original_user_id ORDER BY new_user_id ASC) as split_seq
FROM tmp_new_user_mapping
) m ON t.original_user_id = m.original_user_id AND t.split_seq = m.split_seq;
-- 确认映射
SELECT * FROM tmp_device_user_mapping;
-- ============================================================
-- Step 4: 更新设备的 user_id 指向新用户
-- ============================================================
UPDATE user_device d
JOIN tmp_device_user_mapping m ON d.id = m.device_id
SET d.user_id = m.new_user_id;
-- ============================================================
-- Step 5: 为新用户创建 device auth_method
-- ============================================================
INSERT INTO user_auth_methods (user_id, auth_type, auth_identifier, verified, created_at, updated_at)
SELECT
m.new_user_id,
'device',
m.identifier,
1,
NOW(),
NOW()
FROM tmp_device_user_mapping m;
-- ============================================================
-- Step 6: 更新新用户的 refer_code
-- 用 CONCAT('u', CONV(new_user_id + UNIX_TIMESTAMP(), 10, 36)) 生成简单唯一码
-- Go 代码用 Base62SQL 里用 Base36 近似,长度足够唯一)
-- ============================================================
UPDATE `user` u
JOIN tmp_new_user_mapping m ON u.id = m.new_user_id
SET u.refer_code = CONCAT('u', LOWER(CONV(u.id + UNIX_TIMESTAMP(NOW()), 10, 36)));
-- 清理 remark 标记
UPDATE `user` u
JOIN tmp_new_user_mapping m ON u.id = m.new_user_id
SET u.remark = '';
-- ============================================================
-- Step 7: 创建/确保家庭组(原用户为 owner
-- 先处理已有家庭组的情况,再处理没有的
-- ============================================================
-- 7a. 为没有 active 家庭组的原用户创建家庭组
INSERT INTO user_family (owner_user_id, max_members, status, created_at, updated_at)
SELECT DISTINCT
t.original_user_id,
2,
1, -- active
NOW(),
NOW()
FROM tmp_device_user_mapping t
LEFT JOIN user_family f ON f.owner_user_id = t.original_user_id
AND f.status = 1 AND f.deleted_at IS NULL
WHERE f.id IS NULL;
-- 7b. 确保原用户在家庭组中有 owner 成员记录
INSERT INTO user_family_member (family_id, user_id, role, status, join_source, joined_at, created_at, updated_at)
SELECT
f.id,
f.owner_user_id,
1, -- role = owner
1, -- status = active
'data_fix_split',
NOW(),
NOW(),
NOW()
FROM user_family f
WHERE f.owner_user_id IN (SELECT DISTINCT original_user_id FROM tmp_device_user_mapping)
AND f.status = 1
AND f.deleted_at IS NULL
AND NOT EXISTS (
SELECT 1 FROM user_family_member fm
WHERE fm.family_id = f.id
AND fm.user_id = f.owner_user_id
AND fm.status = 1
AND fm.deleted_at IS NULL
);
-- 7c. 将新用户加入家庭组作为 member
INSERT INTO user_family_member (family_id, user_id, role, status, join_source, joined_at, created_at, updated_at)
SELECT
f.id,
m.new_user_id,
2, -- role = member
1, -- status = active
'data_fix_split',
NOW(),
NOW(),
NOW()
FROM tmp_device_user_mapping m
JOIN user_family f ON f.owner_user_id = m.original_user_id
AND f.status = 1 AND f.deleted_at IS NULL;
-- 7d. 更新 max_members如果原用户有 >2 个设备)
UPDATE user_family f
SET f.max_members = (
SELECT COUNT(*)
FROM user_family_member fm
WHERE fm.family_id = f.id AND fm.status = 1 AND fm.deleted_at IS NULL
)
WHERE f.owner_user_id IN (SELECT DISTINCT original_user_id FROM tmp_device_user_mapping)
AND f.status = 1 AND f.deleted_at IS NULL;
-- ============================================================
-- Step 8: 转移订阅(原用户的订阅保留,新用户不需要订阅)
-- 家庭成员共享 owner 的订阅,所以新用户不需要自己的订阅
-- 如果原用户已有订阅,新用户通过家庭组共享
-- ============================================================
-- (无需操作,代码中 familyBindingHelper.clearMemberSubscribes 会在 joinFamily 时清理)
-- 新用户是刚创建的,没有任何订阅记录,无需清理
-- ============================================================
-- Step 9: 验证修复结果
-- ============================================================
-- 9a. 确认没有用户拥有多个设备了
SELECT
d.user_id,
COUNT(*) as device_count,
GROUP_CONCAT(d.id ORDER BY d.id) as device_ids
FROM user_device d
WHERE d.enabled = 1
GROUP BY d.user_id
HAVING device_count > 1;
-- 预期结果0 行
-- 9b. 确认家庭组关系正确
SELECT
f.id as family_id,
f.owner_user_id,
f.max_members,
f.status as family_status,
GROUP_CONCAT(CONCAT(fm.user_id, '(role=', fm.role, ')') ORDER BY fm.role) as members
FROM user_family f
JOIN user_family_member fm ON fm.family_id = f.id AND fm.status = 1 AND fm.deleted_at IS NULL
WHERE f.owner_user_id IN (SELECT DISTINCT original_user_id FROM tmp_device_user_mapping)
AND f.status = 1 AND f.deleted_at IS NULL
GROUP BY f.id, f.owner_user_id, f.max_members, f.status;
-- 9c. 确认新用户都有 device auth_method
SELECT
m.new_user_id,
m.original_user_id,
m.identifier,
am.id as auth_method_id,
d.id as device_id,
d.user_id as device_user_id
FROM tmp_device_user_mapping m
JOIN user_auth_methods am ON am.user_id = m.new_user_id AND am.auth_type = 'device'
JOIN user_device d ON d.id = m.device_id;
-- ============================================================
-- 清理临时表
-- ============================================================
DROP TEMPORARY TABLE IF EXISTS tmp_devices_to_split;
DROP TEMPORARY TABLE IF EXISTS tmp_new_user_mapping;
DROP TEMPORARY TABLE IF EXISTS tmp_device_user_mapping;