feat: 实现后端和管理后台基础架构
## 后端架构 (Go + Gin + GORM) - ✅ 完整的分层架构 (API/Service/Repository) - ✅ PostgreSQL数据库设计和迁移脚本 - ✅ JWT认证系统和权限控制 - ✅ 用户、照片、分类、标签等核心模型 - ✅ 中间件系统 (认证、CORS、日志) - ✅ 配置管理和环境变量支持 - ✅ 结构化日志和错误处理 - ✅ Makefile构建和部署脚本 ## 管理后台架构 (React + TypeScript) - ✅ Vite + React 18 + TypeScript现代化架构 - ✅ 路由系统和状态管理 (Zustand + TanStack Query) - ✅ 基于Radix UI的组件库基础 - ✅ 认证流程和权限控制 - ✅ 响应式设计和主题系统 ## 数据库设计 - ✅ 用户表 (角色权限、认证信息) - ✅ 照片表 (元数据、EXIF、状态管理) - ✅ 分类表 (层级结构、封面图片) - ✅ 标签表 (使用统计、标签云) - ✅ 关联表 (照片-标签多对多) ## 技术特点 - 🚀 高性能: Gin框架 + GORM ORM - 🔐 安全: JWT认证 + 密码加密 + 权限控制 - 📊 监控: 结构化日志 + 健康检查 - 🎨 现代化: React 18 + TypeScript + Vite - 📱 响应式: Tailwind CSS + Radix UI 参考文档: docs/development/saved-docs/
This commit is contained in:
85
backend/internal/models/category.go
Normal file
85
backend/internal/models/category.go
Normal file
@ -0,0 +1,85 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Category 分类模型
|
||||
type Category struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:100;not null" json:"name"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Parent *Category `gorm:"foreignKey:ParentID" json:"parent,omitempty"`
|
||||
Children []Category `gorm:"foreignKey:ParentID" json:"children,omitempty"`
|
||||
Color string `gorm:"size:7;default:#3b82f6" json:"color"`
|
||||
CoverImage string `gorm:"size:500" json:"cover_image"`
|
||||
Sort int `gorm:"default:0" json:"sort"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
PhotoCount int `gorm:"-" json:"photo_count"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 返回分类表名
|
||||
func (Category) TableName() string {
|
||||
return "categories"
|
||||
}
|
||||
|
||||
// CreateCategoryRequest 创建分类请求
|
||||
type CreateCategoryRequest struct {
|
||||
Name string `json:"name" binding:"required,max=100"`
|
||||
Description string `json:"description"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Color string `json:"color" binding:"omitempty,hexcolor"`
|
||||
CoverImage string `json:"cover_image" binding:"omitempty,max=500"`
|
||||
Sort int `json:"sort"`
|
||||
}
|
||||
|
||||
// UpdateCategoryRequest 更新分类请求
|
||||
type UpdateCategoryRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||
Description *string `json:"description"`
|
||||
ParentID *uint `json:"parent_id"`
|
||||
Color *string `json:"color" binding:"omitempty,hexcolor"`
|
||||
CoverImage *string `json:"cover_image" binding:"omitempty,max=500"`
|
||||
Sort *int `json:"sort"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// CategoryListParams 分类列表查询参数
|
||||
type CategoryListParams struct {
|
||||
IncludeStats bool `form:"include_stats"`
|
||||
IncludeTree bool `form:"include_tree"`
|
||||
ParentID uint `form:"parent_id"`
|
||||
IsActive bool `form:"is_active"`
|
||||
}
|
||||
|
||||
// CategoryResponse 分类响应
|
||||
type CategoryResponse struct {
|
||||
*Category
|
||||
}
|
||||
|
||||
// CategoryTreeNode 分类树节点
|
||||
type CategoryTreeNode struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
PhotoCount int `json:"photo_count"`
|
||||
Children []CategoryTreeNode `json:"children"`
|
||||
}
|
||||
|
||||
// CategoryListResponse 分类列表响应
|
||||
type CategoryListResponse struct {
|
||||
Categories []CategoryResponse `json:"categories"`
|
||||
Tree []CategoryTreeNode `json:"tree,omitempty"`
|
||||
Stats *CategoryStats `json:"stats,omitempty"`
|
||||
}
|
||||
|
||||
// CategoryStats 分类统计
|
||||
type CategoryStats struct {
|
||||
TotalCategories int `json:"total_categories"`
|
||||
MaxLevel int `json:"max_level"`
|
||||
FeaturedCount int `json:"featured_count"`
|
||||
}
|
||||
99
backend/internal/models/photo.go
Normal file
99
backend/internal/models/photo.go
Normal file
@ -0,0 +1,99 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Photo 照片模型
|
||||
type Photo struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Title string `gorm:"size:255;not null" json:"title"`
|
||||
Description string `gorm:"type:text" json:"description"`
|
||||
Filename string `gorm:"size:255;not null" json:"filename"`
|
||||
FilePath string `gorm:"size:500;not null" json:"file_path"`
|
||||
FileSize int64 `json:"file_size"`
|
||||
MimeType string `gorm:"size:100" json:"mime_type"`
|
||||
Width int `json:"width"`
|
||||
Height int `json:"height"`
|
||||
CategoryID uint `json:"category_id"`
|
||||
Category *Category `gorm:"foreignKey:CategoryID" json:"category,omitempty"`
|
||||
Tags []Tag `gorm:"many2many:photo_tags;" json:"tags,omitempty"`
|
||||
EXIF string `gorm:"type:jsonb" json:"exif"`
|
||||
TakenAt *time.Time `json:"taken_at"`
|
||||
Location string `gorm:"size:255" json:"location"`
|
||||
IsPublic bool `gorm:"default:true" json:"is_public"`
|
||||
Status string `gorm:"size:20;default:draft" json:"status"`
|
||||
ViewCount int `gorm:"default:0" json:"view_count"`
|
||||
LikeCount int `gorm:"default:0" json:"like_count"`
|
||||
UserID uint `gorm:"not null" json:"user_id"`
|
||||
User *User `gorm:"foreignKey:UserID" json:"user,omitempty"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 返回照片表名
|
||||
func (Photo) TableName() string {
|
||||
return "photos"
|
||||
}
|
||||
|
||||
// PhotoStatus 照片状态常量
|
||||
const (
|
||||
StatusDraft = "draft"
|
||||
StatusPublished = "published"
|
||||
StatusArchived = "archived"
|
||||
)
|
||||
|
||||
// CreatePhotoRequest 创建照片请求
|
||||
type CreatePhotoRequest struct {
|
||||
Title string `json:"title" binding:"required,max=255"`
|
||||
Description string `json:"description"`
|
||||
CategoryID uint `json:"category_id" binding:"required"`
|
||||
TagIDs []uint `json:"tag_ids"`
|
||||
TakenAt *time.Time `json:"taken_at"`
|
||||
Location string `json:"location" binding:"max=255"`
|
||||
IsPublic *bool `json:"is_public"`
|
||||
Status string `json:"status" binding:"omitempty,oneof=draft published archived"`
|
||||
}
|
||||
|
||||
// UpdatePhotoRequest 更新照片请求
|
||||
type UpdatePhotoRequest struct {
|
||||
Title *string `json:"title" binding:"omitempty,max=255"`
|
||||
Description *string `json:"description"`
|
||||
CategoryID *uint `json:"category_id"`
|
||||
TagIDs []uint `json:"tag_ids"`
|
||||
TakenAt *time.Time `json:"taken_at"`
|
||||
Location *string `json:"location" binding:"omitempty,max=255"`
|
||||
IsPublic *bool `json:"is_public"`
|
||||
Status *string `json:"status" binding:"omitempty,oneof=draft published archived"`
|
||||
}
|
||||
|
||||
// PhotoListParams 照片列表查询参数
|
||||
type PhotoListParams struct {
|
||||
Page int `form:"page,default=1" binding:"min=1"`
|
||||
Limit int `form:"limit,default=20" binding:"min=1,max=100"`
|
||||
CategoryID uint `form:"category_id"`
|
||||
TagID uint `form:"tag_id"`
|
||||
UserID uint `form:"user_id"`
|
||||
Status string `form:"status" binding:"omitempty,oneof=draft published archived"`
|
||||
Search string `form:"search"`
|
||||
SortBy string `form:"sort_by,default=created_at" binding:"omitempty,oneof=created_at taken_at title view_count like_count"`
|
||||
SortOrder string `form:"sort_order,default=desc" binding:"omitempty,oneof=asc desc"`
|
||||
Year int `form:"year"`
|
||||
Month int `form:"month" binding:"min=1,max=12"`
|
||||
}
|
||||
|
||||
// PhotoResponse 照片响应
|
||||
type PhotoResponse struct {
|
||||
*Photo
|
||||
ThumbnailURLs map[string]string `json:"thumbnail_urls,omitempty"`
|
||||
}
|
||||
|
||||
// PhotoListResponse 照片列表响应
|
||||
type PhotoListResponse struct {
|
||||
Photos []PhotoResponse `json:"photos"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
}
|
||||
95
backend/internal/models/tag.go
Normal file
95
backend/internal/models/tag.go
Normal file
@ -0,0 +1,95 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// Tag 标签模型
|
||||
type Tag struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Name string `gorm:"size:50;not null;unique" json:"name"`
|
||||
Color string `gorm:"size:7;default:#6b7280" json:"color"`
|
||||
UseCount int `gorm:"default:0" json:"use_count"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 返回标签表名
|
||||
func (Tag) TableName() string {
|
||||
return "tags"
|
||||
}
|
||||
|
||||
// CreateTagRequest 创建标签请求
|
||||
type CreateTagRequest struct {
|
||||
Name string `json:"name" binding:"required,max=50"`
|
||||
Color string `json:"color" binding:"omitempty,hexcolor"`
|
||||
}
|
||||
|
||||
// UpdateTagRequest 更新标签请求
|
||||
type UpdateTagRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=50"`
|
||||
Color *string `json:"color" binding:"omitempty,hexcolor"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// TagListParams 标签列表查询参数
|
||||
type TagListParams struct {
|
||||
Page int `form:"page,default=1" binding:"min=1"`
|
||||
Limit int `form:"limit,default=50" binding:"min=1,max=100"`
|
||||
Search string `form:"search"`
|
||||
SortBy string `form:"sort_by,default=use_count" binding:"omitempty,oneof=use_count name created_at"`
|
||||
SortOrder string `form:"sort_order,default=desc" binding:"omitempty,oneof=asc desc"`
|
||||
IsActive bool `form:"is_active"`
|
||||
}
|
||||
|
||||
// TagSuggestionsParams 标签建议查询参数
|
||||
type TagSuggestionsParams struct {
|
||||
Query string `form:"q" binding:"required"`
|
||||
Limit int `form:"limit,default=10" binding:"min=1,max=20"`
|
||||
}
|
||||
|
||||
// TagResponse 标签响应
|
||||
type TagResponse struct {
|
||||
*Tag
|
||||
MatchScore float64 `json:"match_score,omitempty"`
|
||||
}
|
||||
|
||||
// TagListResponse 标签列表响应
|
||||
type TagListResponse struct {
|
||||
Tags []TagResponse `json:"tags"`
|
||||
Total int64 `json:"total"`
|
||||
Page int `json:"page"`
|
||||
Limit int `json:"limit"`
|
||||
Groups *TagGroups `json:"groups,omitempty"`
|
||||
}
|
||||
|
||||
// TagGroups 标签分组
|
||||
type TagGroups struct {
|
||||
Style TagGroup `json:"style"`
|
||||
Subject TagGroup `json:"subject"`
|
||||
Technique TagGroup `json:"technique"`
|
||||
Location TagGroup `json:"location"`
|
||||
}
|
||||
|
||||
// TagGroup 标签组
|
||||
type TagGroup struct {
|
||||
Name string `json:"name"`
|
||||
Count int `json:"count"`
|
||||
}
|
||||
|
||||
// TagCloudItem 标签云项目
|
||||
type TagCloudItem struct {
|
||||
ID uint `json:"id"`
|
||||
Name string `json:"name"`
|
||||
UseCount int `json:"use_count"`
|
||||
RelativeSize int `json:"relative_size"`
|
||||
Color string `json:"color"`
|
||||
}
|
||||
|
||||
// TagCloudResponse 标签云响应
|
||||
type TagCloudResponse struct {
|
||||
Tags []TagCloudItem `json:"tags"`
|
||||
}
|
||||
76
backend/internal/models/user.go
Normal file
76
backend/internal/models/user.go
Normal file
@ -0,0 +1,76 @@
|
||||
package models
|
||||
|
||||
import (
|
||||
"time"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// User 用户模型
|
||||
type User struct {
|
||||
ID uint `gorm:"primaryKey" json:"id"`
|
||||
Username string `gorm:"size:50;not null;unique" json:"username"`
|
||||
Email string `gorm:"size:100;not null;unique" json:"email"`
|
||||
Password string `gorm:"size:255;not null" json:"-"`
|
||||
Name string `gorm:"size:100" json:"name"`
|
||||
Avatar string `gorm:"size:500" json:"avatar"`
|
||||
Role string `gorm:"size:20;default:user" json:"role"`
|
||||
IsActive bool `gorm:"default:true" json:"is_active"`
|
||||
LastLogin *time.Time `json:"last_login"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
|
||||
}
|
||||
|
||||
// TableName 返回用户表名
|
||||
func (User) TableName() string {
|
||||
return "users"
|
||||
}
|
||||
|
||||
// UserRole 用户角色常量
|
||||
const (
|
||||
RoleUser = "user"
|
||||
RoleEditor = "editor"
|
||||
RoleAdmin = "admin"
|
||||
)
|
||||
|
||||
// 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 string `json:"role" binding:"omitempty,oneof=user editor admin"`
|
||||
}
|
||||
|
||||
// UpdateUserRequest 更新用户请求
|
||||
type UpdateUserRequest struct {
|
||||
Name *string `json:"name" binding:"omitempty,max=100"`
|
||||
Avatar *string `json:"avatar" binding:"omitempty,max=500"`
|
||||
IsActive *bool `json:"is_active"`
|
||||
}
|
||||
|
||||
// UpdatePasswordRequest 更新密码请求
|
||||
type UpdatePasswordRequest struct {
|
||||
OldPassword string `json:"old_password" binding:"required"`
|
||||
NewPassword string `json:"new_password" binding:"required,min=6"`
|
||||
}
|
||||
|
||||
// LoginRequest 登录请求
|
||||
type LoginRequest struct {
|
||||
Username string `json:"username" binding:"required"`
|
||||
Password string `json:"password" binding:"required"`
|
||||
}
|
||||
|
||||
// LoginResponse 登录响应
|
||||
type LoginResponse struct {
|
||||
AccessToken string `json:"access_token"`
|
||||
RefreshToken string `json:"refresh_token"`
|
||||
TokenType string `json:"token_type"`
|
||||
ExpiresIn int64 `json:"expires_in"`
|
||||
User *User `json:"user"`
|
||||
}
|
||||
|
||||
// RefreshTokenRequest 刷新令牌请求
|
||||
type RefreshTokenRequest struct {
|
||||
RefreshToken string `json:"refresh_token" binding:"required"`
|
||||
}
|
||||
Reference in New Issue
Block a user