feat: 添加测试数据清理脚本并改进设备登录逻辑
Build docker and publish / build (20.15.1) (push) Successful in 7m9s

docs(scripts): 添加测试数据清理脚本的详细使用文档
fix(auth): 修复设备登录时处理孤立认证方法的问题
refactor(public): 改进邮箱绑定逻辑中的推荐码处理
This commit is contained in:
2025-10-27 22:20:18 -07:00
parent cef7150aab
commit 1bcfa321b7
5 changed files with 611 additions and 36 deletions
+51 -28
View File
@@ -86,40 +86,63 @@ func (l *DeviceLoginLogic) DeviceLogin(req *types.DeviceLoginRequest) (resp *typ
}
if authMethod != nil {
// 认证方法存在但设备记录不存在,可能是数据不一致,获取用户信息并重新创建设备记录
// 认证方法存在但设备记录不存在,可能是数据不一致,先检查用户是否存在
userInfo, err = l.svcCtx.UserModel.FindOne(l.ctx, authMethod.UserId)
if err != nil {
l.Errorw("query user by auth method failed",
logger.Field("user_id", authMethod.UserId),
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
}
// 重新创建缺失的设备记录
deviceInfo := &user.Device{
Ip: req.IP,
UserId: userInfo.Id,
UserAgent: req.UserAgent,
Identifier: req.Identifier,
Enabled: true,
Online: false,
}
if err := l.svcCtx.UserModel.InsertDevice(l.ctx, deviceInfo); err != nil {
l.Errorw("failed to recreate device record",
if errors.Is(err, gorm.ErrRecordNotFound) {
// 用户不存在,说明是孤立的认证方法记录,需要清理
l.Errorw("found orphaned auth method record, cleaning up",
logger.Field("auth_method_id", authMethod.Id),
logger.Field("user_id", authMethod.UserId),
logger.Field("identifier", req.Identifier),
)
// 删除孤立的认证方法记录
if deleteErr := l.svcCtx.UserModel.DeleteUserAuthMethods(l.ctx, authMethod.UserId, authMethod.AuthType); deleteErr != nil {
l.Errorw("failed to delete orphaned auth method",
logger.Field("auth_method_id", authMethod.Id),
logger.Field("error", deleteErr.Error()),
)
}
// 创建新用户和设备
userInfo, err = l.registerUserAndDevice(req)
if err != nil {
return nil, err
}
} else {
l.Errorw("query user by auth method failed",
logger.Field("user_id", authMethod.UserId),
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user failed: %v", err.Error())
}
} else {
// 用户存在,重新创建缺失的设备记录
deviceInfo := &user.Device{
Ip: req.IP,
UserId: userInfo.Id,
UserAgent: req.UserAgent,
Identifier: req.Identifier,
Enabled: true,
Online: false,
}
if err := l.svcCtx.UserModel.InsertDevice(l.ctx, deviceInfo); err != nil {
l.Errorw("failed to recreate device record",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "recreate device record failed: %v", err)
}
l.Infow("found existing auth method without device record, recreated device record",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("error", err.Error()),
logger.Field("device_id", deviceInfo.Id),
)
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "recreate device record failed: %v", err)
}
l.Infow("found existing auth method without device record, recreated device record",
logger.Field("user_id", userInfo.Id),
logger.Field("identifier", req.Identifier),
logger.Field("device_id", deviceInfo.Id),
)
} else {
// 设备和认证方法都不存在,创建新用户和设备
userInfo, err = l.registerUserAndDevice(req)
@@ -12,6 +12,7 @@ import (
"github.com/perfect-panel/server/pkg/constant"
"github.com/perfect-panel/server/pkg/jwt"
"github.com/perfect-panel/server/pkg/logger"
"github.com/perfect-panel/server/pkg/uuidx"
"github.com/perfect-panel/server/pkg/xerr"
"github.com/pkg/errors"
"gorm.io/gorm"
@@ -127,13 +128,57 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
// 2. 在事务中执行设备转移
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
// 1. 检查目标邮箱用户状态
_, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
emailUser, err := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId)
if err != nil {
l.Errorw("查询邮箱用户失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
return err
}
// 2. 检查设备是否已经关联到目标用户
// 2. 获取原设备用户信息
deviceUser, err := l.svcCtx.UserModel.FindOne(l.ctx, deviceUserId)
if err != nil {
l.Errorw("查询设备用户失败", logger.Field("error", err.Error()), logger.Field("device_user_id", deviceUserId))
return err
}
// 3. 如果邮箱用户没有ReferCode,则从设备用户转移或生成新的
if emailUser.ReferCode == "" {
if deviceUser.ReferCode != "" {
// 转移设备用户的ReferCode
if err := db.Model(&user.User{}).Where("id = ?", emailUserId).Update("refer_code", deviceUser.ReferCode).Error; err != nil {
l.Errorw("转移ReferCode失败", logger.Field("error", err.Error()))
return err
}
l.Infow("已转移设备用户的ReferCode到邮箱用户",
logger.Field("device_user_id", deviceUserId),
logger.Field("email_user_id", emailUserId),
logger.Field("refer_code", deviceUser.ReferCode))
} else {
// 为邮箱用户生成新的ReferCode
newReferCode := uuidx.UserInviteCode(emailUserId)
if err := db.Model(&user.User{}).Where("id = ?", emailUserId).Update("refer_code", newReferCode).Error; err != nil {
l.Errorw("生成邮箱用户ReferCode失败", logger.Field("error", err.Error()))
return err
}
l.Infow("已为邮箱用户生成新的ReferCode",
logger.Field("email_user_id", emailUserId),
logger.Field("refer_code", newReferCode))
}
}
// 4. 如果邮箱用户没有RefererId,但设备用户有,则转移RefererId
if emailUser.RefererId == 0 && deviceUser.RefererId != 0 {
if err := db.Model(&user.User{}).Where("id = ?", emailUserId).Update("referer_id", deviceUser.RefererId).Error; err != nil {
l.Errorw("转移RefererId失败", logger.Field("error", err.Error()))
return err
}
l.Infow("已转移设备用户的RefererId到邮箱用户",
logger.Field("device_user_id", deviceUserId),
logger.Field("email_user_id", emailUserId),
logger.Field("referer_id", deviceUser.RefererId))
}
// 5. 检查设备是否已经关联到目标用户
existingDevice, err := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, deviceIdentifier)
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
l.Errorw("查询设备信息失败", logger.Field("error", err.Error()), logger.Field("device_identifier", deviceIdentifier))
@@ -146,7 +191,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
return nil
}
// 3. 处理设备冲突 - 删除目标用户的现有设备记录(如果存在)
// 6. 处理设备冲突 - 删除目标用户的现有设备记录(如果存在)
if existingDevice != nil && existingDevice.UserId != emailUserId {
l.Infow("删除冲突的设备记录", logger.Field("existing_device_id", existingDevice.Id), logger.Field("existing_user_id", existingDevice.UserId))
if err := db.Where("identifier = ? AND user_id = ?", deviceIdentifier, existingDevice.UserId).Delete(&user.Device{}).Error; err != nil {
@@ -155,7 +200,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
}
}
// 4. 更新user_auth_methods表 - 将设备认证方式转移到邮箱用户
// 7. 更新user_auth_methods表 - 将设备认证方式转移到邮箱用户
if err := db.Model(&user.AuthMethods{}).
Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", deviceUserId, "device", deviceIdentifier).
Update("user_id", emailUserId).Error; err != nil {
@@ -163,7 +208,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
return err
}
// 5. 更新user_device表 - 将设备记录转移到邮箱用户
// 8. 更新user_device表 - 将设备记录转移到邮箱用户
if err := db.Model(&user.Device{}).
Where("user_id = ? AND identifier = ?", deviceUserId, deviceIdentifier).
Update("user_id", emailUserId).Error; err != nil {
@@ -171,7 +216,7 @@ func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId,
return err
}
// 6. 检查原始设备用户是否还有其他认证方式,如果没有则删除该用户
// 9. 检查原始设备用户是否还有其他认证方式,如果没有则删除该用户
var remainingAuthMethods []user.AuthMethods
if err := db.Where("user_id = ?", deviceUserId).Find(&remainingAuthMethods).Error; err != nil {
l.Errorw("查询原始用户剩余认证方式失败", logger.Field("error", err.Error()), logger.Field("device_user_id", deviceUserId))
@@ -278,13 +323,22 @@ func (l *BindEmailWithVerificationLogic) generateTokenForUser(userId int64) (str
// createEmailUser 创建新的邮箱用户
func (l *BindEmailWithVerificationLogic) createEmailUser(email string) (int64, error) {
// 检查是否启用了强制邀请码
if l.svcCtx.Config.Invite.ForcedInvite {
l.Errorw("邮箱绑定创建新用户时需要邀请码,但当前API不支持邀请码参数",
logger.Field("email", email),
logger.Field("forced_invite", true))
return 0, xerr.NewErrMsg("创建新用户需要邀请码,请使用支持邀请码的注册方式")
}
var newUserId int64
err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
// 1. 创建新用户
enabled := true
newUser := &user.User{
Enable: &enabled, // 启用状态
Enable: &enabled, // 启用状态
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
}
if err := tx.Create(newUser).Error; err != nil {
l.Errorw("创建用户失败", logger.Field("error", err.Error()))
@@ -294,7 +348,17 @@ func (l *BindEmailWithVerificationLogic) createEmailUser(email string) (int64, e
newUserId = newUser.Id
l.Infow("创建新用户成功", logger.Field("user_id", newUserId))
// 2. 创建邮箱认证方法
// 2. 生成并设置用户的ReferCode
newUser.ReferCode = uuidx.UserInviteCode(newUserId)
if err := tx.Model(&user.User{}).Where("id = ?", newUserId).Update("refer_code", newUser.ReferCode).Error; err != nil {
l.Errorw("更新用户ReferCode失败", logger.Field("error", err.Error()))
return err
}
l.Infow("设置用户ReferCode成功",
logger.Field("user_id", newUserId),
logger.Field("refer_code", newUser.ReferCode))
// 3. 创建邮箱认证方法
emailAuth := &user.AuthMethods{
UserId: newUserId,
AuthType: "email",