package service import ( "carrotskin/internal/model" "carrotskin/internal/repository" "context" "errors" "fmt" "github.com/google/uuid" "github.com/jackc/pgx/v5" "go.uber.org/zap" "strconv" "time" "gorm.io/gorm" ) // 常量定义 const ( ExtendedTimeout = 10 * time.Second TokensMaxCount = 10 // 用户最多保留的token数量 ) // NewToken 创建新令牌 func NewToken(db *gorm.DB, logger *zap.Logger, userId int64, UUID string, clientToken string) (*model.Profile, []*model.Profile, string, string, error) { var ( selectedProfileID *model.Profile availableProfiles []*model.Profile ) // 设置超时上下文 _, cancel := context.WithTimeout(context.Background(), DefaultTimeout) defer cancel() // 验证用户存在 _, err := repository.FindProfileByUUID(UUID) if err != nil { return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户信息失败: %w", err) } // 生成令牌 if clientToken == "" { clientToken = uuid.New().String() } accessToken := uuid.New().String() token := model.Token{ AccessToken: accessToken, ClientToken: clientToken, UserID: userId, Usable: true, IssueDate: time.Now(), } // 获取用户配置文件 profiles, err := repository.FindProfilesByUserID(userId) if err != nil { return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户配置文件失败: %w", err) } // 如果用户只有一个配置文件,自动选择 if len(profiles) == 1 { selectedProfileID = profiles[0] token.ProfileId = selectedProfileID.UUID } availableProfiles = profiles // 插入令牌到tokens集合 _, insertCancel := context.WithTimeout(context.Background(), DefaultTimeout) defer insertCancel() err = repository.CreateToken(&token) if err != nil { return selectedProfileID, availableProfiles, "", "", fmt.Errorf("创建Token失败: %w", err) } // 清理多余的令牌 go CheckAndCleanupExcessTokens(db, logger, userId) return selectedProfileID, availableProfiles, accessToken, clientToken, nil } // CheckAndCleanupExcessTokens 检查并清理用户多余的令牌,只保留最新的10个 func CheckAndCleanupExcessTokens(db *gorm.DB, logger *zap.Logger, userId int64) { if userId == 0 { return } // 获取用户所有令牌,按发行日期降序排序 tokens, err := repository.GetTokensByUserId(userId) if err != nil { logger.Error("[ERROR] 获取用户Token失败: ", zap.Error(err), zap.String("userId", strconv.FormatInt(userId, 10))) return } // 如果令牌数量不超过上限,无需清理 if len(tokens) <= TokensMaxCount { return } // 获取需要删除的令牌ID列表 tokensToDelete := make([]string, 0, len(tokens)-TokensMaxCount) for i := TokensMaxCount; i < len(tokens); i++ { tokensToDelete = append(tokensToDelete, tokens[i].AccessToken) } // 执行批量删除,传入上下文和待删除的令牌列表(作为切片参数) DeletedCount, err := repository.BatchDeleteTokens(tokensToDelete) if err != nil { logger.Error("[ERROR] 清理用户多余Token失败: ", zap.Error(err), zap.String("userId", strconv.FormatInt(userId, 10))) return } if DeletedCount > 0 { logger.Info("[INFO] 成功清理用户多余Token", zap.Any("userId:", userId), zap.Any("count:", DeletedCount)) } } // ValidToken 验证令牌有效性 func ValidToken(db *gorm.DB, accessToken string, clientToken string) bool { if accessToken == "" { return false } // 使用投影只获取需要的字段 var token *model.Token token, err := repository.FindTokenByID(accessToken) if err != nil { return false } if !token.Usable { return false } // 如果客户端令牌为空,只验证访问令牌 if clientToken == "" { return true } // 否则验证客户端令牌是否匹配 return token.ClientToken == clientToken } func GetUUIDByAccessToken(db *gorm.DB, accessToken string) (string, error) { return repository.GetUUIDByAccessToken(accessToken) } func GetUserIDByAccessToken(db *gorm.DB, accessToken string) (int64, error) { return repository.GetUserIDByAccessToken(accessToken) } // RefreshToken 刷新令牌 func RefreshToken(db *gorm.DB, logger *zap.Logger, accessToken, clientToken string, selectedProfileID string) (string, string, error) { if accessToken == "" { return "", "", errors.New("accessToken不能为空") } // 查找旧令牌 oldToken, err := repository.GetTokenByAccessToken(accessToken) if err != nil { if errors.Is(err, pgx.ErrNoRows) { return "", "", errors.New("accessToken无效") } logger.Error("[ERROR] 查询Token失败: ", zap.Error(err), zap.Any("accessToken:", accessToken)) return "", "", fmt.Errorf("查询令牌失败: %w", err) } // 验证profile if selectedProfileID != "" { valid, validErr := ValidateProfileByUserID(db, oldToken.UserID, selectedProfileID) if validErr != nil { logger.Error( "验证Profile失败", zap.Error(err), zap.Any("userId", oldToken.UserID), zap.String("profileId", selectedProfileID), ) return "", "", fmt.Errorf("验证角色失败: %w", err) } if !valid { return "", "", errors.New("角色与用户不匹配") } } // 检查 clientToken 是否有效 if clientToken != "" && clientToken != oldToken.ClientToken { return "", "", errors.New("clientToken无效") } // 检查 selectedProfileID 的逻辑 if selectedProfileID != "" { if oldToken.ProfileId != "" && oldToken.ProfileId != selectedProfileID { return "", "", errors.New("原令牌已绑定角色,无法选择新角色") } } else { selectedProfileID = oldToken.ProfileId // 如果未指定,则保持原角色 } // 生成新令牌 newAccessToken := uuid.New().String() newToken := model.Token{ AccessToken: newAccessToken, ClientToken: oldToken.ClientToken, // 新令牌的 clientToken 与原令牌相同 UserID: oldToken.UserID, Usable: true, ProfileId: selectedProfileID, // 绑定到指定角色或保持原角色 IssueDate: time.Now(), } // 使用双重写入模式替代事务,先插入新令牌,再删除旧令牌 err = repository.CreateToken(&newToken) if err != nil { logger.Error( "创建新Token失败", zap.Error(err), zap.String("accessToken", accessToken), ) return "", "", fmt.Errorf("创建新Token失败: %w", err) } err = repository.DeleteTokenByAccessToken(accessToken) if err != nil { // 删除旧令牌失败,记录日志但不阻止操作,因为新令牌已成功创建 logger.Warn( "删除旧Token失败,但新Token已创建", zap.Error(err), zap.String("oldToken", oldToken.AccessToken), zap.String("newToken", newAccessToken), ) } logger.Info( "成功刷新Token", zap.Any("userId", oldToken.UserID), zap.String("accessToken", newAccessToken), ) return newAccessToken, oldToken.ClientToken, nil } // InvalidToken 使令牌失效 func InvalidToken(db *gorm.DB, logger *zap.Logger, accessToken string) { if accessToken == "" { return } err := repository.DeleteTokenByAccessToken(accessToken) if err != nil { logger.Error( "删除Token失败", zap.Error(err), zap.String("accessToken", accessToken), ) return } logger.Info("[INFO] 成功删除", zap.Any("Token:", accessToken)) } // InvalidUserTokens 使用户所有令牌失效 func InvalidUserTokens(db *gorm.DB, logger *zap.Logger, userId int64) { if userId == 0 { return } err := repository.DeleteTokenByUserId(userId) if err != nil { logger.Error( "[ERROR]删除用户Token失败", zap.Error(err), zap.Any("userId", userId), ) return } logger.Info("[INFO] 成功删除用户Token", zap.Any("userId:", userId)) }