refactor: 重构后端架构为 go-zero 框架,优化项目结构

主要变更:
- 采用 go-zero 框架替代 Gin,提升开发效率
- 重构项目结构,API 文件模块化组织
- 将 model 移至 api/internal/model 目录
- 移除 common 包,改为标准 pkg 目录结构
- 实现统一的仓储模式,支持配置驱动数据库切换
- 简化测试策略,专注 API 集成测试
- 更新 CLAUDE.md 文档,提供详细的开发指导

技术栈更新:
- 框架: Gin → go-zero v1.6.0+
- 代码生成: 引入 goctl 工具
- 架构模式: 四层架构 → go-zero 三层架构 (Handler→Logic→Model)
- 项目布局: 遵循 Go 社区标准和 go-zero 最佳实践
This commit is contained in:
xujiang
2025-07-10 15:05:52 +08:00
parent a2f2f66f88
commit 39a42695d3
52 changed files with 6047 additions and 2349 deletions

View File

@ -0,0 +1,196 @@
package dto
import (
"time"
"photography-backend/internal/model/entity"
)
// CreateAlbumRequest 创建相册请求
type CreateAlbumRequest struct {
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
Slug string `json:"slug" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"`
CategoryID *uint `json:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
IsPublic bool `json:"is_public" binding:"omitempty"`
IsFeatured bool `json:"is_featured" binding:"omitempty"`
Password string `json:"password" binding:"omitempty,min=6" validate:"omitempty,min=6"`
}
// UpdateAlbumRequest 更新相册请求
type UpdateAlbumRequest struct {
Title *string `json:"title" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"`
Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
Slug *string `json:"slug" binding:"omitempty,min=1,max=255" validate:"omitempty,min=1,max=255"`
CoverPhotoID *uint `json:"cover_photo_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
CategoryID *uint `json:"category_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
IsPublic *bool `json:"is_public" binding:"omitempty"`
IsFeatured *bool `json:"is_featured" binding:"omitempty"`
Password *string `json:"password" binding:"omitempty,min=0" validate:"omitempty,min=0"` // 空字符串表示移除密码
SortOrder *int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"`
}
// AlbumResponse 相册响应
type AlbumResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Slug string `json:"slug"`
CoverPhotoID *uint `json:"cover_photo_id"`
UserID uint `json:"user_id"`
CategoryID *uint `json:"category_id"`
IsPublic bool `json:"is_public"`
IsFeatured bool `json:"is_featured"`
HasPassword bool `json:"has_password"`
ViewCount int `json:"view_count"`
LikeCount int `json:"like_count"`
PhotoCount int `json:"photo_count"`
SortOrder int `json:"sort_order"`
User *UserResponse `json:"user,omitempty"`
Category *CategoryResponse `json:"category,omitempty"`
CoverPhoto *PhotoListItem `json:"cover_photo,omitempty"`
RecentPhotos []PhotoListItem `json:"recent_photos,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// AlbumListItem 相册列表项(简化版)
type AlbumListItem struct {
ID uint `json:"id"`
Title string `json:"title"`
Slug string `json:"slug"`
IsPublic bool `json:"is_public"`
IsFeatured bool `json:"is_featured"`
HasPassword bool `json:"has_password"`
ViewCount int `json:"view_count"`
LikeCount int `json:"like_count"`
PhotoCount int `json:"photo_count"`
CoverPhoto *PhotoListItem `json:"cover_photo,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ListAlbumsOptions 相册列表查询选项
type ListAlbumsOptions struct {
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id title created_at updated_at view_count like_count photo_count" validate:"omitempty,oneof=id title created_at updated_at view_count like_count photo_count"`
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
IsFeatured *bool `json:"is_featured" form:"is_featured" binding:"omitempty"`
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
}
// AlbumListResponse 相册列表响应
type AlbumListResponse struct {
Albums []AlbumListItem `json:"albums"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// AddPhotosToAlbumRequest 向相册添加照片请求
type AddPhotosToAlbumRequest struct {
PhotoIDs []uint `json:"photo_ids" binding:"required,min=1" validate:"required,min=1"`
}
// RemovePhotosFromAlbumRequest 从相册移除照片请求
type RemovePhotosFromAlbumRequest struct {
PhotoIDs []uint `json:"photo_ids" binding:"required,min=1" validate:"required,min=1"`
}
// AlbumPasswordRequest 相册密码验证请求
type AlbumPasswordRequest struct {
Password string `json:"password" binding:"required" validate:"required"`
}
// AlbumStatsResponse 相册统计响应
type AlbumStatsResponse struct {
Total int64 `json:"total"`
Published int64 `json:"published"`
Private int64 `json:"private"`
Featured int64 `json:"featured"`
WithPassword int64 `json:"with_password"`
TotalViews int64 `json:"total_views"`
TotalLikes int64 `json:"total_likes"`
TotalPhotos int64 `json:"total_photos"`
CategoryCounts map[string]int64 `json:"category_counts"`
Recent []AlbumListItem `json:"recent"`
Popular []AlbumListItem `json:"popular"`
}
// ConvertToAlbumResponse 将相册实体转换为响应DTO
func ConvertToAlbumResponse(album *entity.Album) *AlbumResponse {
if album == nil {
return nil
}
response := &AlbumResponse{
ID: album.ID,
Title: album.Title,
Description: album.Description,
Slug: album.Slug,
CoverPhotoID: album.CoverPhotoID,
UserID: album.UserID,
CategoryID: album.CategoryID,
IsPublic: album.IsPublic,
IsFeatured: album.IsFeatured,
HasPassword: album.HasPassword(),
ViewCount: album.ViewCount,
LikeCount: album.LikeCount,
PhotoCount: album.PhotoCount,
SortOrder: album.SortOrder,
CreatedAt: album.CreatedAt,
UpdatedAt: album.UpdatedAt,
}
// 转换关联对象
if album.User.ID != 0 {
response.User = ConvertToUserResponse(&album.User)
}
if album.Category != nil {
response.Category = ConvertToCategoryResponse(album.Category)
}
if album.CoverPhoto != nil {
coverPhoto := ConvertToPhotoListItem(album.CoverPhoto)
response.CoverPhoto = &coverPhoto
}
// 转换最近照片
if len(album.Photos) > 0 {
recentPhotos := make([]PhotoListItem, 0, len(album.Photos))
for _, photo := range album.Photos {
recentPhotos = append(recentPhotos, ConvertToPhotoListItem(&photo))
}
response.RecentPhotos = recentPhotos
}
return response
}
// ConvertToAlbumListItem 将相册实体转换为列表项DTO
func ConvertToAlbumListItem(album *entity.Album) AlbumListItem {
item := AlbumListItem{
ID: album.ID,
Title: album.Title,
Slug: album.Slug,
IsPublic: album.IsPublic,
IsFeatured: album.IsFeatured,
HasPassword: album.HasPassword(),
ViewCount: album.ViewCount,
LikeCount: album.LikeCount,
PhotoCount: album.PhotoCount,
CreatedAt: album.CreatedAt,
UpdatedAt: album.UpdatedAt,
}
// 转换封面照片
if album.CoverPhoto != nil {
coverPhoto := ConvertToPhotoListItem(album.CoverPhoto)
item.CoverPhoto = &coverPhoto
}
return item
}

View File

@ -0,0 +1,107 @@
package dto
import (
"time"
"photography-backend/internal/model/entity"
)
// LoginRequest 登录请求
type LoginRequest struct {
Email string `json:"email" binding:"required,email" validate:"required,email"`
Password string `json:"password" binding:"required" validate:"required"`
}
// RegisterRequest 注册请求
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=50" validate:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email" validate:"required,email"`
Password string `json:"password" binding:"required,min=6" validate:"required,min=6"`
Name string `json:"name" binding:"max=100" validate:"max=100"`
}
// RefreshTokenRequest 刷新令牌请求
type RefreshTokenRequest struct {
RefreshToken string `json:"refresh_token" binding:"required" validate:"required"`
}
// TokenResponse 令牌响应
type TokenResponse struct {
AccessToken string `json:"access_token"`
RefreshToken string `json:"refresh_token"`
TokenType string `json:"token_type"`
ExpiresIn int64 `json:"expires_in"`
ExpiresAt time.Time `json:"expires_at"`
}
// LoginResponse 登录响应
type LoginResponse struct {
Token TokenResponse `json:"token"`
User UserResponse `json:"user"`
}
// RegisterResponse 注册响应
type RegisterResponse struct {
Token TokenResponse `json:"token"`
User UserResponse `json:"user"`
}
// TokenClaims JWT 令牌声明
type TokenClaims struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
Role entity.UserRole `json:"role"`
TokenID string `json:"token_id"`
IssuedAt time.Time `json:"issued_at"`
ExpiresAt time.Time `json:"expires_at"`
}
// ResetPasswordRequest 重置密码请求
type ResetPasswordRequest struct {
Email string `json:"email" binding:"required,email" validate:"required,email"`
}
// ConfirmResetPasswordRequest 确认重置密码请求
type ConfirmResetPasswordRequest struct {
Token string `json:"token" binding:"required" validate:"required"`
NewPassword string `json:"new_password" binding:"required,min=6" validate:"required,min=6"`
}
// VerifyEmailRequest 验证邮箱请求
type VerifyEmailRequest struct {
Token string `json:"token" binding:"required" validate:"required"`
}
// LogoutRequest 登出请求
type LogoutRequest struct {
Token string `json:"token" binding:"required" validate:"required"`
}
// AuthStatsResponse 认证统计响应
type AuthStatsResponse struct {
TotalLogins int64 `json:"total_logins"`
ActiveSessions int64 `json:"active_sessions"`
FailedAttempts int64 `json:"failed_attempts"`
RecentLogins []LoginInfo `json:"recent_logins"`
LoginsByHour map[string]int64 `json:"logins_by_hour"`
LoginsByDevice map[string]int64 `json:"logins_by_device"`
}
// LoginInfo 登录信息
type LoginInfo struct {
UserID uint `json:"user_id"`
Username string `json:"username"`
Email string `json:"email"`
LoginTime time.Time `json:"login_time"`
IPAddress string `json:"ip_address"`
UserAgent string `json:"user_agent"`
Success bool `json:"success"`
}
// ValidateTokenResponse 验证令牌响应
type ValidateTokenResponse struct {
Valid bool `json:"valid"`
Claims *TokenClaims `json:"claims,omitempty"`
Error string `json:"error,omitempty"`
}

