package user import ( "context" "fmt" "time" "github.com/perfect-panel/server/internal/config" "github.com/perfect-panel/server/internal/model/user" "github.com/perfect-panel/server/internal/svc" "github.com/perfect-panel/server/internal/types" "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/xerr" "github.com/pkg/errors" "gorm.io/gorm" ) type BindEmailWithVerificationLogic struct { logger.Logger ctx context.Context svcCtx *svc.ServiceContext } // NewBindEmailWithVerificationLogic Bind Email With Verification func NewBindEmailWithVerificationLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindEmailWithVerificationLogic { return &BindEmailWithVerificationLogic{ Logger: logger.WithContext(ctx), ctx: ctx, svcCtx: svcCtx, } } func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.BindEmailWithVerificationRequest) (*types.BindEmailWithVerificationResponse, error) { // 获取当前用户 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") } // 获取当前用户的设备标识符 deviceIdentifier, err := l.getCurrentUserDeviceIdentifier(l.ctx, u.Id) if err != nil { l.Errorw("获取用户设备标识符失败", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "获取用户设备信息失败: %v", err) } // 检查邮箱是否已被其他用户绑定 existingMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email) if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { l.Errorw("查询邮箱绑定状态失败", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "查询邮箱绑定状态失败") } var emailUserId int64 if existingMethod != nil { // 邮箱已存在,使用现有的邮箱用户 emailUserId = existingMethod.UserId l.Infow("邮箱已存在,将设备转移到现有邮箱用户", logger.Field("email", req.Email), logger.Field("email_user_id", emailUserId)) } else { // 邮箱不存在,创建新的邮箱用户 emailUserId, err = l.createEmailUser(req.Email) if err != nil { l.Errorw("创建邮箱用户失败", logger.Field("error", err.Error())) return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "创建邮箱用户失败: %v", err) } l.Infow("创建新的邮箱用户", logger.Field("email", req.Email), logger.Field("email_user_id", emailUserId)) } // 执行设备转移到邮箱用户 return l.transferDeviceToEmailUser(u.Id, emailUserId, deviceIdentifier) } // getCurrentUserDeviceIdentifier 获取当前用户的设备标识符 func (l *BindEmailWithVerificationLogic) getCurrentUserDeviceIdentifier(ctx context.Context, userId int64) (string, error) { authMethods, err := l.svcCtx.UserModel.FindUserAuthMethods(ctx, userId) if err != nil { return "", err } // 查找设备认证方式 for _, method := range authMethods { if method.AuthType == "device" { return method.AuthIdentifier, nil } } return "", errors.New("用户没有设备认证方式") } // checkIfPureDeviceUser 检查用户是否为纯设备用户(只有设备认证方式) func (l *BindEmailWithVerificationLogic) checkIfPureDeviceUser(ctx context.Context, userId int64) (bool, string, error) { authMethods, err := l.svcCtx.UserModel.FindUserAuthMethods(ctx, userId) if err != nil { l.Errorw("查询用户认证方式失败", logger.Field("error", err.Error()), logger.Field("user_id", userId)) return false, "", errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "查询用户认证方式失败") } // 检查是否只有一个设备认证方式 if len(authMethods) == 1 && authMethods[0].AuthType == "device" { return true, authMethods[0].AuthIdentifier, nil } return false, "", nil } // transferDeviceToEmailUser 将设备从设备用户转移到邮箱用户 func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId, emailUserId int64, deviceIdentifier string) (*types.BindEmailWithVerificationResponse, error) { l.Infow("开始设备转移", logger.Field("device_user_id", deviceUserId), logger.Field("email_user_id", emailUserId), logger.Field("device_identifier", deviceIdentifier)) // 1. 先获取当前用户的SessionId,用于后续清理 currentSessionId := "" if sessionIdValue := l.ctx.Value(constant.CtxKeySessionID); sessionIdValue != nil { currentSessionId = sessionIdValue.(string) } // 2. 在事务中执行设备转移 err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error { // 1. 检查目标邮箱用户状态 _, 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. 检查设备是否已经关联到目标用户 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)) return err } if existingDevice != nil && existingDevice.UserId == emailUserId { // 设备已经关联到目标用户,直接生成token l.Infow("设备已关联到目标用户", logger.Field("device_id", existingDevice.Id)) return nil } // 3. 处理设备冲突 - 删除目标用户的现有设备记录(如果存在) 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 { l.Errorw("删除冲突设备记录失败", logger.Field("error", err.Error())) return err } } // 4. 更新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 { l.Errorw("更新设备认证方式失败", logger.Field("error", err.Error())) return err } // 5. 更新user_device表 - 将设备记录转移到邮箱用户 if err := db.Model(&user.Device{}). Where("user_id = ? AND identifier = ?", deviceUserId, deviceIdentifier). Update("user_id", emailUserId).Error; err != nil { l.Errorw("更新设备记录失败", logger.Field("error", err.Error())) return err } // 6. 检查原始设备用户是否还有其他认证方式,如果没有则删除该用户 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)) return err } if len(remainingAuthMethods) == 0 { // 获取原始用户信息用于清除缓存 deviceUser, _ := l.svcCtx.UserModel.FindOne(l.ctx, deviceUserId) // 原始用户没有其他认证方式,可以安全删除 if err := db.Where("id = ?", deviceUserId).Delete(&user.User{}).Error; err != nil { l.Errorw("删除原始设备用户失败", logger.Field("error", err.Error()), logger.Field("device_user_id", deviceUserId)) return err } // 清除已删除用户的缓存 if deviceUser != nil { l.svcCtx.UserModel.ClearUserCache(l.ctx, deviceUser) } l.Infow("已删除原始设备用户", logger.Field("device_user_id", deviceUserId)) } else { l.Infow("原始用户还有其他认证方式,保留用户记录", logger.Field("device_user_id", deviceUserId), logger.Field("remaining_auth_count", len(remainingAuthMethods))) } l.Infow("设备转移成功", logger.Field("device_user_id", deviceUserId), logger.Field("email_user_id", emailUserId), logger.Field("device_identifier", deviceIdentifier)) return nil }) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "设备转移失败: %v", err) } // 3. 清理原用户的SessionId缓存(使旧token失效) if currentSessionId != "" { sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, currentSessionId) if err := l.svcCtx.Redis.Del(l.ctx, sessionKey).Err(); err != nil { l.Errorw("清理原SessionId缓存失败", logger.Field("error", err.Error()), logger.Field("session_id", currentSessionId)) // 不返回错误,继续执行 } else { l.Infow("已清理原SessionId缓存", logger.Field("session_id", currentSessionId)) } } // 4. 生成新的JWT token token, err := l.generateTokenForUser(emailUserId) if err != nil { return nil, err } // 5. 清除邮箱用户缓存(确保获取最新数据) emailUser, _ := l.svcCtx.UserModel.FindOne(l.ctx, emailUserId) if emailUser != nil { l.svcCtx.UserModel.ClearUserCache(l.ctx, emailUser) } // 6. 清除设备相关缓存 l.clearDeviceRelatedCache(deviceIdentifier, deviceUserId, emailUserId) return &types.BindEmailWithVerificationResponse{ Success: true, Message: "设备关联成功", Token: token, UserId: emailUserId, }, nil } // generateTokenForUser 为指定用户生成JWT token func (l *BindEmailWithVerificationLogic) generateTokenForUser(userId int64) (string, error) { // 生成JWT token now := time.Now().Unix() accessExpire := l.svcCtx.Config.JwtAuth.AccessExpire sessionId := fmt.Sprintf("device_transfer_%d_%d", userId, now) jwtToken, err := jwt.NewJwtToken( l.svcCtx.Config.JwtAuth.AccessSecret, now, accessExpire, jwt.WithOption("UserId", userId), jwt.WithOption("SessionId", sessionId), ) if err != nil { l.Errorw("生成JWT token失败", logger.Field("error", err.Error()), logger.Field("user_id", userId)) return "", errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "生成token失败: %v", err) } // 设置session缓存 sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) if err := l.svcCtx.Redis.Set(l.ctx, sessionKey, userId, time.Duration(accessExpire)*time.Second).Err(); err != nil { l.Errorw("设置session缓存失败", logger.Field("error", err.Error()), logger.Field("user_id", userId)) // session缓存失败不影响token生成,只记录错误 } l.Infow("为用户生成token成功", logger.Field("user_id", userId)) return jwtToken, nil } // createEmailUser 创建新的邮箱用户 func (l *BindEmailWithVerificationLogic) createEmailUser(email string) (int64, error) { var newUserId int64 err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { // 1. 创建新用户 enabled := true newUser := &user.User{ Enable: &enabled, // 启用状态 } if err := tx.Create(newUser).Error; err != nil { l.Errorw("创建用户失败", logger.Field("error", err.Error())) return err } newUserId = newUser.Id l.Infow("创建新用户成功", logger.Field("user_id", newUserId)) // 2. 创建邮箱认证方法 emailAuth := &user.AuthMethods{ UserId: newUserId, AuthType: "email", AuthIdentifier: email, Verified: true, // 直接设置为已验证 } if err := tx.Create(emailAuth).Error; err != nil { l.Errorw("创建邮箱认证方法失败", logger.Field("error", err.Error())) return err } l.Infow("创建邮箱认证方法成功", logger.Field("user_id", newUserId), logger.Field("email", email)) return nil }) if err != nil { return 0, err } return newUserId, nil } // clearDeviceRelatedCache 清除设备相关缓存 func (l *BindEmailWithVerificationLogic) clearDeviceRelatedCache(deviceIdentifier string, oldUserId, newUserId int64) { // 清除设备相关的缓存键 deviceCacheKeys := []string{ fmt.Sprintf("device:%s", deviceIdentifier), fmt.Sprintf("user_device:%d", oldUserId), fmt.Sprintf("user_device:%d", newUserId), fmt.Sprintf("user_auth:%d", oldUserId), fmt.Sprintf("user_auth:%d", newUserId), } for _, key := range deviceCacheKeys { if err := l.svcCtx.Redis.Del(l.ctx, key).Err(); err != nil { l.Errorw("清除设备缓存失败", logger.Field("error", err.Error()), logger.Field("cache_key", key)) } else { l.Infow("已清除设备缓存", logger.Field("cache_key", key)) } } }