feat: 完成后端服务核心业务逻辑实现
Some checks failed
部署后端服务 / 🧪 测试后端 (push) Failing after 10m41s
部署后端服务 / 🚀 构建并部署 (push) Has been skipped
部署后端服务 / 🔄 回滚部署 (push) Has been skipped

## 主要功能
-  用户认证模块 (登录/注册/JWT)
-  照片管理模块 (上传/查询/分页/搜索)
-  分类管理模块 (创建/查询/分页)
-  用户管理模块 (用户列表/分页查询)
-  健康检查接口

## 技术实现
- 基于 go-zero v1.8.0 标准架构
- Handler → Logic → Model 三层架构
- SQLite/PostgreSQL 数据库支持
- JWT 认证机制
- bcrypt 密码加密
- 统一响应格式
- 自定义模型方法 (分页/搜索)

## API 接口
- POST /api/v1/auth/login - 用户登录
- POST /api/v1/auth/register - 用户注册
- GET /api/v1/health - 健康检查
- GET /api/v1/photos - 照片列表
- POST /api/v1/photos - 上传照片
- GET /api/v1/categories - 分类列表
- POST /api/v1/categories - 创建分类
- GET /api/v1/users - 用户列表

## 配置完成
- 开发环境配置 (SQLite)
- 生产环境支持 (PostgreSQL)
- JWT 认证配置
- 文件上传配置
- Makefile 构建脚本

服务已验证可正常构建和启动。
This commit is contained in:
xujiang
2025-07-10 16:12:12 +08:00
parent 39a42695d3
commit 317dc170f9
145 changed files with 3669 additions and 20721 deletions

31
backend/cmd/api/main.go Normal file
View File

@ -0,0 +1,31 @@
package main
import (
"flag"
"fmt"
"photography-backend/internal/config"
"photography-backend/internal/handler"
"photography-backend/internal/svc"
"github.com/zeromicro/go-zero/core/conf"
"github.com/zeromicro/go-zero/rest"
)
var configFile = flag.String("f", "etc/photographyapi-api.yaml", "the config file")
func main() {
flag.Parse()
var c config.Config
conf.MustLoad(*configFile, &c)
server := rest.MustNewServer(c.RestConf)
defer server.Stop()
ctx := svc.NewServiceContext(c)
handler.RegisterHandlers(server, ctx)
fmt.Printf("Starting server at %s:%d...\n", c.Host, c.Port)
server.Start()
}

View File

