- Updated main.go to initialize email service and include it in the dependency injection container. - Refactored handlers to utilize context in service method calls, improving consistency and error handling. - Introduced new service options for upload, security, and captcha services, enhancing modularity and testability. - Removed unused repository implementations to streamline the codebase. This commit continues the effort to improve the architecture by ensuring all services are properly injected and utilized across the application.
278 lines
7.3 KiB
Go
278 lines
7.3 KiB
Go
package service
|
||
|
||
import (
|
||
"carrotskin/internal/model"
|
||
"carrotskin/internal/repository"
|
||
"context"
|
||
"errors"
|
||
"fmt"
|
||
"strconv"
|
||
"time"
|
||
|
||
"github.com/google/uuid"
|
||
"github.com/jackc/pgx/v5"
|
||
"go.uber.org/zap"
|
||
)
|
||
|
||
// tokenService TokenService的实现
|
||
type tokenService struct {
|
||
tokenRepo repository.TokenRepository
|
||
profileRepo repository.ProfileRepository
|
||
logger *zap.Logger
|
||
}
|
||
|
||
// NewTokenService 创建TokenService实例
|
||
func NewTokenService(
|
||
tokenRepo repository.TokenRepository,
|
||
profileRepo repository.ProfileRepository,
|
||
logger *zap.Logger,
|
||
) TokenService {
|
||
return &tokenService{
|
||
tokenRepo: tokenRepo,
|
||
profileRepo: profileRepo,
|
||
logger: logger,
|
||
}
|
||
}
|
||
|
||
const (
|
||
tokenExtendedTimeout = 10 * time.Second
|
||
tokensMaxCount = 10
|
||
)
|
||
|
||
func (s *tokenService) Create(ctx context.Context, 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()
|
||
|
||
// 验证用户存在
|
||
if UUID != "" {
|
||
_, err := s.profileRepo.FindByUUID(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 := s.profileRepo.FindByUserID(userID)
|
||
if err != nil {
|
||
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("获取用户配置文件失败: %w", err)
|
||
}
|
||
|
||
// 如果用户只有一个配置文件,自动选择
|
||
if len(profiles) == 1 {
|
||
selectedProfileID = profiles[0]
|
||
token.ProfileId = selectedProfileID.UUID
|
||
}
|
||
availableProfiles = profiles
|
||
|
||
// 插入令牌
|
||
err = s.tokenRepo.Create(&token)
|
||
if err != nil {
|
||
return selectedProfileID, availableProfiles, "", "", fmt.Errorf("创建Token失败: %w", err)
|
||
}
|
||
|
||
// 清理多余的令牌
|
||
go s.checkAndCleanupExcessTokens(userID)
|
||
|
||
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
|
||
}
|
||
|
||
func (s *tokenService) Validate(ctx context.Context, accessToken, clientToken string) bool {
|
||
if accessToken == "" {
|
||
return false
|
||
}
|
||
|
||
token, err := s.tokenRepo.FindByAccessToken(accessToken)
|
||
if err != nil {
|
||
return false
|
||
}
|
||
|
||
if !token.Usable {
|
||
return false
|
||
}
|
||
|
||
if clientToken == "" {
|
||
return true
|
||
}
|
||
|
||
return token.ClientToken == clientToken
|
||
}
|
||
|
||
func (s *tokenService) Refresh(ctx context.Context, accessToken, clientToken, selectedProfileID string) (string, string, error) {
|
||
if accessToken == "" {
|
||
return "", "", errors.New("accessToken不能为空")
|
||
}
|
||
|
||
// 查找旧令牌
|
||
oldToken, err := s.tokenRepo.FindByAccessToken(accessToken)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return "", "", errors.New("accessToken无效")
|
||
}
|
||
s.logger.Error("查询Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
||
return "", "", fmt.Errorf("查询令牌失败: %w", err)
|
||
}
|
||
|
||
// 验证profile
|
||
if selectedProfileID != "" {
|
||
valid, validErr := s.validateProfileByUserID(oldToken.UserID, selectedProfileID)
|
||
if validErr != nil {
|
||
s.logger.Error("验证Profile失败",
|
||
zap.Error(err),
|
||
zap.Int64("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,
|
||
UserID: oldToken.UserID,
|
||
Usable: true,
|
||
ProfileId: selectedProfileID,
|
||
IssueDate: time.Now(),
|
||
}
|
||
|
||
// 先插入新令牌,再删除旧令牌
|
||
err = s.tokenRepo.Create(&newToken)
|
||
if err != nil {
|
||
s.logger.Error("创建新Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
||
return "", "", fmt.Errorf("创建新Token失败: %w", err)
|
||
}
|
||
|
||
err = s.tokenRepo.DeleteByAccessToken(accessToken)
|
||
if err != nil {
|
||
s.logger.Warn("删除旧Token失败,但新Token已创建",
|
||
zap.Error(err),
|
||
zap.String("oldToken", oldToken.AccessToken),
|
||
zap.String("newToken", newAccessToken),
|
||
)
|
||
}
|
||
|
||
s.logger.Info("成功刷新Token", zap.Int64("userId", oldToken.UserID), zap.String("accessToken", newAccessToken))
|
||
return newAccessToken, oldToken.ClientToken, nil
|
||
}
|
||
|
||
func (s *tokenService) Invalidate(ctx context.Context, accessToken string) {
|
||
if accessToken == "" {
|
||
return
|
||
}
|
||
|
||
err := s.tokenRepo.DeleteByAccessToken(accessToken)
|
||
if err != nil {
|
||
s.logger.Error("删除Token失败", zap.Error(err), zap.String("accessToken", accessToken))
|
||
return
|
||
}
|
||
s.logger.Info("成功删除Token", zap.String("token", accessToken))
|
||
}
|
||
|
||
func (s *tokenService) InvalidateUserTokens(ctx context.Context, userID int64) {
|
||
if userID == 0 {
|
||
return
|
||
}
|
||
|
||
err := s.tokenRepo.DeleteByUserID(userID)
|
||
if err != nil {
|
||
s.logger.Error("删除用户Token失败", zap.Error(err), zap.Int64("userId", userID))
|
||
return
|
||
}
|
||
|
||
s.logger.Info("成功删除用户Token", zap.Int64("userId", userID))
|
||
}
|
||
|
||
func (s *tokenService) GetUUIDByAccessToken(ctx context.Context, accessToken string) (string, error) {
|
||
return s.tokenRepo.GetUUIDByAccessToken(accessToken)
|
||
}
|
||
|
||
func (s *tokenService) GetUserIDByAccessToken(ctx context.Context, accessToken string) (int64, error) {
|
||
return s.tokenRepo.GetUserIDByAccessToken(accessToken)
|
||
}
|
||
|
||
// 私有辅助方法
|
||
|
||
func (s *tokenService) checkAndCleanupExcessTokens(userID int64) {
|
||
if userID == 0 {
|
||
return
|
||
}
|
||
|
||
tokens, err := s.tokenRepo.GetByUserID(userID)
|
||
if err != nil {
|
||
s.logger.Error("获取用户Token失败", zap.Error(err), zap.String("userId", strconv.FormatInt(userID, 10)))
|
||
return
|
||
}
|
||
|
||
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)
|
||
}
|
||
|
||
deletedCount, err := s.tokenRepo.BatchDelete(tokensToDelete)
|
||
if err != nil {
|
||
s.logger.Error("清理用户多余Token失败", zap.Error(err), zap.String("userId", strconv.FormatInt(userID, 10)))
|
||
return
|
||
}
|
||
|
||
if deletedCount > 0 {
|
||
s.logger.Info("成功清理用户多余Token", zap.Int64("userId", userID), zap.Int64("count", deletedCount))
|
||
}
|
||
}
|
||
|
||
func (s *tokenService) validateProfileByUserID(userID int64, UUID string) (bool, error) {
|
||
if userID == 0 || UUID == "" {
|
||
return false, errors.New("用户ID或配置文件ID不能为空")
|
||
}
|
||
|
||
profile, err := s.profileRepo.FindByUUID(UUID)
|
||
if err != nil {
|
||
if errors.Is(err, pgx.ErrNoRows) {
|
||
return false, errors.New("配置文件不存在")
|
||
}
|
||
return false, fmt.Errorf("验证配置文件失败: %w", err)
|
||
}
|
||
return profile.UserID == userID, nil
|
||
}
|