This commit is contained in:
xujiang
2025-07-10 18:09:11 +08:00
parent 5cbdc5af73
commit 604b9e59ba
95 changed files with 23709 additions and 19 deletions

View File

@ -0,0 +1,786 @@
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"),
})
}