All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m13s
284 lines
9.8 KiB
SQL
284 lines
9.8 KiB
SQL
-- ============================================================
|
||
-- 修复脚本:将同一 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;
|