fix
This commit is contained in:
549
backend-old/internal/service/CLAUDE.md
Normal file
549
backend-old/internal/service/CLAUDE.md
Normal file
@ -0,0 +1,549 @@
|
||||
# Service Layer - CLAUDE.md
|
||||
|
||||
本文件为 Claude Code 在业务逻辑层中工作时提供指导。
|
||||
|
||||
## 🎯 模块概览
|
||||
|
||||
Service 层是业务逻辑的核心,负责处理业务规则、数据转换和服务编排。
|
||||
|
||||
### 主要职责
|
||||
- 📋 业务逻辑处理和规则验证
|
||||
- 🔄 数据转换和格式化
|
||||
- 🧩 服务编排和组合
|
||||
- 🚀 事务管理和数据一致性
|
||||
- 🔐 业务权限控制
|
||||
- 📊 性能优化和缓存策略
|
||||
|
||||
## 📁 模块结构
|
||||
|
||||
```
|
||||
internal/service/
|
||||
├── CLAUDE.md # 📋 当前文件 - 业务逻辑开发指导
|
||||
├── interfaces.go # 🔗 服务接口定义
|
||||
├── auth/ # 🔐 认证服务
|
||||
│ ├── auth_service.go # 认证业务逻辑
|
||||
│ ├── jwt_service.go # JWT 令牌服务
|
||||
│ └── auth_service_test.go # 认证服务测试
|
||||
├── user/ # 👤 用户服务
|
||||
│ ├── user_service.go # 用户业务逻辑
|
||||
│ ├── user_validator.go # 用户数据验证
|
||||
│ └── user_service_test.go # 用户服务测试
|
||||
├── photo/ # 📸 照片服务
|
||||
│ ├── photo_service.go # 照片业务逻辑
|
||||
│ ├── photo_processor.go # 照片处理器
|
||||
│ └── photo_service_test.go # 照片服务测试
|
||||
├── category/ # 📂 分类服务
|
||||
│ ├── category_service.go # 分类业务逻辑
|
||||
│ └── category_service_test.go # 分类服务测试
|
||||
└── storage/ # 📁 存储服务
|
||||
├── storage_service.go # 存储业务逻辑
|
||||
├── local_storage.go # 本地存储实现
|
||||
└── s3_storage.go # S3 存储实现
|
||||
```
|
||||
|
||||
## 🔧 Go 风格服务设计
|
||||
|
||||
### 接口定义规范
|
||||
```go
|
||||
// interfaces.go - 服务接口定义
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"mime/multipart"
|
||||
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// UserServicer 用户服务接口
|
||||
type UserServicer interface {
|
||||
// 用户管理
|
||||
CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error)
|
||||
GetUser(ctx context.Context, id uint) (*entity.User, error)
|
||||
GetUserByEmail(ctx context.Context, email string) (*entity.User, error)
|
||||
UpdateUser(ctx context.Context, id uint, req *dto.UpdateUserRequest) (*entity.User, error)
|
||||
DeleteUser(ctx context.Context, id uint) error
|
||||
ListUsers(ctx context.Context, opts *dto.ListUsersOptions) ([]*entity.User, int64, error)
|
||||
|
||||
// 用户验证
|
||||
ValidateUser(ctx context.Context, email, password string) (*entity.User, error)
|
||||
ChangePassword(ctx context.Context, id uint, req *dto.ChangePasswordRequest) error
|
||||
}
|
||||
|
||||
// PhotoServicer 照片服务接口
|
||||
type PhotoServicer interface {
|
||||
// 照片管理
|
||||
CreatePhoto(ctx context.Context, req *dto.CreatePhotoRequest) (*entity.Photo, error)
|
||||
GetPhoto(ctx context.Context, id uint) (*entity.Photo, error)
|
||||
UpdatePhoto(ctx context.Context, id uint, req *dto.UpdatePhotoRequest) (*entity.Photo, error)
|
||||
DeletePhoto(ctx context.Context, id uint) error
|
||||
ListPhotos(ctx context.Context, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error)
|
||||
|
||||
// 照片处理
|
||||
UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error)
|
||||
ProcessPhoto(ctx context.Context, photoID uint, opts *dto.ProcessPhotoOptions) error
|
||||
|
||||
// 照片查询
|
||||
GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error)
|
||||
GetPhotosByCategory(ctx context.Context, categoryID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error)
|
||||
SearchPhotos(ctx context.Context, query string, opts *dto.SearchPhotosOptions) ([]*entity.Photo, int64, error)
|
||||
}
|
||||
|
||||
// AuthServicer 认证服务接口
|
||||
type AuthServicer interface {
|
||||
// 认证
|
||||
Login(ctx context.Context, req *dto.LoginRequest) (*dto.LoginResponse, error)
|
||||
Register(ctx context.Context, req *dto.RegisterRequest) (*dto.RegisterResponse, error)
|
||||
RefreshToken(ctx context.Context, refreshToken string) (*dto.TokenResponse, error)
|
||||
Logout(ctx context.Context, token string) error
|
||||
|
||||
// 令牌管理
|
||||
ValidateToken(ctx context.Context, token string) (*dto.TokenClaims, error)
|
||||
RevokeToken(ctx context.Context, token string) error
|
||||
}
|
||||
```
|
||||
|
||||
### 服务实现规范
|
||||
```go
|
||||
// user/user_service.go - 用户服务实现
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository"
|
||||
"photography-backend/pkg/logger"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
userRepo repository.UserRepositoryr
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
func NewUserService(userRepo repository.UserRepositoryr, logger logger.Logger) *UserService {
|
||||
return &UserService{
|
||||
userRepo: userRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *UserService) CreateUser(ctx context.Context, req *dto.CreateUserRequest) (*entity.User, error) {
|
||||
// 1. 验证输入
|
||||
if err := s.validateCreateUserRequest(req); err != nil {
|
||||
s.logger.Error("invalid create user request", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 检查用户是否存在
|
||||
existingUser, err := s.userRepo.GetByEmail(ctx, req.Email)
|
||||
if err != nil && !errors.Is(err, repository.ErrNotFound) {
|
||||
s.logger.Error("failed to check existing user", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, errors.New("user already exists")
|
||||
}
|
||||
|
||||
// 3. 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to hash password", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 4. 创建用户实体
|
||||
user := &entity.User{
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Password: string(hashedPassword),
|
||||
Role: entity.UserRoleUser,
|
||||
Status: entity.UserStatusActive,
|
||||
}
|
||||
|
||||
// 5. 保存用户
|
||||
createdUser, err := s.userRepo.Create(ctx, user)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to create user", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("user created successfully",
|
||||
zap.String("user_id", fmt.Sprintf("%d", createdUser.ID)),
|
||||
zap.String("email", createdUser.Email))
|
||||
|
||||
return createdUser, nil
|
||||
}
|
||||
|
||||
func (s *UserService) GetUser(ctx context.Context, id uint) (*entity.User, error) {
|
||||
user, err := s.userRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", id))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) ValidateUser(ctx context.Context, email, password string) (*entity.User, error) {
|
||||
// 1. 获取用户
|
||||
user, err := s.userRepo.GetByEmail(ctx, email)
|
||||
if err != nil {
|
||||
if errors.Is(err, repository.ErrNotFound) {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
s.logger.Error("failed to get user by email", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 验证密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
// 3. 检查用户状态
|
||||
if user.Status != entity.UserStatusActive {
|
||||
return nil, errors.New("user account is not active")
|
||||
}
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
func (s *UserService) validateCreateUserRequest(req *dto.CreateUserRequest) error {
|
||||
if req.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
if req.Username == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
if req.Password == "" {
|
||||
return errors.New("password is required")
|
||||
}
|
||||
if len(req.Password) < 6 {
|
||||
return errors.New("password must be at least 6 characters")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
```
|
||||
|
||||
### 照片服务实现
|
||||
```go
|
||||
// photo/photo_service.go - 照片服务实现
|
||||
package photo
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository"
|
||||
"photography-backend/internal/service"
|
||||
"photography-backend/pkg/logger"
|
||||
)
|
||||
|
||||
type PhotoService struct {
|
||||
photoRepo repository.PhotoRepositoryr
|
||||
userRepo repository.UserRepositoryr
|
||||
storageService service.StorageServicer
|
||||
logger logger.Logger
|
||||
}
|
||||
|
||||
func NewPhotoService(
|
||||
photoRepo repository.PhotoRepositoryr,
|
||||
userRepo repository.UserRepositoryr,
|
||||
storageService service.StorageServicer,
|
||||
logger logger.Logger,
|
||||
) *PhotoService {
|
||||
return &PhotoService{
|
||||
photoRepo: photoRepo,
|
||||
userRepo: userRepo,
|
||||
storageService: storageService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
func (s *PhotoService) UploadPhoto(ctx context.Context, file *multipart.FileHeader, userID uint) (*entity.Photo, error) {
|
||||
// 1. 验证文件
|
||||
if err := s.validatePhotoFile(file); err != nil {
|
||||
s.logger.Error("invalid photo file", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 2. 验证用户
|
||||
user, err := s.userRepo.GetByID(ctx, userID)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to get user", zap.Error(err), zap.Uint("user_id", userID))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 3. 生成文件名
|
||||
filename := s.generatePhotoFilename(file.Filename, userID)
|
||||
|
||||
// 4. 上传文件
|
||||
uploadedFile, err := s.storageService.UploadFile(ctx, file, filename)
|
||||
if err != nil {
|
||||
s.logger.Error("failed to upload photo", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 5. 创建照片记录
|
||||
photo := &entity.Photo{
|
||||
UserID: userID,
|
||||
Title: strings.TrimSuffix(file.Filename, filepath.Ext(file.Filename)),
|
||||
Description: "",
|
||||
Filename: uploadedFile.Filename,
|
||||
FilePath: uploadedFile.Path,
|
||||
FileSize: uploadedFile.Size,
|
||||
MimeType: uploadedFile.MimeType,
|
||||
Width: uploadedFile.Width,
|
||||
Height: uploadedFile.Height,
|
||||
Status: entity.PhotoStatusActive,
|
||||
CreatedAt: time.Now(),
|
||||
UpdatedAt: time.Now(),
|
||||
}
|
||||
|
||||
// 6. 保存照片记录
|
||||
createdPhoto, err := s.photoRepo.Create(ctx, photo)
|
||||
if err != nil {
|
||||
// 如果数据库保存失败,删除已上传的文件
|
||||
s.storageService.DeleteFile(ctx, uploadedFile.Path)
|
||||
s.logger.Error("failed to create photo record", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("photo uploaded successfully",
|
||||
zap.String("photo_id", fmt.Sprintf("%d", createdPhoto.ID)),
|
||||
zap.String("filename", createdPhoto.Filename),
|
||||
zap.Uint("user_id", userID))
|
||||
|
||||
return createdPhoto, nil
|
||||
}
|
||||
|
||||
func (s *PhotoService) GetPhotosByUser(ctx context.Context, userID uint, opts *dto.ListPhotosOptions) ([]*entity.Photo, int64, error) {
|
||||
// 设置默认选项
|
||||
if opts == nil {
|
||||
opts = &dto.ListPhotosOptions{
|
||||
Page: 1,
|
||||
Limit: 20,
|
||||
Sort: "created_at",
|
||||
Order: "desc",
|
||||
}
|
||||
}
|
||||
|
||||
// 查询照片
|
||||
photos, total, err := s.photoRepo.ListByUserID(ctx, userID, &repository.ListPhotosOptions{
|
||||
Page: opts.Page,
|
||||
Limit: opts.Limit,
|
||||
Sort: opts.Sort,
|
||||
Order: opts.Order,
|
||||
Status: string(entity.PhotoStatusActive),
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
s.logger.Error("failed to list photos by user", zap.Error(err), zap.Uint("user_id", userID))
|
||||
return nil, 0, err
|
||||
}
|
||||
|
||||
return photos, total, nil
|
||||
}
|
||||
|
||||
func (s *PhotoService) validatePhotoFile(file *multipart.FileHeader) error {
|
||||
// 检查文件大小 (10MB 限制)
|
||||
const maxFileSize = 10 * 1024 * 1024
|
||||
if file.Size > maxFileSize {
|
||||
return errors.New("file size exceeds limit")
|
||||
}
|
||||
|
||||
// 检查文件类型
|
||||
ext := strings.ToLower(filepath.Ext(file.Filename))
|
||||
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
|
||||
|
||||
isAllowed := false
|
||||
for _, allowedExt := range allowedExts {
|
||||
if ext == allowedExt {
|
||||
isAllowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isAllowed {
|
||||
return errors.New("file type not allowed")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *PhotoService) generatePhotoFilename(originalFilename string, userID uint) string {
|
||||
ext := filepath.Ext(originalFilename)
|
||||
timestamp := time.Now().Unix()
|
||||
return fmt.Sprintf("photos/%d_%d%s", userID, timestamp, ext)
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 服务测试
|
||||
|
||||
### 单元测试规范
|
||||
```go
|
||||
// user/user_service_test.go - 用户服务测试
|
||||
package user
|
||||
|
||||
import (
|
||||
"context"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
"github.com/stretchr/testify/mock"
|
||||
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository/mocks"
|
||||
"photography-backend/pkg/logger"
|
||||
)
|
||||
|
||||
func TestUserService_CreateUser(t *testing.T) {
|
||||
// Setup
|
||||
mockUserRepo := new(mocks.UserRepositoryr)
|
||||
mockLogger := logger.NewNoop()
|
||||
userService := NewUserService(mockUserRepo, mockLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
req := &dto.CreateUserRequest{
|
||||
Email: "test@example.com",
|
||||
Username: "testuser",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
mockUserRepo.On("GetByEmail", ctx, req.Email).Return(nil, repository.ErrNotFound)
|
||||
mockUserRepo.On("Create", ctx, mock.AnythingOfType("*entity.User")).
|
||||
Return(&entity.User{
|
||||
ID: 1,
|
||||
Email: req.Email,
|
||||
Username: req.Username,
|
||||
Role: entity.UserRoleUser,
|
||||
Status: entity.UserStatusActive,
|
||||
}, nil)
|
||||
|
||||
// Execute
|
||||
user, err := userService.CreateUser(ctx, req)
|
||||
|
||||
// Assert
|
||||
assert.NoError(t, err)
|
||||
assert.NotNil(t, user)
|
||||
assert.Equal(t, req.Email, user.Email)
|
||||
assert.Equal(t, req.Username, user.Username)
|
||||
assert.Equal(t, entity.UserRoleUser, user.Role)
|
||||
assert.Equal(t, entity.UserStatusActive, user.Status)
|
||||
|
||||
mockUserRepo.AssertExpectations(t)
|
||||
}
|
||||
|
||||
func TestUserService_CreateUser_UserAlreadyExists(t *testing.T) {
|
||||
// Setup
|
||||
mockUserRepo := new(mocks.UserRepositoryr)
|
||||
mockLogger := logger.NewNoop()
|
||||
userService := NewUserService(mockUserRepo, mockLogger)
|
||||
|
||||
ctx := context.Background()
|
||||
req := &dto.CreateUserRequest{
|
||||
Email: "test@example.com",
|
||||
Username: "testuser",
|
||||
Password: "password123",
|
||||
}
|
||||
|
||||
existingUser := &entity.User{
|
||||
ID: 1,
|
||||
Email: req.Email,
|
||||
}
|
||||
|
||||
// Mock expectations
|
||||
mockUserRepo.On("GetByEmail", ctx, req.Email).Return(existingUser, nil)
|
||||
|
||||
// Execute
|
||||
user, err := userService.CreateUser(ctx, req)
|
||||
|
||||
// Assert
|
||||
assert.Error(t, err)
|
||||
assert.Nil(t, user)
|
||||
assert.Contains(t, err.Error(), "user already exists")
|
||||
|
||||
mockUserRepo.AssertExpectations(t)
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 错误处理
|
||||
|
||||
### 自定义错误类型
|
||||
```go
|
||||
// errors.go - 服务层错误定义
|
||||
package service
|
||||
|
||||
import "errors"
|
||||
|
||||
var (
|
||||
// 通用错误
|
||||
ErrInvalidInput = errors.New("invalid input")
|
||||
ErrInternalError = errors.New("internal error")
|
||||
ErrNotFound = errors.New("not found")
|
||||
ErrUnauthorized = errors.New("unauthorized")
|
||||
ErrForbidden = errors.New("forbidden")
|
||||
|
||||
// 用户相关错误
|
||||
ErrUserNotFound = errors.New("user not found")
|
||||
ErrUserExists = errors.New("user already exists")
|
||||
ErrInvalidCredentials = errors.New("invalid credentials")
|
||||
ErrUserInactive = errors.New("user account is not active")
|
||||
|
||||
// 照片相关错误
|
||||
ErrPhotoNotFound = errors.New("photo not found")
|
||||
ErrInvalidFileType = errors.New("invalid file type")
|
||||
ErrFileSizeExceeded = errors.New("file size exceeded")
|
||||
ErrUploadFailed = errors.New("file upload failed")
|
||||
|
||||
// 认证相关错误
|
||||
ErrInvalidToken = errors.New("invalid token")
|
||||
ErrTokenExpired = errors.New("token expired")
|
||||
ErrTokenRevoked = errors.New("token revoked")
|
||||
)
|
||||
```
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 服务设计原则
|
||||
1. **单一职责**: 每个服务只负责一个业务领域
|
||||
2. **接口优先**: 先定义接口,再实现具体逻辑
|
||||
3. **依赖注入**: 通过构造函数注入依赖
|
||||
4. **错误处理**: 明确的错误类型和处理
|
||||
5. **日志记录**: 关键操作的日志记录
|
||||
|
||||
### 性能优化
|
||||
1. **缓存策略**: 合理使用缓存减少数据库查询
|
||||
2. **批量操作**: 优化批量数据处理
|
||||
3. **异步处理**: 使用 goroutine 处理耗时操作
|
||||
4. **数据库优化**: 合理使用索引和查询优化
|
||||
|
||||
### 安全考虑
|
||||
1. **输入验证**: 严格验证所有输入参数
|
||||
2. **权限检查**: 确保用户只能操作自己的数据
|
||||
3. **敏感信息**: 避免在日志中记录敏感信息
|
||||
4. **SQL 注入**: 使用参数化查询防止 SQL 注入
|
||||
|
||||
本模块是业务逻辑的核心,确保代码质量和测试覆盖率是关键。
|
||||
249
backend-old/internal/service/auth/auth_service.go
Normal file
249
backend-old/internal/service/auth/auth_service.go
Normal file
@ -0,0 +1,249 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/repository/postgres"
|
||||
)
|
||||
|
||||
// AuthService 认证服务
|
||||
type AuthService struct {
|
||||
userRepo postgres.UserRepository
|
||||
jwtService *JWTService
|
||||
}
|
||||
|
||||
// NewAuthService 创建认证服务
|
||||
func NewAuthService(userRepo postgres.UserRepository, jwtService *JWTService) *AuthService {
|
||||
return &AuthService{
|
||||
userRepo: userRepo,
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (s *AuthService) Login(req *dto.LoginRequest) (*dto.LoginResponse, error) {
|
||||
// 根据用户名或邮箱查找用户
|
||||
var user *entity.User
|
||||
var err error
|
||||
|
||||
// 按邮箱查找用户
|
||||
user, err = s.userRepo.GetByEmail(req.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user by email: %w", err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
// 检查用户是否激活
|
||||
if !user.IsActive {
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
|
||||
return nil, fmt.Errorf("invalid credentials")
|
||||
}
|
||||
|
||||
// 生成JWT令牌
|
||||
tokenPair, err := s.jwtService.GenerateTokenPair(user.ID, user.Username, string(user.Role))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
||||
}
|
||||
|
||||
// 更新最后登录时间
|
||||
if err := s.userRepo.UpdateLastLogin(user.ID); err != nil {
|
||||
// 记录错误但不中断登录流程
|
||||
fmt.Printf("failed to update last login: %v\n", err)
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
|
||||
return &dto.LoginResponse{
|
||||
Token: dto.TokenResponse{
|
||||
AccessToken: tokenPair.AccessToken,
|
||||
RefreshToken: tokenPair.RefreshToken,
|
||||
TokenType: tokenPair.TokenType,
|
||||
ExpiresIn: tokenPair.ExpiresIn,
|
||||
},
|
||||
User: *dto.ConvertToUserResponse(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func (s *AuthService) Register(req *dto.CreateUserRequest) (*entity.User, error) {
|
||||
// 检查用户名是否已存在
|
||||
existingUser, err := s.userRepo.GetByUsername(req.Username)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check username: %w", err)
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, fmt.Errorf("username already exists")
|
||||
}
|
||||
|
||||
// 检查邮箱是否已存在
|
||||
existingUser, err = s.userRepo.GetByEmail(req.Email)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to check email: %w", err)
|
||||
}
|
||||
if existingUser != nil {
|
||||
return nil, fmt.Errorf("email already exists")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
user := &entity.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: string(hashedPassword),
|
||||
Name: req.Name,
|
||||
Role: req.Role,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
// 如果没有指定角色,默认为普通用户
|
||||
if user.Role == "" {
|
||||
user.Role = entity.UserRoleUser
|
||||
}
|
||||
|
||||
if err := s.userRepo.Create(user); err != nil {
|
||||
return nil, fmt.Errorf("failed to create user: %w", err)
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// RefreshToken 刷新令牌
|
||||
func (s *AuthService) RefreshToken(req *dto.RefreshTokenRequest) (*dto.LoginResponse, error) {
|
||||
// 验证刷新令牌
|
||||
claims, err := s.jwtService.ValidateToken(req.RefreshToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid refresh token: %w", err)
|
||||
}
|
||||
|
||||
// 获取用户信息
|
||||
user, err := s.userRepo.GetByID(claims.UserID)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// 检查用户是否激活
|
||||
if !user.IsActive {
|
||||
return nil, fmt.Errorf("user account is deactivated")
|
||||
}
|
||||
|
||||
// 生成新的令牌对
|
||||
tokenPair, err := s.jwtService.GenerateTokenPair(user.ID, user.Username, string(user.Role))
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate tokens: %w", err)
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
|
||||
return &dto.LoginResponse{
|
||||
Token: dto.TokenResponse{
|
||||
AccessToken: tokenPair.AccessToken,
|
||||
RefreshToken: tokenPair.RefreshToken,
|
||||
TokenType: tokenPair.TokenType,
|
||||
ExpiresIn: tokenPair.ExpiresIn,
|
||||
},
|
||||
User: *dto.ConvertToUserResponse(user),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户
|
||||
func (s *AuthService) GetUserByID(id uint) (*entity.User, error) {
|
||||
user, err := s.userRepo.GetByID(id)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return nil, fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// 清除密码字段
|
||||
user.Password = ""
|
||||
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdatePassword 更新密码
|
||||
func (s *AuthService) UpdatePassword(userID uint, req *dto.ChangePasswordRequest) error {
|
||||
// 获取用户信息
|
||||
user, err := s.userRepo.GetByID(userID)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to get user: %w", err)
|
||||
}
|
||||
|
||||
if user == nil {
|
||||
return fmt.Errorf("user not found")
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
|
||||
return fmt.Errorf("invalid old password")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
return fmt.Errorf("failed to hash password: %w", err)
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
user.Password = string(hashedPassword)
|
||||
if err := s.userRepo.Update(user); err != nil {
|
||||
return fmt.Errorf("failed to update password: %w", err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// CheckPermission 检查权限
|
||||
func (s *AuthService) CheckPermission(userRole entity.UserRole, requiredRole entity.UserRole) bool {
|
||||
roleLevel := map[entity.UserRole]int{
|
||||
entity.UserRoleUser: 1,
|
||||
entity.UserRolePhotographer: 2,
|
||||
entity.UserRoleAdmin: 3,
|
||||
}
|
||||
|
||||
userLevel, exists := roleLevel[userRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
requiredLevel, exists := roleLevel[requiredRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return userLevel >= requiredLevel
|
||||
}
|
||||
|
||||
// IsAdmin 检查是否为管理员
|
||||
func (s *AuthService) IsAdmin(userRole entity.UserRole) bool {
|
||||
return userRole == entity.UserRoleAdmin
|
||||
}
|
||||
|
||||
// IsPhotographer 检查是否为摄影师或以上
|
||||
func (s *AuthService) IsPhotographer(userRole entity.UserRole) bool {
|
||||
return userRole == entity.UserRolePhotographer || userRole == entity.UserRoleAdmin
|
||||
}
|
||||
132
backend-old/internal/service/auth/jwt_service.go
Normal file
132
backend-old/internal/service/auth/jwt_service.go
Normal file
@ -0,0 +1,132 @@
|
||||
package auth
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"time"
|
||||
"github.com/golang-jwt/jwt/v5"
|
||||
"photography-backend/internal/config"
|
||||
)
|
||||
|
||||
// JWTService JWT服务
|
||||
type JWTService struct {
|
||||
secretKey []byte
|
||||
accessTokenDuration time.Duration
|
||||
refreshTokenDuration time.Duration
|
||||
}
|
||||
|
||||
// JWTClaims JWT声明
|
||||
type JWTClaims struct {
|
||||
UserID uint `json:"user_id"`
|
||||
Username string `json:"username"`
|
||||
Role string `json:"role"`
|
||||
jwt.RegisteredClaims
|
||||
}
|
||||
|
||||
// TokenPair 令牌对
|
||||
type TokenPair struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
}
|
||||
|
||||
// NewJWTService 创建JWT服务
|
||||
func NewJWTService(cfg *config.JWTConfig) *JWTService {
|
||||
accessDuration, _ := time.ParseDuration(cfg.ExpiresIn)
|
||||
refreshDuration, _ := time.ParseDuration(cfg.RefreshExpiresIn)
|
||||
|
||||
return &JWTService{
|
||||
secretKey: []byte(cfg.Secret),
|
||||
accessTokenDuration: accessDuration,
|
||||
refreshTokenDuration: refreshDuration,
|
||||
}
|
||||
}
|
||||
|
||||
// GenerateTokenPair 生成令牌对
|
||||
func (s *JWTService) GenerateTokenPair(userID uint, username, role string) (*TokenPair, error) {
|
||||
// 生成访问令牌
|
||||
accessToken, err := s.generateToken(userID, username, role, s.accessTokenDuration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate access token: %w", err)
|
||||
}
|
||||
|
||||
// 生成刷新令牌
|
||||
refreshToken, err := s.generateToken(userID, username, role, s.refreshTokenDuration)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to generate refresh token: %w", err)
|
||||
}
|
||||
|
||||
return &TokenPair{
|
||||
AccessToken: accessToken,
|
||||
RefreshToken: refreshToken,
|
||||
TokenType: "Bearer",
|
||||
ExpiresIn: int64(s.accessTokenDuration.Seconds()),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// generateToken 生成令牌
|
||||
func (s *JWTService) generateToken(userID uint, username, role string, duration time.Duration) (string, error) {
|
||||
now := time.Now()
|
||||
claims := &JWTClaims{
|
||||
UserID: userID,
|
||||
Username: username,
|
||||
Role: role,
|
||||
RegisteredClaims: jwt.RegisteredClaims{
|
||||
ExpiresAt: jwt.NewNumericDate(now.Add(duration)),
|
||||
IssuedAt: jwt.NewNumericDate(now),
|
||||
NotBefore: jwt.NewNumericDate(now),
|
||||
Issuer: "photography-backend",
|
||||
},
|
||||
}
|
||||
|
||||
token := jwt.NewWithClaims(jwt.SigningMethodHS256, claims)
|
||||
return token.SignedString(s.secretKey)
|
||||
}
|
||||
|
||||
// ValidateToken 验证令牌
|
||||
func (s *JWTService) ValidateToken(tokenString string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
if _, ok := token.Method.(*jwt.SigningMethodHMAC); !ok {
|
||||
return nil, fmt.Errorf("unexpected signing method: %v", token.Header["alg"])
|
||||
}
|
||||
return s.secretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("failed to parse token: %w", err)
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok && token.Valid {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid token")
|
||||
}
|
||||
|
||||
// RefreshToken 刷新令牌
|
||||
func (s *JWTService) RefreshToken(refreshToken string) (*TokenPair, error) {
|
||||
claims, err := s.ValidateToken(refreshToken)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("invalid refresh token: %w", err)
|
||||
}
|
||||
|
||||
// 生成新的令牌对
|
||||
return s.GenerateTokenPair(claims.UserID, claims.Username, claims.Role)
|
||||
}
|
||||
|
||||
// GetClaimsFromToken 从令牌中获取声明
|
||||
func (s *JWTService) GetClaimsFromToken(tokenString string) (*JWTClaims, error) {
|
||||
token, err := jwt.ParseWithClaims(tokenString, &JWTClaims{}, func(token *jwt.Token) (interface{}, error) {
|
||||
return s.secretKey, nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if claims, ok := token.Claims.(*JWTClaims); ok {
|
||||
return claims, nil
|
||||
}
|
||||
|
||||
return nil, fmt.Errorf("invalid claims")
|
||||
}
|
||||
233
backend-old/internal/service/category_service.go
Normal file
233
backend-old/internal/service/category_service.go
Normal file
@ -0,0 +1,233 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository/interfaces"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type CategoryService struct {
|
||||
categoryRepo interfaces.CategoryRepository
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewCategoryService(categoryRepo interfaces.CategoryRepository, logger *zap.Logger) *CategoryService {
|
||||
return &CategoryService{
|
||||
categoryRepo: categoryRepo,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCategories 获取分类列表
|
||||
func (s *CategoryService) GetCategories(ctx context.Context, parentID *uint) ([]*entity.Category, error) {
|
||||
categories, err := s.categoryRepo.List(ctx, parentID)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get categories", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return categories, nil
|
||||
}
|
||||
|
||||
// GetCategoryTree 获取分类树
|
||||
func (s *CategoryService) GetCategoryTree(ctx context.Context) ([]*entity.CategoryTree, error) {
|
||||
tree, err := s.categoryRepo.GetTree(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get category tree", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tree, nil
|
||||
}
|
||||
|
||||
// GetCategoryByID 根据ID获取分类
|
||||
func (s *CategoryService) GetCategoryByID(ctx context.Context, id uint) (*entity.Category, error) {
|
||||
category, err := s.categoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get category by ID", zap.Error(err), zap.Uint("id", id))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// GetCategoryBySlug 根据slug获取分类
|
||||
func (s *CategoryService) GetCategoryBySlug(ctx context.Context, slug string) (*entity.Category, error) {
|
||||
category, err := s.categoryRepo.GetBySlug(ctx, slug)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get category by slug", zap.Error(err), zap.String("slug", slug))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// CreateCategory 创建分类
|
||||
func (s *CategoryService) CreateCategory(ctx context.Context, req *entity.CreateCategoryRequest) (*entity.Category, error) {
|
||||
// 验证slug唯一性
|
||||
if err := s.categoryRepo.ValidateSlugUnique(ctx, req.Slug, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证父分类存在性
|
||||
if req.ParentID != nil {
|
||||
if err := s.categoryRepo.ValidateParentCategory(ctx, 0, *req.ParentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 获取排序顺序
|
||||
sortOrder, err := s.categoryRepo.GetNextSortOrder(ctx, req.ParentID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
category := &entity.Category{
|
||||
Name: req.Name,
|
||||
Slug: req.Slug,
|
||||
Description: req.Description,
|
||||
ParentID: req.ParentID,
|
||||
SortOrder: sortOrder,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.categoryRepo.Create(ctx, category); err != nil {
|
||||
s.logger.Error("Failed to create category", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Category created successfully", zap.Uint("id", category.ID))
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// UpdateCategory 更新分类
|
||||
func (s *CategoryService) UpdateCategory(ctx context.Context, id uint, req *entity.UpdateCategoryRequest) (*entity.Category, error) {
|
||||
// 检查分类是否存在
|
||||
category, err := s.categoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get category", zap.Error(err), zap.Uint("id", id))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证slug唯一性
|
||||
if req.Slug != nil && *req.Slug != category.Slug {
|
||||
if err := s.categoryRepo.ValidateSlugUnique(ctx, *req.Slug, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 验证父分类(防止循环引用)
|
||||
if req.ParentID != nil {
|
||||
// 检查是否有变更
|
||||
if (category.ParentID == nil && *req.ParentID != 0) || (category.ParentID != nil && *req.ParentID != *category.ParentID) {
|
||||
if err := s.categoryRepo.ValidateParentCategory(ctx, id, *req.ParentID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 更新字段
|
||||
if req.Name != nil {
|
||||
category.Name = *req.Name
|
||||
}
|
||||
if req.Slug != nil {
|
||||
category.Slug = *req.Slug
|
||||
}
|
||||
if req.Description != nil {
|
||||
category.Description = *req.Description
|
||||
}
|
||||
if req.ParentID != nil {
|
||||
if *req.ParentID == 0 {
|
||||
category.ParentID = nil
|
||||
} else {
|
||||
category.ParentID = req.ParentID
|
||||
}
|
||||
}
|
||||
if req.SortOrder != nil {
|
||||
category.SortOrder = *req.SortOrder
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
category.IsActive = *req.IsActive
|
||||
}
|
||||
|
||||
// 保存更新
|
||||
if err := s.categoryRepo.Update(ctx, category); err != nil {
|
||||
s.logger.Error("Failed to update category", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Category updated successfully", zap.Uint("id", id))
|
||||
return category, nil
|
||||
}
|
||||
|
||||
// DeleteCategory 删除分类
|
||||
func (s *CategoryService) DeleteCategory(ctx context.Context, id uint) error {
|
||||
// 检查分类是否存在
|
||||
_, err := s.categoryRepo.GetByID(ctx, id)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get category", zap.Error(err), zap.Uint("id", id))
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否有子分类
|
||||
children, err := s.categoryRepo.GetChildren(ctx, id)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if len(children) > 0 {
|
||||
return errors.New("cannot delete category with subcategories")
|
||||
}
|
||||
|
||||
// 直接删除分类,在Repository层检查照片关联
|
||||
if err := s.categoryRepo.Delete(ctx, id); err != nil {
|
||||
s.logger.Error("Failed to delete category", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Category deleted successfully", zap.Uint("id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ReorderCategories 重新排序分类
|
||||
func (s *CategoryService) ReorderCategories(ctx context.Context, parentID *uint, categoryIDs []uint) error {
|
||||
if len(categoryIDs) == 0 {
|
||||
return nil
|
||||
}
|
||||
|
||||
// 重新排序分类
|
||||
if err := s.categoryRepo.Reorder(ctx, parentID, categoryIDs); err != nil {
|
||||
s.logger.Error("Failed to reorder categories", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Categories reordered successfully", zap.Int("count", len(categoryIDs)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetCategoryStats 获取分类统计信息
|
||||
func (s *CategoryService) GetCategoryStats(ctx context.Context) (*entity.CategoryStats, error) {
|
||||
stats, err := s.categoryRepo.GetStats(ctx)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get category stats", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return stats, nil
|
||||
}
|
||||
|
||||
|
||||
// GenerateSlug 生成唯一slug
|
||||
func (s *CategoryService) GenerateSlug(ctx context.Context, name string) (string, error) {
|
||||
slug, err := s.categoryRepo.GenerateUniqueSlug(ctx, name)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate unique slug", zap.Error(err))
|
||||
return "", err
|
||||
}
|
||||
|
||||
return slug, nil
|
||||
}
|
||||
677
backend-old/internal/service/photo_service.go
Normal file
677
backend-old/internal/service/photo_service.go
Normal file
@ -0,0 +1,677 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"mime/multipart"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/config"
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/repository/interfaces"
|
||||
"photography-backend/internal/service/storage"
|
||||
"photography-backend/internal/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type PhotoService struct {
|
||||
photoRepo interfaces.PhotoRepository
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
storageService *storage.StorageService
|
||||
}
|
||||
|
||||
func NewPhotoService(photoRepo interfaces.PhotoRepository, config *config.Config, logger *zap.Logger, storageService *storage.StorageService) *PhotoService {
|
||||
return &PhotoService{
|
||||
photoRepo: photoRepo,
|
||||
config: config,
|
||||
logger: logger,
|
||||
storageService: storageService,
|
||||
}
|
||||
}
|
||||
|
||||
// PhotoListParams 照片列表查询参数
|
||||
type PhotoListParams struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
Search string `json:"search" form:"search"`
|
||||
Status string `json:"status" form:"status"`
|
||||
CategoryID uint `json:"category_id" form:"category_id"`
|
||||
Tags []string `json:"tags" form:"tags"`
|
||||
StartDate string `json:"start_date" form:"start_date"`
|
||||
EndDate string `json:"end_date" form:"end_date"`
|
||||
SortBy string `json:"sort_by" form:"sort_by"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order"`
|
||||
}
|
||||
|
||||
// PhotoListResponse 照片列表响应
|
||||
type PhotoListResponse struct {
|
||||
Photos []entity.Photo `json:"photos"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
// GetPhotos 获取照片列表
|
||||
func (s *PhotoService) GetPhotos(ctx context.Context, params PhotoListParams) (*PhotoListResponse, error) {
|
||||
// 设置默认值
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.Limit <= 0 {
|
||||
params.Limit = 20
|
||||
}
|
||||
if params.Limit > 100 {
|
||||
params.Limit = 100
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := s.db.WithContext(ctx).
|
||||
Preload("Categories").
|
||||
Preload("Tags").
|
||||
Preload("Formats")
|
||||
|
||||
// 搜索过滤
|
||||
if params.Search != "" {
|
||||
searchPattern := "%" + params.Search + "%"
|
||||
query = query.Where("title ILIKE ? OR description ILIKE ? OR original_filename ILIKE ?",
|
||||
searchPattern, searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if params.Status != "" {
|
||||
query = query.Where("status = ?", params.Status)
|
||||
}
|
||||
|
||||
// 分类过滤
|
||||
if params.CategoryID > 0 {
|
||||
query = query.Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id").
|
||||
Where("photo_categories.category_id = ?", params.CategoryID)
|
||||
}
|
||||
|
||||
// 标签过滤
|
||||
if len(params.Tags) > 0 {
|
||||
query = query.Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
|
||||
Joins("JOIN tags ON photo_tags.tag_id = tags.id").
|
||||
Where("tags.slug IN ?", params.Tags)
|
||||
}
|
||||
|
||||
// 日期过滤
|
||||
if params.StartDate != "" {
|
||||
if startDate, err := time.Parse("2006-01-02", params.StartDate); err == nil {
|
||||
query = query.Where("taken_at >= ?", startDate)
|
||||
}
|
||||
}
|
||||
if params.EndDate != "" {
|
||||
if endDate, err := time.Parse("2006-01-02", params.EndDate); err == nil {
|
||||
query = query.Where("taken_at <= ?", endDate)
|
||||
}
|
||||
}
|
||||
|
||||
// 排序
|
||||
sortBy := "created_at"
|
||||
sortOrder := "desc"
|
||||
if params.SortBy != "" {
|
||||
allowedSortFields := []string{"created_at", "updated_at", "taken_at", "title", "file_size"}
|
||||
if utils.Contains(allowedSortFields, params.SortBy) {
|
||||
sortBy = params.SortBy
|
||||
}
|
||||
}
|
||||
if params.SortOrder == "asc" {
|
||||
sortOrder = "asc"
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
countQuery := query
|
||||
if err := countQuery.Model(&entity.Photo{}).Count(&total).Error; err != nil {
|
||||
s.logger.Error("Failed to count photos", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
var photos []entity.Photo
|
||||
if err := query.
|
||||
Order(fmt.Sprintf("%s %s", sortBy, sortOrder)).
|
||||
Offset(offset).
|
||||
Limit(params.Limit).
|
||||
Find(&photos).Error; err != nil {
|
||||
s.logger.Error("Failed to get photos", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
pages := int((total + int64(params.Limit) - 1) / int64(params.Limit))
|
||||
|
||||
return &PhotoListResponse{
|
||||
Photos: photos,
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
Limit: params.Limit,
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetPhotoByID 根据ID获取照片
|
||||
func (s *PhotoService) GetPhotoByID(ctx context.Context, id uint) (*entity.Photo, error) {
|
||||
var photo entity.Photo
|
||||
if err := s.db.WithContext(ctx).
|
||||
Preload("Categories").
|
||||
Preload("Tags").
|
||||
Preload("Formats").
|
||||
First(&photo, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("photo not found")
|
||||
}
|
||||
s.logger.Error("Failed to get photo by ID", zap.Error(err), zap.Uint("id", id))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &photo, nil
|
||||
}
|
||||
|
||||
// CreatePhoto 创建照片
|
||||
func (s *PhotoService) CreatePhoto(ctx context.Context, req *entity.CreatePhotoRequest) (*entity.Photo, error) {
|
||||
// 生成唯一的文件名
|
||||
uniqueFilename := utils.GenerateUniqueFilename(req.OriginalFilename)
|
||||
|
||||
photo := &entity.Photo{
|
||||
Title: req.Title,
|
||||
Description: req.Description,
|
||||
OriginalFilename: req.OriginalFilename,
|
||||
UniqueFilename: uniqueFilename,
|
||||
FileSize: req.FileSize,
|
||||
Status: entity.PhotoStatus(req.Status),
|
||||
Camera: req.Camera,
|
||||
Lens: req.Lens,
|
||||
ISO: req.ISO,
|
||||
Aperture: req.Aperture,
|
||||
ShutterSpeed: req.ShutterSpeed,
|
||||
FocalLength: req.FocalLength,
|
||||
TakenAt: req.TakenAt,
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx := s.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 创建照片记录
|
||||
if err := tx.Create(photo).Error; err != nil {
|
||||
s.logger.Error("Failed to create photo", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 关联分类
|
||||
if len(req.CategoryIDs) > 0 {
|
||||
var categories []entity.Category
|
||||
if err := tx.Where("id IN ?", req.CategoryIDs).Find(&categories).Error; err != nil {
|
||||
s.logger.Error("Failed to find categories", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Model(photo).Association("Categories").Replace(categories); err != nil {
|
||||
s.logger.Error("Failed to associate categories", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 关联标签
|
||||
if len(req.TagIDs) > 0 {
|
||||
var tags []entity.Tag
|
||||
if err := tx.Where("id IN ?", req.TagIDs).Find(&tags).Error; err != nil {
|
||||
s.logger.Error("Failed to find tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
if err := tx.Model(photo).Association("Tags").Replace(tags); err != nil {
|
||||
s.logger.Error("Failed to associate tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 重新加载关联数据
|
||||
if err := s.db.WithContext(ctx).
|
||||
Preload("Categories").
|
||||
Preload("Tags").
|
||||
Preload("Formats").
|
||||
First(photo, photo.ID).Error; err != nil {
|
||||
s.logger.Error("Failed to reload photo", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Photo created successfully", zap.Uint("id", photo.ID))
|
||||
return photo, nil
|
||||
}
|
||||
|
||||
// UpdatePhoto 更新照片
|
||||
func (s *PhotoService) UpdatePhoto(ctx context.Context, id uint, req *entity.UpdatePhotoRequest) (*entity.Photo, error) {
|
||||
// 检查照片是否存在
|
||||
var photo entity.Photo
|
||||
if err := s.db.WithContext(ctx).First(&photo, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("photo not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx := s.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return nil, tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 更新照片基本信息
|
||||
updates := map[string]interface{}{}
|
||||
if req.Title != nil {
|
||||
updates["title"] = *req.Title
|
||||
}
|
||||
if req.Description != nil {
|
||||
updates["description"] = *req.Description
|
||||
}
|
||||
if req.Status != nil {
|
||||
updates["status"] = *req.Status
|
||||
}
|
||||
if req.Camera != nil {
|
||||
updates["camera"] = *req.Camera
|
||||
}
|
||||
if req.Lens != nil {
|
||||
updates["lens"] = *req.Lens
|
||||
}
|
||||
if req.ISO != nil {
|
||||
updates["iso"] = *req.ISO
|
||||
}
|
||||
if req.Aperture != nil {
|
||||
updates["aperture"] = *req.Aperture
|
||||
}
|
||||
if req.ShutterSpeed != nil {
|
||||
updates["shutter_speed"] = *req.ShutterSpeed
|
||||
}
|
||||
if req.FocalLength != nil {
|
||||
updates["focal_length"] = *req.FocalLength
|
||||
}
|
||||
if req.TakenAt != nil {
|
||||
updates["taken_at"] = *req.TakenAt
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := tx.Model(&photo).Updates(updates).Error; err != nil {
|
||||
s.logger.Error("Failed to update photo", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 更新分类关联
|
||||
if req.CategoryIDs != nil {
|
||||
var categories []entity.Category
|
||||
if len(*req.CategoryIDs) > 0 {
|
||||
if err := tx.Where("id IN ?", *req.CategoryIDs).Find(&categories).Error; err != nil {
|
||||
s.logger.Error("Failed to find categories", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&photo).Association("Categories").Replace(categories); err != nil {
|
||||
s.logger.Error("Failed to update categories", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 更新标签关联
|
||||
if req.TagIDs != nil {
|
||||
var tags []entity.Tag
|
||||
if len(*req.TagIDs) > 0 {
|
||||
if err := tx.Where("id IN ?", *req.TagIDs).Find(&tags).Error; err != nil {
|
||||
s.logger.Error("Failed to find tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
if err := tx.Model(&photo).Association("Tags").Replace(tags); err != nil {
|
||||
s.logger.Error("Failed to update tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 重新加载照片数据
|
||||
if err := s.db.WithContext(ctx).
|
||||
Preload("Categories").
|
||||
Preload("Tags").
|
||||
Preload("Formats").
|
||||
First(&photo, id).Error; err != nil {
|
||||
s.logger.Error("Failed to reload photo", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Photo updated successfully", zap.Uint("id", id))
|
||||
return &photo, nil
|
||||
}
|
||||
|
||||
// DeletePhoto 删除照片
|
||||
func (s *PhotoService) DeletePhoto(ctx context.Context, id uint) error {
|
||||
// 检查照片是否存在
|
||||
var photo entity.Photo
|
||||
if err := s.db.WithContext(ctx).First(&photo, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("photo not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx := s.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 删除关联的格式文件
|
||||
if err := tx.Where("photo_id = ?", id).Delete(&entity.PhotoFormat{}).Error; err != nil {
|
||||
s.logger.Error("Failed to delete photo formats", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除关联关系
|
||||
if err := tx.Model(&photo).Association("Categories").Clear(); err != nil {
|
||||
s.logger.Error("Failed to clear categories", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Model(&photo).Association("Tags").Clear(); err != nil {
|
||||
s.logger.Error("Failed to clear tags", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除照片记录
|
||||
if err := tx.Delete(&photo).Error; err != nil {
|
||||
s.logger.Error("Failed to delete photo", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
s.logger.Error("Failed to commit transaction", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 异步删除文件
|
||||
go func() {
|
||||
if err := (*s.storageService).DeletePhoto(photo.UniqueFilename); err != nil {
|
||||
s.logger.Error("Failed to delete photo files", zap.Error(err), zap.String("filename", photo.UniqueFilename))
|
||||
}
|
||||
}()
|
||||
|
||||
s.logger.Info("Photo deleted successfully", zap.Uint("id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// UploadPhoto 上传照片
|
||||
func (s *PhotoService) UploadPhoto(ctx context.Context, file multipart.File, header *multipart.FileHeader, req *entity.CreatePhotoRequest) (*entity.Photo, error) {
|
||||
// 验证文件类型
|
||||
if !s.isValidImageFile(header.Filename) {
|
||||
return nil, errors.New("invalid file type")
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if header.Size > s.config.Upload.MaxFileSize {
|
||||
return nil, errors.New("file size too large")
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
uniqueFilename := utils.GenerateUniqueFilename(header.Filename)
|
||||
|
||||
// 上传文件到存储服务
|
||||
uploadedFile, err := (*s.storageService).UploadPhoto(ctx, file, uniqueFilename)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to upload photo", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 创建照片记录
|
||||
req.OriginalFilename = header.Filename
|
||||
req.FileSize = header.Size
|
||||
|
||||
photo, err := s.CreatePhoto(ctx, req)
|
||||
if err != nil {
|
||||
// 如果创建记录失败,删除已上传的文件
|
||||
go func() {
|
||||
if err := (*s.storageService).DeletePhoto(uniqueFilename); err != nil {
|
||||
s.logger.Error("Failed to cleanup uploaded file", zap.Error(err))
|
||||
}
|
||||
}()
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 异步处理图片格式转换
|
||||
go func() {
|
||||
s.processPhotoFormats(context.Background(), photo, uploadedFile)
|
||||
}()
|
||||
|
||||
return photo, nil
|
||||
}
|
||||
|
||||
// BatchUpdatePhotos 批量更新照片
|
||||
func (s *PhotoService) BatchUpdatePhotos(ctx context.Context, ids []uint, req *entity.BatchUpdatePhotosRequest) error {
|
||||
if len(ids) == 0 {
|
||||
return errors.New("no photos to update")
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx := s.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 构建更新数据
|
||||
updates := map[string]interface{}{}
|
||||
if req.Status != nil {
|
||||
updates["status"] = *req.Status
|
||||
}
|
||||
|
||||
// 基础字段更新
|
||||
if len(updates) > 0 {
|
||||
if err := tx.Model(&entity.Photo{}).Where("id IN ?", ids).Updates(updates).Error; err != nil {
|
||||
s.logger.Error("Failed to batch update photos", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新分类
|
||||
if req.CategoryIDs != nil {
|
||||
// 先删除现有关联
|
||||
if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 添加新关联
|
||||
if len(*req.CategoryIDs) > 0 {
|
||||
for _, photoID := range ids {
|
||||
for _, categoryID := range *req.CategoryIDs {
|
||||
if err := tx.Exec("INSERT INTO photo_categories (photo_id, category_id) VALUES (?, ?)", photoID, categoryID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 批量更新标签
|
||||
if req.TagIDs != nil {
|
||||
// 先删除现有关联
|
||||
if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 添加新关联
|
||||
if len(*req.TagIDs) > 0 {
|
||||
for _, photoID := range ids {
|
||||
for _, tagID := range *req.TagIDs {
|
||||
if err := tx.Exec("INSERT INTO photo_tags (photo_id, tag_id) VALUES (?, ?)", photoID, tagID).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
s.logger.Error("Failed to commit batch update", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Batch update completed", zap.Int("count", len(ids)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDeletePhotos 批量删除照片
|
||||
func (s *PhotoService) BatchDeletePhotos(ctx context.Context, ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return errors.New("no photos to delete")
|
||||
}
|
||||
|
||||
// 获取要删除的照片信息
|
||||
var photos []entity.Photo
|
||||
if err := s.db.WithContext(ctx).Where("id IN ?", ids).Find(&photos).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 开始事务
|
||||
tx := s.db.WithContext(ctx).Begin()
|
||||
if tx.Error != nil {
|
||||
return tx.Error
|
||||
}
|
||||
defer tx.Rollback()
|
||||
|
||||
// 删除关联的格式文件
|
||||
if err := tx.Where("photo_id IN ?", ids).Delete(&entity.PhotoFormat{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除关联关系
|
||||
if err := tx.Exec("DELETE FROM photo_categories WHERE photo_id IN ?", ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if err := tx.Exec("DELETE FROM photo_tags WHERE photo_id IN ?", ids).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除照片记录
|
||||
if err := tx.Where("id IN ?", ids).Delete(&entity.Photo{}).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 提交事务
|
||||
if err := tx.Commit().Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
// 异步删除文件
|
||||
go func() {
|
||||
for _, photo := range photos {
|
||||
if err := (*s.storageService).DeletePhoto(photo.UniqueFilename); err != nil {
|
||||
s.logger.Error("Failed to delete photo files", zap.Error(err), zap.String("filename", photo.UniqueFilename))
|
||||
}
|
||||
}
|
||||
}()
|
||||
|
||||
s.logger.Info("Batch delete completed", zap.Int("count", len(ids)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPhotoStats 获取照片统计信息
|
||||
func (s *PhotoService) GetPhotoStats(ctx context.Context) (*entity.PhotoStats, error) {
|
||||
var stats entity.PhotoStats
|
||||
|
||||
// 总数统计
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).Count(&stats.Total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 按状态统计
|
||||
var statusStats []struct {
|
||||
Status string `json:"status"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Select("status, COUNT(*) as count").
|
||||
Group("status").
|
||||
Find(&statusStats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats.StatusStats = make(map[string]int64)
|
||||
for _, stat := range statusStats {
|
||||
stats.StatusStats[stat.Status] = stat.Count
|
||||
}
|
||||
|
||||
// 本月新增
|
||||
startOfMonth := time.Now().AddDate(0, 0, -time.Now().Day()+1)
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("created_at >= ?", startOfMonth).
|
||||
Count(&stats.ThisMonth).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 今日新增
|
||||
startOfDay := time.Now().Truncate(24 * time.Hour)
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Where("created_at >= ?", startOfDay).
|
||||
Count(&stats.Today).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 总存储大小
|
||||
var totalSize sql.NullInt64
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Photo{}).
|
||||
Select("SUM(file_size)").
|
||||
Row().Scan(&totalSize); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if totalSize.Valid {
|
||||
stats.TotalSize = totalSize.Int64
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// isValidImageFile 验证图片文件类型
|
||||
func (s *PhotoService) isValidImageFile(filename string) bool {
|
||||
ext := strings.ToLower(filepath.Ext(filename))
|
||||
allowedExts := []string{".jpg", ".jpeg", ".png", ".gif", ".bmp", ".webp"}
|
||||
return utils.Contains(allowedExts, ext)
|
||||
}
|
||||
|
||||
// processPhotoFormats 处理照片格式转换
|
||||
func (s *PhotoService) processPhotoFormats(ctx context.Context, photo *entity.Photo, uploadedFile *storage.UploadedFile) {
|
||||
// 这里将实现图片格式转换逻辑
|
||||
// 生成不同尺寸和格式的图片
|
||||
// 更新 photo_formats 表
|
||||
|
||||
s.logger.Info("Processing photo formats", zap.Uint("photo_id", photo.ID))
|
||||
|
||||
// TODO: 实现图片处理逻辑
|
||||
// 1. 生成缩略图
|
||||
// 2. 生成不同尺寸的图片
|
||||
// 3. 转换为不同格式 (WebP, AVIF)
|
||||
// 4. 更新数据库记录
|
||||
}
|
||||
218
backend-old/internal/service/storage/storage.go
Normal file
218
backend-old/internal/service/storage/storage.go
Normal file
@ -0,0 +1,218 @@
|
||||
package storage
|
||||
|
||||
import (
|
||||
"context"
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
|
||||
"photography-backend/internal/config"
|
||||
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// UploadedFile 上传后的文件信息
|
||||
type UploadedFile struct {
|
||||
Filename string `json:"filename"`
|
||||
OriginalURL string `json:"original_url"`
|
||||
ThumbnailURL string `json:"thumbnail_url,omitempty"`
|
||||
Size int64 `json:"size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
// StorageService 存储服务接口
|
||||
type StorageService interface {
|
||||
UploadPhoto(ctx context.Context, file multipart.File, filename string) (*UploadedFile, error)
|
||||
DeletePhoto(filename string) error
|
||||
GetPhotoURL(filename string) string
|
||||
GenerateThumbnail(ctx context.Context, filename string) error
|
||||
}
|
||||
|
||||
// LocalStorageService 本地存储服务实现
|
||||
type LocalStorageService struct {
|
||||
config *config.Config
|
||||
logger *zap.Logger
|
||||
uploadDir string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// NewLocalStorageService 创建本地存储服务
|
||||
func NewLocalStorageService(config *config.Config, logger *zap.Logger) *LocalStorageService {
|
||||
uploadDir := config.Storage.Local.BasePath
|
||||
if uploadDir == "" {
|
||||
uploadDir = "./uploads"
|
||||
}
|
||||
|
||||
baseURL := config.Storage.Local.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = fmt.Sprintf("http://localhost:%d/uploads", config.App.Port)
|
||||
}
|
||||
|
||||
// 确保上传目录存在
|
||||
if err := os.MkdirAll(uploadDir, 0755); err != nil {
|
||||
logger.Error("Failed to create upload directory", zap.Error(err))
|
||||
}
|
||||
|
||||
// 创建子目录
|
||||
dirs := []string{"photos", "thumbnails", "temp"}
|
||||
for _, dir := range dirs {
|
||||
dirPath := filepath.Join(uploadDir, dir)
|
||||
if err := os.MkdirAll(dirPath, 0755); err != nil {
|
||||
logger.Error("Failed to create subdirectory", zap.String("dir", dir), zap.Error(err))
|
||||
}
|
||||
}
|
||||
|
||||
return &LocalStorageService{
|
||||
config: config,
|
||||
logger: logger,
|
||||
uploadDir: uploadDir,
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadPhoto 上传照片
|
||||
func (s *LocalStorageService) UploadPhoto(ctx context.Context, file multipart.File, filename string) (*UploadedFile, error) {
|
||||
// 保存原图
|
||||
photoPath := filepath.Join(s.uploadDir, "photos", filename)
|
||||
|
||||
out, err := os.Create(photoPath)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to create file", zap.String("path", photoPath), zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
defer out.Close()
|
||||
|
||||
// 重置文件指针
|
||||
file.Seek(0, 0)
|
||||
|
||||
// 复制文件内容
|
||||
size, err := io.Copy(out, file)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to copy file", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
_, err = out.Stat()
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to get file info", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
uploadedFile := &UploadedFile{
|
||||
Filename: filename,
|
||||
OriginalURL: s.GetPhotoURL(filename),
|
||||
Size: size,
|
||||
MimeType: s.getMimeType(filename),
|
||||
}
|
||||
|
||||
s.logger.Info("Photo uploaded successfully",
|
||||
zap.String("filename", filename),
|
||||
zap.Int64("size", size))
|
||||
|
||||
return uploadedFile, nil
|
||||
}
|
||||
|
||||
// DeletePhoto 删除照片
|
||||
func (s *LocalStorageService) DeletePhoto(filename string) error {
|
||||
// 删除原图
|
||||
photoPath := filepath.Join(s.uploadDir, "photos", filename)
|
||||
if err := os.Remove(photoPath); err != nil && !os.IsNotExist(err) {
|
||||
s.logger.Error("Failed to delete photo", zap.String("path", photoPath), zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除缩略图
|
||||
thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename)
|
||||
if err := os.Remove(thumbnailPath); err != nil && !os.IsNotExist(err) {
|
||||
s.logger.Warn("Failed to delete thumbnail", zap.String("path", thumbnailPath), zap.Error(err))
|
||||
}
|
||||
|
||||
s.logger.Info("Photo deleted successfully", zap.String("filename", filename))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPhotoURL 获取照片 URL
|
||||
func (s *LocalStorageService) GetPhotoURL(filename string) string {
|
||||
return fmt.Sprintf("%s/photos/%s", s.baseURL, filename)
|
||||
}
|
||||
|
||||
// GetThumbnailURL 获取缩略图 URL
|
||||
func (s *LocalStorageService) GetThumbnailURL(filename string) string {
|
||||
return fmt.Sprintf("%s/thumbnails/%s", s.baseURL, filename)
|
||||
}
|
||||
|
||||
// GenerateThumbnail 生成缩略图
|
||||
func (s *LocalStorageService) GenerateThumbnail(ctx context.Context, filename string) error {
|
||||
// TODO: 实现缩略图生成逻辑
|
||||
// 这里需要使用图像处理库,如 imaging 或 bild
|
||||
s.logger.Info("Generating thumbnail", zap.String("filename", filename))
|
||||
|
||||
// 示例实现 - 实际项目中应该使用图像处理库
|
||||
photoPath := filepath.Join(s.uploadDir, "photos", filename)
|
||||
thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename)
|
||||
|
||||
// 检查原图是否存在
|
||||
if _, err := os.Stat(photoPath); os.IsNotExist(err) {
|
||||
return fmt.Errorf("original photo not found: %s", filename)
|
||||
}
|
||||
|
||||
// 这里应该实现实际的缩略图生成逻辑
|
||||
// 暂时复制原图作为缩略图
|
||||
sourceFile, err := os.Open(photoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer sourceFile.Close()
|
||||
|
||||
destFile, err := os.Create(thumbnailPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
defer destFile.Close()
|
||||
|
||||
_, err = io.Copy(destFile, sourceFile)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Thumbnail generated successfully", zap.String("filename", filename))
|
||||
return nil
|
||||
}
|
||||
|
||||
// getMimeType 根据文件扩展名获取 MIME 类型
|
||||
func (s *LocalStorageService) getMimeType(filename string) string {
|
||||
ext := filepath.Ext(filename)
|
||||
switch ext {
|
||||
case ".jpg", ".jpeg":
|
||||
return "image/jpeg"
|
||||
case ".png":
|
||||
return "image/png"
|
||||
case ".gif":
|
||||
return "image/gif"
|
||||
case ".webp":
|
||||
return "image/webp"
|
||||
case ".bmp":
|
||||
return "image/bmp"
|
||||
default:
|
||||
return "application/octet-stream"
|
||||
}
|
||||
}
|
||||
|
||||
// NewStorageService 根据配置创建存储服务
|
||||
func NewStorageService(config *config.Config, logger *zap.Logger) StorageService {
|
||||
switch config.Storage.Type {
|
||||
case "s3":
|
||||
// TODO: 实现 S3 存储服务
|
||||
logger.Warn("S3 storage not implemented yet, using local storage")
|
||||
return NewLocalStorageService(config, logger)
|
||||
case "minio":
|
||||
// TODO: 实现 MinIO 存储服务
|
||||
logger.Warn("MinIO storage not implemented yet, using local storage")
|
||||
return NewLocalStorageService(config, logger)
|
||||
default:
|
||||
return NewLocalStorageService(config, logger)
|
||||
}
|
||||
}
|
||||
482
backend-old/internal/service/tag_service.go
Normal file
482
backend-old/internal/service/tag_service.go
Normal file
@ -0,0 +1,482 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/utils"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type TagService struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewTagService(db *gorm.DB, logger *zap.Logger) *TagService {
|
||||
return &TagService{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// TagListParams 标签列表查询参数
|
||||
type TagListParams struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
Search string `json:"search" form:"search"`
|
||||
IsActive *bool `json:"is_active" form:"is_active"`
|
||||
SortBy string `json:"sort_by" form:"sort_by"`
|
||||
SortOrder string `json:"sort_order" form:"sort_order"`
|
||||
}
|
||||
|
||||
// TagListResponse 标签列表响应
|
||||
type TagListResponse struct {
|
||||
Tags []entity.Tag `json:"tags"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
// GetTags 获取标签列表
|
||||
func (s *TagService) GetTags(ctx context.Context, params TagListParams) (*TagListResponse, error) {
|
||||
// 设置默认值
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.Limit <= 0 {
|
||||
params.Limit = 20
|
||||
}
|
||||
if params.Limit > 100 {
|
||||
params.Limit = 100
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := s.db.WithContext(ctx)
|
||||
|
||||
// 搜索过滤
|
||||
if params.Search != "" {
|
||||
searchPattern := "%" + params.Search + "%"
|
||||
query = query.Where("name ILIKE ? OR slug ILIKE ?", searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if params.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *params.IsActive)
|
||||
}
|
||||
|
||||
// 排序
|
||||
sortBy := "created_at"
|
||||
sortOrder := "desc"
|
||||
if params.SortBy != "" {
|
||||
allowedSortFields := []string{"created_at", "updated_at", "name", "photo_count"}
|
||||
if utils.Contains(allowedSortFields, params.SortBy) {
|
||||
sortBy = params.SortBy
|
||||
}
|
||||
}
|
||||
if params.SortOrder == "asc" {
|
||||
sortOrder = "asc"
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
countQuery := query
|
||||
if err := countQuery.Model(&entity.Tag{}).Count(&total).Error; err != nil {
|
||||
s.logger.Error("Failed to count tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
var tags []entity.Tag
|
||||
if err := query.
|
||||
Order(fmt.Sprintf("%s %s", sortBy, sortOrder)).
|
||||
Offset(offset).
|
||||
Limit(params.Limit).
|
||||
Find(&tags).Error; err != nil {
|
||||
s.logger.Error("Failed to get tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
pages := int((total + int64(params.Limit) - 1) / int64(params.Limit))
|
||||
|
||||
return &TagListResponse{
|
||||
Tags: tags,
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
Limit: params.Limit,
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetAllTags 获取所有活跃标签
|
||||
func (s *TagService) GetAllTags(ctx context.Context) ([]entity.Tag, error) {
|
||||
var tags []entity.Tag
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("is_active = ?", true).
|
||||
Order("name ASC").
|
||||
Find(&tags).Error; err != nil {
|
||||
s.logger.Error("Failed to get all tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetTagByID 根据ID获取标签
|
||||
func (s *TagService) GetTagByID(ctx context.Context, id uint) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
if err := s.db.WithContext(ctx).First(&tag, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("tag not found")
|
||||
}
|
||||
s.logger.Error("Failed to get tag by ID", zap.Error(err), zap.Uint("id", id))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// GetTagBySlug 根据slug获取标签
|
||||
func (s *TagService) GetTagBySlug(ctx context.Context, slug string) (*entity.Tag, error) {
|
||||
var tag entity.Tag
|
||||
if err := s.db.WithContext(ctx).Where("slug = ?", slug).First(&tag).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("tag not found")
|
||||
}
|
||||
s.logger.Error("Failed to get tag by slug", zap.Error(err), zap.String("slug", slug))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// CreateTag 创建标签
|
||||
func (s *TagService) CreateTag(ctx context.Context, req *entity.CreateTagRequest) (*entity.Tag, error) {
|
||||
// 验证slug唯一性
|
||||
if err := s.validateSlugUnique(ctx, req.Slug, 0); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
tag := &entity.Tag{
|
||||
Name: req.Name,
|
||||
Slug: req.Slug,
|
||||
Description: req.Description,
|
||||
Color: req.Color,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).Create(tag).Error; err != nil {
|
||||
s.logger.Error("Failed to create tag", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("Tag created successfully", zap.Uint("id", tag.ID))
|
||||
return tag, nil
|
||||
}
|
||||
|
||||
// UpdateTag 更新标签
|
||||
func (s *TagService) UpdateTag(ctx context.Context, id uint, req *entity.UpdateTagRequest) (*entity.Tag, error) {
|
||||
// 检查标签是否存在
|
||||
var tag entity.Tag
|
||||
if err := s.db.WithContext(ctx).First(&tag, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("tag not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 验证slug唯一性
|
||||
if req.Slug != nil && *req.Slug != tag.Slug {
|
||||
if err := s.validateSlugUnique(ctx, *req.Slug, id); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updates := map[string]interface{}{}
|
||||
if req.Name != nil {
|
||||
updates["name"] = *req.Name
|
||||
}
|
||||
if req.Slug != nil {
|
||||
updates["slug"] = *req.Slug
|
||||
}
|
||||
if req.Description != nil {
|
||||
updates["description"] = *req.Description
|
||||
}
|
||||
if req.Color != nil {
|
||||
updates["color"] = *req.Color
|
||||
}
|
||||
if req.IsActive != nil {
|
||||
updates["is_active"] = *req.IsActive
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := s.db.WithContext(ctx).Model(&tag).Updates(updates).Error; err != nil {
|
||||
s.logger.Error("Failed to update tag", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Tag updated successfully", zap.Uint("id", id))
|
||||
return &tag, nil
|
||||
}
|
||||
|
||||
// DeleteTag 删除标签
|
||||
func (s *TagService) DeleteTag(ctx context.Context, id uint) error {
|
||||
// 检查标签是否存在
|
||||
var tag entity.Tag
|
||||
if err := s.db.WithContext(ctx).First(&tag, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("tag not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 检查是否有关联的照片
|
||||
var photoCount int64
|
||||
if err := s.db.WithContext(ctx).Table("photo_tags").
|
||||
Where("tag_id = ?", id).Count(&photoCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if photoCount > 0 {
|
||||
return errors.New("cannot delete tag with associated photos")
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
if err := s.db.WithContext(ctx).Delete(&tag).Error; err != nil {
|
||||
s.logger.Error("Failed to delete tag", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Tag deleted successfully", zap.Uint("id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// BatchDeleteTags 批量删除标签
|
||||
func (s *TagService) BatchDeleteTags(ctx context.Context, ids []uint) error {
|
||||
if len(ids) == 0 {
|
||||
return errors.New("no tags to delete")
|
||||
}
|
||||
|
||||
// 检查是否有关联的照片
|
||||
var photoCount int64
|
||||
if err := s.db.WithContext(ctx).Table("photo_tags").
|
||||
Where("tag_id IN ?", ids).Count(&photoCount).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if photoCount > 0 {
|
||||
return errors.New("cannot delete tags with associated photos")
|
||||
}
|
||||
|
||||
// 删除标签
|
||||
if err := s.db.WithContext(ctx).Where("id IN ?", ids).Delete(&entity.Tag{}).Error; err != nil {
|
||||
s.logger.Error("Failed to batch delete tags", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Batch delete tags completed", zap.Int("count", len(ids)))
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPopularTags 获取热门标签
|
||||
func (s *TagService) GetPopularTags(ctx context.Context, limit int) ([]entity.TagWithCount, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
var tags []entity.TagWithCount
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("tags").
|
||||
Select("tags.*, COUNT(photo_tags.photo_id) as photo_count").
|
||||
Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Where("tags.is_active = ?", true).
|
||||
Group("tags.id").
|
||||
Order("photo_count DESC").
|
||||
Limit(limit).
|
||||
Find(&tags).Error; err != nil {
|
||||
s.logger.Error("Failed to get popular tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// GetTagCloud 获取标签云数据
|
||||
func (s *TagService) GetTagCloud(ctx context.Context) ([]entity.TagCloudItem, error) {
|
||||
var items []entity.TagCloudItem
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("tags").
|
||||
Select("tags.name, tags.slug, tags.color, COUNT(photo_tags.photo_id) as count").
|
||||
Joins("LEFT JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Where("tags.is_active = ?", true).
|
||||
Group("tags.id, tags.name, tags.slug, tags.color").
|
||||
Having("COUNT(photo_tags.photo_id) > 0").
|
||||
Order("count DESC").
|
||||
Find(&items).Error; err != nil {
|
||||
s.logger.Error("Failed to get tag cloud", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return items, nil
|
||||
}
|
||||
|
||||
// GetTagStats 获取标签统计信息
|
||||
func (s *TagService) GetTagStats(ctx context.Context) (*entity.TagStats, error) {
|
||||
var stats entity.TagStats
|
||||
|
||||
// 总标签数
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Tag{}).Count(&stats.Total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 活跃标签数
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Tag{}).
|
||||
Where("is_active = ?", true).Count(&stats.Active).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 已使用标签数
|
||||
if err := s.db.WithContext(ctx).
|
||||
Table("tags").
|
||||
Joins("JOIN photo_tags ON tags.id = photo_tags.tag_id").
|
||||
Where("tags.is_active = ?", true).
|
||||
Group("tags.id").
|
||||
Count(&stats.Used).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 未使用标签数
|
||||
stats.Unused = stats.Active - stats.Used
|
||||
|
||||
// 平均每个标签的照片数
|
||||
var totalPhotos int64
|
||||
if err := s.db.WithContext(ctx).Table("photo_tags").
|
||||
Joins("JOIN tags ON photo_tags.tag_id = tags.id").
|
||||
Where("tags.is_active = ?", true).
|
||||
Count(&totalPhotos).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
if stats.Used > 0 {
|
||||
stats.AvgPhotosPerTag = float64(totalPhotos) / float64(stats.Used)
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// SearchTags 搜索标签
|
||||
func (s *TagService) SearchTags(ctx context.Context, query string, limit int) ([]entity.Tag, error) {
|
||||
if limit <= 0 {
|
||||
limit = 10
|
||||
}
|
||||
|
||||
var tags []entity.Tag
|
||||
searchPattern := "%" + query + "%"
|
||||
|
||||
if err := s.db.WithContext(ctx).
|
||||
Where("is_active = ? AND (name ILIKE ? OR slug ILIKE ?)", true, searchPattern, searchPattern).
|
||||
Order("name ASC").
|
||||
Limit(limit).
|
||||
Find(&tags).Error; err != nil {
|
||||
s.logger.Error("Failed to search tags", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// CreateTagsFromNames 从名称列表创建标签
|
||||
func (s *TagService) CreateTagsFromNames(ctx context.Context, names []string) ([]entity.Tag, error) {
|
||||
var tags []entity.Tag
|
||||
|
||||
for _, name := range names {
|
||||
name = strings.TrimSpace(name)
|
||||
if name == "" {
|
||||
continue
|
||||
}
|
||||
|
||||
// 生成slug
|
||||
slug, err := s.GenerateSlug(ctx, name)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to generate slug", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
// 检查标签是否已存在
|
||||
var existingTag entity.Tag
|
||||
if err := s.db.WithContext(ctx).Where("slug = ?", slug).First(&existingTag).Error; err == nil {
|
||||
tags = append(tags, existingTag)
|
||||
continue
|
||||
}
|
||||
|
||||
// 创建新标签
|
||||
tag := entity.Tag{
|
||||
Name: name,
|
||||
Slug: slug,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).Create(&tag).Error; err != nil {
|
||||
s.logger.Error("Failed to create tag", zap.Error(err))
|
||||
continue
|
||||
}
|
||||
|
||||
tags = append(tags, tag)
|
||||
}
|
||||
|
||||
return tags, nil
|
||||
}
|
||||
|
||||
// validateSlugUnique 验证slug唯一性
|
||||
func (s *TagService) validateSlugUnique(ctx context.Context, slug string, excludeID uint) error {
|
||||
var count int64
|
||||
query := s.db.WithContext(ctx).Model(&entity.Tag{}).Where("slug = ?", slug)
|
||||
|
||||
if excludeID > 0 {
|
||||
query = query.Where("id != ?", excludeID)
|
||||
}
|
||||
|
||||
if err := query.Count(&count).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if count > 0 {
|
||||
return errors.New("slug already exists")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GenerateSlug 生成slug
|
||||
func (s *TagService) GenerateSlug(ctx context.Context, name string) (string, error) {
|
||||
baseSlug := utils.GenerateSlug(name)
|
||||
slug := baseSlug
|
||||
|
||||
counter := 1
|
||||
for {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).Model(&entity.Tag{}).
|
||||
Where("slug = ?", slug).Count(&count).Error; err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
if count == 0 {
|
||||
break
|
||||
}
|
||||
|
||||
slug = fmt.Sprintf("%s-%d", baseSlug, counter)
|
||||
counter++
|
||||
}
|
||||
|
||||
return slug, nil
|
||||
}
|
||||
187
backend-old/internal/service/upload/upload_service.go
Normal file
187
backend-old/internal/service/upload/upload_service.go
Normal file
@ -0,0 +1,187 @@
|
||||
package upload
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io"
|
||||
"mime/multipart"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/config"
|
||||
)
|
||||
|
||||
// UploadService 文件上传服务
|
||||
type UploadService struct {
|
||||
config *config.Config
|
||||
uploadDir string
|
||||
baseURL string
|
||||
}
|
||||
|
||||
// UploadResult 上传结果
|
||||
type UploadResult struct {
|
||||
Filename string `json:"filename"`
|
||||
OriginalName string `json:"original_name"`
|
||||
FilePath string `json:"file_path"`
|
||||
FileURL string `json:"file_url"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `json:"mime_type"`
|
||||
}
|
||||
|
||||
// NewUploadService 创建文件上传服务
|
||||
func NewUploadService(cfg *config.Config) *UploadService {
|
||||
uploadDir := cfg.Storage.Local.BasePath
|
||||
if uploadDir == "" {
|
||||
uploadDir = "./uploads"
|
||||
}
|
||||
|
||||
baseURL := cfg.Storage.Local.BaseURL
|
||||
if baseURL == "" {
|
||||
baseURL = fmt.Sprintf("http://localhost:%d/uploads", cfg.App.Port)
|
||||
}
|
||||
|
||||
// 确保上传目录存在
|
||||
os.MkdirAll(uploadDir, 0755)
|
||||
|
||||
// 创建子目录
|
||||
subdirs := []string{"photos", "thumbnails", "temp"}
|
||||
for _, subdir := range subdirs {
|
||||
os.MkdirAll(filepath.Join(uploadDir, subdir), 0755)
|
||||
}
|
||||
|
||||
return &UploadService{
|
||||
config: cfg,
|
||||
uploadDir: uploadDir,
|
||||
baseURL: baseURL,
|
||||
}
|
||||
}
|
||||
|
||||
// UploadPhoto 上传照片
|
||||
func (s *UploadService) UploadPhoto(file multipart.File, header *multipart.FileHeader) (*UploadResult, error) {
|
||||
// 验证文件类型
|
||||
if !s.isValidImageType(header.Header.Get("Content-Type")) {
|
||||
return nil, fmt.Errorf("不支持的文件类型: %s", header.Header.Get("Content-Type"))
|
||||
}
|
||||
|
||||
// 验证文件大小
|
||||
if header.Size > s.config.Upload.MaxFileSize {
|
||||
return nil, fmt.Errorf("文件大小超过限制: %d bytes", header.Size)
|
||||
}
|
||||
|
||||
// 生成唯一文件名
|
||||
filename := s.generateUniqueFilename(header.Filename)
|
||||
|
||||
// 保存文件到 photos 目录
|
||||
photoPath := filepath.Join(s.uploadDir, "photos", filename)
|
||||
|
||||
// 创建目标文件
|
||||
dst, err := os.Create(photoPath)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("创建文件失败: %w", err)
|
||||
}
|
||||
defer dst.Close()
|
||||
|
||||
// 复制文件内容
|
||||
_, err = io.Copy(dst, file)
|
||||
if err != nil {
|
||||
return nil, fmt.Errorf("保存文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 构造文件URL
|
||||
fileURL := fmt.Sprintf("%s/photos/%s", s.baseURL, filename)
|
||||
|
||||
return &UploadResult{
|
||||
Filename: filename,
|
||||
OriginalName: header.Filename,
|
||||
FilePath: photoPath,
|
||||
FileURL: fileURL,
|
||||
FileSize: header.Size,
|
||||
MimeType: header.Header.Get("Content-Type"),
|
||||
}, nil
|
||||
}
|
||||
|
||||
// DeletePhoto 删除照片
|
||||
func (s *UploadService) DeletePhoto(filename string) error {
|
||||
// 删除原图
|
||||
photoPath := filepath.Join(s.uploadDir, "photos", filename)
|
||||
if err := os.Remove(photoPath); err != nil && !os.IsNotExist(err) {
|
||||
return fmt.Errorf("删除文件失败: %w", err)
|
||||
}
|
||||
|
||||
// 删除缩略图
|
||||
thumbnailPath := filepath.Join(s.uploadDir, "thumbnails", filename)
|
||||
os.Remove(thumbnailPath) // 忽略错误,因为缩略图可能不存在
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
// GetPhotoURL 获取照片URL
|
||||
func (s *UploadService) GetPhotoURL(filename string) string {
|
||||
return fmt.Sprintf("%s/photos/%s", s.baseURL, filename)
|
||||
}
|
||||
|
||||
// isValidImageType 验证是否为有效的图片类型
|
||||
func (s *UploadService) isValidImageType(mimeType string) bool {
|
||||
allowedTypes := s.config.Upload.AllowedTypes
|
||||
if len(allowedTypes) == 0 {
|
||||
// 默认允许的图片类型
|
||||
allowedTypes = []string{
|
||||
"image/jpeg",
|
||||
"image/jpg",
|
||||
"image/png",
|
||||
"image/gif",
|
||||
"image/webp",
|
||||
"image/tiff",
|
||||
}
|
||||
}
|
||||
|
||||
for _, allowedType := range allowedTypes {
|
||||
if mimeType == allowedType {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
|
||||
// generateUniqueFilename 生成唯一文件名
|
||||
func (s *UploadService) generateUniqueFilename(originalName string) string {
|
||||
ext := filepath.Ext(originalName)
|
||||
timestamp := time.Now().Unix()
|
||||
|
||||
// 清理原文件名
|
||||
baseName := strings.TrimSuffix(originalName, ext)
|
||||
baseName = strings.ReplaceAll(baseName, " ", "_")
|
||||
|
||||
return fmt.Sprintf("%s_%d%s", baseName, timestamp, ext)
|
||||
}
|
||||
|
||||
// GetUploadStats 获取上传统计信息
|
||||
func (s *UploadService) GetUploadStats() (map[string]interface{}, error) {
|
||||
photosDir := filepath.Join(s.uploadDir, "photos")
|
||||
|
||||
var totalFiles int
|
||||
var totalSize int64
|
||||
|
||||
err := filepath.Walk(photosDir, func(path string, info os.FileInfo, err error) error {
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if !info.IsDir() {
|
||||
totalFiles++
|
||||
totalSize += info.Size()
|
||||
}
|
||||
return nil
|
||||
})
|
||||
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return map[string]interface{}{
|
||||
"total_files": totalFiles,
|
||||
"total_size": totalSize,
|
||||
"upload_dir": s.uploadDir,
|
||||
"base_url": s.baseURL,
|
||||
}, nil
|
||||
}
|
||||
432
backend-old/internal/service/user_service.go
Normal file
432
backend-old/internal/service/user_service.go
Normal file
@ -0,0 +1,432 @@
|
||||
package service
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"time"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
|
||||
"go.uber.org/zap"
|
||||
"golang.org/x/crypto/bcrypt"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
type UserService struct {
|
||||
db *gorm.DB
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewUserService(db *gorm.DB, logger *zap.Logger) *UserService {
|
||||
return &UserService{
|
||||
db: db,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// UserListParams 用户列表查询参数
|
||||
type UserListParams struct {
|
||||
Page int `json:"page" form:"page"`
|
||||
Limit int `json:"limit" form:"limit"`
|
||||
Search string `json:"search" form:"search"`
|
||||
Role string `json:"role" form:"role"`
|
||||
IsActive *bool `json:"is_active" form:"is_active"`
|
||||
}
|
||||
|
||||
// UserListResponse 用户列表响应
|
||||
type UserListResponse struct {
|
||||
Users []entity.User `json:"users"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Pages int `json:"pages"`
|
||||
}
|
||||
|
||||
// GetUsers 获取用户列表
|
||||
func (s *UserService) GetUsers(ctx context.Context, params UserListParams) (*UserListResponse, error) {
|
||||
// 设置默认值
|
||||
if params.Page <= 0 {
|
||||
params.Page = 1
|
||||
}
|
||||
if params.Limit <= 0 {
|
||||
params.Limit = 20
|
||||
}
|
||||
if params.Limit > 100 {
|
||||
params.Limit = 100
|
||||
}
|
||||
|
||||
// 构建查询
|
||||
query := s.db.WithContext(ctx)
|
||||
|
||||
// 搜索过滤
|
||||
if params.Search != "" {
|
||||
searchPattern := "%" + params.Search + "%"
|
||||
query = query.Where("username ILIKE ? OR email ILIKE ?", searchPattern, searchPattern)
|
||||
}
|
||||
|
||||
// 角色过滤
|
||||
if params.Role != "" {
|
||||
query = query.Where("role = ?", params.Role)
|
||||
}
|
||||
|
||||
// 状态过滤
|
||||
if params.IsActive != nil {
|
||||
query = query.Where("is_active = ?", *params.IsActive)
|
||||
}
|
||||
|
||||
// 计算总数
|
||||
var total int64
|
||||
countQuery := query
|
||||
if err := countQuery.Model(&entity.User{}).Count(&total).Error; err != nil {
|
||||
s.logger.Error("Failed to count users", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 分页查询
|
||||
offset := (params.Page - 1) * params.Limit
|
||||
var users []entity.User
|
||||
if err := query.
|
||||
Order("created_at DESC").
|
||||
Offset(offset).
|
||||
Limit(params.Limit).
|
||||
Find(&users).Error; err != nil {
|
||||
s.logger.Error("Failed to get users", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 计算总页数
|
||||
pages := int((total + int64(params.Limit) - 1) / int64(params.Limit))
|
||||
|
||||
return &UserListResponse{
|
||||
Users: users,
|
||||
Total: total,
|
||||
Page: params.Page,
|
||||
Limit: params.Limit,
|
||||
Pages: pages,
|
||||
}, nil
|
||||
}
|
||||
|
||||
// GetUserByID 根据ID获取用户
|
||||
func (s *UserService) GetUserByID(ctx context.Context, id uint) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
s.logger.Error("Failed to get user by ID", zap.Error(err), zap.Uint("id", id))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByUsername 根据用户名获取用户
|
||||
func (s *UserService) GetUserByUsername(ctx context.Context, username string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := s.db.WithContext(ctx).Where("username = ?", username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
s.logger.Error("Failed to get user by username", zap.Error(err), zap.String("username", username))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserByEmail 根据邮箱获取用户
|
||||
func (s *UserService) GetUserByEmail(ctx context.Context, email string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
if err := s.db.WithContext(ctx).Where("email = ?", email).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
s.logger.Error("Failed to get user by email", zap.Error(err), zap.String("email", email))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// CreateUser 创建用户
|
||||
func (s *UserService) CreateUser(ctx context.Context, req *entity.CreateUserRequest) (*entity.User, error) {
|
||||
// 验证用户名唯一性
|
||||
var existingUser entity.User
|
||||
if err := s.db.WithContext(ctx).Where("username = ?", req.Username).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
|
||||
// 验证邮箱唯一性
|
||||
if err := s.db.WithContext(ctx).Where("email = ?", req.Email).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("email already exists")
|
||||
}
|
||||
|
||||
// 加密密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.Password), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to hash password", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
user := &entity.User{
|
||||
Username: req.Username,
|
||||
Email: req.Email,
|
||||
Password: string(hashedPassword),
|
||||
Role: req.Role,
|
||||
IsActive: true,
|
||||
}
|
||||
|
||||
if err := s.db.WithContext(ctx).Create(user).Error; err != nil {
|
||||
s.logger.Error("Failed to create user", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
s.logger.Info("User created successfully", zap.Uint("id", user.ID))
|
||||
return user, nil
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户
|
||||
func (s *UserService) UpdateUser(ctx context.Context, id uint, req *entity.UpdateUserRequest) (*entity.User, error) {
|
||||
// 检查用户是否存在
|
||||
var user entity.User
|
||||
if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updates := map[string]interface{}{}
|
||||
|
||||
if req.Username != nil {
|
||||
// 验证用户名唯一性
|
||||
var existingUser entity.User
|
||||
if err := s.db.WithContext(ctx).Where("username = ? AND id != ?", *req.Username, id).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
updates["username"] = *req.Username
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
// 验证邮箱唯一性
|
||||
var existingUser entity.User
|
||||
if err := s.db.WithContext(ctx).Where("email = ? AND id != ?", *req.Email, id).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("email already exists")
|
||||
}
|
||||
updates["email"] = *req.Email
|
||||
}
|
||||
|
||||
if req.Role != nil {
|
||||
updates["role"] = *req.Role
|
||||
}
|
||||
|
||||
if req.IsActive != nil {
|
||||
updates["is_active"] = *req.IsActive
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := s.db.WithContext(ctx).Model(&user).Updates(updates).Error; err != nil {
|
||||
s.logger.Error("Failed to update user", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("User updated successfully", zap.Uint("id", id))
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// UpdateCurrentUser 更新当前用户信息
|
||||
func (s *UserService) UpdateCurrentUser(ctx context.Context, id uint, req *entity.UpdateCurrentUserRequest) (*entity.User, error) {
|
||||
// 检查用户是否存在
|
||||
var user entity.User
|
||||
if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("user not found")
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 构建更新数据
|
||||
updates := map[string]interface{}{}
|
||||
|
||||
if req.Username != nil {
|
||||
// 验证用户名唯一性
|
||||
var existingUser entity.User
|
||||
if err := s.db.WithContext(ctx).Where("username = ? AND id != ?", *req.Username, id).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("username already exists")
|
||||
}
|
||||
updates["username"] = *req.Username
|
||||
}
|
||||
|
||||
if req.Email != nil {
|
||||
// 验证邮箱唯一性
|
||||
var existingUser entity.User
|
||||
if err := s.db.WithContext(ctx).Where("email = ? AND id != ?", *req.Email, id).First(&existingUser).Error; err == nil {
|
||||
return nil, errors.New("email already exists")
|
||||
}
|
||||
updates["email"] = *req.Email
|
||||
}
|
||||
|
||||
if len(updates) > 0 {
|
||||
if err := s.db.WithContext(ctx).Model(&user).Updates(updates).Error; err != nil {
|
||||
s.logger.Error("Failed to update current user", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
}
|
||||
|
||||
s.logger.Info("Current user updated successfully", zap.Uint("id", id))
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户
|
||||
func (s *UserService) DeleteUser(ctx context.Context, id uint) error {
|
||||
// 检查用户是否存在
|
||||
var user entity.User
|
||||
if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 删除用户
|
||||
if err := s.db.WithContext(ctx).Delete(&user).Error; err != nil {
|
||||
s.logger.Error("Failed to delete user", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("User deleted successfully", zap.Uint("id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ChangePassword 修改密码
|
||||
func (s *UserService) ChangePassword(ctx context.Context, id uint, req *entity.ChangePasswordRequest) error {
|
||||
// 检查用户是否存在
|
||||
var user entity.User
|
||||
if err := s.db.WithContext(ctx).First(&user, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return errors.New("user not found")
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 验证旧密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.OldPassword)); err != nil {
|
||||
return errors.New("old password is incorrect")
|
||||
}
|
||||
|
||||
// 加密新密码
|
||||
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(req.NewPassword), bcrypt.DefaultCost)
|
||||
if err != nil {
|
||||
s.logger.Error("Failed to hash new password", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
// 更新密码
|
||||
if err := s.db.WithContext(ctx).Model(&user).Update("password", string(hashedPassword)).Error; err != nil {
|
||||
s.logger.Error("Failed to update password", zap.Error(err))
|
||||
return err
|
||||
}
|
||||
|
||||
s.logger.Info("Password changed successfully", zap.Uint("id", id))
|
||||
return nil
|
||||
}
|
||||
|
||||
// ValidateCredentials 验证用户凭据
|
||||
func (s *UserService) ValidateCredentials(ctx context.Context, username, password string) (*entity.User, error) {
|
||||
var user entity.User
|
||||
|
||||
// 根据用户名或邮箱查找用户
|
||||
if err := s.db.WithContext(ctx).Where("username = ? OR email = ?", username, username).First(&user).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
s.logger.Error("Failed to find user", zap.Error(err))
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 检查用户是否激活
|
||||
if !user.IsActive {
|
||||
return nil, errors.New("user account is disabled")
|
||||
}
|
||||
|
||||
// 验证密码
|
||||
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(password)); err != nil {
|
||||
return nil, errors.New("invalid credentials")
|
||||
}
|
||||
|
||||
return &user, nil
|
||||
}
|
||||
|
||||
// GetUserStats 获取用户统计信息
|
||||
func (s *UserService) GetUserStats(ctx context.Context) (*entity.UserStats, error) {
|
||||
var stats entity.UserStats
|
||||
|
||||
// 总用户数
|
||||
if err := s.db.WithContext(ctx).Model(&entity.User{}).Count(&stats.Total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 活跃用户数
|
||||
if err := s.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("is_active = ?", true).Count(&stats.Active).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 按角色统计
|
||||
var roleStats []struct {
|
||||
Role string `json:"role"`
|
||||
Count int64 `json:"count"`
|
||||
}
|
||||
if err := s.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Select("role, COUNT(*) as count").
|
||||
Where("is_active = ?", true).
|
||||
Group("role").
|
||||
Find(&roleStats).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
stats.RoleStats = make(map[string]int64)
|
||||
for _, stat := range roleStats {
|
||||
stats.RoleStats[stat.Role] = stat.Count
|
||||
}
|
||||
|
||||
// 本月新增用户
|
||||
startOfMonth := time.Now().AddDate(0, 0, -time.Now().Day()+1)
|
||||
if err := s.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("created_at >= ?", startOfMonth).
|
||||
Count(&stats.ThisMonth).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
// 今日新增用户
|
||||
startOfDay := time.Now().Truncate(24 * time.Hour)
|
||||
if err := s.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("created_at >= ?", startOfDay).
|
||||
Count(&stats.Today).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return &stats, nil
|
||||
}
|
||||
|
||||
// IsUsernameAvailable 检查用户名是否可用
|
||||
func (s *UserService) IsUsernameAvailable(ctx context.Context, username string) (bool, error) {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("username = ?", username).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
|
||||
// IsEmailAvailable 检查邮箱是否可用
|
||||
func (s *UserService) IsEmailAvailable(ctx context.Context, email string) (bool, error) {
|
||||
var count int64
|
||||
if err := s.db.WithContext(ctx).Model(&entity.User{}).
|
||||
Where("email = ?", email).Count(&count).Error; err != nil {
|
||||
return false, err
|
||||
}
|
||||
return count == 0, nil
|
||||
}
|
||||
Reference in New Issue
Block a user