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"), }) }