邮箱修复
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m13s

This commit is contained in:
shanshanzhong 2026-03-12 01:24:21 -07:00
parent a3cc23bbd4
commit 7d5b4fcb84
4 changed files with 476 additions and 2 deletions

View File

@ -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
}

View File

@ -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_memberowner 身份)
-- 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

View File

@ -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 代码用 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;

17
说明文档.md Normal file
View File

@ -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 |