View File

@ -0,0 +1,143 @@
package dto
import (
"time"
"photography-backend/internal/model/entity"
)
// CreateCategoryRequest 创建分类请求
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required,min=1,max=100" validate:"required,min=1,max=100"`
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
Slug string `json:"slug" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
ParentID *uint `json:"parent_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Color string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
CoverImage string `json:"cover_image" binding:"omitempty,url" validate:"omitempty,url"`
SortOrder int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"`
}
// UpdateCategoryRequest 更新分类请求
type UpdateCategoryRequest struct {
Name *string `json:"name" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
Slug *string `json:"slug" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
ParentID *uint `json:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
Color *string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
CoverImage *string `json:"cover_image" binding:"omitempty,url" validate:"omitempty,url"`
SortOrder *int `json:"sort_order" binding:"omitempty,min=0" validate:"omitempty,min=0"`
IsActive *bool `json:"is_active" binding:"omitempty"`
}
// CategoryResponse 分类响应
type CategoryResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Slug string `json:"slug"`
ParentID *uint `json:"parent_id"`
Color string `json:"color"`
CoverImage string `json:"cover_image"`
Sort int `json:"sort"`
IsActive bool `json:"is_active"`
PhotoCount int64 `json:"photo_count"`
AlbumCount int64 `json:"album_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CategoryTreeResponse 分类树响应
type CategoryTreeResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Description string `json:"description"`
Slug string `json:"slug"`
ParentID *uint `json:"parent_id"`
Color string `json:"color"`
CoverImage string `json:"cover_image"`
Sort int `json:"sort"`
IsActive bool `json:"is_active"`
PhotoCount int64 `json:"photo_count"`
AlbumCount int64 `json:"album_count"`
Children []CategoryTreeResponse `json:"children"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// ListCategoriesOptions 分类列表查询选项
type ListCategoriesOptions struct {
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id name sort created_at updated_at" validate:"omitempty,oneof=id name sort created_at updated_at"`
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
ParentID *uint `json:"parent_id" form:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
WithCount bool `json:"with_count" form:"with_count" binding:"omitempty"`
}
// CategoryListResponse 分类列表响应
type CategoryListResponse struct {
Categories []CategoryResponse `json:"categories"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// ReorderCategoriesRequest 重新排序分类请求
type ReorderCategoriesRequest struct {
ParentID *uint `json:"parent_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
CategoryIDs []uint `json:"category_ids" binding:"required,min=1" validate:"required,min=1"`
}
// CategoryStatsResponse 分类统计响应
type CategoryStatsResponse struct {
Total int64 `json:"total"`
Active int64 `json:"active"`
TopLevel int64 `json:"top_level"`
PhotoCounts map[string]int64 `json:"photo_counts"`
Popular []CategoryResponse `json:"popular"`
}
// ConvertToCategoryResponse 将分类实体转换为响应DTO
func ConvertToCategoryResponse(category *entity.Category) *CategoryResponse {
if category == nil {
return nil
}
return &CategoryResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
ParentID: category.ParentID,
Color: category.Color,
CoverImage: category.CoverImage,
Sort: category.Sort,
IsActive: category.IsActive,
PhotoCount: category.PhotoCount,
CreatedAt: category.CreatedAt,
UpdatedAt: category.UpdatedAt,
}
}
// ConvertToCategoryTreeResponse 将分类树转换为响应DTO
func ConvertToCategoryTreeResponse(tree []entity.CategoryTree) []CategoryTreeResponse {
result := make([]CategoryTreeResponse, len(tree))
for i, category := range tree {
result[i] = CategoryTreeResponse{
ID: category.ID,
Name: category.Name,
Description: category.Description,
ParentID: category.ParentID,
Color: category.Color,
CoverImage: category.CoverImage,
Sort: category.Sort,
IsActive: category.IsActive,
PhotoCount: category.PhotoCount,
Children: ConvertToCategoryTreeResponse(category.Children),
CreatedAt: category.CreatedAt,
UpdatedAt: category.UpdatedAt,
}
}
return result
}

View File

@ -0,0 +1,264 @@
package dto
import (
"mime/multipart"
"time"
"photography-backend/internal/model/entity"
)
// CreatePhotoRequest 创建照片请求
type CreatePhotoRequest struct {
Title string `json:"title" binding:"required,min=1,max=200" validate:"required,min=1,max=200"`
Description string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
Filename string `json:"filename" binding:"required" validate:"required"`
OriginalURL string `json:"original_url" binding:"required,url" validate:"required,url"`
FileSize int64 `json:"file_size" binding:"omitempty,min=0" validate:"omitempty,min=0"`
MimeType string `json:"mime_type" binding:"omitempty" validate:"omitempty"`
Width int `json:"width" binding:"omitempty,min=0" validate:"omitempty,min=0"`
Height int `json:"height" binding:"omitempty,min=0" validate:"omitempty,min=0"`
UserID uint `json:"user_id" binding:"required,min=1" validate:"required,min=1"`
AlbumID *uint `json:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
CategoryID *uint `json:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
TagIDs []uint `json:"tag_ids" binding:"omitempty" validate:"omitempty"`
IsPublic bool `json:"is_public" binding:"omitempty"`
IsFeatured bool `json:"is_featured" binding:"omitempty"`
}
// UpdatePhotoRequest 更新照片请求
type UpdatePhotoRequest struct {
Title *string `json:"title" binding:"omitempty,min=1,max=200" validate:"omitempty,min=1,max=200"`
Description *string `json:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
AlbumID *uint `json:"album_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
CategoryID *uint `json:"category_id" binding:"omitempty,min=0" validate:"omitempty,min=0"`
TagIDs []uint `json:"tag_ids" binding:"omitempty" validate:"omitempty"`
IsPublic *bool `json:"is_public" binding:"omitempty"`
IsFeatured *bool `json:"is_featured" binding:"omitempty"`
LocationName *string `json:"location_name" binding:"omitempty,max=200" validate:"omitempty,max=200"`
Latitude *float64 `json:"latitude" binding:"omitempty,min=-90,max=90" validate:"omitempty,min=-90,max=90"`
Longitude *float64 `json:"longitude" binding:"omitempty,min=-180,max=180" validate:"omitempty,min=-180,max=180"`
}
// UploadPhotoRequest 上传照片请求
type UploadPhotoRequest struct {
File *multipart.FileHeader `form:"photo" binding:"required" validate:"required"`
Title string `form:"title" binding:"omitempty,max=200" validate:"omitempty,max=200"`
Description string `form:"description" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
AlbumID *uint `form:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
CategoryID *uint `form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
TagNames []string `form:"tag_names" binding:"omitempty" validate:"omitempty"`
IsPublic bool `form:"is_public" binding:"omitempty"`
IsFeatured bool `form:"is_featured" binding:"omitempty"`
}
// PhotoResponse 照片响应
type PhotoResponse struct {
ID uint `json:"id"`
Title string `json:"title"`
Description string `json:"description"`
Filename string `json:"filename"`
OriginalURL string `json:"original_url"`
ThumbnailURL string `json:"thumbnail_url"`
MediumURL string `json:"medium_url"`
LargeURL string `json:"large_url"`
FileSize int64 `json:"file_size"`
MimeType string `json:"mime_type"`
Width int `json:"width"`
Height int `json:"height"`
AspectRatio float64 `json:"aspect_ratio"`
// EXIF 信息
CameraMake string `json:"camera_make"`
CameraModel string `json:"camera_model"`
LensModel string `json:"lens_model"`
FocalLength *float64 `json:"focal_length"`
Aperture *float64 `json:"aperture"`
ShutterSpeed string `json:"shutter_speed"`
ISO *int `json:"iso"`
TakenAt *time.Time `json:"taken_at"`
// 地理位置
LocationName string `json:"location_name"`
Latitude *float64 `json:"latitude"`
Longitude *float64 `json:"longitude"`
// 关联信息
UserID uint `json:"user_id"`
AlbumID *uint `json:"album_id"`
CategoryID *uint `json:"category_id"`
User *UserResponse `json:"user,omitempty"`
Album *AlbumResponse `json:"album,omitempty"`
Category *CategoryResponse `json:"category,omitempty"`
Tags []TagResponse `json:"tags,omitempty"`
// 状态和统计
IsPublic bool `json:"is_public"`
IsFeatured bool `json:"is_featured"`
ViewCount int `json:"view_count"`
LikeCount int `json:"like_count"`
DownloadCount int `json:"download_count"`
SortOrder int `json:"sort_order"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// PhotoListItem 照片列表项(简化版)
type PhotoListItem struct {
ID uint `json:"id"`
Title string `json:"title"`
ThumbnailURL string `json:"thumbnail_url"`
Width int `json:"width"`
Height int `json:"height"`
AspectRatio float64 `json:"aspect_ratio"`
IsPublic bool `json:"is_public"`
IsFeatured bool `json:"is_featured"`
ViewCount int `json:"view_count"`
LikeCount int `json:"like_count"`
CreatedAt time.Time `json:"created_at"`
}
// ListPhotosOptions 照片列表查询选项
type ListPhotosOptions struct {
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id title created_at updated_at taken_at view_count like_count" validate:"omitempty,oneof=id title created_at updated_at taken_at view_count like_count"`
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
AlbumID *uint `json:"album_id" form:"album_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
TagIDs []uint `json:"tag_ids" form:"tag_ids" binding:"omitempty" validate:"omitempty"`
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
IsFeatured *bool `json:"is_featured" form:"is_featured" binding:"omitempty"`
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
Year *int `json:"year" form:"year" binding:"omitempty,min=1900,max=2100" validate:"omitempty,min=1900,max=2100"`
Month *int `json:"month" form:"month" binding:"omitempty,min=1,max=12" validate:"omitempty,min=1,max=12"`
}
// SearchPhotosOptions 照片搜索选项
type SearchPhotosOptions struct {
Query string `json:"query" form:"query" binding:"required,min=1" validate:"required,min=1"`
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=relevance created_at view_count like_count" validate:"omitempty,oneof=relevance created_at view_count like_count"`
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
CategoryID *uint `json:"category_id" form:"category_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
TagIDs []uint `json:"tag_ids" form:"tag_ids" binding:"omitempty" validate:"omitempty"`
UserID *uint `json:"user_id" form:"user_id" binding:"omitempty,min=1" validate:"omitempty,min=1"`
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
}
// PhotoListResponse 照片列表响应
type PhotoListResponse struct {
Photos []PhotoListItem `json:"photos"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// ProcessPhotoOptions 照片处理选项
type ProcessPhotoOptions struct {
GenerateThumbnails bool `json:"generate_thumbnails"`
ThumbnailSizes []string `json:"thumbnail_sizes"`
ExtractEXIF bool `json:"extract_exif"`
GenerateHash bool `json:"generate_hash"`
OptimizeSize bool `json:"optimize_size"`
WatermarkEnabled bool `json:"watermark_enabled"`
}
// PhotoStatsResponse 照片统计响应
type PhotoStatsResponse struct {
Total int64 `json:"total"`
Published int64 `json:"published"`
Private int64 `json:"private"`
Featured int64 `json:"featured"`
TotalViews int64 `json:"total_views"`
TotalLikes int64 `json:"total_likes"`
TotalDownloads int64 `json:"total_downloads"`
FileSize int64 `json:"file_size"`
CategoryCounts map[string]int64 `json:"category_counts"`
TagCounts map[string]int64 `json:"tag_counts"`
Recent []PhotoListItem `json:"recent"`
Popular []PhotoListItem `json:"popular"`
}
// ConvertToPhotoResponse 将照片实体转换为响应DTO
func ConvertToPhotoResponse(photo *entity.Photo) *PhotoResponse {
if photo == nil {
return nil
}
response := &PhotoResponse{
ID: photo.ID,
Title: photo.Title,
Description: photo.Description,
Filename: photo.Filename,
OriginalURL: photo.OriginalURL,
ThumbnailURL: photo.ThumbnailURL,
MediumURL: photo.MediumURL,
LargeURL: photo.LargeURL,
FileSize: photo.FileSize,
MimeType: photo.MimeType,
Width: photo.Width,
Height: photo.Height,
AspectRatio: photo.GetAspectRatio(),
// EXIF
CameraMake: photo.CameraMake,
CameraModel: photo.CameraModel,
LensModel: photo.LensModel,
FocalLength: photo.FocalLength,
Aperture: photo.Aperture,
ShutterSpeed: photo.ShutterSpeed,
ISO: photo.ISO,
TakenAt: photo.TakenAt,
// 地理位置
LocationName: photo.LocationName,
Latitude: photo.Latitude,
Longitude: photo.Longitude,
// 关联
UserID: photo.UserID,
AlbumID: photo.AlbumID,
CategoryID: photo.CategoryID,
// 状态
IsPublic: photo.IsPublic,
IsFeatured: photo.IsFeatured,
ViewCount: photo.ViewCount,
LikeCount: photo.LikeCount,
DownloadCount: photo.DownloadCount,
SortOrder: photo.SortOrder,
CreatedAt: photo.CreatedAt,
UpdatedAt: photo.UpdatedAt,
}
// 转换关联对象
if photo.User.ID != 0 {
response.User = ConvertToUserResponse(&photo.User)
}
if photo.Category != nil {
response.Category = ConvertToCategoryResponse(photo.Category)
}
return response
}
// ConvertToPhotoListItem 将照片实体转换为列表项DTO
func ConvertToPhotoListItem(photo *entity.Photo) PhotoListItem {
return PhotoListItem{
ID: photo.ID,
Title: photo.Title,
ThumbnailURL: photo.ThumbnailURL,
Width: photo.Width,
Height: photo.Height,
AspectRatio: photo.GetAspectRatio(),
IsPublic: photo.IsPublic,
IsFeatured: photo.IsFeatured,
ViewCount: photo.ViewCount,
LikeCount: photo.LikeCount,
CreatedAt: photo.CreatedAt,
}
}

View File

@ -0,0 +1,135 @@
package dto
import (
"time"
"photography-backend/internal/model/entity"
)
// CreateTagRequest 创建标签请求
type CreateTagRequest struct {
Name string `json:"name" binding:"required,min=1,max=50" validate:"required,min=1,max=50"`
Color string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
}
// UpdateTagRequest 更新标签请求
type UpdateTagRequest struct {
Name *string `json:"name" binding:"omitempty,min=1,max=50" validate:"omitempty,min=1,max=50"`
Color *string `json:"color" binding:"omitempty,len=7" validate:"omitempty,len=7"`
IsActive *bool `json:"is_active" binding:"omitempty"`
}
// TagResponse 标签响应
type TagResponse struct {
ID uint `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
UseCount int `json:"use_count"`
IsActive bool `json:"is_active"`
IsPopular bool `json:"is_popular"`
PhotoCount int64 `json:"photo_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// TagListItem 标签列表项(简化版)
type TagListItem struct {
ID uint `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
UseCount int `json:"use_count"`
IsActive bool `json:"is_active"`
IsPopular bool `json:"is_popular"`
}
// ListTagsOptions 标签列表查询选项
type ListTagsOptions struct {
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id name use_count created_at updated_at" validate:"omitempty,oneof=id name use_count created_at updated_at"`
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
Popular bool `json:"popular" form:"popular" binding:"omitempty"`
}
// TagListResponse 标签列表响应
type TagListResponse struct {
Tags []TagResponse `json:"tags"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// TagCloudResponse 标签云响应
type TagCloudResponse struct {
Tags []TagCloudItem `json:"tags"`
}
// TagCloudItem 标签云项
type TagCloudItem struct {
ID uint `json:"id"`
Name string `json:"name"`
Color string `json:"color"`
UseCount int `json:"use_count"`
Weight int `json:"weight"` // 1-10 的权重,用于控制标签大小
}
// TagStatsResponse 标签统计响应
type TagStatsResponse struct {
Total int64 `json:"total"`
Active int64 `json:"active"`
Popular []TagResponse `json:"popular"`
PhotoCounts map[string]int64 `json:"photo_counts"`
Recent []TagResponse `json:"recent"`
}
// ConvertToTagResponse 将标签实体转换为响应DTO
func ConvertToTagResponse(tag *entity.Tag) *TagResponse {
if tag == nil {
return nil
}
return &TagResponse{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
UseCount: tag.UseCount,
IsActive: tag.IsActive,
IsPopular: tag.IsPopular(),
CreatedAt: tag.CreatedAt,
UpdatedAt: tag.UpdatedAt,
}
}
// ConvertToTagListItem 将标签实体转换为列表项DTO
func ConvertToTagListItem(tag *entity.Tag) TagListItem {
return TagListItem{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
UseCount: tag.UseCount,
IsActive: tag.IsActive,
IsPopular: tag.IsPopular(),
}
}
// ConvertToTagCloudItem 将标签实体转换为标签云项
func ConvertToTagCloudItem(tag *entity.Tag, maxUseCount int) TagCloudItem {
// 计算权重1-10
weight := 1
if maxUseCount > 0 {
weight = int(float64(tag.UseCount)/float64(maxUseCount)*9) + 1
if weight > 10 {
weight = 10
}
}
return TagCloudItem{
ID: tag.ID,
Name: tag.Name,
Color: tag.Color,
UseCount: tag.UseCount,
Weight: weight,
}
}

View File

@ -0,0 +1,148 @@
package dto
import (
"time"
"photography-backend/internal/model/entity"
)
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50" validate:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email" validate:"required,email"`
Password string `json:"password" binding:"required,min=6" validate:"required,min=6"`
Name string `json:"name" binding:"max=100" validate:"max=100"`
Role entity.UserRole `json:"role" binding:"omitempty,oneof=user admin photographer" validate:"omitempty,oneof=user admin photographer"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=50" validate:"omitempty,min=3,max=50"`
Email *string `json:"email" binding:"omitempty,email" validate:"omitempty,email"`
Name *string `json:"name" binding:"omitempty,max=100" validate:"omitempty,max=100"`
Avatar *string `json:"avatar" binding:"omitempty,url" validate:"omitempty,url"`
Bio *string `json:"bio" binding:"omitempty,max=1000" validate:"omitempty,max=1000"`
Website *string `json:"website" binding:"omitempty,url" validate:"omitempty,url"`
Location *string `json:"location" binding:"omitempty,max=100" validate:"omitempty,max=100"`
IsActive *bool `json:"is_active" binding:"omitempty"`
IsPublic *bool `json:"is_public" binding:"omitempty"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required" validate:"required"`
NewPassword string `json:"new_password" binding:"required,min=6" validate:"required,min=6"`
}
// UserResponse 用户响应
type UserResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Email string `json:"email"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
Website string `json:"website"`
Location string `json:"location"`
Role entity.UserRole `json:"role"`
IsActive bool `json:"is_active"`
IsPublic bool `json:"is_public"`
EmailVerified bool `json:"email_verified"`
LastLogin *time.Time `json:"last_login"`
LoginCount int `json:"login_count"`
PhotoCount int64 `json:"photo_count"`
AlbumCount int64 `json:"album_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// UserProfileResponse 用户档案响应(公开信息)
type UserProfileResponse struct {
ID uint `json:"id"`
Username string `json:"username"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Bio string `json:"bio"`
Website string `json:"website"`
Location string `json:"location"`
Role entity.UserRole `json:"role"`
PhotoCount int64 `json:"photo_count"`
AlbumCount int64 `json:"album_count"`
CreatedAt time.Time `json:"created_at"`
}
// ListUsersOptions 用户列表查询选项
type ListUsersOptions struct {
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
Sort string `json:"sort" form:"sort" binding:"omitempty,oneof=id username email created_at updated_at" validate:"omitempty,oneof=id username email created_at updated_at"`
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
Role entity.UserRole `json:"role" form:"role" binding:"omitempty,oneof=user admin photographer" validate:"omitempty,oneof=user admin photographer"`
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
IsPublic *bool `json:"is_public" form:"is_public" binding:"omitempty"`
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
}
// UserListResponse 用户列表响应
type UserListResponse struct {
Users []UserResponse `json:"users"`
Total int64 `json:"total"`
Page int `json:"page"`
Limit int `json:"limit"`
}
// UserStatsResponse 用户统计响应
type UserStatsResponse struct {
Total int64 `json:"total"`
Active int64 `json:"active"`
Inactive int64 `json:"inactive"`
Verified int64 `json:"verified"`
Unverified int64 `json:"unverified"`
RoleCounts map[entity.UserRole]int64 `json:"role_counts"`
RecentLogins []UserResponse `json:"recent_logins"`
}
// ConvertToUserResponse 将用户实体转换为响应DTO
func ConvertToUserResponse(user *entity.User) *UserResponse {
if user == nil {
return nil
}
return &UserResponse{
ID: user.ID,
Username: user.Username,
Email: user.Email,
Name: user.Name,
Avatar: user.Avatar,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
Role: user.Role,
IsActive: user.IsActive,
IsPublic: user.IsPublic,
EmailVerified: user.EmailVerified,
LastLogin: user.LastLogin,
LoginCount: user.LoginCount,
CreatedAt: user.CreatedAt,
UpdatedAt: user.UpdatedAt,
}
}
// ConvertToUserProfile 将用户实体转换为公开档案DTO
func ConvertToUserProfile(user *entity.User) *UserProfileResponse {
if user == nil {
return nil
}
return &UserProfileResponse{
ID: user.ID,
Username: user.Username,
Name: user.Name,
Avatar: user.Avatar,
Bio: user.Bio,
Website: user.Website,
Location: user.Location,
Role: user.Role,
CreatedAt: user.CreatedAt,
}
}

View File

@ -0,0 +1,84 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// Album 相册实体
type Album struct {
ID uint `json:"id" gorm:"primarykey"`
Title string `json:"title" gorm:"not null;size:200"`
Description string `json:"description" gorm:"type:text"`
Slug string `json:"slug" gorm:"uniqueIndex;size:255"`
CoverPhotoID *uint `json:"cover_photo_id" gorm:"index"`
UserID uint `json:"user_id" gorm:"not null;index"`
CategoryID *uint `json:"category_id" gorm:"index"`
IsPublic bool `json:"is_public" gorm:"default:true;index"`
IsFeatured bool `json:"is_featured" gorm:"default:false;index"`
Password string `json:"-" gorm:"size:255"` // 私密相册密码
ViewCount int `json:"view_count" gorm:"default:0;index"`
LikeCount int `json:"like_count" gorm:"default:0;index"`
PhotoCount int `json:"photo_count" gorm:"default:0;index"`
SortOrder int `json:"sort_order" gorm:"default:0;index"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
// 关联
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Category *Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
CoverPhoto *Photo `json:"cover_photo,omitempty" gorm:"foreignKey:CoverPhotoID"`
Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:AlbumID"`
}
// AlbumStats 相册统计信息
type AlbumStats struct {
Total int64 `json:"total"` // 总相册数
Published int64 `json:"published"` // 已发布相册数
Private int64 `json:"private"` // 私有相册数
Featured int64 `json:"featured"` // 精选相册数
TotalViews int64 `json:"total_views"` // 总浏览量
TotalLikes int64 `json:"total_likes"` // 总点赞数
TotalPhotos int64 `json:"total_photos"` // 总照片数
CategoryCounts map[string]int64 `json:"category_counts"` // 各分类相册数量
}
// TableName 指定表名
func (Album) TableName() string {
return "albums"
}
// HasPassword 检查是否设置了密码
func (a *Album) HasPassword() bool {
return a.Password != ""
}
// IsEmpty 检查相册是否为空
func (a *Album) IsEmpty() bool {
return a.PhotoCount == 0
}
// CanViewBy 检查指定用户是否可以查看相册
func (a *Album) CanViewBy(user *User) bool {
// 公开相册
if a.IsPublic && !a.HasPassword() {
return true
}
// 相册所有者或管理员
if user != nil && (user.ID == a.UserID || user.IsAdmin()) {
return true
}
return false
}
// CanEditBy 检查指定用户是否可以编辑相册
func (a *Album) CanEditBy(user *User) bool {
if user == nil {
return false
}
return user.ID == a.UserID || user.IsAdmin()
}

View File

@ -0,0 +1,131 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// Category 分类实体
type Category struct {
ID uint `json:"id" gorm:"primarykey"`
Name string `json:"name" gorm:"not null;size:100"`
Slug string `json:"slug" gorm:"uniqueIndex;not null;size:100"`
Description string `json:"description" gorm:"type:text"`
ParentID *uint `json:"parent_id" gorm:"index"`
Color string `json:"color" gorm:"default:#3b82f6;size:7"`
CoverImage string `json:"cover_image" gorm:"size:500"`
Sort int `json:"sort" gorm:"default:0;index"`
SortOrder int `json:"sort_order" gorm:"default:0;index"`
IsActive bool `json:"is_active" gorm:"default:true;index"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
// 关联
Parent *Category `json:"parent,omitempty" gorm:"foreignKey:ParentID"`
Children []Category `json:"children,omitempty" gorm:"foreignKey:ParentID"`
Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:CategoryID"`
Albums []Album `json:"albums,omitempty" gorm:"foreignKey:CategoryID"`
PhotoCount int64 `json:"photo_count" gorm:"-"` // 照片数量,不存储在数据库中
}
// CategoryTree 分类树结构(用于前端显示)
type CategoryTree struct {
ID uint `json:"id"`
Name string `json:"name"`
Slug string `json:"slug"`
Description string `json:"description"`
ParentID *uint `json:"parent_id"`
Color string `json:"color"`
CoverImage string `json:"cover_image"`
Sort int `json:"sort"`
SortOrder int `json:"sort_order"`
IsActive bool `json:"is_active"`
PhotoCount int64 `json:"photo_count"`
Children []CategoryTree `json:"children"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
}
// CategoryStats 分类统计信息
type CategoryStats struct {
Total int64 `json:"total"` // 总分类数
Active int64 `json:"active"` // 活跃分类数
TopLevel int64 `json:"top_level"` // 顶级分类数
TotalCategories int64 `json:"total_categories"` // 总分类数(别名)
MaxLevel int64 `json:"max_level"` // 最大层级
FeaturedCount int64 `json:"featured_count"` // 特色分类数
PhotoCounts map[string]int64 `json:"photo_counts"` // 各分类照片数量
}
// CategoryListParams 分类列表查询参数
type CategoryListParams struct {
Page int `json:"page" form:"page"`
Limit int `json:"limit" form:"limit"`
Search string `json:"search" form:"search"`
ParentID *uint `json:"parent_id" form:"parent_id"`
IsActive *bool `json:"is_active" form:"is_active"`
IncludeStats bool `json:"include_stats" form:"include_stats"`
SortBy string `json:"sort_by" form:"sort_by"`
Order string `json:"order" form:"order"`
}
// CreateCategoryRequest 创建分类请求
type CreateCategoryRequest struct {
Name string `json:"name" binding:"required,max=100"`
Slug string `json:"slug" binding:"required,max=100"`
Description string `json:"description" binding:"max=500"`
ParentID *uint `json:"parent_id"`
Color string `json:"color" binding:"max=7"`
CoverImage string `json:"cover_image" binding:"max=500"`
Sort int `json:"sort"`
}
// UpdateCategoryRequest 更新分类请求
type UpdateCategoryRequest struct {
Name *string `json:"name" binding:"omitempty,max=100"`
Slug *string `json:"slug" binding:"omitempty,max=100"`
Description *string `json:"description" binding:"max=500"`
ParentID *uint `json:"parent_id"`
Color *string `json:"color" binding:"omitempty,max=7"`
CoverImage *string `json:"cover_image" binding:"omitempty,max=500"`
SortOrder *int `json:"sort_order"`
IsActive *bool `json:"is_active"`
}
// ReorderCategoriesRequest 重新排序分类请求
type ReorderCategoriesRequest struct {
ParentID *uint `json:"parent_id"`
CategoryIDs []uint `json:"category_ids" binding:"required,min=1"`
}
// GenerateSlugRequest 生成slug请求
type GenerateSlugRequest struct {
Name string `json:"name" binding:"required,max=100"`
}
// GenerateSlugResponse 生成slug响应
type GenerateSlugResponse struct {
Slug string `json:"slug"`
}
// SuccessResponse 成功响应
type SuccessResponse struct {
Message string `json:"message"`
}
// TableName 指定表名
func (Category) TableName() string {
return "categories"
}
// IsTopLevel 检查是否为顶级分类
func (c *Category) IsTopLevel() bool {
return c.ParentID == nil
}
// HasChildren 检查是否有子分类
func (c *Category) HasChildren() bool {
return len(c.Children) > 0
}

View File

@ -0,0 +1,244 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// Photo 照片实体
type Photo struct {
ID uint `json:"id" gorm:"primarykey"`
Title string `json:"title" gorm:"not null;size:200"`
Description string `json:"description" gorm:"type:text"`
Filename string `json:"filename" gorm:"not null;size:255"`
OriginalFilename string `json:"original_filename" gorm:"not null;size:255"`
UniqueFilename string `json:"unique_filename" gorm:"not null;size:255"`
FilePath string `json:"file_path" gorm:"not null;size:500"`
OriginalURL string `json:"original_url" gorm:"not null;size:500"`
ThumbnailURL string `json:"thumbnail_url" gorm:"size:500"`
MediumURL string `json:"medium_url" gorm:"size:500"`
LargeURL string `json:"large_url" gorm:"size:500"`
FileSize int64 `json:"file_size"`
MimeType string `json:"mime_type" gorm:"size:100"`
Width int `json:"width"`
Height int `json:"height"`
Status PhotoStatus `json:"status" gorm:"default:active;size:20"`
// EXIF 信息
Camera string `json:"camera" gorm:"size:100"`
Lens string `json:"lens" gorm:"size:100"`
CameraMake string `json:"camera_make" gorm:"size:100"`
CameraModel string `json:"camera_model" gorm:"size:100"`
LensModel string `json:"lens_model" gorm:"size:100"`
FocalLength *float64 `json:"focal_length" gorm:"type:decimal(5,2)"`
Aperture *float64 `json:"aperture" gorm:"type:decimal(3,1)"`
ShutterSpeed string `json:"shutter_speed" gorm:"size:20"`
ISO *int `json:"iso"`
TakenAt *time.Time `json:"taken_at"`
// 地理位置信息
LocationName string `json:"location_name" gorm:"size:200"`
Latitude *float64 `json:"latitude" gorm:"type:decimal(10,8)"`
Longitude *float64 `json:"longitude" gorm:"type:decimal(11,8)"`
// 关联
UserID uint `json:"user_id" gorm:"not null;index"`
AlbumID *uint `json:"album_id" gorm:"index"`
CategoryID *uint `json:"category_id" gorm:"index"`
// 状态和统计
IsPublic bool `json:"is_public" gorm:"default:true;index"`
IsFeatured bool `json:"is_featured" gorm:"default:false;index"`
ViewCount int `json:"view_count" gorm:"default:0;index"`
LikeCount int `json:"like_count" gorm:"default:0;index"`
DownloadCount int `json:"download_count" gorm:"default:0"`
SortOrder int `json:"sort_order" gorm:"default:0;index"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
// 关联对象
User User `json:"user,omitempty" gorm:"foreignKey:UserID"`
Album *Album `json:"album,omitempty" gorm:"foreignKey:AlbumID"`
Category *Category `json:"category,omitempty" gorm:"foreignKey:CategoryID"`
Tags []Tag `json:"tags,omitempty" gorm:"many2many:photo_tags;"`
}
// PhotoStatus 照片状态枚举
type PhotoStatus string
const (
PhotoStatusActive PhotoStatus = "active"
PhotoStatusInactive PhotoStatus = "inactive"
PhotoStatusDeleted PhotoStatus = "deleted"
PhotoStatusDraft PhotoStatus = "draft"
PhotoStatusPrivate PhotoStatus = "private"
)
// Status constants for compatibility
const (
StatusPublished PhotoStatus = "active"
StatusDraft PhotoStatus = "draft"
StatusArchived PhotoStatus = "inactive"
)
// PhotoTag 照片标签关联表
type PhotoTag struct {
PhotoID uint `json:"photo_id" gorm:"primaryKey"`
TagID uint `json:"tag_id" gorm:"primaryKey"`
}
// PhotoStats 照片统计信息
type PhotoStats struct {
Total int64 `json:"total"` // 总照片数
Published int64 `json:"published"` // 已发布照片数
Private int64 `json:"private"` // 私有照片数
Featured int64 `json:"featured"` // 精选照片数
TotalViews int64 `json:"total_views"` // 总浏览量
TotalLikes int64 `json:"total_likes"` // 总点赞数
TotalDownloads int64 `json:"total_downloads"` // 总下载数
FileSize int64 `json:"file_size"` // 总文件大小
TotalSize int64 `json:"total_size"` // 总大小(别名)
ThisMonth int64 `json:"this_month"` // 本月新增
Today int64 `json:"today"` // 今日新增
StatusStats map[string]int64 `json:"status_stats"` // 状态统计
CategoryCounts map[string]int64 `json:"category_counts"` // 各分类照片数量
TagCounts map[string]int64 `json:"tag_counts"` // 各标签照片数量
}
// PhotoListParams 照片列表查询参数
type PhotoListParams struct {
Page int `json:"page" form:"page"`
Limit int `json:"limit" form:"limit"`
Sort string `json:"sort" form:"sort"`
Order string `json:"order" form:"order"`
Search string `json:"search" form:"search"`
UserID *uint `json:"user_id" form:"user_id"`
Status *PhotoStatus `json:"status" form:"status"`
CategoryID *uint `json:"category_id" form:"category_id"`
TagID *uint `json:"tag_id" form:"tag_id"`
DateFrom *time.Time `json:"date_from" form:"date_from"`
DateTo *time.Time `json:"date_to" form:"date_to"`
}
// CreatePhotoRequest 创建照片请求
type CreatePhotoRequest struct {
Title string `json:"title" binding:"required,max=200"`
Description string `json:"description" binding:"max=1000"`
OriginalFilename string `json:"original_filename"`
FileSize int64 `json:"file_size"`
Status string `json:"status" binding:"oneof=active inactive"`
Camera string `json:"camera" binding:"max=100"`
Lens string `json:"lens" binding:"max=100"`
ISO *int `json:"iso"`
Aperture *float64 `json:"aperture"`
ShutterSpeed string `json:"shutter_speed" binding:"max=20"`
FocalLength *float64 `json:"focal_length"`
TakenAt *time.Time `json:"taken_at"`
CategoryIDs []uint `json:"category_ids"`
TagIDs []uint `json:"tag_ids"`
}
// UpdatePhotoRequest 更新照片请求
type UpdatePhotoRequest struct {
Title *string `json:"title" binding:"omitempty,max=200"`
Description *string `json:"description" binding:"max=1000"`
Status *string `json:"status" binding:"omitempty,oneof=active inactive"`
Camera *string `json:"camera" binding:"omitempty,max=100"`
Lens *string `json:"lens" binding:"omitempty,max=100"`
ISO *int `json:"iso"`
Aperture *float64 `json:"aperture"`
ShutterSpeed *string `json:"shutter_speed" binding:"omitempty,max=20"`
FocalLength *float64 `json:"focal_length"`
TakenAt *time.Time `json:"taken_at"`
CategoryIDs *[]uint `json:"category_ids"`
TagIDs *[]uint `json:"tag_ids"`
}
// BatchUpdatePhotosRequest 批量更新照片请求
type BatchUpdatePhotosRequest struct {
Status *string `json:"status" binding:"omitempty,oneof=active inactive"`
CategoryIDs *[]uint `json:"category_ids"`
TagIDs *[]uint `json:"tag_ids"`
}
// PhotoFormat 照片格式
type PhotoFormat struct {
ID uint `json:"id" gorm:"primarykey"`
PhotoID uint `json:"photo_id" gorm:"not null;index"`
Format string `json:"format" gorm:"not null;size:20"` // jpg, png, webp
Quality int `json:"quality" gorm:"not null"` // 1-100
Width int `json:"width" gorm:"not null"`
Height int `json:"height" gorm:"not null"`
FileSize int64 `json:"file_size" gorm:"not null"`
URL string `json:"url" gorm:"not null;size:500"`
CreatedAt time.Time `json:"created_at"`
}
func (PhotoFormat) TableName() string {
return "photo_formats"
}
// TableName 指定表名
func (Photo) TableName() string {
return "photos"
}
// TableName 指定关联表名
func (PhotoTag) TableName() string {
return "photo_tags"
}
// GetAspectRatio 获取宽高比
func (p *Photo) GetAspectRatio() float64 {
if p.Height == 0 {
return 0
}
return float64(p.Width) / float64(p.Height)
}
// IsLandscape 是否为横向
func (p *Photo) IsLandscape() bool {
return p.Width > p.Height
}
// IsPortrait 是否为纵向
func (p *Photo) IsPortrait() bool {
return p.Width < p.Height
}
// IsSquare 是否为正方形
func (p *Photo) IsSquare() bool {
return p.Width == p.Height
}
// HasLocation 是否有地理位置信息
func (p *Photo) HasLocation() bool {
return p.Latitude != nil && p.Longitude != nil
}
// HasEXIF 是否有EXIF信息
func (p *Photo) HasEXIF() bool {
return p.CameraMake != "" || p.CameraModel != "" || p.TakenAt != nil
}
// GetDisplayURL 获取显示URL根据尺寸
func (p *Photo) GetDisplayURL(size string) string {
switch size {
case "thumbnail":
if p.ThumbnailURL != "" {
return p.ThumbnailURL
}
case "medium":
if p.MediumURL != "" {
return p.MediumURL
}
case "large":
if p.LargeURL != "" {
return p.LargeURL
}
}
return p.OriginalURL
}

View File

@ -0,0 +1,99 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// Tag 标签实体
type Tag struct {
ID uint `json:"id" gorm:"primarykey"`
Name string `json:"name" gorm:"uniqueIndex;not null;size:50"`
Slug string `json:"slug" gorm:"uniqueIndex;not null;size:50"`
Description string `json:"description" gorm:"type:text"`
Color string `json:"color" gorm:"default:#6b7280;size:7"`
UseCount int `json:"use_count" gorm:"default:0;index"`
PhotoCount int64 `json:"photo_count" gorm:"-"` // 不存储在数据库中
IsActive bool `json:"is_active" gorm:"default:true;index"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
// 关联
Photos []Photo `json:"photos,omitempty" gorm:"many2many:photo_tags;"`
}
// TagStats 标签统计信息
type TagStats struct {
Total int64 `json:"total"` // 总标签数
Active int64 `json:"active"` // 活跃标签数
Used int64 `json:"used"` // 已使用标签数
Unused int64 `json:"unused"` // 未使用标签数
AvgPhotosPerTag float64 `json:"avg_photos_per_tag"` // 平均每个标签的照片数
Popular []Tag `json:"popular"` // 热门标签
PhotoCounts map[string]int64 `json:"photo_counts"` // 各标签照片数量
}
// 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"`
}
// CreateTagRequest 创建标签请求
type CreateTagRequest struct {
Name string `json:"name" binding:"required,max=50"`
Slug string `json:"slug" binding:"required,max=50"`
Description string `json:"description" binding:"max=500"`
Color string `json:"color" binding:"max=7"`
}
// UpdateTagRequest 更新标签请求
type UpdateTagRequest struct {
Name *string `json:"name" binding:"omitempty,max=50"`
Slug *string `json:"slug" binding:"omitempty,max=50"`
Description *string `json:"description" binding:"max=500"`
Color *string `json:"color" binding:"omitempty,max=7"`
IsActive *bool `json:"is_active"`
}
// TagWithCount 带有照片数量的标签
type TagWithCount struct {
Tag
PhotoCount int64 `json:"photo_count"`
}
// TagCloudItem 标签云项目
type TagCloudItem struct {
Name string `json:"name"`
Slug string `json:"slug"`
Color string `json:"color"`
Count int64 `json:"count"`
}
// TableName 指定表名
func (Tag) TableName() string {
return "tags"
}
// IsPopular 检查是否为热门标签(使用次数 >= 10
func (t *Tag) IsPopular() bool {
return t.UseCount >= 10
}
// IncrementUseCount 增加使用次数
func (t *Tag) IncrementUseCount() {
t.UseCount++
}
// DecrementUseCount 减少使用次数
func (t *Tag) DecrementUseCount() {
if t.UseCount > 0 {
t.UseCount--
}
}

View File

@ -0,0 +1,150 @@
package entity
import (
"time"
"gorm.io/gorm"
)
// UserRole 用户角色枚举
type UserRole string
const (
UserRoleUser UserRole = "user"
UserRoleAdmin UserRole = "admin"
UserRolePhotographer UserRole = "photographer"
)
// User 用户实体
type User struct {
ID uint `json:"id" gorm:"primarykey"`
Username string `json:"username" gorm:"uniqueIndex;not null;size:50"`
Email string `json:"email" gorm:"uniqueIndex;not null;size:100"`
Password string `json:"-" gorm:"not null;size:255"`
Name string `json:"name" gorm:"size:100"`
Avatar string `json:"avatar" gorm:"size:500"`
Bio string `json:"bio" gorm:"type:text"`
Website string `json:"website" gorm:"size:200"`
Location string `json:"location" gorm:"size:100"`
Role UserRole `json:"role" gorm:"default:user;size:20"`
IsActive bool `json:"is_active" gorm:"default:true"`
IsPublic bool `json:"is_public" gorm:"default:true"`
EmailVerified bool `json:"email_verified" gorm:"default:false"`
LastLogin *time.Time `json:"last_login"`
LoginCount int `json:"login_count" gorm:"default:0"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `json:"deleted_at" gorm:"index"`
// 关联
Photos []Photo `json:"photos,omitempty" gorm:"foreignKey:UserID"`
Albums []Album `json:"albums,omitempty" gorm:"foreignKey:UserID"`
}
// TableName 指定表名
func (User) TableName() string {
return "users"
}
// IsAdmin 检查是否为管理员
func (u *User) IsAdmin() bool {
return u.Role == UserRoleAdmin
}
// IsPhotographer 检查是否为摄影师
func (u *User) IsPhotographer() bool {
return u.Role == UserRolePhotographer || u.Role == UserRoleAdmin
}
// CanManagePhoto 检查是否可以管理指定照片
func (u *User) CanManagePhoto(photo *Photo) bool {
return u.ID == photo.UserID || u.IsAdmin()
}
// CanManageAlbum 检查是否可以管理指定相册
func (u *User) CanManageAlbum(album *Album) bool {
return u.ID == album.UserID || u.IsAdmin()
}
// UserStats 用户统计信息
type UserStats struct {
Total int64 `json:"total"` // 总用户数
Active int64 `json:"active"` // 活跃用户数
ThisMonth int64 `json:"this_month"` // 本月新增
Today int64 `json:"today"` // 今日新增
RoleStats map[string]int64 `json:"role_stats"` // 角色统计
}
// CreateUserRequest 创建用户请求
type CreateUserRequest struct {
Username string `json:"username" binding:"required,min=3,max=50"`
Email string `json:"email" binding:"required,email"`
Password string `json:"password" binding:"required,min=6"`
Name string `json:"name" binding:"max=100"`
Role UserRole `json:"role" binding:"oneof=user admin photographer"`
}
// UpdateUserRequest 更新用户请求
type UpdateUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
Name *string `json:"name" binding:"omitempty,max=100"`
Avatar *string `json:"avatar" binding:"omitempty,max=500"`
Bio *string `json:"bio" binding:"omitempty,max=1000"`
Website *string `json:"website" binding:"omitempty,max=200"`
Location *string `json:"location" binding:"omitempty,max=100"`
Role *UserRole `json:"role" binding:"omitempty,oneof=user admin photographer"`
IsActive *bool `json:"is_active"`
}
// UpdateCurrentUserRequest 更新当前用户请求
type UpdateCurrentUserRequest struct {
Username *string `json:"username" binding:"omitempty,min=3,max=50"`
Email *string `json:"email" binding:"omitempty,email"`
Name *string `json:"name" binding:"omitempty,max=100"`
Avatar *string `json:"avatar" binding:"omitempty,max=500"`
Bio *string `json:"bio" binding:"omitempty,max=1000"`
Website *string `json:"website" binding:"omitempty,max=200"`
Location *string `json:"location" binding:"omitempty,max=100"`
}
// ChangePasswordRequest 修改密码请求
type ChangePasswordRequest struct {
OldPassword string `json:"old_password" binding:"required"`
NewPassword string `json:"new_password" binding:"required,min=6"`
}
// UserStatus 用户状态
type UserStatus string
const (
UserStatusActive UserStatus = "active"
UserStatusInactive UserStatus = "inactive"
UserStatusBanned UserStatus = "banned"
UserStatusPending UserStatus = "pending"
)
// UserListParams 用户列表查询参数
type UserListParams struct {
Page int `json:"page"`
Limit int `json:"limit"`
Sort string `json:"sort"`
Order string `json:"order"`
Role *UserRole `json:"role"`
Status *UserStatus `json:"status"`
Search string `json:"search"`
CreatedFrom *time.Time `json:"created_from"`
CreatedTo *time.Time `json:"created_to"`
LastLoginFrom *time.Time `json:"last_login_from"`
LastLoginTo *time.Time `json:"last_login_to"`
}
// UserGlobalStats 全局用户统计信息
type UserGlobalStats struct {
Total int64 `json:"total"`
Active int64 `json:"active"`
Admins int64 `json:"admins"`
Editors int64 `json:"editors"`
Users int64 `json:"users"`
MonthlyRegistrations int64 `json:"monthly_registrations"`
}

View File

@ -0,0 +1,90 @@
package request
// PaginationRequest 分页请求
type PaginationRequest struct {
Page int `json:"page" form:"page" binding:"omitempty,min=1" validate:"omitempty,min=1"`
Limit int `json:"limit" form:"limit" binding:"omitempty,min=1,max=100" validate:"omitempty,min=1,max=100"`
}
// SortRequest 排序请求
type SortRequest struct {
Sort string `json:"sort" form:"sort" binding:"omitempty" validate:"omitempty"`
Order string `json:"order" form:"order" binding:"omitempty,oneof=asc desc" validate:"omitempty,oneof=asc desc"`
}
// SearchRequest 搜索请求
type SearchRequest struct {
Search string `json:"search" form:"search" binding:"omitempty,max=100" validate:"omitempty,max=100"`
}
// BaseListRequest 基础列表请求
type BaseListRequest struct {
PaginationRequest
SortRequest
SearchRequest
}
// IDRequest ID 请求
type IDRequest struct {
ID uint `json:"id" uri:"id" binding:"required,min=1" validate:"required,min=1"`
}
// SlugRequest Slug 请求
type SlugRequest struct {
Slug string `json:"slug" uri:"slug" binding:"required,min=1" validate:"required,min=1"`
}
// BulkIDsRequest 批量 ID 请求
type BulkIDsRequest struct {
IDs []uint `json:"ids" binding:"required,min=1" validate:"required,min=1"`
}
// StatusRequest 状态请求
type StatusRequest struct {
IsActive *bool `json:"is_active" form:"is_active" binding:"omitempty"`
}
// TimeRangeRequest 时间范围请求
type TimeRangeRequest struct {
StartDate string `json:"start_date" form:"start_date" binding:"omitempty" validate:"omitempty,datetime=2006-01-02"`
EndDate string `json:"end_date" form:"end_date" binding:"omitempty" validate:"omitempty,datetime=2006-01-02"`
}
// GetDefaultPagination 获取默认分页参数
func (p *PaginationRequest) GetDefaultPagination() (int, int) {
page := p.Page
if page <= 0 {
page = 1
}
limit := p.Limit
if limit <= 0 {
limit = 20
}
if limit > 100 {
limit = 100
}
return page, limit
}
// GetDefaultSort 获取默认排序参数
func (s *SortRequest) GetDefaultSort(defaultSort, defaultOrder string) (string, string) {
sort := s.Sort
if sort == "" {
sort = defaultSort
}
order := s.Order
if order == "" {
order = defaultOrder
}
return sort, order
}
// GetOffset 计算偏移量
func (p *PaginationRequest) GetOffset() int {
page, limit := p.GetDefaultPagination()
return (page - 1) * limit
}