@ -1,363 +0,0 @@
# Server Entry Point - CLAUDE.md
本文件为 Claude Code 在服务器入口模块中工作时提供指导。
## 🎯 模块概览
这是后端服务的启动入口模块,负责应用初始化、配置加载、依赖注入和服务启动。
### 主要职责
- 🚀 应用启动和生命周期管理
- ⚙️ 配置文件加载和环境变量解析
- 🔌 依赖注入和组件初始化
- 🌐 HTTP 服务器启动和路由注册
- 📊 数据库连接和迁移管理
- 🔧 优雅关闭和资源清理
### 推荐文件结构Go 风格)
```
cmd/server/
├── CLAUDE.md # 📋 当前文件 - 服务启动指导
├── main.go # 🚀 统一入口点
├── app.go # 🏗️ 应用构建器
├── config.go # ⚙️ 配置加载器
├── dependencies.go # 🔌 依赖注入容器
└── server.go # 🌐 HTTP 服务器管理
```
## 🚀 启动模式说明
### 生产模式 (`main.go`)
```go
// 特点:
// - PostgreSQL 数据库
// - Redis 缓存
// - 完整的中间件栈
// - 生产级日志配置
// - 健康检查端点
```
**适用场景**: 生产环境、集成测试、性能测试
### 开发模式 (`main_with_db.go`)
```go
// 特点:
// - SQLite 数据库
// - 内存缓存
// - 开发友好的日志
// - 热重载支持
// - 调试工具集成
```
**适用场景**: 本地开发、单元测试、功能验证
### Mock 模式 (`simple_main.go`)
```go
// 特点:
// - 内存数据存储
// - 固定响应数据
// - 极快启动速度
// - 无数据库依赖
// - 最小化配置
```
**适用场景**: 前端开发、API 测试、演示环境
## 🔧 配置管理
### 配置文件层次
```
configs/
├── config.yaml # 基础配置
├── config.dev.yaml # 开发环境覆盖
└── config.prod.yaml # 生产环境覆盖
```
### 环境变量优先级
```
环境变量 > 配置文件 > 默认值
```
### 配置结构
```go
type Config struct {
Server ServerConfig `yaml:"server"`
Database DatabaseConfig `yaml:"database"`
Cache CacheConfig `yaml:"cache"`
Storage StorageConfig `yaml:"storage"`
Auth AuthConfig `yaml:"auth"`
Log LogConfig `yaml:"log"`
}
```
## 🏗️ 依赖注入
### 依赖层次
```
main.go
├── 📋 Config Service
├── 📊 Database Service
├── 💾 Cache Service
├── 📁 Storage Service
├── 🔐 Auth Service
├── 📚 Repository Layer
├── 🏢 Service Layer
└── 🌐 Handler Layer
```
### 依赖注入模式
```go
// 1. 配置加载
config := config.LoadConfig()
// 2. 基础服务初始化
db := database.NewConnection(config.Database)
cache := cache.NewRedisClient(config.Cache)
storage := storage.NewService(config.Storage)
// 3. 仓储层初始化
userRepo := repository.NewUserRepository(db)
photoRepo := repository.NewPhotoRepository(db)
// 4. 服务层初始化
userService := service.NewUserService(userRepo, cache)
photoService := service.NewPhotoService(photoRepo, storage)
// 5. 处理器层初始化
userHandler := handler.NewUserHandler(userService)
photoHandler := handler.NewPhotoHandler(photoService)
// 6. 路由设置
router := gin.New()
v1 := router.Group("/api/v1")
{
v1.POST("/users", userHandler.Create)
v1.GET("/users", userHandler.List)
v1.GET("/photos", photoHandler.List)
v1.POST("/photos", photoHandler.Create)
}
```
## 🔄 启动流程
### 标准启动流程
1. **配置加载**: 解析配置文件和环境变量
2. **日志初始化**: 设置日志级别和输出格式
3. **数据库连接**: 建立数据库连接池
4. **缓存连接**: 初始化 Redis 连接
5. **服务注册**: 注册各层服务
6. **中间件设置**: 配置认证、日志、CORS 等中间件
7. **路由注册**: 注册所有 API 路由
8. **健康检查**: 启动健康检查端点
9. **服务启动**: 启动 HTTP 服务器
10. **优雅关闭**: 处理关闭信号
### 启动命令
```bash
# 生产模式
go run cmd/server/main.go
# 开发模式
go run cmd/server/main_with_db.go
# Mock 模式
go run cmd/server/simple_main.go
# 使用 Makefile
make dev # 开发模式
make dev-simple # Mock 模式
make prod # 生产模式
```
## 📊 健康检查
### 健康检查端点
```go
GET /health
GET /health/ready
GET /health/live
```
### 健康检查内容
- **数据库连接状态**
- **缓存服务状态**
- **存储服务状态**
- **外部服务状态**
- **系统资源状态**
### 响应格式
```json
{
"status": "healthy",
"timestamp": "2024-01-01T00:00:00Z",
"version": "1.0.0",
"checks": {
"database": "healthy",
"cache": "healthy",
"storage": "healthy"
}
}
```
## 🔧 优雅关闭
### 关闭信号处理
```go
// 监听关闭信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
// 等待关闭信号
<-quit
log.Println("Shutting down server...")
// 设置关闭超时
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
// 优雅关闭服务器
if err := srv.Shutdown(ctx); err != nil {
log.Fatal("Server forced to shutdown:", err)
}
```
### 关闭流程
1. **停止接收新请求**
2. **等待现有请求完成**
3. **关闭数据库连接**
4. **关闭缓存连接**
5. **清理临时资源**
6. **记录关闭日志**
## 📋 环境变量配置
### 数据库配置
```bash
# PostgreSQL
DB_HOST=localhost
DB_PORT=5432
DB_NAME=photography
DB_USER=postgres
DB_PASSWORD=your_password
DB_SSL_MODE=disable
# SQLite (开发模式)
DB_TYPE=sqlite
DB_PATH=./photography.db
```
### 缓存配置
```bash
# Redis
REDIS_HOST=localhost
REDIS_PORT=6379
REDIS_PASSWORD=
REDIS_DB=0
```
### 认证配置
```bash
# JWT
JWT_SECRET=your_jwt_secret
JWT_EXPIRES_IN=24h
JWT_REFRESH_EXPIRES_IN=168h
```
### 文件存储配置
```bash
# 本地存储
STORAGE_TYPE=local
STORAGE_PATH=./uploads
STORAGE_MAX_SIZE=10MB
# S3 存储
STORAGE_TYPE=s3
AWS_ACCESS_KEY_ID=your_access_key
AWS_SECRET_ACCESS_KEY=your_secret_key
AWS_BUCKET_NAME=your_bucket
AWS_REGION=us-east-1
```
## 🧪 开发调试
### 调试模式启动
```bash
# 开启调试模式
export GIN_MODE=debug
export LOG_LEVEL=debug
# 启动服务
go run cmd/server/main_with_db.go
```
### 调试工具
- **pprof**: 性能分析
- **gin-debug**: 路由调试
- **hot-reload**: 代码热重载
- **swagger**: API 文档
### 调试端点
```
GET /debug/pprof/ # 性能分析
GET /debug/routes # 路由列表
GET /debug/vars # 运行时变量
GET /swagger/* # API 文档
```
## 📊 监控指标
### 内置指标
- **HTTP 请求数量**
- **HTTP 响应时间**
- **数据库连接数**
- **缓存命中率**
- **内存使用量**
- **CPU 使用率**
### 指标端点
```
GET /metrics # Prometheus 指标
GET /stats # 应用统计信息
```
## 🔍 常见问题
### 启动失败
1. **端口被占用**: 检查端口配置,使用 `lsof -i :8080` 查看
2. **配置文件错误**: 检查 YAML 语法,验证配置项
3. **数据库连接失败**: 检查数据库服务状态和连接配置
4. **权限问题**: 检查文件读写权限
### 性能问题
1. **启动慢**: 检查数据库连接池配置
2. **内存泄漏**: 使用 pprof 分析内存使用
3. **连接超时**: 调整超时配置和连接池大小
### 日志问题
1. **日志文件过大**: 配置日志轮转
2. **日志格式混乱**: 统一日志格式配置
3. **敏感信息泄露**: 配置敏感信息过滤
## 💡 最佳实践
### 配置管理
- 使用环境变量覆盖敏感配置
- 配置验证和默认值设置
- 配置变更的版本控制
### 错误处理
- 统一错误响应格式
- 详细的错误日志记录
- 适当的错误码设计
### 安全考虑
- 敏感信息不在日志中输出
- 配置文件权限控制
- 环境变量加密存储
### 性能优化
- 合理的超时配置
- 连接池大小调优
- 资源及时释放
本模块为整个应用的入口,确保配置正确、启动流程清晰是项目成功的关键。在开发过程中,优先使用开发模式进行功能验证,在集成测试时使用生产模式。

View File

