Files
photography/backend/internal/api/handlers/category_handler.go
xujiang 72414d0979 feat: 完成后端-管理后台集成及部署配置
🚀 主要功能:
- 完善后端API服务层,实现完整的CRUD操作
- 开发管理后台所有核心页面 (仪表板、照片、分类、标签、用户、设置)
- 完成前后端完全集成,所有API接口正常对接
- 配置完整的CI/CD流水线,支持自动化部署

🎯 后端完善:
- 实现PhotoService, CategoryService, TagService, UserService
- 添加完整的API处理器和路由配置
- 支持Docker容器化部署
- 添加数据库迁移和健康检查

🎨 管理后台完成:
- 仪表板: 实时统计数据展示
- 照片管理: 完整的CRUD操作,支持批量处理
- 分类管理: 树形结构展示和管理
- 标签管理: 颜色标签和统计信息
- 用户管理: 角色权限控制
- 系统设置: 多标签配置界面
- 添加pre-commit代码质量检查

🔧 部署配置:
- Docker Compose完整配置
- 后端CI/CD流水线 (Docker部署)
- 管理后台CI/CD流水线 (静态文件部署)
- 前端CI/CD流水线优化
- 自动化脚本: 部署、备份、监控
- 完整的部署文档和运维指南

 集成完成:
- 所有API接口正常连接
- 认证系统完整集成
- 数据获取和状态管理
- 错误处理和用户反馈
- 响应式设计优化
2025-07-09 16:23:18 +08:00

434 lines
12 KiB
Go

