-- ============================================================ -- 修复脚本:将同一 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 代码用 Base62,SQL 里用 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;