@ -1,147 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"os/signal"
"syscall"
"context"
"net/http"
"time"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
"photography-backend/internal/config"
"photography-backend/internal/database"
"photography-backend/internal/repository/postgres"
"photography-backend/internal/service"
"photography-backend/internal/service/auth"
"photography-backend/internal/api/handlers"
"photography-backend/internal/api/middleware"
"photography-backend/internal/api/routes"
"photography-backend/pkg/logger"
)
func main() {
// 加载配置
cfg, err := config.LoadConfig("configs/config.yaml")
if err != nil {
log.Fatalf("Failed to load config: %v", err)
}
// 初始化日志
zapLogger, err := logger.InitLogger(&cfg.Logger)
if err != nil {
log.Fatalf("Failed to initialize logger: %v", err)
}
defer zapLogger.Sync()
// 初始化数据库
db, err := database.New(cfg)
if err != nil {
zapLogger.Fatal("Failed to connect to database", zap.Error(err))
}
defer db.Close()
// 自动迁移数据库
if err := db.AutoMigrate(); err != nil {
zapLogger.Fatal("Failed to migrate database", zap.Error(err))
}
// 填充种子数据
if err := db.Seed(); err != nil {
zapLogger.Warn("Failed to seed database", zap.Error(err))
}
// 初始化仓库
userRepo := postgres.NewUserRepository(db.GetDB())
photoRepo := postgres.NewPhotoRepository(db.GetDB())
categoryRepo := postgres.NewCategoryRepository(db.GetDB())
tagRepo := postgres.NewTagRepository(db.GetDB())
// 初始化服务
jwtService := auth.NewJWTService(&cfg.JWT)
authService := auth.NewAuthService(userRepo, jwtService)
photoService := service.NewPhotoService(photoRepo)
categoryService := service.NewCategoryService(categoryRepo)
tagService := service.NewTagService(tagRepo)
userService := service.NewUserService(userRepo)
// 初始化处理器
authHandler := handlers.NewAuthHandler(authService)
photoHandler := handlers.NewPhotoHandler(photoService)
categoryHandler := handlers.NewCategoryHandler(categoryService)
tagHandler := handlers.NewTagHandler(tagService)
userHandler := handlers.NewUserHandler(userService)
// 初始化中间件
authMiddleware := middleware.NewAuthMiddleware(jwtService)
// 设置Gin模式
if cfg.IsProduction() {
gin.SetMode(gin.ReleaseMode)
}
// 创建Gin引擎
r := gin.New()
// 添加中间件
r.Use(middleware.RequestIDMiddleware())
r.Use(middleware.LoggerMiddleware(zapLogger))
r.Use(middleware.CORSMiddleware(&cfg.CORS))
r.Use(gin.Recovery())
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"timestamp": time.Now().Unix(),
"version": cfg.App.Version,
})
})
// 设置路由
routes.SetupRoutes(r, &routes.Handlers{
AuthHandler: authHandler,
PhotoHandler: photoHandler,
CategoryHandler: categoryHandler,
TagHandler: tagHandler,
UserHandler: userHandler,
}, authMiddleware, zapLogger)
// 创建HTTP服务器
server := &http.Server{
Addr: cfg.GetServerAddr(),
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
// 启动服务器
go func() {
zapLogger.Info("Starting server", zap.String("addr", server.Addr))
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
zapLogger.Fatal("Failed to start server", zap.Error(err))
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
zapLogger.Info("Shutting down server...")
// 优雅关闭
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
zapLogger.Fatal("Server forced to shutdown", zap.Error(err))
}
zapLogger.Info("Server exited")
}

View File

