786 lines
22 KiB
Go
786 lines
22 KiB
Go
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"),
|
|
})
|
|
} |