hi-server/internal/logic/public/user/bindEmailWithVerificationLogic.go
shanshanzhong 00255a7118
Some checks failed
Build docker and publish / build (20.15.1) (push) Has been cancelled
feat: 新增多密码验证支持及架构文档
refactor: 重构用户模型和密码验证逻辑
feat(epay): 添加支付类型支持
docs: 添加安装和配置指南文档
fix: 修复优惠券过期检查逻辑
perf: 优化设备解绑缓存清理流程
test: 添加密码验证测试用例
chore: 更新依赖版本
2025-10-27 18:54:07 -07:00

342 lines
12 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/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))
}
}
}