@ -1,683 +0,0 @@
package main
import (
"context"
"fmt"
"log"
"net/http"
"os"
"os/signal"
"syscall"
"time"
"github.com/gin-gonic/gin"
"photography-backend/internal/config"
"photography-backend/internal/database"
"photography-backend/internal/model/entity"
"photography-backend/internal/model/dto"
"photography-backend/internal/service/upload"
"photography-backend/internal/service/auth"
"photography-backend/internal/api/handlers"
"photography-backend/internal/api/middleware"
)
func main() {
// 加载配置
configPath := os.Getenv("CONFIG_PATH")
if configPath == "" {
configPath = "configs/config.yaml"
}
cfg, err := config.LoadConfig(configPath)
if err != nil {
log.Fatalf("加载配置失败: %v", err)
}
// 初始化数据库
if err := database.InitDatabase(cfg); err != nil {
log.Fatalf("数据库初始化失败: %v", err)
}
// 自动迁移数据表
if err := database.AutoMigrate(); err != nil {
log.Fatalf("数据库迁移失败: %v", err)
}
// 创建服务
uploadService := upload.NewUploadService(cfg)
jwtService := auth.NewJWTService(&cfg.JWT)
// 创建处理器
uploadHandler := handlers.NewUploadHandler(uploadService)
// 创建中间件
authMiddleware := middleware.NewAuthMiddleware(jwtService)
// 设置 Gin 模式
if cfg.IsProduction() {
gin.SetMode(gin.ReleaseMode)
}
// 创建 Gin 引擎
r := gin.Default()
// 添加 CORS 中间件
r.Use(func(c *gin.Context) {
c.Header("Access-Control-Allow-Origin", "*")
c.Header("Access-Control-Allow-Methods", "GET, POST, PUT, DELETE, OPTIONS")
c.Header("Access-Control-Allow-Headers", "Content-Type, Authorization, X-Requested-With")
if c.Request.Method == "OPTIONS" {
c.AbortWithStatus(204)
return
}
c.Next()
})
// 健康检查
r.GET("/health", func(c *gin.Context) {
// 检查数据库连接
if err := database.HealthCheck(); err != nil {
c.JSON(http.StatusServiceUnavailable, gin.H{
"status": "error",
"message": "数据库连接失败",
"error": err.Error(),
})
return
}
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"timestamp": time.Now().Unix(),
"version": cfg.App.Version,
"database": "connected",
})
})
// 数据库统计
r.GET("/stats", func(c *gin.Context) {
stats := database.GetStats()
c.JSON(http.StatusOK, gin.H{
"database_stats": stats,
})
})
// 静态文件服务
r.Static("/uploads", cfg.Storage.Local.BasePath)
// 基础 API 路由
api := r.Group("/api/v1")
{
// 文件上传路由
upload := api.Group("/upload")
{
upload.POST("/photo", authMiddleware.RequireAuth(), uploadHandler.UploadPhoto)
upload.DELETE("/photo/:id", authMiddleware.RequireAuth(), uploadHandler.DeletePhoto)
upload.GET("/stats", authMiddleware.RequireAdmin(), uploadHandler.GetUploadStats)
}
// 认证路由
auth := api.Group("/auth")
{
auth.POST("/login", login)
auth.POST("/register", register)
auth.POST("/refresh", refreshToken)
}
// 用户相关
users := api.Group("/users")
{
users.GET("", getUsers)
users.POST("", createUser)
users.GET("/:id", getUser)
users.PUT("/:id", updateUser)
users.DELETE("/:id", deleteUser)
}
// 分类相关
categories := api.Group("/categories")
{
categories.GET("", getCategories)
categories.POST("", createCategory)
categories.GET("/:id", getCategory)
categories.PUT("/:id", updateCategory)
categories.DELETE("/:id", deleteCategory)
}
// 标签相关
tags := api.Group("/tags")
{
tags.GET("", getTags)
tags.POST("", createTag)
tags.GET("/:id", getTag)
tags.PUT("/:id", updateTag)
tags.DELETE("/:id", deleteTag)
}
// 相册相关
albums := api.Group("/albums")
{
albums.GET("", getAlbums)
albums.POST("", createAlbum)
albums.GET("/:id", getAlbum)
albums.PUT("/:id", updateAlbum)
albums.DELETE("/:id", deleteAlbum)
}
// 照片相关
photos := api.Group("/photos")
{
photos.GET("", getPhotos)
photos.POST("", createPhoto)
photos.GET("/:id", getPhoto)
photos.PUT("/:id", updatePhoto)
photos.DELETE("/:id", deletePhoto)
}
}
// 创建 HTTP 服务器
server := &http.Server{
Addr: cfg.GetServerAddr(),
Handler: r,
ReadTimeout: 10 * time.Second,
WriteTimeout: 10 * time.Second,
MaxHeaderBytes: 1 << 20,
}
// 启动服务器
go func() {
fmt.Printf("服务器启动在 %s\n", server.Addr)
if err := server.ListenAndServe(); err != nil && err != http.ErrServerClosed {
log.Fatalf("服务器启动失败: %v", err)
}
}()
// 等待中断信号
quit := make(chan os.Signal, 1)
signal.Notify(quit, syscall.SIGINT, syscall.SIGTERM)
<-quit
fmt.Println("正在关闭服务器...")
// 优雅关闭
ctx, cancel := context.WithTimeout(context.Background(), 30*time.Second)
defer cancel()
if err := server.Shutdown(ctx); err != nil {
log.Fatalf("服务器强制关闭: %v", err)
}
// 关闭数据库连接
if err := database.Close(); err != nil {
log.Printf("关闭数据库连接失败: %v", err)
}
fmt.Println("服务器已关闭")
}
// 认证相关处理函数
func login(c *gin.Context) {
var req dto.LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 查找用户
var user entity.User
db := database.GetDB()
// 可以使用用户名或邮箱登录
if err := db.Where("username = ? OR email = ?", req.Email, req.Email).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
// TODO: 验证密码 (需要集成bcrypt)
// 这里暂时跳过密码验证
// 生成JWT令牌 (简化实现)
c.JSON(http.StatusOK, gin.H{
"message": "登录成功",
"user": gin.H{
"id": user.ID,
"username": user.Username,
"email": user.Email,
"role": user.Role,
},
"token": "mock-jwt-token", // 实际项目中应该生成真实的JWT
})
}
func register(c *gin.Context) {
var req dto.CreateUserRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 检查用户名是否已存在
var existingUser entity.User
db := database.GetDB()
if err := db.Where("username = ? OR email = ?", req.Username, req.Email).First(&existingUser).Error; err == nil {
c.JSON(http.StatusConflict, gin.H{"error": "用户名或邮箱已存在"})
return
}
// 创建用户
user := entity.User{
Username: req.Username,
Email: req.Email,
Password: req.Password, // 实际项目中应该加密
Name: req.Name,
Role: entity.UserRoleUser,
IsActive: true,
}
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
// 清除密码
user.Password = ""
c.JSON(http.StatusCreated, gin.H{
"message": "注册成功",
"user": user,
})
}
func refreshToken(c *gin.Context) {
// TODO: 实现刷新令牌逻辑
c.JSON(http.StatusOK, gin.H{
"message": "刷新令牌成功",
"token": "new-mock-jwt-token",
})
}
// 用户 CRUD 操作
func getUsers(c *gin.Context) {
var users []entity.User
db := database.GetDB()
if err := db.Find(&users).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": users})
}
func createUser(c *gin.Context) {
var user entity.User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": user})
}
func getUser(c *gin.Context) {
id := c.Param("id")
var user entity.User
db := database.GetDB()
if err := db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
func updateUser(c *gin.Context) {
id := c.Param("id")
var user entity.User
db := database.GetDB()
if err := db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
func deleteUser(c *gin.Context) {
id := c.Param("id")
db := database.GetDB()
if err := db.Delete(&entity.User{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "用户删除成功"})
}
// 分类 CRUD 操作
func getCategories(c *gin.Context) {
var categories []entity.Category
db := database.GetDB()
if err := db.Find(&categories).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": categories})
}
func createCategory(c *gin.Context) {
var category entity.Category
if err := c.ShouldBindJSON(&category); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
if err := db.Create(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": category})
}
func getCategory(c *gin.Context) {
id := c.Param("id")
var category entity.Category
db := database.GetDB()
if err := db.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
func updateCategory(c *gin.Context) {
id := c.Param("id")
var category entity.Category
db := database.GetDB()
if err := db.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
if err := c.ShouldBindJSON(&category); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
func deleteCategory(c *gin.Context) {
id := c.Param("id")
db := database.GetDB()
if err := db.Delete(&entity.Category{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "分类删除成功"})
}
// 标签 CRUD 操作
func getTags(c *gin.Context) {
var tags []entity.Tag
db := database.GetDB()
if err := db.Find(&tags).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tags})
}
func createTag(c *gin.Context) {
var tag entity.Tag
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
if err := db.Create(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": tag})
}
func getTag(c *gin.Context) {
id := c.Param("id")
var tag entity.Tag
db := database.GetDB()
if err := db.First(&tag, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
func updateTag(c *gin.Context) {
id := c.Param("id")
var tag entity.Tag
db := database.GetDB()
if err := db.First(&tag, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
func deleteTag(c *gin.Context) {
id := c.Param("id")
db := database.GetDB()
if err := db.Delete(&entity.Tag{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "标签删除成功"})
}
// 相册 CRUD 操作
func getAlbums(c *gin.Context) {
var albums []entity.Album
db := database.GetDB()
if err := db.Find(&albums).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": albums})
}
func createAlbum(c *gin.Context) {
var album entity.Album
if err := c.ShouldBindJSON(&album); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
if err := db.Create(&album).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": album})
}
func getAlbum(c *gin.Context) {
id := c.Param("id")
var album entity.Album
db := database.GetDB()
if err := db.First(&album, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "相册不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": album})
}
func updateAlbum(c *gin.Context) {
id := c.Param("id")
var album entity.Album
db := database.GetDB()
if err := db.First(&album, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "相册不存在"})
return
}
if err := c.ShouldBindJSON(&album); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&album).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": album})
}
func deleteAlbum(c *gin.Context) {
id := c.Param("id")
db := database.GetDB()
if err := db.Delete(&entity.Album{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "相册删除成功"})
}
// 照片 CRUD 操作
func getPhotos(c *gin.Context) {
var photos []entity.Photo
db := database.GetDB()
if err := db.Find(&photos).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": photos})
}
func createPhoto(c *gin.Context) {
var photo entity.Photo
if err := c.ShouldBindJSON(&photo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
db := database.GetDB()
if err := db.Create(&photo).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusCreated, gin.H{"data": photo})
}
func getPhoto(c *gin.Context) {
id := c.Param("id")
var photo entity.Photo
db := database.GetDB()
if err := db.First(&photo, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": photo})
}
func updatePhoto(c *gin.Context) {
id := c.Param("id")
var photo entity.Photo
db := database.GetDB()
if err := db.First(&photo, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"})
return
}
if err := c.ShouldBindJSON(&photo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&photo).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"data": photo})
}
func deletePhoto(c *gin.Context) {
id := c.Param("id")
db := database.GetDB()
if err := db.Delete(&entity.Photo{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": err.Error()})
return
}
c.JSON(http.StatusOK, gin.H{"message": "照片删除成功"})
}

View File

@ -1,786 +0,0 @@
package main
import (
"fmt"
"io"
"log"
"net/http"
"os"
"path/filepath"
"strings"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
"golang.org/x/crypto/bcrypt"
"gorm.io/driver/sqlite"
"gorm.io/gorm"
"gorm.io/gorm/logger"
)
// User 用户模型
type User struct {
ID uint `gorm:"primaryKey" json:"id"`
Username string `gorm:"size:50;unique;not null" json:"username"`
Email string `gorm:"size:100;unique;not null" 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:"-"`
}
// Category 分类模型
type Category struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:100;not null" json:"name"`
Slug string `gorm:"size:100;unique;not null" json:"slug"`
Description string `gorm:"type:text" json:"description"`
ParentID *uint `json:"parent_id"`
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:"-"`
}
// Tag 标签模型
type Tag struct {
ID uint `gorm:"primaryKey" json:"id"`
Name string `gorm:"size:50;unique;not null" json:"name"`
Slug string `gorm:"size:50;unique;not null" json:"slug"`
Description string `gorm:"type:text" json:"description"`
Color string `gorm:"size:7;default:#10b981" json:"color"`
PhotoCount int `gorm:"-" json:"photo_count"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// Photo 照片模型
type Photo struct {
ID uint `gorm:"primaryKey" json:"id"`
Title string `gorm:"size:200;not null" json:"title"`
Description string `gorm:"type:text" json:"description"`
URL string `gorm:"size:500;not null" json:"url"`
ThumbnailURL string `gorm:"size:500" json:"thumbnail_url"`
OriginalFilename string `gorm:"size:255" json:"original_filename"`
FileSize int64 `json:"file_size"`
MimeType string `gorm:"size:100" json:"mime_type"`
Width int `json:"width"`
Height int `json:"height"`
Status string `gorm:"size:20;default:draft" json:"status"`
UserID uint `json:"user_id"`
User User `gorm:"foreignKey:UserID" json:"user,omitempty"`
Categories []Category `gorm:"many2many:photo_categories;" json:"categories,omitempty"`
Tags []Tag `gorm:"many2many:photo_tags;" json:"tags,omitempty"`
CreatedAt time.Time `json:"created_at"`
UpdatedAt time.Time `json:"updated_at"`
DeletedAt gorm.DeletedAt `gorm:"index" json:"-"`
}
// LoginRequest 登录请求
type LoginRequest struct {
Username string `json:"username" binding:"required"`
Password string `json:"password" binding:"required"`
}
// LoginResponse 登录响应
type LoginResponse struct {
User User `json:"user"`
AccessToken string `json:"access_token"`
ExpiresIn int64 `json:"expires_in"`
}
var db *gorm.DB
func main() {
// 初始化数据库
var err error
db, err = gorm.Open(sqlite.Open("photography.db"), &gorm.Config{
Logger: logger.Default.LogMode(logger.Info),
})
if err != nil {
log.Fatalf("Failed to connect to database: %v", err)
}
// 自动迁移
err = db.AutoMigrate(&User{}, &Category{}, &Tag{}, &Photo{})
if err != nil {
log.Fatalf("Failed to migrate database: %v", err)
}
// 初始化测试数据
initTestData()
// 创建 Gin 引擎
r := gin.Default()
// 配置 CORS
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000", "http://localhost:3002", "http://localhost:3003"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// 静态文件服务
r.Static("/uploads", "./uploads")
// 健康检查
r.GET("/health", healthHandler)
// API 路由组
v1 := r.Group("/api/v1")
{
// 认证相关
auth := v1.Group("/auth")
{
auth.POST("/login", loginHandler)
}
// 照片相关
v1.GET("/photos", getPhotosHandler)
v1.POST("/photos", createPhotoHandler)
v1.GET("/photos/:id", getPhotoHandler)
v1.PUT("/photos/:id", updatePhotoHandler)
v1.DELETE("/photos/:id", deletePhotoHandler)
// 文件上传
v1.POST("/upload", uploadFileHandler)
// 分类相关
v1.GET("/categories", getCategoriesHandler)
v1.POST("/categories", createCategoryHandler)
v1.GET("/categories/:id", getCategoryHandler)
v1.PUT("/categories/:id", updateCategoryHandler)
v1.DELETE("/categories/:id", deleteCategoryHandler)
// 标签相关
v1.GET("/tags", getTagsHandler)
v1.POST("/tags", createTagHandler)
v1.GET("/tags/:id", getTagHandler)
v1.PUT("/tags/:id", updateTagHandler)
v1.DELETE("/tags/:id", deleteTagHandler)
// 用户相关
v1.GET("/users", getUsersHandler)
v1.POST("/users", createUserHandler)
v1.GET("/users/:id", getUserHandler)
v1.PUT("/users/:id", updateUserHandler)
v1.DELETE("/users/:id", deleteUserHandler)
// 仪表板统计
v1.GET("/dashboard/stats", getDashboardStatsHandler)
}
// 启动服务器
log.Println("🚀 Backend server with SQLite database starting on :8080")
log.Fatal(r.Run(":8080"))
}
// 初始化测试数据
func initTestData() {
// 检查是否已有管理员用户
var count int64
db.Model(&User{}).Where("role = ?", "admin").Count(&count)
if count > 0 {
return // 已有管理员用户,跳过初始化
}
// 创建管理员用户
hashedPassword, _ := bcrypt.GenerateFromPassword([]byte("admin123"), bcrypt.DefaultCost)
admin := User{
Username: "admin",
Email: "admin@photography.com",
Password: string(hashedPassword),
Name: "管理员",
Role: "admin",
IsActive: true,
}
db.Create(&admin)
// 创建编辑用户
hashedPassword, _ = bcrypt.GenerateFromPassword([]byte("editor123"), bcrypt.DefaultCost)
editor := User{
Username: "editor",
Email: "editor@photography.com",
Password: string(hashedPassword),
Name: "编辑员",
Role: "editor",
IsActive: true,
}
db.Create(&editor)
// 创建分类
categories := []Category{
{Name: "风景", Slug: "landscape", Description: "自然风景摄影", Color: "#059669"},
{Name: "人像", Slug: "portrait", Description: "人物肖像摄影", Color: "#dc2626"},
{Name: "街拍", Slug: "street", Description: "街头摄影", Color: "#7c3aed"},
{Name: "建筑", Slug: "architecture", Description: "建筑摄影", Color: "#ea580c"},
{Name: "动物", Slug: "animal", Description: "动物摄影", Color: "#0891b2"},
}
for _, category := range categories {
db.Create(&category)
}
// 创建标签
tags := []Tag{
{Name: "日落", Slug: "sunset", Color: "#f59e0b"},
{Name: "城市", Slug: "city", Color: "#6b7280"},
{Name: "自然", Slug: "nature", Color: "#10b981"},
{Name: "黑白", Slug: "black-white", Color: "#374151"},
{Name: "夜景", Slug: "night", Color: "#1e40af"},
{Name: "微距", Slug: "macro", Color: "#7c2d12"},
{Name: "旅行", Slug: "travel", Color: "#be185d"},
{Name: "艺术", Slug: "art", Color: "#9333ea"},
}
for _, tag := range tags {
db.Create(&tag)
}
// 创建示例照片
photos := []Photo{
{
Title: "金色日落",
Description: "美丽的金色日落景象",
URL: "https://picsum.photos/800/600?random=1",
ThumbnailURL: "https://picsum.photos/300/200?random=1",
OriginalFilename: "sunset.jpg",
FileSize: 1024000,
MimeType: "image/jpeg",
Width: 800,
Height: 600,
Status: "published",
UserID: admin.ID,
},
{
Title: "城市夜景",
Description: "繁华的城市夜晚",
URL: "https://picsum.photos/800/600?random=2",
ThumbnailURL: "https://picsum.photos/300/200?random=2",
OriginalFilename: "citynight.jpg",
FileSize: 2048000,
MimeType: "image/jpeg",
Width: 800,
Height: 600,
Status: "published",
UserID: admin.ID,
},
{
Title: "人像写真",
Description: "优雅的人像摄影",
URL: "https://picsum.photos/600/800?random=3",
ThumbnailURL: "https://picsum.photos/300/400?random=3",
OriginalFilename: "portrait.jpg",
FileSize: 1536000,
MimeType: "image/jpeg",
Width: 600,
Height: 800,
Status: "published",
UserID: editor.ID,
},
{
Title: "建筑之美",
Description: "现代建筑的几何美学",
URL: "https://picsum.photos/800/600?random=4",
ThumbnailURL: "https://picsum.photos/300/200?random=4",
OriginalFilename: "architecture.jpg",
FileSize: 1792000,
MimeType: "image/jpeg",
Width: 800,
Height: 600,
Status: "draft",
UserID: editor.ID,
},
}
for _, photo := range photos {
db.Create(&photo)
}
log.Println("✅ Test data initialized successfully")
}
// 健康检查处理器
func healthHandler(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"timestamp": time.Now().Unix(),
"version": "1.0.0",
"database": "connected",
})
}
// 登录处理器
func loginHandler(c *gin.Context) {
var req LoginRequest
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
var user User
if err := db.Where("username = ? OR email = ?", req.Username, req.Username).First(&user).Error; err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
if err := bcrypt.CompareHashAndPassword([]byte(user.Password), []byte(req.Password)); err != nil {
c.JSON(http.StatusUnauthorized, gin.H{"error": "用户名或密码错误"})
return
}
// 更新最后登录时间
now := time.Now()
user.LastLogin = &now
db.Save(&user)
c.JSON(http.StatusOK, LoginResponse{
User: user,
AccessToken: "mock-jwt-token-" + fmt.Sprintf("%d", user.ID),
ExpiresIn: 86400,
})
}
// 照片相关处理器
func getPhotosHandler(c *gin.Context) {
var photos []Photo
query := db.Preload("User").Preload("Categories").Preload("Tags")
// 分页参数
page := 1
limit := 10
if p := c.Query("page"); p != "" {
fmt.Sscanf(p, "%d", &page)
}
if l := c.Query("limit"); l != "" {
fmt.Sscanf(l, "%d", &limit)
}
offset := (page - 1) * limit
// 搜索和过滤
if search := c.Query("search"); search != "" {
query = query.Where("title LIKE ? OR description LIKE ?", "%"+search+"%", "%"+search+"%")
}
if status := c.Query("status"); status != "" {
query = query.Where("status = ?", status)
}
var total int64
query.Model(&Photo{}).Count(&total)
query.Offset(offset).Limit(limit).Find(&photos)
c.JSON(http.StatusOK, gin.H{
"data": photos,
"total": total,
"page": page,
"limit": limit,
"totalPages": (total + int64(limit) - 1) / int64(limit),
})
}
func createPhotoHandler(c *gin.Context) {
var photo Photo
if err := c.ShouldBindJSON(&photo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Create(&photo).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建照片失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": photo})
}
func getPhotoHandler(c *gin.Context) {
id := c.Param("id")
var photo Photo
if err := db.Preload("User").Preload("Categories").Preload("Tags").First(&photo, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": photo})
}
func updatePhotoHandler(c *gin.Context) {
id := c.Param("id")
var photo Photo
if err := db.First(&photo, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "照片不存在"})
return
}
if err := c.ShouldBindJSON(&photo); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&photo).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新照片失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": photo})
}
func deletePhotoHandler(c *gin.Context) {
id := c.Param("id")
if err := db.Delete(&Photo{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除照片失败"})
return
}
c.JSON(http.StatusNoContent, nil)
}
// 分类相关处理器
func getCategoriesHandler(c *gin.Context) {
var categories []Category
var total int64
query := db.Model(&Category{})
query.Count(&total)
query.Find(&categories)
// 计算每个分类的照片数量
for i := range categories {
var count int64
db.Model(&Photo{}).Joins("JOIN photo_categories ON photos.id = photo_categories.photo_id").
Where("photo_categories.category_id = ?", categories[i].ID).Count(&count)
categories[i].PhotoCount = int(count)
}
c.JSON(http.StatusOK, gin.H{
"data": categories,
"total": total,
})
}
func createCategoryHandler(c *gin.Context) {
var category Category
if err := c.ShouldBindJSON(&category); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Create(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建分类失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": category})
}
func getCategoryHandler(c *gin.Context) {
id := c.Param("id")
var category Category
if err := db.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
func updateCategoryHandler(c *gin.Context) {
id := c.Param("id")
var category Category
if err := db.First(&category, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "分类不存在"})
return
}
if err := c.ShouldBindJSON(&category); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&category).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新分类失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": category})
}
func deleteCategoryHandler(c *gin.Context) {
id := c.Param("id")
if err := db.Delete(&Category{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除分类失败"})
return
}
c.JSON(http.StatusNoContent, nil)
}
// 标签相关处理器
func getTagsHandler(c *gin.Context) {
var tags []Tag
var total int64
query := db.Model(&Tag{})
query.Count(&total)
query.Find(&tags)
// 计算每个标签的照片数量
for i := range tags {
var count int64
db.Model(&Photo{}).Joins("JOIN photo_tags ON photos.id = photo_tags.photo_id").
Where("photo_tags.tag_id = ?", tags[i].ID).Count(&count)
tags[i].PhotoCount = int(count)
}
c.JSON(http.StatusOK, gin.H{
"data": tags,
"total": total,
})
}
func createTagHandler(c *gin.Context) {
var tag Tag
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Create(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建标签失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": tag})
}
func getTagHandler(c *gin.Context) {
id := c.Param("id")
var tag Tag
if err := db.First(&tag, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
func updateTagHandler(c *gin.Context) {
id := c.Param("id")
var tag Tag
if err := db.First(&tag, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "标签不存在"})
return
}
if err := c.ShouldBindJSON(&tag); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&tag).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新标签失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": tag})
}
func deleteTagHandler(c *gin.Context) {
id := c.Param("id")
if err := db.Delete(&Tag{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除标签失败"})
return
}
c.JSON(http.StatusNoContent, nil)
}
// 用户相关处理器
func getUsersHandler(c *gin.Context) {
var users []User
var total int64
query := db.Model(&User{})
query.Count(&total)
query.Find(&users)
c.JSON(http.StatusOK, gin.H{
"data": users,
"total": total,
})
}
func createUserHandler(c *gin.Context) {
var user User
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 加密密码
hashedPassword, err := bcrypt.GenerateFromPassword([]byte(user.Password), bcrypt.DefaultCost)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "密码加密失败"})
return
}
user.Password = string(hashedPassword)
if err := db.Create(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建用户失败"})
return
}
c.JSON(http.StatusCreated, gin.H{"data": user})
}
func getUserHandler(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
func updateUserHandler(c *gin.Context) {
id := c.Param("id")
var user User
if err := db.First(&user, id).Error; err != nil {
c.JSON(http.StatusNotFound, gin.H{"error": "用户不存在"})
return
}
if err := c.ShouldBindJSON(&user); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
if err := db.Save(&user).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "更新用户失败"})
return
}
c.JSON(http.StatusOK, gin.H{"data": user})
}
func deleteUserHandler(c *gin.Context) {
id := c.Param("id")
if err := db.Delete(&User{}, id).Error; err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "删除用户失败"})
return
}
c.JSON(http.StatusNoContent, nil)
}
// 仪表板统计处理器
func getDashboardStatsHandler(c *gin.Context) {
var photoCount, categoryCount, tagCount, userCount int64
var publishedCount, draftCount int64
var todayCount, monthCount int64
// 基础统计
db.Model(&Photo{}).Count(&photoCount)
db.Model(&Category{}).Count(&categoryCount)
db.Model(&Tag{}).Count(&tagCount)
db.Model(&User{}).Count(&userCount)
// 照片状态统计
db.Model(&Photo{}).Where("status = ?", "published").Count(&publishedCount)
db.Model(&Photo{}).Where("status = ?", "draft").Count(&draftCount)
// 时间统计
today := time.Now().Format("2006-01-02")
thisMonth := time.Now().Format("2006-01")
db.Model(&Photo{}).Where("DATE(created_at) = ?", today).Count(&todayCount)
db.Model(&Photo{}).Where("strftime('%Y-%m', created_at) = ?", thisMonth).Count(&monthCount)
c.JSON(http.StatusOK, gin.H{
"photos": gin.H{
"total": photoCount,
"published": publishedCount,
"draft": draftCount,
"thisMonth": monthCount,
"today": todayCount,
},
"categories": gin.H{
"total": categoryCount,
"active": categoryCount, // 简化统计
},
"tags": gin.H{
"total": tagCount,
},
"users": gin.H{
"total": userCount,
"active": userCount, // 简化统计
},
})
}
// 文件上传处理器
func uploadFileHandler(c *gin.Context) {
// 创建上传目录
uploadDir := "uploads"
if err := os.MkdirAll(uploadDir, 0755); err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建上传目录失败"})
return
}
// 获取上传的文件
file, header, err := c.Request.FormFile("file")
if err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": "获取上传文件失败"})
return
}
defer file.Close()
// 验证文件类型
allowedTypes := []string{".jpg", ".jpeg", ".png", ".gif", ".webp"}
ext := strings.ToLower(filepath.Ext(header.Filename))
isAllowed := false
for _, allowedType := range allowedTypes {
if ext == allowedType {
isAllowed = true
break
}
}
if !isAllowed {
c.JSON(http.StatusBadRequest, gin.H{"error": "不支持的文件类型"})
return
}
// 生成文件名
filename := fmt.Sprintf("%d_%s", time.Now().Unix(), header.Filename)
filePath := filepath.Join(uploadDir, filename)
// 保存文件
out, err := os.Create(filePath)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "创建文件失败"})
return
}
defer out.Close()
// 复制文件内容
_, err = io.Copy(out, file)
if err != nil {
c.JSON(http.StatusInternalServerError, gin.H{"error": "保存文件失败"})
return
}
// 返回文件信息
fileURL := fmt.Sprintf("http://localhost:8080/uploads/%s", filename)
c.JSON(http.StatusOK, gin.H{
"message": "文件上传成功",
"filename": filename,
"url": fileURL,
"size": header.Size,
"type": header.Header.Get("Content-Type"),
})
}

