2025-11-28 23:30:49 +08:00
|
|
|
|
package service
|
|
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
|
"carrotskin/internal/model"
|
|
|
|
|
|
"carrotskin/internal/repository"
|
|
|
|
|
|
"context"
|
|
|
|
|
|
"errors"
|
|
|
|
|
|
"fmt"
|
|
|
|
|
|
"strconv"
|
|
|
|
|
|
"time"
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
"github.com/google/uuid"
|
|
|
|
|
|
"github.com/jackc/pgx/v5"
|
|
|
|
|
|
"go.uber.org/zap"
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
// tokenService TokenService的实现
|
|
|
|
|
|
type tokenService struct {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
tokenRepo repository.TokenRepository
|
|
|
|
|
|
profileRepo repository.ProfileRepository
|
|
|
|
|
|
logger *zap.Logger
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// NewTokenService 创建TokenService实例
|
|
|
|
|
|
func NewTokenService(
|
|
|
|
|
|
tokenRepo repository.TokenRepository,
|
|
|
|
|
|
profileRepo repository.ProfileRepository,
|
|
|
|
|
|
logger *zap.Logger,
|
|
|
|
|
|
) TokenService {
|
2025-12-02 22:52:33 +08:00
|
|
|
|
return &tokenService{
|
2025-12-02 19:43:39 +08:00
|
|
|
|
tokenRepo: tokenRepo,
|
|
|
|
|
|
profileRepo: profileRepo,
|
|
|
|
|
|
logger: logger,
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
const (
|
2025-12-02 19:43:39 +08:00
|
|
|
|
tokenExtendedTimeout = 10 * time.Second
|
|
|
|
|
|
tokensMaxCount = 10
|
2025-11-28 23:30:49 +08:00
|
|
|
|
)
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
func (s *tokenService) Create(ctx context.Context, userID int64, UUID string, clientToken string) (*model.Profile, []*model.Profile, string, string, error) {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
var (
|
|
|
|
|
|
selectedProfileID *model.Profile
|
|
|
|
|
|
availableProfiles []*model.Profile
|
|
|
|
|
|
)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
// 设置超时上下文
|
2025-12-03 15:27:12 +08:00
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
|
|
// 验证用户存在
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if UUID != "" {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
_, err := s.profileRepo.FindByUUID(ctx, UUID)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户信息失败: %w", err)
|
|
|
|
|
|
}
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成令牌
|
|
|
|
|
|
if clientToken == "" {
|
|
|
|
|
|
clientToken = uuid.New().String()
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
accessToken := uuid.New().String()
|
|
|
|
|
|
token := model.Token{
|
|
|
|
|
|
AccessToken: accessToken,
|
|
|
|
|
|
ClientToken: clientToken,
|
2025-12-02 19:43:39 +08:00
|
|
|
|
UserID: userID,
|
2025-11-28 23:30:49 +08:00
|
|
|
|
Usable: true,
|
|
|
|
|
|
IssueDate: time.Now(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 获取用户配置文件
|
2025-12-03 15:27:12 +08:00
|
|
|
|
profiles, err := s.profileRepo.FindByUserID(ctx, userID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户配置文件失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 如果用户只有一个配置文件,自动选择
|
|
|
|
|
|
if len(profiles) == 1 {
|
|
|
|
|
|
selectedProfileID = profiles[0]
|
|
|
|
|
|
token.ProfileId = selectedProfileID.UUID
|
|
|
|
|
|
}
|
|
|
|
|
|
availableProfiles = profiles
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// 插入令牌
|
2025-12-03 15:27:12 +08:00
|
|
|
|
err = s.tokenRepo.Create(ctx, &token)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("创建Token失败: %w", err)
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 清理多余的令牌(使用独立的后台上下文)
|
|
|
|
|
|
go s.checkAndCleanupExcessTokens(context.Background(), userID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
|
|
|
|
|
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
func (s *tokenService) Validate(ctx context.Context, accessToken, clientToken string) bool {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 设置超时上下文
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if accessToken == "" {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
token, err := s.tokenRepo.FindByAccessToken(ctx, accessToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if !token.Usable {
|
|
|
|
|
|
return false
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
if clientToken == "" {
|
|
|
|
|
|
return true
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
return token.ClientToken == clientToken
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
func (s *tokenService) Refresh(ctx context.Context, accessToken, clientToken, selectedProfileID string) (string, string, error) {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 设置超时上下文
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if accessToken == "" {
|
|
|
|
|
|
return "", "", errors.New("accessToken不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 查找旧令牌
|
2025-12-03 15:27:12 +08:00
|
|
|
|
oldToken, err := s.tokenRepo.FindByAccessToken(ctx, accessToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
|
return "", "", errors.New("accessToken无效")
|
|
|
|
|
|
}
|
2025-12-02 19:43:39 +08:00
|
|
|
|
s.logger.Error("查询Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return "", "", fmt.Errorf("查询令牌失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 验证profile
|
|
|
|
|
|
if selectedProfileID != "" {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
valid, validErr := s.validateProfileByUserID(ctx, oldToken.UserID, selectedProfileID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if validErr != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
s.logger.Error("验证Profile失败",
|
2025-11-28 23:30:49 +08:00
|
|
|
|
zap.Error(err),
|
2025-12-02 19:43:39 +08:00
|
|
|
|
zap.Int64("userId", oldToken.UserID),
|
2025-11-28 23:30:49 +08:00
|
|
|
|
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 {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
selectedProfileID = oldToken.ProfileId
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
// 生成新令牌
|
|
|
|
|
|
newAccessToken := uuid.New().String()
|
|
|
|
|
|
newToken := model.Token{
|
|
|
|
|
|
AccessToken: newAccessToken,
|
2025-12-02 19:43:39 +08:00
|
|
|
|
ClientToken: oldToken.ClientToken,
|
2025-11-28 23:30:49 +08:00
|
|
|
|
UserID: oldToken.UserID,
|
|
|
|
|
|
Usable: true,
|
2025-12-02 19:43:39 +08:00
|
|
|
|
ProfileId: selectedProfileID,
|
2025-11-28 23:30:49 +08:00
|
|
|
|
IssueDate: time.Now(),
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// 先插入新令牌,再删除旧令牌
|
2025-12-03 15:27:12 +08:00
|
|
|
|
err = s.tokenRepo.Create(ctx, &newToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
s.logger.Error("创建新Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return "", "", fmt.Errorf("创建新Token失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
err = s.tokenRepo.DeleteByAccessToken(ctx, accessToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
s.logger.Warn("删除旧Token失败,但新Token已创建",
|
2025-11-28 23:30:49 +08:00
|
|
|
|
zap.Error(err),
|
|
|
|
|
|
zap.String("oldToken", oldToken.AccessToken),
|
|
|
|
|
|
zap.String("newToken", newAccessToken),
|
|
|
|
|
|
)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
s.logger.Info("成功刷新Token", zap.Int64("userId", oldToken.UserID), zap.String("accessToken", newAccessToken))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return newAccessToken, oldToken.ClientToken, nil
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
func (s *tokenService) Invalidate(ctx context.Context, accessToken string) {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 设置超时上下文
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if accessToken == "" {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
err := s.tokenRepo.DeleteByAccessToken(ctx, accessToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
s.logger.Error("删除Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
s.logger.Info("成功删除Token", zap.String("token", accessToken))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
func (s *tokenService) InvalidateUserTokens(ctx context.Context, userID int64) {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 设置超时上下文
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if userID == 0 {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
err := s.tokenRepo.DeleteByUserID(ctx, userID)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.logger.Error("删除用户Token失败", zap.Error(err), zap.Int64("userId", userID))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
s.logger.Info("成功删除用户Token", zap.Int64("userId", userID))
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
func (s *tokenService) GetUUIDByAccessToken(ctx context.Context, accessToken string) (string, error) {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 设置超时上下文
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
|
|
return s.tokenRepo.GetUUIDByAccessToken(ctx, accessToken)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 22:52:33 +08:00
|
|
|
|
func (s *tokenService) GetUserIDByAccessToken(ctx context.Context, accessToken string) (int64, error) {
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 设置超时上下文
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, DefaultTimeout)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
|
|
return s.tokenRepo.GetUserIDByAccessToken(ctx, accessToken)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
// 私有辅助方法
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
func (s *tokenService) checkAndCleanupExcessTokens(ctx context.Context, userID int64) {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if userID == 0 {
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
// 为清理操作设置更长的超时时间
|
|
|
|
|
|
ctx, cancel := context.WithTimeout(ctx, tokenExtendedTimeout)
|
|
|
|
|
|
defer cancel()
|
|
|
|
|
|
|
|
|
|
|
|
tokens, err := s.tokenRepo.GetByUserID(ctx, userID)
|
2025-11-28 23:30:49 +08:00
|
|
|
|
if err != nil {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
s.logger.Error("获取用户Token失败", zap.Error(err), zap.String("userId", strconv.FormatInt(userID, 10)))
|
2025-11-28 23:30:49 +08:00
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if len(tokens) <= tokensMaxCount {
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
|
|
tokensToDelete := make([]string, 0, len(tokens)-tokensMaxCount)
|
|
|
|
|
|
for i := tokensMaxCount; i < len(tokens); i++ {
|
|
|
|
|
|
tokensToDelete = append(tokensToDelete, tokens[i].AccessToken)
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
deletedCount, err := s.tokenRepo.BatchDelete(ctx, tokensToDelete)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
s.logger.Error("清理用户多余Token失败", zap.Error(err), zap.String("userId", strconv.FormatInt(userID, 10)))
|
|
|
|
|
|
return
|
|
|
|
|
|
}
|
2025-11-28 23:30:49 +08:00
|
|
|
|
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if deletedCount > 0 {
|
|
|
|
|
|
s.logger.Info("成功清理用户多余Token", zap.Int64("userId", userID), zap.Int64("count", deletedCount))
|
|
|
|
|
|
}
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
func (s *tokenService) validateProfileByUserID(ctx context.Context, userID int64, UUID string) (bool, error) {
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if userID == 0 || UUID == "" {
|
|
|
|
|
|
return false, errors.New("用户ID或配置文件ID不能为空")
|
|
|
|
|
|
}
|
|
|
|
|
|
|
2025-12-03 15:27:12 +08:00
|
|
|
|
profile, err := s.profileRepo.FindByUUID(ctx, UUID)
|
2025-12-02 19:43:39 +08:00
|
|
|
|
if err != nil {
|
|
|
|
|
|
if errors.Is(err, pgx.ErrNoRows) {
|
|
|
|
|
|
return false, errors.New("配置文件不存在")
|
|
|
|
|
|
}
|
|
|
|
|
|
return false, fmt.Errorf("验证配置文件失败: %w", err)
|
|
|
|
|
|
}
|
|
|
|
|
|
return profile.UserID == userID, nil
|
2025-11-28 23:30:49 +08:00
|
|
|
|
}
|