package handlers
import (
"errors"
"net/http"
"strconv"
"photography-backend/internal/models"
"photography-backend/internal/service"
"github.com/gin-gonic/gin"
"go.uber.org/zap"
)
type CategoryHandler struct {
categoryService *service.CategoryService
logger *zap.Logger
}
func NewCategoryHandler(categoryService *service.CategoryService, logger *zap.Logger) *CategoryHandler {
return &CategoryHandler{
categoryService: categoryService,
logger: logger,
}
}
// GetCategories 获取分类列表
// @Summary 获取分类列表
// @Description 获取分类列表,可指定父分类
// @Tags categories
// @Accept json
// @Produce json
// @Param parent_id query int false "父分类ID"
// @Success 200 {array} models.Category
// @Failure 500 {object} models.ErrorResponse
// @Router /categories [get]
func (h *CategoryHandler) GetCategories(c *gin.Context) {
var parentID *uint
if parentIDStr := c.Query("parent_id"); parentIDStr != "" {
id, err := strconv.ParseUint(parentIDStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid parent_id",
Message: "Parent ID must be a valid number",
})
return
}
parentIDUint := uint(id)
parentID = &parentIDUint
}
categories, err := h.categoryService.GetCategories(c.Request.Context(), parentID)
if err != nil {
h.logger.Error("Failed to get categories", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to get categories",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, categories)
}
// GetCategoryTree 获取分类树
// @Summary 获取分类树
// @Description 获取完整的分类树结构
// @Tags categories
// @Accept json
// @Produce json
// @Success 200 {array} models.CategoryTree
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/tree [get]
func (h *CategoryHandler) GetCategoryTree(c *gin.Context) {
tree, err := h.categoryService.GetCategoryTree(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get category tree", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to get category tree",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, tree)
}
// GetCategory 获取分类详情
// @Summary 获取分类详情
// @Description 根据ID获取分类详情
// @Tags categories
// @Accept json
// @Produce json
// @Param id path int true "分类ID"
// @Success 200 {object} models.Category
// @Failure 400 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/{id} [get]
func (h *CategoryHandler) GetCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid category ID",
Message: "Category ID must be a valid number",
})
return
}
category, err := h.categoryService.GetCategoryByID(c.Request.Context(), uint(id))
if err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Error: "Category not found",
Message: "The requested category does not exist",
})
return
}
h.logger.Error("Failed to get category", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to get category",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, category)
}
// GetCategoryBySlug 根据slug获取分类
// @Summary 根据slug获取分类
// @Description 根据slug获取分类详情
// @Tags categories
// @Accept json
// @Produce json
// @Param slug path string true "分类slug"
// @Success 200 {object} models.Category
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/slug/{slug} [get]
func (h *CategoryHandler) GetCategoryBySlug(c *gin.Context) {
slug := c.Param("slug")
category, err := h.categoryService.GetCategoryBySlug(c.Request.Context(), slug)
if err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Error: "Category not found",
Message: "The requested category does not exist",
})
return
}
h.logger.Error("Failed to get category by slug", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to get category",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, category)
}
// CreateCategory 创建分类
// @Summary 创建分类
// @Description 创建新的分类
// @Tags categories
// @Accept json
// @Produce json
// @Param category body models.CreateCategoryRequest true "分类信息"
// @Success 201 {object} models.Category
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /categories [post]
func (h *CategoryHandler) CreateCategory(c *gin.Context) {
var req models.CreateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Failed to bind JSON", zap.Error(err))
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
// 验证请求数据
if err := h.validateCreateCategoryRequest(&req); err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request data",
Message: err.Error(),
})
return
}
category, err := h.categoryService.CreateCategory(c.Request.Context(), &req)
if err != nil {
h.logger.Error("Failed to create category", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to create category",
Message: err.Error(),
})
return
}
c.JSON(http.StatusCreated, category)
}
// UpdateCategory 更新分类
// @Summary 更新分类
// @Description 更新分类信息
// @Tags categories
// @Accept json
// @Produce json
// @Param id path int true "分类ID"
// @Param category body models.UpdateCategoryRequest true "分类信息"
// @Success 200 {object} models.Category
// @Failure 400 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/{id} [put]
func (h *CategoryHandler) UpdateCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid category ID",
Message: "Category ID must be a valid number",
})
return
}
var req models.UpdateCategoryRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Failed to bind JSON", zap.Error(err))
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
category, err := h.categoryService.UpdateCategory(c.Request.Context(), uint(id), &req)
if err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Error: "Category not found",
Message: "The requested category does not exist",
})
return
}
h.logger.Error("Failed to update category", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to update category",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, category)
}
// DeleteCategory 删除分类
// @Summary 删除分类
// @Description 删除分类
// @Tags categories
// @Accept json
// @Produce json
// @Param id path int true "分类ID"
// @Success 204 "No Content"
// @Failure 400 {object} models.ErrorResponse
// @Failure 404 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/{id} [delete]
func (h *CategoryHandler) DeleteCategory(c *gin.Context) {
idStr := c.Param("id")
id, err := strconv.ParseUint(idStr, 10, 32)
if err != nil {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid category ID",
Message: "Category ID must be a valid number",
})
return
}
err = h.categoryService.DeleteCategory(c.Request.Context(), uint(id))
if err != nil {
if err.Error() == "category not found" {
c.JSON(http.StatusNotFound, models.ErrorResponse{
Error: "Category not found",
Message: "The requested category does not exist",
})
return
}
h.logger.Error("Failed to delete category", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to delete category",
Message: err.Error(),
})
return
}
c.Status(http.StatusNoContent)
}
// ReorderCategories 重新排序分类
// @Summary 重新排序分类
// @Description 重新排序分类
// @Tags categories
// @Accept json
// @Produce json
// @Param request body models.ReorderCategoriesRequest true "排序请求"
// @Success 200 {object} models.SuccessResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/reorder [post]
func (h *CategoryHandler) ReorderCategories(c *gin.Context) {
var req models.ReorderCategoriesRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Failed to bind JSON", zap.Error(err))
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
if len(req.CategoryIDs) == 0 {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request",
Message: "No category IDs provided",
})
return
}
err := h.categoryService.ReorderCategories(c.Request.Context(), req.ParentID, req.CategoryIDs)
if err != nil {
h.logger.Error("Failed to reorder categories", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to reorder categories",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, models.SuccessResponse{
Message: "Categories reordered successfully",
})
}
// GetCategoryStats 获取分类统计信息
// @Summary 获取分类统计信息
// @Description 获取分类统计信息
// @Tags categories
// @Accept json
// @Produce json
// @Success 200 {object} models.CategoryStats
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/stats [get]
func (h *CategoryHandler) GetCategoryStats(c *gin.Context) {
stats, err := h.categoryService.GetCategoryStats(c.Request.Context())
if err != nil {
h.logger.Error("Failed to get category stats", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to get category stats",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, stats)
}
// GenerateSlug 生成分类slug
// @Summary 生成分类slug
// @Description 根据分类名称生成唯一的slug
// @Tags categories
// @Accept json
// @Produce json
// @Param request body models.GenerateSlugRequest true "生成slug请求"
// @Success 200 {object} models.GenerateSlugResponse
// @Failure 400 {object} models.ErrorResponse
// @Failure 500 {object} models.ErrorResponse
// @Router /categories/generate-slug [post]
func (h *CategoryHandler) GenerateSlug(c *gin.Context) {
var req models.GenerateSlugRequest
if err := c.ShouldBindJSON(&req); err != nil {
h.logger.Error("Failed to bind JSON", zap.Error(err))
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request body",
Message: err.Error(),
})
return
}
if req.Name == "" {
c.JSON(http.StatusBadRequest, models.ErrorResponse{
Error: "Invalid request",
Message: "Name is required",
})
return
}
slug, err := h.categoryService.GenerateSlug(c.Request.Context(), req.Name)
if err != nil {
h.logger.Error("Failed to generate slug", zap.Error(err))
c.JSON(http.StatusInternalServerError, models.ErrorResponse{
Error: "Failed to generate slug",
Message: err.Error(),
})
return
}
c.JSON(http.StatusOK, models.GenerateSlugResponse{
Slug: slug,
})
}
// validateCreateCategoryRequest 验证创建分类请求
func (h *CategoryHandler) validateCreateCategoryRequest(req *models.CreateCategoryRequest) error {
if req.Name == "" {
return errors.New("name is required")
}
if req.Slug == "" {
return errors.New("slug is required")
}
return nil
}