hi-server/internal/logic/public/user/bindEmailWithVerificationLogic.go
shanshanzhong 0f38b3fcd3
Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 6m39s
refactor(auth): 优化设备登录逻辑,移除冗余代码并添加设备缓存
feat(database): 添加用户算法和盐字段的迁移脚本

fix(subscribe): 修复服务器用户列表缓存问题,临时禁用缓存

style(model): 清理用户模型注释,简化代码结构

chore: 删除无用脚本和测试文件

docs: 添加用户绑定流程文档

perf(login): 优化设备登录性能,添加设备缓存键

fix(unbind): 修复设备解绑时的缓存清理逻辑

refactor(verify): 简化邮箱验证逻辑,移除冗余代码

build(docker): 更新Dockerfile配置,使用scratch基础镜像
2025-10-28 20:46:21 -07:00

383 lines
15 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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/uuidx"
"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. 检查目标邮箱用户状态
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. 获取原设备用户信息
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))
return err
}
if existingDevice != nil && existingDevice.UserId == emailUserId {
// 设备已经关联到目标用户直接生成token
l.Infow("设备已关联到目标用户", logger.Field("device_id", existingDevice.Id))
return nil
}
// 6. 处理设备冲突 - 将现有设备记录的归属修改为邮箱用户
if existingDevice != nil && existingDevice.UserId != emailUserId {
l.Infow("更新冲突设备记录的归属", logger.Field("existing_device_id", existingDevice.Id), logger.Field("old_user_id", existingDevice.UserId), logger.Field("new_user_id", emailUserId))
if err := db.Model(&user.Device{}).Where("identifier = ? AND user_id = ?", deviceIdentifier, existingDevice.UserId).Update("user_id", emailUserId).Error; err != nil {
l.Errorw("更新冲突设备记录归属失败", logger.Field("error", err.Error()))
return err
}
}
// 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 {
l.Errorw("更新设备认证方式失败", logger.Field("error", err.Error()))
return err
}
// 8. 更新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
}
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 {
// // 清除用户的批量相关缓存(包括设备、认证方法等)
// if err := l.svcCtx.UserModel.BatchClearRelatedCache(l.ctx, emailUser); err != nil {
// l.Errorw("清理邮箱用户相关缓存失败", logger.Field("error", err.Error()), logger.Field("user_id", emailUser.Id))
// }
// }
// 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) {
// 检查是否启用了强制邀请码
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, // 启用状态
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
}
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. 生成并设置用户的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",
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("cache:device:identifier:%s", deviceIdentifier),
fmt.Sprintf("cache:user:devices:%d", oldUserId),
fmt.Sprintf("cache:user:devices:%d", newUserId),
fmt.Sprintf("cache:user:auth_methods:%d", oldUserId),
fmt.Sprintf("cache:user:auth_methods:%d", newUserId),
fmt.Sprintf("cache:user:%d", oldUserId),
fmt.Sprintf("cache:user:%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))
}
}
}