feat: 完成后端服务核心业务逻辑实现
## 主要功能 - ✅ 用户认证模块 (登录/注册/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:
31
backend/cmd/api/main.go
Normal file
31
backend/cmd/api/main.go
Normal 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()
|
||||
}
|
||||
@ -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. **敏感信息泄露**: 配置敏感信息过滤
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 配置管理
|
||||
- 使用环境变量覆盖敏感配置
|
||||
- 配置验证和默认值设置
|
||||
- 配置变更的版本控制
|
||||
|
||||
### 错误处理
|
||||
- 统一错误响应格式
|
||||
- 详细的错误日志记录
|
||||
- 适当的错误码设计
|
||||
|
||||
### 安全考虑
|
||||
- 敏感信息不在日志中输出
|
||||
- 配置文件权限控制
|
||||
- 环境变量加密存储
|
||||
|
||||
### 性能优化
|
||||
- 合理的超时配置
|
||||
- 连接池大小调优
|
||||
- 资源及时释放
|
||||
|
||||
本模块为整个应用的入口,确保配置正确、启动流程清晰是项目成功的关键。在开发过程中,优先使用开发模式进行功能验证,在集成测试时使用生产模式。
|
||||
@ -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")
|
||||
}
|
||||
@ -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": "照片删除成功"})
|
||||
}
|
||||
@ -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"),
|
||||
})
|
||||
}
|
||||
@ -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"))
|
||||
}
|
||||
Reference in New Issue
Block a user