View File

@ -1,186 +0,0 @@
package main
import (
"log"
"net/http"
"time"
"github.com/gin-gonic/gin"
"github.com/gin-contrib/cors"
)
func main() {
// 创建 Gin 引擎
r := gin.Default()
// 配置 CORS
r.Use(cors.New(cors.Config{
AllowOrigins: []string{"http://localhost:3000", "http://localhost:3002", "http://localhost:3003"},
AllowMethods: []string{"GET", "POST", "PUT", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Origin", "Content-Type", "Authorization"},
AllowCredentials: true,
MaxAge: 12 * time.Hour,
}))
// 健康检查
r.GET("/health", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"status": "ok",
"timestamp": time.Now().Unix(),
"version": "1.0.0",
})
})
// 模拟登录接口
r.POST("/api/v1/auth/login", func(c *gin.Context) {
var req struct {
Username string `json:"username"`
Password string `json:"password"`
}
if err := c.ShouldBindJSON(&req); err != nil {
c.JSON(http.StatusBadRequest, gin.H{"error": err.Error()})
return
}
// 简单验证
if req.Username == "admin" && req.Password == "admin123" {
c.JSON(http.StatusOK, gin.H{
"user": gin.H{
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"isActive": true,
},
"access_token": "mock-jwt-token",
"expires_in": 86400,
})
} else {
c.JSON(http.StatusUnauthorized, gin.H{"error": "Invalid credentials"})
}
})
// 模拟照片列表接口
r.GET("/api/v1/photos", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []gin.H{
{
"id": "1",
"title": "Sample Photo 1",
"description": "This is a sample photo",
"url": "https://picsum.photos/800/600?random=1",
"status": "published",
"createdAt": time.Now().Format(time.RFC3339),
},
{
"id": "2",
"title": "Sample Photo 2",
"description": "This is another sample photo",
"url": "https://picsum.photos/800/600?random=2",
"status": "published",
"createdAt": time.Now().Format(time.RFC3339),
},
},
"total": 2,
"page": 1,
"limit": 10,
"totalPages": 1,
})
})
// 模拟仪表板统计接口
r.GET("/api/v1/dashboard/stats", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"photos": gin.H{
"total": 156,
"thisMonth": 23,
"today": 5,
},
"categories": gin.H{
"total": 12,
"active": 10,
},
"tags": gin.H{
"total": 45,
},
"users": gin.H{
"total": 8,
"active": 7,
},
})
})
// 模拟分类列表接口
r.GET("/api/v1/categories", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []gin.H{
{
"id": "1",
"name": "风景",
"slug": "landscape",
"description": "自然风景摄影",
"photoCount": 25,
"isActive": true,
},
{
"id": "2",
"name": "人像",
"slug": "portrait",
"description": "人物肖像摄影",
"photoCount": 18,
"isActive": true,
},
},
"total": 2,
})
})
// 模拟标签列表接口
r.GET("/api/v1/tags", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []gin.H{
{
"id": "1",
"name": "日落",
"slug": "sunset",
"photoCount": 12,
},
{
"id": "2",
"name": "城市",
"slug": "city",
"photoCount": 8,
},
},
"total": 2,
})
})
// 模拟用户列表接口
r.GET("/api/v1/users", func(c *gin.Context) {
c.JSON(http.StatusOK, gin.H{
"data": []gin.H{
{
"id": "1",
"username": "admin",
"email": "admin@example.com",
"role": "admin",
"isActive": true,
},
{
"id": "2",
"username": "editor",
"email": "editor@example.com",
"role": "editor",
"isActive": true,
},
},
"total": 2,
})
})
// 启动服务器
log.Println("🚀 Simple backend server starting on :8080")
log.Fatal(r.Run(":8080"))
}