refactor: 重构用户模型和密码验证逻辑 feat(epay): 添加支付类型支持 docs: 添加安装和配置指南文档 fix: 修复优惠券过期检查逻辑 perf: 优化设备解绑缓存清理流程 test: 添加密码验证测试用例 chore: 更新依赖版本
This commit is contained in:
@@ -1,175 +0,0 @@
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"time"
|
||||
|
||||
"github.com/perfect-panel/server/internal/logic/auth"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"github.com/perfect-panel/server/pkg/tool"
|
||||
|
||||
"github.com/perfect-panel/server/internal/config"
|
||||
"github.com/perfect-panel/server/internal/model/user"
|
||||
"github.com/perfect-panel/server/pkg/jwt"
|
||||
"github.com/perfect-panel/server/pkg/uuidx"
|
||||
"github.com/perfect-panel/server/pkg/xerr"
|
||||
"github.com/pkg/errors"
|
||||
"gorm.io/gorm"
|
||||
|
||||
"github.com/perfect-panel/server/internal/svc"
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/logger"
|
||||
)
|
||||
|
||||
type BindEmailWithPasswordLogic struct {
|
||||
logger.Logger
|
||||
ctx context.Context
|
||||
svcCtx *svc.ServiceContext
|
||||
}
|
||||
|
||||
// NewBindEmailWithPasswordLogic Bind Email With Password
|
||||
func NewBindEmailWithPasswordLogic(ctx context.Context, svcCtx *svc.ServiceContext) *BindEmailWithPasswordLogic {
|
||||
return &BindEmailWithPasswordLogic{
|
||||
Logger: logger.WithContext(ctx),
|
||||
ctx: ctx,
|
||||
svcCtx: svcCtx,
|
||||
}
|
||||
}
|
||||
|
||||
func (l *BindEmailWithPasswordLogic) BindEmailWithPassword(req *types.BindEmailWithPasswordRequest) (*types.LoginResponse, error) {
|
||||
// 获取当前设备用户
|
||||
currentUser, 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")
|
||||
}
|
||||
|
||||
// 验证邮箱和密码是否匹配现有用户
|
||||
emailUser, err := l.svcCtx.UserModel.FindOneByEmail(l.ctx, req.Email)
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserNotExist), "email not registered: %v", req.Email)
|
||||
}
|
||||
logger.WithContext(l.ctx).Error(err)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "query user by email failed: %v", err.Error())
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if !tool.VerifyPassWord(req.Password, emailUser.Password) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserPasswordError), "password incorrect")
|
||||
}
|
||||
|
||||
// 检查当前用户是否已经绑定了邮箱
|
||||
currentEmailMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "email", currentUser.Id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId error")
|
||||
}
|
||||
|
||||
// 最终用户ID(可能是当前用户或邮箱用户)
|
||||
finalUserId := currentUser.Id
|
||||
|
||||
// 如果当前用户已经绑定了邮箱,检查是否是同一个邮箱
|
||||
if currentEmailMethod.Id > 0 {
|
||||
// 如果绑定的是同一个邮箱,直接生成Token返回
|
||||
if currentEmailMethod.AuthIdentifier == req.Email {
|
||||
l.Infow("user is binding the same email that is already bound",
|
||||
logger.Field("user_id", currentUser.Id),
|
||||
logger.Field("email", req.Email),
|
||||
)
|
||||
// 直接使用当前用户ID生成Token
|
||||
} else {
|
||||
// 如果是不同的邮箱,不允许重复绑定
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "current user already has email bound")
|
||||
}
|
||||
} else {
|
||||
// 检查该邮箱是否已经被其他用户绑定
|
||||
existingEmailMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error")
|
||||
}
|
||||
|
||||
// 如果邮箱已经被其他用户绑定,需要进行数据迁移
|
||||
if existingEmailMethod.Id > 0 && existingEmailMethod.UserId != currentUser.Id {
|
||||
// 调用设备绑定逻辑,这会触发数据迁移
|
||||
bindLogic := auth.NewBindDeviceLogic(l.ctx, l.svcCtx)
|
||||
|
||||
// 获取当前用户的设备标识符
|
||||
deviceMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "device", currentUser.Id)
|
||||
if err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByUserId device error")
|
||||
}
|
||||
|
||||
if deviceMethod.Id == 0 {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "current user has no device identifier")
|
||||
}
|
||||
|
||||
// 执行设备重新绑定,这会触发数据迁移
|
||||
if err := bindLogic.BindDeviceToUser(deviceMethod.AuthIdentifier, "", "", emailUser.Id); err != nil {
|
||||
l.Errorw("failed to bind device to email user",
|
||||
logger.Field("current_user_id", currentUser.Id),
|
||||
logger.Field("email_user_id", emailUser.Id),
|
||||
logger.Field("device_identifier", deviceMethod.AuthIdentifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "bind device to email user failed")
|
||||
}
|
||||
|
||||
l.Infow("successfully bound device to email user with data migration",
|
||||
logger.Field("current_user_id", currentUser.Id),
|
||||
logger.Field("email_user_id", emailUser.Id),
|
||||
logger.Field("device_identifier", deviceMethod.AuthIdentifier),
|
||||
)
|
||||
|
||||
// 数据迁移后,使用邮箱用户的ID
|
||||
finalUserId = emailUser.Id
|
||||
} else {
|
||||
// 邮箱未被绑定,直接为当前用户创建邮箱绑定
|
||||
emailMethod := &user.AuthMethods{
|
||||
UserId: currentUser.Id,
|
||||
AuthType: "email",
|
||||
AuthIdentifier: req.Email,
|
||||
Verified: true, // 通过密码验证,直接设为已验证
|
||||
}
|
||||
|
||||
if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, emailMethod); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error")
|
||||
}
|
||||
|
||||
l.Infow("successfully bound email to current user",
|
||||
logger.Field("user_id", currentUser.Id),
|
||||
logger.Field("email", req.Email),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// 生成新的Token
|
||||
sessionId := uuidx.NewUUID().String()
|
||||
loginType := "device"
|
||||
if l.ctx.Value(constant.LoginType) != nil {
|
||||
loginType = l.ctx.Value(constant.LoginType).(string)
|
||||
}
|
||||
|
||||
token, err := jwt.NewJwtToken(
|
||||
l.svcCtx.Config.JwtAuth.AccessSecret,
|
||||
time.Now().Unix(),
|
||||
l.svcCtx.Config.JwtAuth.AccessExpire,
|
||||
jwt.WithOption("UserId", finalUserId),
|
||||
jwt.WithOption("SessionId", sessionId),
|
||||
jwt.WithOption("LoginType", loginType),
|
||||
)
|
||||
if err != nil {
|
||||
l.Logger.Error("[BindEmailWithPassword] token generate error", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error())
|
||||
}
|
||||
|
||||
// 设置session缓存
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
if err = l.svcCtx.Redis.Set(l.ctx, sessionIdCacheKey, finalUserId, time.Duration(l.svcCtx.Config.JwtAuth.AccessExpire)*time.Second).Err(); err != nil {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "set session id error: %v", err.Error())
|
||||
}
|
||||
|
||||
return &types.LoginResponse{
|
||||
Token: token,
|
||||
}, nil
|
||||
}
|
||||
@@ -0,0 +1,341 @@
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -41,7 +41,11 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user")
|
||||
}
|
||||
|
||||
return l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
|
||||
// 保存设备信息用于后续缓存清理
|
||||
deviceIdentifier := device.Identifier
|
||||
userId := device.UserId
|
||||
|
||||
err = 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 {
|
||||
@@ -55,6 +59,9 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
err = tx.Model(&userAuth).Where("auth_identifier = ? and auth_type = ?", deleteDevice.Identifier, "device").First(&userAuth).Error
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
l.Infow("设备认证方法不存在,可能已被删除",
|
||||
logger.Field("device_identifier", deleteDevice.Identifier),
|
||||
logger.Field("user_id", userId))
|
||||
return nil
|
||||
}
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "find device online record err: %v", err)
|
||||
@@ -64,9 +71,69 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "delete device online record err: %v", err)
|
||||
}
|
||||
sessionId := l.ctx.Value(constant.CtxKeySessionID)
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey)
|
||||
|
||||
l.Infow("设备解绑成功",
|
||||
logger.Field("device_id", req.Id),
|
||||
logger.Field("device_identifier", deviceIdentifier),
|
||||
logger.Field("user_id", userId))
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 事务成功后进行缓存清理
|
||||
l.clearUnbindDeviceCache(deviceIdentifier, userId, userInfo)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// clearUnbindDeviceCache 清除设备解绑相关的缓存
|
||||
func (l *UnbindDeviceLogic) clearUnbindDeviceCache(deviceIdentifier string, userId int64, userInfo *user.User) {
|
||||
// 1. 清除当前SessionId缓存(使当前token失效)
|
||||
if sessionId := l.ctx.Value(constant.CtxKeySessionID); sessionId != nil {
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
if err := l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err(); err != nil {
|
||||
l.Errorw("清理SessionId缓存失败",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("session_id", sessionId))
|
||||
} else {
|
||||
l.Infow("已清理SessionId缓存", logger.Field("session_id", sessionId))
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 清除用户缓存
|
||||
if err := l.svcCtx.UserModel.ClearUserCache(l.ctx, userInfo); err != nil {
|
||||
l.Errorw("清理用户缓存失败",
|
||||
logger.Field("error", err.Error()),
|
||||
logger.Field("user_id", userId))
|
||||
} else {
|
||||
l.Infow("已清理用户缓存", logger.Field("user_id", userId))
|
||||
}
|
||||
|
||||
// 3. 清除设备相关缓存
|
||||
l.clearDeviceRelatedCache(deviceIdentifier, userId)
|
||||
}
|
||||
|
||||
// clearDeviceRelatedCache 清除设备相关缓存
|
||||
func (l *UnbindDeviceLogic) clearDeviceRelatedCache(deviceIdentifier string, userId int64) {
|
||||
// 清除设备相关的缓存键
|
||||
deviceCacheKeys := []string{
|
||||
fmt.Sprintf("device:%s", deviceIdentifier),
|
||||
fmt.Sprintf("user_device:%d", userId),
|
||||
fmt.Sprintf("user_auth:%d", userId),
|
||||
fmt.Sprintf("device_auth:%s", deviceIdentifier),
|
||||
}
|
||||
|
||||
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))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,50 +30,37 @@ func NewUpdateBindEmailLogic(ctx context.Context, svcCtx *svc.ServiceContext) *U
|
||||
}
|
||||
}
|
||||
|
||||
// UpdateBindEmail 更新用户绑定的邮箱地址
|
||||
// 该方法用于用户更新或绑定新的邮箱地址,支持首次绑定和修改已绑定邮箱
|
||||
func (l *UpdateBindEmailLogic) UpdateBindEmail(req *types.UpdateBindEmailRequest) error {
|
||||
// 从上下文中获取当前用户信息
|
||||
u, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
if !ok {
|
||||
logger.Error("current user is not found in context")
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access")
|
||||
}
|
||||
|
||||
// 查询当前用户是否已有邮箱认证方式
|
||||
method, err := l.svcCtx.UserModel.FindUserAuthMethodByUserId(l.ctx, "email", u.Id)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error")
|
||||
}
|
||||
|
||||
// 检查要绑定的邮箱是否已被其他用户使用
|
||||
m, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email)
|
||||
if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "FindUserAuthMethodByOpenID error")
|
||||
}
|
||||
|
||||
// 如果邮箱已被绑定,返回错误
|
||||
// email already bind
|
||||
if m.Id > 0 {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.UserExist), "email already bind")
|
||||
}
|
||||
|
||||
// 如果用户还没有邮箱认证方式,创建新的认证记录
|
||||
if method.Id == 0 {
|
||||
method = &user.AuthMethods{
|
||||
UserId: u.Id, // 用户ID
|
||||
AuthType: "email", // 认证类型为邮箱
|
||||
AuthIdentifier: req.Email, // 邮箱地址
|
||||
Verified: false, // 初始状态为未验证
|
||||
UserId: u.Id,
|
||||
AuthType: "email",
|
||||
AuthIdentifier: req.Email,
|
||||
Verified: false,
|
||||
}
|
||||
// 插入新的认证方式记录
|
||||
if err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, method); err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "InsertUserAuthMethods error")
|
||||
}
|
||||
} else {
|
||||
// 如果用户已有邮箱认证方式,更新邮箱地址
|
||||
method.Verified = false // 重置验证状态
|
||||
method.AuthIdentifier = req.Email // 更新邮箱地址
|
||||
// 更新认证方式记录
|
||||
method.Verified = false
|
||||
method.AuthIdentifier = req.Email
|
||||
if err := l.svcCtx.UserModel.UpdateUserAuthMethods(l.ctx, method); err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "UpdateUserAuthMethods error")
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user