From 7d5b4fcb849215d637b3b53cc1ed6b0adb48c888 Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Thu, 12 Mar 2026 01:24:21 -0700 Subject: [PATCH] =?UTF-8?q?=E9=82=AE=E7=AE=B1=E4=BF=AE=E5=A4=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../logic/public/user/deleteAccountLogic.go | 69 ++++- scripts/fix_orphan_devices_family.sql | 109 +++++++ scripts/fix_split_multi_device_users.sql | 283 ++++++++++++++++++ 说明文档.md | 17 ++ 4 files changed, 476 insertions(+), 2 deletions(-) create mode 100644 scripts/fix_orphan_devices_family.sql create mode 100644 scripts/fix_split_multi_device_users.sql create mode 100644 说明文档.md diff --git a/internal/logic/public/user/deleteAccountLogic.go b/internal/logic/public/user/deleteAccountLogic.go index b57b549..7d54d4b 100644 --- a/internal/logic/public/user/deleteAccountLogic.go +++ b/internal/logic/public/user/deleteAccountLogic.go @@ -47,6 +47,25 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon // 事务前先查出 AuthMethods,用于事务后精确清缓存 authMethods, _ := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, currentUser.Id) + // 事务前查出家庭关系,判断当前用户是否为 member 且 owner 是邮箱用户 + var familyOwnerUserID int64 + if relation, relErr := l.findActiveFamilyRelation(currentUser.Id); relErr == nil && relation.Role == user.FamilyRoleMember { + // 当前用户是 member,查 owner + var ownerMember user.UserFamilyMember + if ownerErr := l.svcCtx.DB.WithContext(l.ctx). + Model(&user.UserFamilyMember{}). + Where("family_id = ? AND role = ? AND status = ?", relation.FamilyId, user.FamilyRoleOwner, user.FamilyMemberActive). + First(&ownerMember).Error; ownerErr == nil { + familyOwnerUserID = ownerMember.UserId + } + } + + // 事务前查出 owner 的 AuthMethods(用于事务后清缓存) + var ownerAuthMethods []*user.AuthMethods + if familyOwnerUserID > 0 { + ownerAuthMethods, _ = l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, familyOwnerUserID) + } + affectedUserIDs := []int64{currentUser.Id} err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { familyUserIDs, collectErr := l.collectAffectedFamilyUserIDs(tx, currentUser.Id) @@ -70,6 +89,36 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "delete user subscribes failed") } + // 如果 owner(邮箱用户)没有其他活跃成员了,也清理 owner 的数据 + if familyOwnerUserID > 0 { + var remainingMembers int64 + if err := tx.Model(&user.UserFamilyMember{}). + Where("family_id IN (SELECT id FROM user_family WHERE owner_user_id = ? AND deleted_at IS NULL) AND status = ? AND deleted_at IS NULL", + familyOwnerUserID, user.FamilyMemberActive). + Count(&remainingMembers).Error; err == nil && remainingMembers <= 1 { + // 只剩 owner 自己(或没人了),清理 owner 数据 + // 删除 owner 的 auth_methods + if err := tx.Where("user_id = ?", familyOwnerUserID).Delete(&user.AuthMethods{}).Error; err != nil { + l.Errorw("delete owner auth methods failed", logger.Field("owner_id", familyOwnerUserID), logger.Field("error", err.Error())) + } + // 删除 owner 的订阅 + if err := tx.Where("user_id = ?", familyOwnerUserID).Delete(&user.Subscribe{}).Error; err != nil { + l.Errorw("delete owner subscribes failed", logger.Field("owner_id", familyOwnerUserID), logger.Field("error", err.Error())) + } + // 解散 owner 的家庭 + exitHelper2 := newFamilyExitHelper(l.ctx, l.svcCtx) + if removeErr := exitHelper2.removeUserFromActiveFamily(tx, familyOwnerUserID, true); removeErr != nil { + l.Errorw("remove owner from family failed", logger.Field("owner_id", familyOwnerUserID), logger.Field("error", removeErr.Error())) + } + // 将 owner 加入 affectedUserIDs 以便清缓存 + affectedUserIDs = append(affectedUserIDs, familyOwnerUserID) + l.Infow("cleaned up family owner (email user) on device account deletion", + logger.Field("device_user_id", currentUser.Id), + logger.Field("owner_user_id", familyOwnerUserID), + ) + } + } + return nil }) if err != nil { @@ -96,9 +145,12 @@ func (l *DeleteAccountLogic) DeleteAccountAll() (resp *types.DeleteAccountRespon } // 主动清 auth method 相关缓存(含 email/mobile 等 key),避免缓存未命中时无法生成正确 key - if len(authMethods) > 0 { + allAuthMethods := make([]*user.AuthMethods, 0) + allAuthMethods = append(allAuthMethods, authMethods...) + allAuthMethods = append(allAuthMethods, ownerAuthMethods...) + if len(allAuthMethods) > 0 { var authCacheKeys []string - for _, am := range authMethods { + for _, am := range allAuthMethods { if am.AuthType == "email" && am.AuthIdentifier != "" { authCacheKeys = append(authCacheKeys, fmt.Sprintf("cache:user:email:%s", am.AuthIdentifier)) } @@ -334,3 +386,16 @@ func (l *DeleteAccountLogic) clearAllSessions(userId int64) { logger.Field("count", len(sessionSet)), ) } + +// findActiveFamilyRelation 查找用户的活跃家庭成员记录 +func (l *DeleteAccountLogic) findActiveFamilyRelation(userID int64) (*user.UserFamilyMember, error) { + var relation user.UserFamilyMember + err := l.svcCtx.DB.WithContext(l.ctx). + Model(&user.UserFamilyMember{}). + Where("user_id = ? AND status = ?", userID, user.FamilyMemberActive). + First(&relation).Error + if err != nil { + return nil, err + } + return &relation, nil +} diff --git a/scripts/fix_orphan_devices_family.sql b/scripts/fix_orphan_devices_family.sql new file mode 100644 index 0000000..4845ec0 --- /dev/null +++ b/scripts/fix_orphan_devices_family.sql @@ -0,0 +1,109 @@ +-- ============================================================ +-- 修复脚本:为有多设备但无家庭组的用户补建家庭组 +-- 影响用户数:30 +-- 每个用户:2 个设备,0 个家庭组 +-- 执行前请先备份! +-- ============================================================ + +-- ============================================================ +-- Step 0: 确认受影响数据(只读,不做任何修改) +-- ============================================================ +SELECT + d.user_id, + COUNT(*) as device_count, + GROUP_CONCAT(d.id ORDER BY d.id) as device_ids +FROM user_device d +LEFT JOIN user_family_member fm ON fm.user_id = d.user_id AND fm.status = 1 +WHERE d.enabled = 1 AND fm.id IS NULL +GROUP BY d.user_id +HAVING device_count > 1 +ORDER BY d.user_id; + +-- 预期结果:30 行 + +-- ============================================================ +-- Step 1: 为每个用户创建 user_family(家庭组) +-- owner_user_id = user_id, max_members = 2, status = 1(active) +-- ============================================================ +INSERT INTO user_family (owner_user_id, max_members, status, created_at, updated_at) +SELECT + d.user_id, + 2, -- max_members = 2(当前都是 2 设备) + 1, -- status = active + MIN(d.created_at), -- 用最早设备的创建时间 + NOW() +FROM user_device d +LEFT JOIN user_family_member fm ON fm.user_id = d.user_id AND fm.status = 1 +LEFT JOIN user_family f ON f.owner_user_id = d.user_id AND f.deleted_at IS NULL +WHERE d.enabled = 1 + AND fm.id IS NULL -- 没有 active 家庭成员记录 + AND f.id IS NULL -- 没有已存在的家庭 +GROUP BY d.user_id +HAVING COUNT(*) > 1; + +-- 预期影响:30 行 + +-- ============================================================ +-- Step 2: 为每个用户创建 user_family_member(owner 身份) +-- role = 1(owner), status = 1(active), join_source = 'data_fix' +-- ============================================================ +INSERT INTO user_family_member (family_id, user_id, role, status, join_source, joined_at, created_at, updated_at) +SELECT + f.id, -- 刚创建的 family_id + f.owner_user_id, -- user_id + 1, -- role = owner + 1, -- status = active + 'data_fix', -- 标记来源,方便追溯 + f.created_at, -- joined_at = family 创建时间 + NOW(), + NOW() +FROM user_family f +LEFT JOIN user_family_member fm ON fm.user_id = f.owner_user_id AND fm.status = 1 +WHERE fm.id IS NULL -- 还没有 active 家庭成员记录 + AND f.deleted_at IS NULL + AND f.owner_user_id IN ( + -- 只处理我们目标用户 + SELECT d.user_id + FROM user_device d + WHERE d.enabled = 1 + GROUP BY d.user_id + HAVING COUNT(*) > 1 + ); + +-- 预期影响:30 行 + +-- ============================================================ +-- Step 3: 验证修复结果 +-- ============================================================ + +-- 3a. 确认所有多设备用户都有了家庭组 +SELECT + d.user_id, + COUNT(DISTINCT d.id) as device_count, + f.id as family_id, + f.max_members, + fm.role, + fm.status as member_status, + fm.join_source +FROM user_device d +JOIN user_family f ON f.owner_user_id = d.user_id AND f.deleted_at IS NULL +JOIN user_family_member fm ON fm.user_id = d.user_id AND fm.status = 1 +WHERE d.enabled = 1 + AND fm.join_source = 'data_fix' +GROUP BY d.user_id, f.id, f.max_members, fm.role, fm.status, fm.join_source +ORDER BY d.user_id; + +-- 预期结果:30 行,每行 device_count=2, role=1, member_status=1 + +-- 3b. 确认没有遗漏(多设备无家庭组的用户应该为 0) +SELECT COUNT(*) as remaining_orphans +FROM ( + SELECT d.user_id + FROM user_device d + LEFT JOIN user_family_member fm ON fm.user_id = d.user_id AND fm.status = 1 + WHERE d.enabled = 1 AND fm.id IS NULL + GROUP BY d.user_id + HAVING COUNT(*) > 1 +) orphans; + +-- 预期结果:0 diff --git a/scripts/fix_split_multi_device_users.sql b/scripts/fix_split_multi_device_users.sql new file mode 100644 index 0000000..62faab5 --- /dev/null +++ b/scripts/fix_split_multi_device_users.sql @@ -0,0 +1,283 @@ +-- ============================================================ +-- 修复脚本:将同一 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; diff --git a/说明文档.md b/说明文档.md new file mode 100644 index 0000000..cc372d2 --- /dev/null +++ b/说明文档.md @@ -0,0 +1,17 @@ +# 说明文档.md + +## 项目规划 +本任务旨在帮助用户在云端 Docker 日志中查找 "database insert error" 错误,特别是以 "Database" 开头的报错信息。 + +## 实施方案 +1. **环境确认**:确认如何访问云端容器(SSH 或远程 Docker 上下文)。 +2. **日志检索**:使用 `docker logs` 结合 `grep` 进行过滤。 +3. **结果分析**:提取具体的报错信息,分析可能的原因。 +4. **反馈**:将查找到的错误信息整理反馈给用户。 + +## 进度记录 +| 时间节点 | 任务说明 | 进度 | 结果说明 | +| :--- | :--- | :--- | :--- | +| 2026-03-11 | 初始化文档并提供指令 | [x] 已完成 | 已提供查找日志的命令并记录在文档中 | +| 2026-03-11 | 提供今天所有 ERROR 报错指令 | [x] 已完成 | 已提供根据日期过滤 ERROR 的命令 | +| 2026-03-12 | 分析并确认 Unknown column 错误 | [x] 已完成 | 确认为 `user_device` 缺少 `short_code` 字段,已提供 SQL |