Some checks failed
Build docker and publish / build (20.15.1) (push) Failing after 6m39s
feat(database): 添加用户算法和盐字段的迁移脚本 fix(subscribe): 修复服务器用户列表缓存问题,临时禁用缓存 style(model): 清理用户模型注释,简化代码结构 chore: 删除无用脚本和测试文件 docs: 添加用户绑定流程文档 perf(login): 优化设备登录性能,添加设备缓存键 fix(unbind): 修复设备解绑时的缓存清理逻辑 refactor(verify): 简化邮箱验证逻辑,移除冗余代码 build(docker): 更新Dockerfile配置,使用scratch基础镜像
383 lines
15 KiB
Go
383 lines
15 KiB
Go
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))
|
||
}
|
||
}
|
||
}
|