diff --git a/apis/auth/auth.api b/apis/auth/auth.api index dfe6642..5150827 100644 --- a/apis/auth/auth.api +++ b/apis/auth/auth.api @@ -51,6 +51,7 @@ type ( CfToken string `json:"cf_token,optional"` } EmailLoginRequest { + Identifier string `json:"identifier"` Email string `json:"email" validate:"required"` Code string `json:"code" validate:"required"` Invite string `json:"invite,optional"` diff --git a/internal/logic/auth/emailLoginLogic.go b/internal/logic/auth/emailLoginLogic.go index d259a9e..60eaa7d 100644 --- a/internal/logic/auth/emailLoginLogic.go +++ b/internal/logic/auth/emailLoginLogic.go @@ -200,6 +200,28 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types. ) } + // Bind device to user if identifier is provided + var deviceId int64 + if req.Identifier != "" { + bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) + if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { + var ce *xerr.CodeError + if errors.As(err, &ce) && ce.GetErrCode() == xerr.DeviceBindLimitExceeded { + return nil, ce + } + l.Errorw("failed to bind device to user", + logger.Field("user_id", userInfo.Id), + logger.Field("identifier", req.Identifier), + logger.Field("error", err.Error()), + ) + } else { + // Query device info to get DeviceId + if device, dErr := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier); dErr == nil { + deviceId = device.Id + } + } + } + // Login (Generate Token) if l.ctx.Value(constant.LoginType) != nil { req.LoginType = l.ctx.Value(constant.LoginType).(string) @@ -213,6 +235,7 @@ func (l *EmailLoginLogic) EmailLogin(req *types.EmailLoginRequest) (resp *types. jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), jwt.WithOption("LoginType", req.LoginType), + jwt.WithOption("DeviceId", deviceId), ) if err != nil { return nil, errors.Wrapf(xerr.NewErrCode(xerr.ERROR), "token generate error: %v", err.Error()) diff --git a/internal/logic/auth/userLoginLogic.go b/internal/logic/auth/userLoginLogic.go index d63060f..9d6eb16 100644 --- a/internal/logic/auth/userLoginLogic.go +++ b/internal/logic/auth/userLoginLogic.go @@ -92,6 +92,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log } // Bind device to user if identifier is provided + var deviceId int64 if req.Identifier != "" { bindLogic := NewBindDeviceLogic(l.ctx, l.svcCtx) if err := bindLogic.BindDeviceToUser(req.Identifier, req.IP, req.UserAgent, userInfo.Id); err != nil { @@ -104,6 +105,11 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log logger.Field("identifier", req.Identifier), logger.Field("error", err.Error()), ) + } else { + // Query device info to get DeviceId + if device, dErr := l.svcCtx.UserModel.FindOneDeviceByIdentifier(l.ctx, req.Identifier); dErr == nil { + deviceId = device.Id + } } } if l.ctx.Value(constant.LoginType) != nil { @@ -119,6 +125,7 @@ func (l *UserLoginLogic) UserLogin(req *types.UserLoginRequest) (resp *types.Log jwt.WithOption("UserId", userInfo.Id), jwt.WithOption("SessionId", sessionId), jwt.WithOption("LoginType", req.LoginType), + jwt.WithOption("DeviceId", deviceId), ) if err != nil { l.Logger.Error("[UserLogin] token generate error", logger.Field("error", err.Error())) diff --git a/internal/logic/public/user/deleteAccountLogic.go b/internal/logic/public/user/deleteAccountLogic.go index 6d7d080..4fa5cb5 100644 --- a/internal/logic/public/user/deleteAccountLogic.go +++ b/internal/logic/public/user/deleteAccountLogic.go @@ -33,11 +33,7 @@ func NewDeleteAccountLogic(ctx context.Context, svcCtx *svc.ServiceContext) *Del } } -// DeleteAccount 注销账号逻辑 -// 1. 获取当前用户信息 -// 2. 删除所有关联数据(用户、认证方式、设备) -// 3. 根据原设备信息创建全新账号 -// 4. 返回新账号信息 +// DeleteAccount 注销当前设备账号逻辑 (改为精准解绑) func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, err error) { // 获取当前用户 currentUser, ok := l.ctx.Value(constant.CtxKeyUser).(*user.User) @@ -45,48 +41,74 @@ func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, return nil, errors.Wrapf(xerr.NewErrCode(xerr.InvalidAccess), "Invalid Access") } + // 获取当前调用设备 ID + currentDeviceId, _ := l.ctx.Value(constant.CtxKeyDeviceID).(int64) + resp = &types.DeleteAccountResponse{} var newUserId int64 + // 如果没有识别到设备 ID (可能是旧版 Token),则执行安全注销:仅清除 Session + if currentDeviceId == 0 { + l.Infow("未识别到设备 ID,仅清理当前会话", logger.Field("user_id", currentUser.Id)) + l.clearCurrentSession(currentUser.Id) + resp.Success = true + resp.Message = "会话已清除" + return resp, nil + } + // 开始数据库事务 err = l.svcCtx.UserModel.Transaction(l.ctx, func(tx *gorm.DB) error { - // 1. 查找用户的所有设备(用于后续创建新账号) - devices, _, err := l.svcCtx.UserModel.QueryDeviceList(l.ctx, currentUser.Id) - if err != nil { - return errors.Wrap(err, "查询用户设备失败") + // 1. 查找当前设备 + var currentDevice user.Device + if err := tx.Where("id = ? AND user_id = ?", currentDeviceId, currentUser.Id).First(¤tDevice).Error; err != nil { + l.Infow("当前请求设备记录不存在或归属不匹配", logger.Field("device_id", currentDeviceId), logger.Field("error", err.Error())) + return nil // 不抛错,直接走清理 Session 流程 } - // 2. 删除用户的所有认证方式 - err = tx.Model(&user.AuthMethods{}).Where("`user_id` = ?", currentUser.Id).Delete(&user.AuthMethods{}).Error - if err != nil { - return errors.Wrap(err, "删除用户认证方式失败") - } + // 2. 检查用户是否有其他认证方式 (如邮箱) 或 其他设备 + var authMethodsCount int64 + tx.Model(&user.AuthMethods{}).Where("user_id = ?", currentUser.Id).Count(&authMethodsCount) - // 3. 删除用户的所有设备 - err = tx.Model(&user.Device{}).Where("`user_id` = ?", currentUser.Id).Delete(&user.Device{}).Error - if err != nil { - return errors.Wrap(err, "删除用户设备失败") - } + var devicesCount int64 + tx.Model(&user.Device{}).Where("user_id = ?", currentUser.Id).Count(&devicesCount) - // 4. 删除用户的订阅信息 - err = tx.Model(&user.Subscribe{}).Where("`user_id` = ?", currentUser.Id).Delete(&user.Subscribe{}).Error - if err != nil { - return errors.Wrap(err, "删除用户订阅信息失败") - } + // 判定是否是主账号解绑:如果除了当前设备外,还有邮箱或其他设备,则只解绑当前设备 + isMainAccount := authMethodsCount > 1 || devicesCount > 1 - // 5. 删除用户本身 - err = tx.Model(&user.User{}).Where("`id` = ?", currentUser.Id).Delete(&user.User{}).Error - if err != nil { - return errors.Wrap(err, "删除用户失败") - } + if isMainAccount { + l.Infow("主账号解绑,仅迁移当前设备", logger.Field("user_id", currentUser.Id), logger.Field("device_id", currentDeviceId)) - // 7. 为每个原设备创建新的用户(使用同一事务) - for _, oldDevice := range devices { - userInfo, err := l.registerUserAndDevice(tx, oldDevice.Identifier, oldDevice.Ip, oldDevice.UserAgent) + // 为当前设备创建新用户并迁移 + newUser, err := l.registerUserAndDevice(tx, currentDevice.Identifier, currentDevice.Ip, currentDevice.UserAgent) if err != nil { - return errors.Wrap(err, "创建新用户失败") + return err } - newUserId = userInfo.Id // 保留最后一个新用户ID + newUserId = newUser.Id + + // 从原用户删除当前设备的认证方式 (按 Identifier 准确删除) + if err := tx.Where("user_id = ? AND auth_type = ? AND auth_identifier = ?", currentUser.Id, "device", currentDevice.Identifier).Delete(&user.AuthMethods{}).Error; err != nil { + return errors.Wrap(err, "删除原设备认证失败") + } + + // 从原用户删除当前设备记录 + if err := tx.Where("id = ?", currentDeviceId).Delete(&user.Device{}).Error; err != nil { + return errors.Wrap(err, "删除原设备记录失败") + } + } else { + l.Infow("纯设备账号注销,执行物理删除并重置", logger.Field("user_id", currentUser.Id), logger.Field("device_id", currentDeviceId)) + + // 完全删除原用户相关资产 + tx.Where("user_id = ?", currentUser.Id).Delete(&user.AuthMethods{}) + tx.Where("user_id = ?", currentUser.Id).Delete(&user.Device{}) + tx.Where("user_id = ?", currentUser.Id).Delete(&user.Subscribe{}) + tx.Delete(&user.User{}, currentUser.Id) + + // 重新注册一个新用户 + newUser, err := l.registerUserAndDevice(tx, currentDevice.Identifier, currentDevice.Ip, currentDevice.UserAgent) + if err != nil { + return err + } + newUserId = newUser.Id } return nil @@ -96,23 +118,28 @@ func (l *DeleteAccountLogic) DeleteAccount() (resp *types.DeleteAccountResponse, return nil, err } - // 注销当前会话token(删除Redis中的会话标记,并从用户会话集合移除) - if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" { - sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) - _ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err() - // 从用户会话集合中移除当前session,避免残留 - sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, currentUser.Id) - _ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err() - } + // 最终清理当前 Session + l.clearCurrentSession(currentUser.Id) resp.Success = true - resp.Message = "账户注销成功" + resp.Message = "注销成功" resp.UserId = newUserId resp.Code = 200 return resp, nil } +// clearCurrentSession 清理当前请求的会话 +func (l *DeleteAccountLogic) clearCurrentSession(userId int64) { + if sessionId, ok := l.ctx.Value(constant.CtxKeySessionID).(string); ok && sessionId != "" { + sessionKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) + _ = l.svcCtx.Redis.Del(l.ctx, sessionKey).Err() + // 从用户会话集合中移除当前session + sessionsKey := fmt.Sprintf("%s%v", config.UserSessionsKeyPrefix, userId) + _ = l.svcCtx.Redis.ZRem(l.ctx, sessionsKey, sessionId).Err() + } +} + // generateReferCode 生成推荐码 func generateReferCode() string { bytes := make([]byte, 4) diff --git a/internal/middleware/authMiddleware.go b/internal/middleware/authMiddleware.go index 8c8086b..34625e3 100644 --- a/internal/middleware/authMiddleware.go +++ b/internal/middleware/authMiddleware.go @@ -50,6 +50,11 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { userId := int64(claims["UserId"].(float64)) // get session id from token sessionId := claims["SessionId"].(string) + // get device id from token + var deviceId int64 + if claims["DeviceId"] != nil { + deviceId = int64(claims["DeviceId"].(float64)) + } // get session id from redis sessionIdCacheKey := fmt.Sprintf("%v:%v", config.SessionIdKey, sessionId) value, err := svc.Redis.Get(c, sessionIdCacheKey).Result() @@ -91,6 +96,9 @@ func AuthMiddleware(svc *svc.ServiceContext) func(c *gin.Context) { ctx = context.WithValue(ctx, constant.LoginType, loginType) ctx = context.WithValue(ctx, constant.CtxKeyUser, userInfo) ctx = context.WithValue(ctx, constant.CtxKeySessionID, sessionId) + if deviceId > 0 { + ctx = context.WithValue(ctx, constant.CtxKeyDeviceID, deviceId) + } c.Request = c.Request.WithContext(ctx) c.Next() } diff --git a/internal/types/types.go b/internal/types/types.go index e235d4c..870b6ff 100644 --- a/internal/types/types.go +++ b/internal/types/types.go @@ -596,13 +596,14 @@ type EmailAuthticateConfig struct { } type EmailLoginRequest struct { - Email string `json:"email" validate:"required"` - Code string `json:"code" validate:"required"` - Invite string `json:"invite,optional"` - IP string `header:"X-Original-Forwarded-For"` - UserAgent string `header:"User-Agent"` - LoginType string `header:"Login-Type"` - CfToken string `json:"cf_token,optional"` + Identifier string `json:"identifier"` + Email string `json:"email" validate:"required"` + Code string `json:"code" validate:"required"` + Invite string `json:"invite,optional"` + IP string `header:"X-Original-Forwarded-For"` + UserAgent string `header:"User-Agent"` + LoginType string `header:"Login-Type"` + CfToken string `json:"cf_token,optional"` } type FilterBalanceLogRequest struct { diff --git a/pkg/constant/context.go b/pkg/constant/context.go index 759936d..051fc77 100644 --- a/pkg/constant/context.go +++ b/pkg/constant/context.go @@ -9,5 +9,6 @@ const ( CtxKeyPlatform CtxKey = "platform" CtxKeyPayment CtxKey = "payment" LoginType CtxKey = "loginType" + CtxKeyDeviceID CtxKey = "deviceId" CtxKeyIncludeExpired CtxKey = "includeExpired" ) diff --git a/server b/server new file mode 100755 index 0000000..8251e23 Binary files /dev/null and b/server differ