feat(用户绑定): 实现邮箱绑定功能并优化设备解绑逻辑
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m20s
All checks were successful
Build docker and publish / build (20.15.1) (push) Successful in 7m20s
添加邮箱绑定错误码和消息 修改解绑设备逻辑,解绑后创建新用户设备记录 重构邮箱绑定逻辑,支持检测已绑定邮箱并处理设备转移
This commit is contained in:
parent
e23809b32e
commit
c5d59b86b0
@ -38,7 +38,6 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
||||
// 获取当前用户
|
||||
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")
|
||||
}
|
||||
|
||||
@ -49,15 +48,30 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "获取用户设备信息失败: %v", err)
|
||||
}
|
||||
|
||||
// 检测当设备是否已经绑定过邮箱
|
||||
existingDevice, err := l.svcCtx.UserModel.FindUserAuthMethods(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
l.Errorw("查询用户设备信息失败", logger.Field("error", err.Error()), logger.Field("device_identifier", deviceIdentifier))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "查询用户设备信息失败")
|
||||
}
|
||||
for _, method := range existingDevice {
|
||||
if method.AuthType == "email" {
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.EmailBindError), "该设备已绑定邮箱")
|
||||
}
|
||||
}
|
||||
|
||||
// 检查邮箱是否已被其他用户绑定
|
||||
existingMethod, err := l.svcCtx.UserModel.FindUserAuthMethodByOpenID(l.ctx, "email", req.Email)
|
||||
var emailUserId int64
|
||||
|
||||
if err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 邮箱不存在,创建新的邮箱用户
|
||||
l.Infow("邮箱未绑定,将创建新的邮箱用户", logger.Field("email", req.Email))
|
||||
emailUserId, err = l.createEmailUser(req.Email)
|
||||
// 邮箱不存在,创建新的邮箱用户: 不需要创建邮箱用户
|
||||
l.Infow(" 为当前设备做 邮箱绑定操作; 在 user_auth_methods 中添加记录", logger.Field("email", req.Email))
|
||||
err = l.addAuthMethodForEmailUser(u.Id, req.Email)
|
||||
if err != nil {
|
||||
l.Errorw("添加邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("user_id", u.Id))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "添加邮箱用户认证方法失败")
|
||||
}
|
||||
} else {
|
||||
// 数据库查询错误
|
||||
l.Errorw("查询邮箱绑定状态失败", logger.Field("error", err.Error()))
|
||||
@ -69,21 +83,52 @@ func (l *BindEmailWithVerificationLogic) BindEmailWithVerification(req *types.Bi
|
||||
l.Infow("邮箱已存在,将设备转移到现有邮箱用户",
|
||||
logger.Field("email", req.Email),
|
||||
logger.Field("email_user_id", emailUserId))
|
||||
} else {
|
||||
// 这种情况理论上不应该发生(查询成功但返回零值结构体)
|
||||
l.Infow("查询到邮箱记录但ID为0,将创建新的邮箱用户", logger.Field("email", req.Email))
|
||||
emailUserId, err = l.createEmailUser(req.Email)
|
||||
// 创建前 需要 吧 原本的 user_devices 表中过的数据删除掉 防止出现两个记录
|
||||
devices, _, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, u.Id)
|
||||
if err != nil {
|
||||
l.Errorw("创建邮箱用户失败", logger.Field("error", err.Error()))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "创建邮箱用户失败: %v", err)
|
||||
l.Errorw("查询用户设备列表失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "查询用户设备列表失败")
|
||||
}
|
||||
for _, device := range devices {
|
||||
// 删除原本的设备记录
|
||||
err = l.svcCtx.UserModel.DeleteDevice(l.ctx, device.Id)
|
||||
if err != nil {
|
||||
l.Errorw("删除邮箱用户设备记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "删除原本的设备记录失败")
|
||||
}
|
||||
}
|
||||
// 再次更新 user_auth_method : 因为之前 默认 设备登录的时候 创建了一个设备认证数据
|
||||
// 现在需要 更新 为 邮箱认证
|
||||
err = l.updateAuthMethodForEmailUser(emailUserId, deviceIdentifier)
|
||||
if err != nil {
|
||||
l.Errorw("更新邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "更新邮箱用户认证方法失败")
|
||||
}
|
||||
// 需要删除原本 user 表中的 记录: 根据 设备 ID
|
||||
err = l.deleteUserRecordForEmailUser(u.Id)
|
||||
if err != nil {
|
||||
l.Errorw("删除用户记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseDeletedError), "删除用户记录失败")
|
||||
}
|
||||
l.Infow("创建新的邮箱用户",
|
||||
logger.Field("email", req.Email),
|
||||
logger.Field("email_user_id", emailUserId))
|
||||
}
|
||||
|
||||
// 执行设备转移到邮箱用户
|
||||
return l.transferDeviceToEmailUser(u.Id, emailUserId, deviceIdentifier)
|
||||
err = l.createDeviceRecordForEmailUser(emailUserId, deviceIdentifier, "")
|
||||
if err != nil {
|
||||
l.Errorw("创建邮箱用户设备记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "创建邮箱用户设备记录失败")
|
||||
}
|
||||
}
|
||||
// 4. 生成新的JWT token
|
||||
token, err := l.generateTokenForUser(emailUserId)
|
||||
if err != nil {
|
||||
l.Errorw("生成JWT token失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return nil, errors.Wrapf(xerr.NewErrCode(xerr.DatabaseQueryError), "生成JWT token失败")
|
||||
}
|
||||
return &types.BindEmailWithVerificationResponse{
|
||||
Success: true,
|
||||
Message: "设备关联成功",
|
||||
Token: token,
|
||||
UserId: emailUserId,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// getCurrentUserDeviceIdentifier 获取当前用户的设备标识符
|
||||
@ -119,7 +164,57 @@ func (l *BindEmailWithVerificationLogic) checkIfPureDeviceUser(ctx context.Conte
|
||||
return false, "", nil
|
||||
}
|
||||
|
||||
// transferDeviceToEmailUser 将设备从设备用户转移到邮箱用户
|
||||
// 邮箱存在的情况:在 user_devices 中创建一条设备记录
|
||||
func (l *BindEmailWithVerificationLogic) createDeviceRecordForEmailUser(emailUserId int64, deviceIdentifier string, userAgent string) error {
|
||||
// online 默认 0 enabled 默认 1
|
||||
l.Infow("创建邮箱用户设备记录",
|
||||
logger.Field("email_user_id", emailUserId),
|
||||
logger.Field("device_identifier", deviceIdentifier),
|
||||
logger.Field("online", true),
|
||||
logger.Field("enabled", false),
|
||||
logger.Field("user_agent", userAgent))
|
||||
|
||||
err := l.svcCtx.UserModel.InsertDevice(l.ctx, &user.Device{
|
||||
UserId: emailUserId,
|
||||
Identifier: deviceIdentifier,
|
||||
Online: false,
|
||||
Enabled: false,
|
||||
UserAgent: userAgent,
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorw("创建邮箱用户设备记录失败", logger.Field("error", err.Error()), logger.Field("email_user_id", emailUserId))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "创建邮箱用户设备记录失败")
|
||||
}
|
||||
|
||||
l.Infow("成功创建邮箱用户设备记录",
|
||||
logger.Field("email_user_id", emailUserId),
|
||||
logger.Field("device_identifier", deviceIdentifier))
|
||||
return nil
|
||||
}
|
||||
|
||||
// 邮箱不存在的情况: 在 user_auth_methods 中添加记录 以当前设备ID 做关联, 此时 user_devices 中不需要变动
|
||||
func (l *BindEmailWithVerificationLogic) addAuthMethodForEmailUser(userId int64, email string) error {
|
||||
l.Infow("添加邮箱用户认证方法",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("email", email))
|
||||
|
||||
// 插入邮箱用户认证方法
|
||||
err := l.svcCtx.UserModel.InsertUserAuthMethods(l.ctx, &user.AuthMethods{
|
||||
UserId: userId,
|
||||
AuthType: "email",
|
||||
AuthIdentifier: email,
|
||||
})
|
||||
if err != nil {
|
||||
l.Errorw("插入邮箱用户认证方法失败", logger.Field("error", err.Error()), logger.Field("user_id", userId))
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "插入邮箱用户认证方法失败")
|
||||
}
|
||||
l.Infow("成功添加邮箱用户认证方法",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("email", email))
|
||||
return nil
|
||||
}
|
||||
|
||||
// transferDeviceToEmailUser
|
||||
func (l *BindEmailWithVerificationLogic) transferDeviceToEmailUser(deviceUserId, emailUserId int64, deviceIdentifier string) (*types.BindEmailWithVerificationResponse, error) {
|
||||
l.Infow("开始设备转移",
|
||||
logger.Field("device_user_id", deviceUserId),
|
||||
@ -448,3 +543,60 @@ func (l *BindEmailWithVerificationLogic) activeTrial(userId int64, tx *gorm.DB)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// updateAuthMethodForEmailUser 根据 设备ID 找到原本的记录 然后 调整 user_id
|
||||
func (l *BindEmailWithVerificationLogic) updateAuthMethodForEmailUser(userId int64, deviceIdentifier string) error {
|
||||
var userAuth user.AuthMethods
|
||||
if err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||
// 查询设备认证方法
|
||||
if err := tx.Model(&user.AuthMethods{}).
|
||||
Where("auth_identifier = ? AND auth_type = ?", deviceIdentifier, "device").
|
||||
First(&userAuth).Error; err != nil {
|
||||
l.Errorw("查询设备认证方法失败",
|
||||
logger.Field("device_identifier", deviceIdentifier),
|
||||
logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新用户认证方法为 email
|
||||
if err := tx.Model(&user.AuthMethods{}).
|
||||
Where("id = ?", userAuth.Id).
|
||||
Update("user_id", userId).Error; err != nil {
|
||||
l.Errorw("更新用户设备 用户关联 失败",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("device_identifier", deviceIdentifier),
|
||||
logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infow("更新用户设备 用户关联 成功",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("device_identifier", deviceIdentifier))
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// 根据用户iD 删除 user 表中的记录
|
||||
func (l *BindEmailWithVerificationLogic) deleteUserRecordForEmailUser(userId int64) error {
|
||||
if err := l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error {
|
||||
// 删除用户记录
|
||||
if err := tx.Delete(&user.User{}, userId).Error; err != nil {
|
||||
l.Errorw("删除用户记录失败",
|
||||
logger.Field("user_id", userId),
|
||||
logger.Field("error", err.Error()))
|
||||
return err
|
||||
}
|
||||
|
||||
l.Infow("删除用户记录成功", logger.Field("user_id", userId))
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
@ -10,6 +10,7 @@ import (
|
||||
"github.com/perfect-panel/server/internal/types"
|
||||
"github.com/perfect-panel/server/pkg/constant"
|
||||
"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"
|
||||
@ -31,7 +32,9 @@ func NewUnbindDeviceLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Unbi
|
||||
}
|
||||
|
||||
func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
// 获取当前 token 登录的用户
|
||||
userInfo := l.ctx.Value(constant.CtxKeyUser).(*user.User)
|
||||
// 查询解绑设备是否存在
|
||||
device, err := l.svcCtx.UserModel.FindOneDevice(l.ctx, req.Id)
|
||||
if err != nil {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DeviceNotExist), "find device")
|
||||
@ -40,9 +43,11 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
if device.UserId != userInfo.Id {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.InvalidParams), "device not belong to user")
|
||||
}
|
||||
|
||||
return l.svcCtx.DB.Transaction(func(tx *gorm.DB) error {
|
||||
identifier := device.Identifier
|
||||
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 {
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.QueueEnqueueError), "find device err: %v", err)
|
||||
@ -72,6 +77,94 @@ func (l *UnbindDeviceLogic) UnbindDevice(req *types.UnbindDeviceRequest) error {
|
||||
sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId)
|
||||
_ = l.svcCtx.Redis.Del(l.ctx, sessionIdCacheKey).Err()
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
// 最后 创建一个 新的 设备 用户信息 绕过 赠送套餐
|
||||
l.registerUserAndDevice(identifier)
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (l *UnbindDeviceLogic) registerUserAndDevice(identifier string) (*user.User, error) {
|
||||
l.Infow("删除新建 设备 用户",
|
||||
logger.Field("identifier", identifier),
|
||||
)
|
||||
|
||||
var userInfo *user.User
|
||||
err := l.svcCtx.UserModel.Transaction(l.ctx, func(db *gorm.DB) error {
|
||||
// Create new user
|
||||
userInfo = &user.User{
|
||||
Salt: "default",
|
||||
OnlyFirstPurchase: &l.svcCtx.Config.Invite.OnlyFirstPurchase,
|
||||
}
|
||||
if err := db.Create(userInfo).Error; err != nil {
|
||||
l.Errorw("failed to create user",
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create user failed: %v", err)
|
||||
}
|
||||
|
||||
// Update refer code
|
||||
userInfo.ReferCode = uuidx.UserInviteCode(userInfo.Id)
|
||||
if err := db.Model(&user.User{}).Where("id = ?", userInfo.Id).Update("refer_code", userInfo.ReferCode).Error; err != nil {
|
||||
l.Errorw("failed to update refer code",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseUpdateError), "update refer code failed: %v", err)
|
||||
}
|
||||
|
||||
// Create device auth method
|
||||
authMethod := &user.AuthMethods{
|
||||
UserId: userInfo.Id,
|
||||
AuthType: "device",
|
||||
AuthIdentifier: identifier,
|
||||
Verified: true,
|
||||
}
|
||||
if err := db.Create(authMethod).Error; err != nil {
|
||||
l.Errorw("failed to create device auth method",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "create device auth method failed: %v", err)
|
||||
}
|
||||
|
||||
// Insert device record
|
||||
deviceInfo := &user.Device{
|
||||
Ip: "",
|
||||
UserId: userInfo.Id,
|
||||
UserAgent: "",
|
||||
Identifier: identifier,
|
||||
Enabled: true,
|
||||
Online: false,
|
||||
}
|
||||
if err := db.Create(deviceInfo).Error; err != nil {
|
||||
l.Errorw("failed to insert device",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return errors.Wrapf(xerr.NewErrCode(xerr.DatabaseInsertError), "insert device failed: %v", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
l.Errorw("device registration failed",
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("error", err.Error()),
|
||||
)
|
||||
return nil, err
|
||||
}
|
||||
|
||||
l.Infow("device registration completed successfully",
|
||||
logger.Field("user_id", userInfo.Id),
|
||||
logger.Field("identifier", identifier),
|
||||
logger.Field("refer_code", userInfo.ReferCode),
|
||||
)
|
||||
|
||||
return userInfo, nil
|
||||
}
|
||||
|
||||
@ -27,6 +27,8 @@ const (
|
||||
TelegramNotBound uint32 = 20007
|
||||
UserNotBindOauth uint32 = 20008
|
||||
InviteCodeError uint32 = 20009
|
||||
// 邮箱绑定错误
|
||||
EmailBindError uint32 = 20010
|
||||
)
|
||||
|
||||
// Node error
|
||||
|
||||
@ -33,6 +33,8 @@ func init() {
|
||||
TelegramNotBound: "Telegram not bound ",
|
||||
UserNotBindOauth: "User not bind oauth method",
|
||||
InviteCodeError: "Invite code error",
|
||||
// 邮箱绑定错误
|
||||
EmailBindError: "已经绑定该邮箱",
|
||||
|
||||
// Node error
|
||||
NodeExist: "Node already exists",
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user