From c5d59b86b0353077f172ed52808b3152721e392d Mon Sep 17 00:00:00 2001 From: shanshanzhong Date: Fri, 31 Oct 2025 00:14:22 -0700 Subject: [PATCH] =?UTF-8?q?feat(=E7=94=A8=E6=88=B7=E7=BB=91=E5=AE=9A):=20?= =?UTF-8?q?=E5=AE=9E=E7=8E=B0=E9=82=AE=E7=AE=B1=E7=BB=91=E5=AE=9A=E5=8A=9F?= =?UTF-8?q?=E8=83=BD=E5=B9=B6=E4=BC=98=E5=8C=96=E8=AE=BE=E5=A4=87=E8=A7=A3?= =?UTF-8?q?=E7=BB=91=E9=80=BB=E8=BE=91?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 添加邮箱绑定错误码和消息 修改解绑设备逻辑,解绑后创建新用户设备记录 重构邮箱绑定逻辑,支持检测已绑定邮箱并处理设备转移 --- .../user/bindEmailWithVerificationLogic.go | 188 ++++++++++++++++-- .../logic/public/user/unbindDeviceLogic.go | 97 ++++++++- pkg/xerr/errCode.go | 2 + pkg/xerr/errMsg.go | 2 + 4 files changed, 269 insertions(+), 20 deletions(-) diff --git a/internal/logic/public/user/bindEmailWithVerificationLogic.go b/internal/logic/public/user/bindEmailWithVerificationLogic.go index a303a10..4f1d6e5 100644 --- a/internal/logic/public/user/bindEmailWithVerificationLogic.go +++ b/internal/logic/public/user/bindEmailWithVerificationLogic.go @@ -38,7 +38,6 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi // 获取当前用户 u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) if !ok { - logger.Error("current user is not found in context") return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } @@ -49,15 +48,30 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "获取用户设备信息失败: %v", err) } + // 检测当设备是否已经绑定过邮箱 + existingDevice, err := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, u.Id) + if err != nil { + l.Errorw("查询用户设备信息失败", logger.Field("error", err.Error()), logger.Field("device_identifier", deviceIdentifier)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "查询用户设备信息失败") + } + for _, method := range existingDevice { + if method.AuthType == "email" { + return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailBindError), "该设备已绑定邮箱") + } + } + // 检查邮箱是否已被其他用户绑定 existingMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) var emailUserId int64 - if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { - // 邮箱不存在,创建新的邮箱用户 - l.Infow("邮箱未绑定,将创建新的邮箱用户", logger.Field("email", req.Email)) - emailUserId, err = l.createEmailUser(req.Email) + // 邮箱不存在,创建新的邮箱用户: 不需要创建邮箱用户 + l.Infow(" 为当前设备做 邮箱绑定操作; 在 user_auth_methods 中添加记录", logger.Field("email", req.Email)) + err = l.addAuthMethodForEmailUser(u.Id, req.Email) + if err != nil { + l.Errorw("添加邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("user_id", u.Id)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "添加邮箱用户认证方法失败") + } } else { // 数据库查询错误 l.Errorw("查询邮箱绑定状态失败", logger.Field("error", err.Error())) @@ -69,21 +83,52 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi l.Infow("邮箱已存在,将设备转移到现有邮箱用户", logger.Field("email", req.Email), logger.Field("email_user_id", emailUserId)) - } else { - // 这种情况理论上不应该发生(查询成功但返回零值结构体) - l.Infow("查询到邮箱记录但ID为0,将创建新的邮箱用户", logger.Field("email", req.Email)) - emailUserId, err = l.createEmailUser(req.Email) + // 创建前 需要 吧 原本的 user_devices 表中过的数据删除掉 防止出现两个记录 + devices, _, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, u.Id) if err != nil { - l.Errorw("创建邮箱用户失败", logger.Field("error", err.Error())) - return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "创建邮箱用户失败: %v", err) + l.Errorw("查询用户设备列表失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "查询用户设备列表失败") + } + for _, device := range devices { + // 删除原本的设备记录 + err = l.svcCtx.UserModel.DeleteDevice(l.ctx, device.Id) + if err != nil { + l.Errorw("删除邮箱用户设备记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "删除原本的设备记录失败") + } + } + // 再次更新 user_auth_method : 因为之前 默认 设备登录的时候 创建了一个设备认证数据 + // 现在需要 更新 为 邮箱认证 + err = l.updateAuthMethodForEmailUser(emailUserId, deviceIdentifier) + if err != nil { + l.Errorw("更新邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "更新邮箱用户认证方法失败") + } + // 需要删除原本 user 表中的 记录: 根据 设备 ID + err = l.deleteUserRecordForEmailUser(u.Id) + if err != nil { + l.Errorw("删除用户记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "删除用户记录失败") } - l.Infow("创建新的邮箱用户", - logger.Field("email", req.Email), - logger.Field("email_user_id", emailUserId)) - } - // 执行设备转移到邮箱用户 - return l.transferDeviceToEmailUser(u.Id, emailUserId, deviceIdentifier) + err = l.createDeviceRecordForEmailUser(emailUserId, deviceIdentifier, "") + if err != nil { + l.Errorw("创建邮箱用户设备记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "创建邮箱用户设备记录失败") + } + } + // 4. 生成新的JWT token + token, err := l.generateTokenForUser(emailUserId) + if err != nil { + l.Errorw("生成JWT token失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId)) + return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "生成JWT token失败") + } + return &types.BindEmailWithVerificationResponse{ + Success: true, + Message: "设备关联成功", + Token: token, + UserId: emailUserId, + }, nil } // getCurrentUserDeviceIdentifier 获取当前用户的设备标识符 @@ -119,7 +164,57 @@ func (l *BindEmailWithVerificationLogic) checkIfPureDeviceUser(ctx context.Conte return false, "", nil } -// transferDeviceToEmailUser 将设备从设备用户转移到邮箱用户 +// 邮箱存在的情况:在 user_devices 中创建一条设备记录 +func (l *BindEmailWithVerificationLogic) createDeviceRecordForEmailUser(emailUserId int64, deviceIdentifier string, userAgent string) error { + // online 默认 0 enabled 默认 1 + l.Infow("创建邮箱用户设备记录", + logger.Field("email_user_id", emailUserId), + logger.Field("device_identifier", deviceIdentifier), + logger.Field("online", true), + logger.Field("enabled", false), + logger.Field("user_agent", userAgent)) + + err := l.svcCtx.UserModel.InsertDevice(l.ctx, &user.Device{ + UserId: emailUserId, + Identifier: deviceIdentifier, + Online: false, + Enabled: false, + UserAgent: userAgent, + }) + if err != nil { + l.Errorw("创建邮箱用户设备记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "创建邮箱用户设备记录失败") + } + + l.Infow("成功创建邮箱用户设备记录", + logger.Field("email_user_id", emailUserId), + logger.Field("device_identifier", deviceIdentifier)) + return nil +} + +// 邮箱不存在的情况: 在 user_auth_methods 中添加记录 以当前设备ID 做关联, 此时 user_devices 中不需要变动 +func (l *BindEmailWithVerificationLogic) addAuthMethodForEmailUser(userId int64, email string) error { + l.Infow("添加邮箱用户认证方法", + logger.Field("user_id", userId), + logger.Field("email", email)) + + // 插入邮箱用户认证方法 + err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, &user.AuthMethods{ + UserId: userId, + AuthType: "email", + AuthIdentifier: email, + }) + if err != nil { + l.Errorw("插入邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("user_id", userId)) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "插入邮箱用户认证方法失败") + } + l.Infow("成功添加邮箱用户认证方法", + logger.Field("user_id", userId), + logger.Field("email", email)) + return nil +} + +// transferDeviceToEmailUser func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId, emailUserId int64, deviceIdentifier string) (*types.BindEmailWithVerificationResponse, error) { l.Infow("开始设备转移", logger.Field("device_user_id", deviceUserId), @@ -448,3 +543,60 @@ func (l *BindEmailWithVerificationLogic) activeTrial(userId int64, tx *gorm.DB) return nil } + +// updateAuthMethodForEmailUser 根据 设备ID 找到原本的记录 然后 调整 user_id +func (l *BindEmailWithVerificationLogic) updateAuthMethodForEmailUser(userId int64, deviceIdentifier string) error { + var userAuth user.AuthMethods + if err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { + // 查询设备认证方法 + if err := tx.Model(&user.AuthMethods{}). + Where("auth_identifier = ? AND auth_type = ?", deviceIdentifier, "device"). + First(&userAuth).Error; err != nil { + l.Errorw("查询设备认证方法失败", + logger.Field("device_identifier", deviceIdentifier), + logger.Field("error", err.Error())) + return err + } + + // 更新用户认证方法为 email + if err := tx.Model(&user.AuthMethods{}). + Where("id = ?", userAuth.Id). + Update("user_id", userId).Error; err != nil { + l.Errorw("更新用户设备 用户关联 失败", + logger.Field("user_id", userId), + logger.Field("device_identifier", deviceIdentifier), + logger.Field("error", err.Error())) + return err + } + + l.Infow("更新用户设备 用户关联 成功", + logger.Field("user_id", userId), + logger.Field("device_identifier", deviceIdentifier)) + + return nil + }); err != nil { + return err + } + + return nil +} + +// 根据用户iD 删除 user 表中的记录 +func (l *BindEmailWithVerificationLogic) deleteUserRecordForEmailUser(userId int64) error { + if err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { + // 删除用户记录 + if err := tx.Delete(&user.User{}, userId).Error; err != nil { + l.Errorw("删除用户记录失败", + logger.Field("user_id", userId), + logger.Field("error", err.Error())) + return err + } + + l.Infow("删除用户记录成功", logger.Field("user_id", userId)) + return nil + }); err != nil { + return err + } + + return nil +} diff --git a/internal/logic/public/user/unbindDeviceLogic.go b/internal/logic/public/user/unbindDeviceLogic.go index e4f519b..8be6c64 100644 --- a/internal/logic/public/user/unbindDeviceLogic.go +++ b/internal/logic/public/user/unbindDeviceLogic.go @@ -10,6 +10,7 @@ import ( "github.com/perfect-panel/server/internal/types" "github.com/perfect-panel/server/pkg/constant" "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" @@ -31,7 +32,9 @@ func NewUnbindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Unbi } func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { + // 获取当前 token 登录的用户 userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User) + // 查询解绑设备是否存在 device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id) if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device") @@ -40,9 +43,11 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { if device.UserId != userInfo.Id { return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user") } - - return l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + identifier := device.Identifier + l.svcCtx.DB.Transaction(func(tx *gorm.DB) error { + // 业务逻辑修改: 如果解绑; 那么 就把 设备关系 和 邮箱关系 拆开 var deleteDevice user.Device + // 删除了 设备 记录 err = tx.Model(&deleteDevice).Where("id = ?", req.Id).First(&deleteDevice).Error if err != nil { return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "find device err: %v", err) @@ -72,6 +77,94 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error { sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) _ = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err() } + return nil }) + // 最后 创建一个 新的 设备 用户信息 绕过 赠送套餐 + l.registerUserAndDevice(identifier) + + return nil +} + +func (l *UnbindDeviceLogic) registerUserAndDevice(identifier string) (*user.User, error) { + l.Infow("删除新建 设备 用户", + logger.Field("identifier", identifier), + ) + + var userInfo *user.User + err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { + // Create new user + userInfo = &user.User{ + Salt: "default", + OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase, + } + if err := db.Create(userInfo).Error; err != nil { + l.Errorw("failed to create user", + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err) + } + + // Update refer code + userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id) + if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil { + l.Errorw("failed to update refer code", + logger.Field("user_id", userInfo.Id), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err) + } + + // Create device auth method + authMethod := &user.AuthMethods{ + UserId: userInfo.Id, + AuthType: "device", + AuthIdentifier: identifier, + Verified: true, + } + if err := db.Create(authMethod).Error; err != nil { + l.Errorw("failed to create device auth method", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err) + } + + // Insert device record + deviceInfo := &user.Device{ + Ip: "", + UserId: userInfo.Id, + UserAgent: "", + Identifier: identifier, + Enabled: true, + Online: false, + } + if err := db.Create(deviceInfo).Error; err != nil { + l.Errorw("failed to insert device", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err) + } + + return nil + }) + + if err != nil { + l.Errorw("device registration failed", + logger.Field("identifier", identifier), + logger.Field("error", err.Error()), + ) + return nil, err + } + + l.Infow("device registration completed successfully", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", identifier), + logger.Field("refer_code", userInfo.ReferCode), + ) + + return userInfo, nil } diff --git a/pkg/xerr/errCode.go b/pkg/xerr/errCode.go index c9377c4..ba2501f 100644 --- a/pkg/xerr/errCode.go +++ b/pkg/xerr/errCode.go @@ -27,6 +27,8 @@ const ( TelegramNotBound uint32 = 20007 UserNotBindOauth uint32 = 20008 InviteCodeError uint32 = 20009 + // 邮箱绑定错误 + EmailBindError uint32 = 20010 ) // Node error diff --git a/pkg/xerr/errMsg.go b/pkg/xerr/errMsg.go index f688854..2698a75 100644 --- a/pkg/xerr/errMsg.go +++ b/pkg/xerr/errMsg.go @@ -33,6 +33,8 @@ func init() { TelegramNotBound: "Telegram not bound ", UserNotBindOauth: "User not bind oauth method", InviteCodeError: "Invite code error", + // 邮箱绑定错误 + EmailBindError: "已经绑定该邮箱", // Node error NodeExist: "Node already exists",