fix
This commit is contained in:
565
backend-old/internal/api/CLAUDE.md
Normal file
565
backend-old/internal/api/CLAUDE.md
Normal file
@ -0,0 +1,565 @@
|
||||
# HTTP 接口层 - CLAUDE.md
|
||||
|
||||
本文件为 Claude Code 在 HTTP 接口层模块中工作时提供指导。
|
||||
|
||||
## 🎯 模块概览
|
||||
|
||||
HTTP 接口层负责处理所有 HTTP 请求,包括路由定义、请求处理、响应格式化、中间件管理等。
|
||||
|
||||
### 职责范围
|
||||
- 🌐 HTTP 路由定义和管理
|
||||
- 📝 请求处理和响应格式化
|
||||
- 🛡️ 中间件管理和应用
|
||||
- ✅ 请求参数验证
|
||||
- 📊 HTTP 状态码管理
|
||||
|
||||
### 文件结构
|
||||
```
|
||||
internal/api/
|
||||
├── CLAUDE.md # 📋 当前文件 - API 接口指导
|
||||
├── handlers/ # 🎯 HTTP 处理器
|
||||
│ ├── user.go # 用户相关处理器
|
||||
│ ├── photo.go # 照片相关处理器
|
||||
│ ├── category.go # 分类相关处理器
|
||||
│ ├── tag.go # 标签相关处理器
|
||||
│ ├── auth.go # 认证相关处理器
|
||||
│ ├── upload.go # 上传相关处理器
|
||||
│ └── health.go # 健康检查处理器
|
||||
├── middleware/ # 🛡️ 中间件
|
||||
│ ├── auth.go # 认证中间件
|
||||
│ ├── cors.go # CORS 中间件
|
||||
│ ├── logger.go # 日志中间件
|
||||
│ ├── rate_limit.go # 限流中间件
|
||||
│ ├── recovery.go # 错误恢复中间件
|
||||
│ └── validator.go # 验证中间件
|
||||
├── routes/ # 🗺️ 路由定义
|
||||
│ ├── v1.go # API v1 路由
|
||||
│ ├── auth.go # 认证路由
|
||||
│ ├── public.go # 公共路由
|
||||
│ └── admin.go # 管理员路由
|
||||
└── validators/ # ✅ 请求验证器
|
||||
├── user.go # 用户验证器
|
||||
├── photo.go # 照片验证器
|
||||
├── category.go # 分类验证器
|
||||
└── common.go # 通用验证器
|
||||
```
|
||||
|
||||
## 🎯 处理器模式
|
||||
|
||||
### 标准处理器结构
|
||||
```go
|
||||
type UserHandler struct {
|
||||
userService service.UserServiceInterface
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewUserHandler(userService service.UserServiceInterface, logger *zap.Logger) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// 创建用户
|
||||
func (h *UserHandler) Create(c *gin.Context) {
|
||||
var req validators.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
response.Error(c, http.StatusBadRequest, "INVALID_REQUEST", "请求参数无效", err)
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.Create(c.Request.Context(), req)
|
||||
if err != nil {
|
||||
h.logger.Error("创建用户失败", zap.Error(err))
|
||||
response.Error(c, http.StatusInternalServerError, "CREATE_USER_FAILED", "创建用户失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
response.Success(c, user, "用户创建成功")
|
||||
}
|
||||
```
|
||||
|
||||
### 处理器职责
|
||||
1. **请求绑定**: 解析 HTTP 请求参数
|
||||
2. **参数验证**: 验证请求参数的有效性
|
||||
3. **服务调用**: 调用应用服务层处理业务逻辑
|
||||
4. **响应格式化**: 格式化响应数据
|
||||
5. **错误处理**: 处理和记录错误信息
|
||||
|
||||
## 🗺️ 路由设计
|
||||
|
||||
### API 版本管理
|
||||
```go
|
||||
// v1 路由组
|
||||
v1 := router.Group("/api/v1")
|
||||
{
|
||||
// 公共路由 (无需认证)
|
||||
v1.POST("/auth/login", authHandler.Login)
|
||||
v1.POST("/auth/register", authHandler.Register)
|
||||
v1.GET("/photos/public", photoHandler.GetPublicPhotos)
|
||||
|
||||
// 需要认证的路由
|
||||
authenticated := v1.Group("")
|
||||
authenticated.Use(middleware.AuthRequired())
|
||||
{
|
||||
authenticated.GET("/users/profile", userHandler.GetProfile)
|
||||
authenticated.PUT("/users/profile", userHandler.UpdateProfile)
|
||||
authenticated.POST("/photos", photoHandler.Create)
|
||||
authenticated.GET("/photos", photoHandler.List)
|
||||
}
|
||||
|
||||
// 管理员路由
|
||||
admin := v1.Group("/admin")
|
||||
admin.Use(middleware.AuthRequired(), middleware.AdminRequired())
|
||||
{
|
||||
admin.GET("/users", userHandler.ListUsers)
|
||||
admin.DELETE("/users/:id", userHandler.DeleteUser)
|
||||
admin.GET("/photos/all", photoHandler.ListAllPhotos)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### RESTful API 设计
|
||||
```go
|
||||
// 用户资源
|
||||
GET /api/v1/users # 获取用户列表
|
||||
POST /api/v1/users # 创建用户
|
||||
GET /api/v1/users/:id # 获取用户详情
|
||||
PUT /api/v1/users/:id # 更新用户
|
||||
DELETE /api/v1/users/:id # 删除用户
|
||||
|
||||
// 照片资源
|
||||
GET /api/v1/photos # 获取照片列表
|
||||
POST /api/v1/photos # 创建照片
|
||||
GET /api/v1/photos/:id # 获取照片详情
|
||||
PUT /api/v1/photos/:id # 更新照片
|
||||
DELETE /api/v1/photos/:id # 删除照片
|
||||
|
||||
// 分类资源
|
||||
GET /api/v1/categories # 获取分类列表
|
||||
POST /api/v1/categories # 创建分类
|
||||
GET /api/v1/categories/:id # 获取分类详情
|
||||
PUT /api/v1/categories/:id # 更新分类
|
||||
DELETE /api/v1/categories/:id # 删除分类
|
||||
```
|
||||
|
||||
## 🛡️ 中间件管理
|
||||
|
||||
### 认证中间件
|
||||
```go
|
||||
func AuthRequired() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
token := c.GetHeader("Authorization")
|
||||
if token == "" {
|
||||
response.Error(c, http.StatusUnauthorized, "MISSING_TOKEN", "缺少认证令牌", nil)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 验证 JWT Token
|
||||
claims, err := jwt.VerifyToken(token)
|
||||
if err != nil {
|
||||
response.Error(c, http.StatusUnauthorized, "INVALID_TOKEN", "无效的认证令牌", err)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 设置用户信息到上下文
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("user_role", claims.Role)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 日志中间件
|
||||
```go
|
||||
func Logger(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
c.Next()
|
||||
duration := time.Since(start)
|
||||
|
||||
logger.Info("HTTP Request",
|
||||
zap.String("method", c.Request.Method),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("query", c.Request.URL.RawQuery),
|
||||
zap.Int("status", c.Writer.Status()),
|
||||
zap.Duration("duration", duration),
|
||||
zap.String("user_agent", c.Request.UserAgent()),
|
||||
zap.String("ip", c.ClientIP()),
|
||||
)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### CORS 中间件
|
||||
```go
|
||||
func CORS() gin.HandlerFunc {
|
||||
return 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")
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(204)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 限流中间件
|
||||
```go
|
||||
func RateLimit(limit int, window time.Duration) gin.HandlerFunc {
|
||||
limiter := rate.NewLimiter(rate.Every(window), limit)
|
||||
|
||||
return func(c *gin.Context) {
|
||||
if !limiter.Allow() {
|
||||
response.Error(c, http.StatusTooManyRequests, "RATE_LIMIT_EXCEEDED", "请求过于频繁", nil)
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## ✅ 请求验证
|
||||
|
||||
### 验证器结构
|
||||
```go
|
||||
type CreateUserRequest struct {
|
||||
Username string `json:"username" binding:"required,min=3,max=20"`
|
||||
Email string `json:"email" binding:"required,email"`
|
||||
Password string `json:"password" binding:"required,min=6"`
|
||||
Role string `json:"role" binding:"oneof=user editor admin"`
|
||||
}
|
||||
|
||||
type UpdateUserRequest struct {
|
||||
Username string `json:"username,omitempty" binding:"omitempty,min=3,max=20"`
|
||||
Email string `json:"email,omitempty" binding:"omitempty,email"`
|
||||
Role string `json:"role,omitempty" binding:"omitempty,oneof=user editor admin"`
|
||||
}
|
||||
```
|
||||
|
||||
### 自定义验证器
|
||||
```go
|
||||
func ValidateUsername(fl validator.FieldLevel) bool {
|
||||
username := fl.Field().String()
|
||||
// 用户名只能包含字母、数字和下划线
|
||||
matched, _ := regexp.MatchString(`^[a-zA-Z0-9_]+$`, username)
|
||||
return matched
|
||||
}
|
||||
|
||||
func ValidatePhotoFormat(fl validator.FieldLevel) bool {
|
||||
format := fl.Field().String()
|
||||
allowedFormats := []string{"jpg", "jpeg", "png", "gif", "webp"}
|
||||
for _, allowed := range allowedFormats {
|
||||
if format == allowed {
|
||||
return true
|
||||
}
|
||||
}
|
||||
return false
|
||||
}
|
||||
```
|
||||
|
||||
### 验证错误处理
|
||||
```go
|
||||
func FormatValidationErrors(err error) map[string]string {
|
||||
errors := make(map[string]string)
|
||||
|
||||
if validationErrors, ok := err.(validator.ValidationErrors); ok {
|
||||
for _, e := range validationErrors {
|
||||
field := strings.ToLower(e.Field())
|
||||
switch e.Tag() {
|
||||
case "required":
|
||||
errors[field] = "此字段为必填项"
|
||||
case "email":
|
||||
errors[field] = "请输入有效的邮箱地址"
|
||||
case "min":
|
||||
errors[field] = fmt.Sprintf("最小长度为 %s", e.Param())
|
||||
case "max":
|
||||
errors[field] = fmt.Sprintf("最大长度为 %s", e.Param())
|
||||
default:
|
||||
errors[field] = "字段值无效"
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return errors
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 响应格式
|
||||
|
||||
### 统一响应结构
|
||||
```go
|
||||
type Response struct {
|
||||
Success bool `json:"success"`
|
||||
Data interface{} `json:"data,omitempty"`
|
||||
Message string `json:"message"`
|
||||
Error *ErrorInfo `json:"error,omitempty"`
|
||||
Timestamp time.Time `json:"timestamp"`
|
||||
}
|
||||
|
||||
type ErrorInfo struct {
|
||||
Code string `json:"code"`
|
||||
Message string `json:"message"`
|
||||
Details interface{} `json:"details,omitempty"`
|
||||
}
|
||||
```
|
||||
|
||||
### 响应帮助函数
|
||||
```go
|
||||
func Success(c *gin.Context, data interface{}, message string) {
|
||||
c.JSON(http.StatusOK, Response{
|
||||
Success: true,
|
||||
Data: data,
|
||||
Message: message,
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
|
||||
func Error(c *gin.Context, statusCode int, errorCode, message string, details interface{}) {
|
||||
c.JSON(statusCode, Response{
|
||||
Success: false,
|
||||
Message: message,
|
||||
Error: &ErrorInfo{
|
||||
Code: errorCode,
|
||||
Message: message,
|
||||
Details: details,
|
||||
},
|
||||
Timestamp: time.Now(),
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
### 分页响应
|
||||
```go
|
||||
type PaginatedResponse struct {
|
||||
Data interface{} `json:"data"`
|
||||
Pagination Pagination `json:"pagination"`
|
||||
}
|
||||
|
||||
type Pagination struct {
|
||||
Page int `json:"page"`
|
||||
PageSize int `json:"page_size"`
|
||||
Total int64 `json:"total"`
|
||||
TotalPages int `json:"total_pages"`
|
||||
HasNext bool `json:"has_next"`
|
||||
HasPrevious bool `json:"has_previous"`
|
||||
}
|
||||
```
|
||||
|
||||
## 🔍 错误处理
|
||||
|
||||
### 错误分类
|
||||
```go
|
||||
const (
|
||||
// 请求错误
|
||||
ErrInvalidRequest = "INVALID_REQUEST"
|
||||
ErrMissingParameter = "MISSING_PARAMETER"
|
||||
ErrInvalidParameter = "INVALID_PARAMETER"
|
||||
|
||||
// 认证错误
|
||||
ErrMissingToken = "MISSING_TOKEN"
|
||||
ErrInvalidToken = "INVALID_TOKEN"
|
||||
ErrTokenExpired = "TOKEN_EXPIRED"
|
||||
ErrInsufficientPermission = "INSUFFICIENT_PERMISSION"
|
||||
|
||||
// 业务错误
|
||||
ErrUserNotFound = "USER_NOT_FOUND"
|
||||
ErrUserAlreadyExists = "USER_ALREADY_EXISTS"
|
||||
ErrPhotoNotFound = "PHOTO_NOT_FOUND"
|
||||
ErrCategoryNotFound = "CATEGORY_NOT_FOUND"
|
||||
|
||||
// 系统错误
|
||||
ErrInternalServer = "INTERNAL_SERVER_ERROR"
|
||||
ErrDatabaseError = "DATABASE_ERROR"
|
||||
ErrStorageError = "STORAGE_ERROR"
|
||||
)
|
||||
```
|
||||
|
||||
### 错误恢复中间件
|
||||
```go
|
||||
func Recovery(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
defer func() {
|
||||
if err := recover(); err != nil {
|
||||
logger.Error("Panic recovered",
|
||||
zap.Any("error", err),
|
||||
zap.String("path", c.Request.URL.Path),
|
||||
zap.String("method", c.Request.Method),
|
||||
)
|
||||
|
||||
response.Error(c, http.StatusInternalServerError, "INTERNAL_SERVER_ERROR", "服务器内部错误", nil)
|
||||
}
|
||||
}()
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 📊 性能优化
|
||||
|
||||
### 响应压缩
|
||||
```go
|
||||
func Compression() gin.HandlerFunc {
|
||||
return gzip.Gzip(gzip.DefaultCompression)
|
||||
}
|
||||
```
|
||||
|
||||
### 缓存控制
|
||||
```go
|
||||
func CacheControl(maxAge int) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Header("Cache-Control", fmt.Sprintf("max-age=%d", maxAge))
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 请求大小限制
|
||||
```go
|
||||
func LimitRequestSize(maxSize int64) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, maxSize)
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
## 🧪 测试策略
|
||||
|
||||
### 处理器测试
|
||||
```go
|
||||
func TestUserHandler_Create(t *testing.T) {
|
||||
// 模拟服务
|
||||
mockService := &MockUserService{}
|
||||
handler := NewUserHandler(mockService, zap.NewNop())
|
||||
|
||||
// 创建测试请求
|
||||
reqBody := `{"username":"test","email":"test@example.com","password":"123456"}`
|
||||
req, _ := http.NewRequest("POST", "/api/v1/users", strings.NewReader(reqBody))
|
||||
req.Header.Set("Content-Type", "application/json")
|
||||
|
||||
// 执行请求
|
||||
w := httptest.NewRecorder()
|
||||
router := gin.New()
|
||||
router.POST("/api/v1/users", handler.Create)
|
||||
router.ServeHTTP(w, req)
|
||||
|
||||
// 验证结果
|
||||
assert.Equal(t, http.StatusOK, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "success")
|
||||
}
|
||||
```
|
||||
|
||||
### 中间件测试
|
||||
```go
|
||||
func TestAuthMiddleware(t *testing.T) {
|
||||
middleware := AuthRequired()
|
||||
|
||||
// 测试缺少 token
|
||||
req, _ := http.NewRequest("GET", "/api/v1/users", nil)
|
||||
w := httptest.NewRecorder()
|
||||
c, _ := gin.CreateTestContext(w)
|
||||
c.Request = req
|
||||
|
||||
middleware(c)
|
||||
|
||||
assert.Equal(t, http.StatusUnauthorized, w.Code)
|
||||
assert.Contains(t, w.Body.String(), "MISSING_TOKEN")
|
||||
}
|
||||
```
|
||||
|
||||
## 📚 API 文档
|
||||
|
||||
### Swagger 注释
|
||||
```go
|
||||
// CreateUser 创建用户
|
||||
// @Summary 创建用户
|
||||
// @Description 创建新用户账户
|
||||
// @Tags 用户管理
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user body validators.CreateUserRequest true "用户信息"
|
||||
// @Success 200 {object} response.Response{data=models.User} "成功创建用户"
|
||||
// @Failure 400 {object} response.Response "请求参数错误"
|
||||
// @Failure 500 {object} response.Response "服务器内部错误"
|
||||
// @Router /api/v1/users [post]
|
||||
func (h *UserHandler) Create(c *gin.Context) {
|
||||
// 处理器实现
|
||||
}
|
||||
```
|
||||
|
||||
### API 文档生成
|
||||
```bash
|
||||
# 安装 swag
|
||||
go install github.com/swaggo/swag/cmd/swag@latest
|
||||
|
||||
# 生成文档
|
||||
swag init -g cmd/server/main.go
|
||||
|
||||
# 启动时访问文档
|
||||
# http://localhost:8080/swagger/index.html
|
||||
```
|
||||
|
||||
## 🔧 开发工具
|
||||
|
||||
### 路由调试
|
||||
```go
|
||||
func PrintRoutes(router *gin.Engine) {
|
||||
routes := router.Routes()
|
||||
for _, route := range routes {
|
||||
fmt.Printf("[%s] %s -> %s\n", route.Method, route.Path, route.Handler)
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
### 请求日志
|
||||
```go
|
||||
func RequestLogger() gin.HandlerFunc {
|
||||
return gin.LoggerWithFormatter(func(param gin.LogFormatterParams) string {
|
||||
return fmt.Sprintf("[%s] %s %s %d %s\n",
|
||||
param.TimeStamp.Format("2006-01-02 15:04:05"),
|
||||
param.Method,
|
||||
param.Path,
|
||||
param.StatusCode,
|
||||
param.Latency,
|
||||
)
|
||||
})
|
||||
}
|
||||
```
|
||||
|
||||
## 💡 最佳实践
|
||||
|
||||
### 处理器设计
|
||||
- 保持处理器轻量,业务逻辑放在服务层
|
||||
- 统一错误处理和响应格式
|
||||
- 完善的参数验证和错误提示
|
||||
- 适当的日志记录
|
||||
|
||||
### 中间件使用
|
||||
- 按需应用中间件,避免过度使用
|
||||
- 注意中间件的执行顺序
|
||||
- 错误处理中间件应该最先应用
|
||||
- 认证中间件应该在业务中间件之前
|
||||
|
||||
### 路由设计
|
||||
- 遵循 RESTful 设计原则
|
||||
- 合理的路由分组和版本管理
|
||||
- 清晰的路由命名和结构
|
||||
- 适当的权限控制
|
||||
|
||||
### 性能考虑
|
||||
- 使用响应压缩减少传输大小
|
||||
- 适当的缓存控制
|
||||
- 限制请求大小和频率
|
||||
- 异步处理耗时操作
|
||||
|
||||
本模块是整个 API 的入口和门面,确保接口设计合理、响应格式统一、错误处理完善是项目成功的关键。
|
||||
119
backend-old/internal/api/handlers/auth_handler.go
Normal file
119
backend-old/internal/api/handlers/auth_handler.go
Normal file
@ -0,0 +1,119 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gin-gonic/gin"
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/service/auth"
|
||||
"photography-backend/internal/api/middleware"
|
||||
"photography-backend/pkg/response"
|
||||
)
|
||||
|
||||
// AuthHandler 认证处理器
|
||||
type AuthHandler struct {
|
||||
authService *auth.AuthService
|
||||
}
|
||||
|
||||
// NewAuthHandler 创建认证处理器
|
||||
func NewAuthHandler(authService *auth.AuthService) *AuthHandler {
|
||||
return &AuthHandler{
|
||||
authService: authService,
|
||||
}
|
||||
}
|
||||
|
||||
// Login 用户登录
|
||||
func (h *AuthHandler) Login(c *gin.Context) {
|
||||
var req dto.LoginRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
loginResp, err := h.authService.Login(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.Success(loginResp))
|
||||
}
|
||||
|
||||
// Register 用户注册
|
||||
func (h *AuthHandler) Register(c *gin.Context) {
|
||||
var req dto.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.Register(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, response.Success(user))
|
||||
}
|
||||
|
||||
// RefreshToken 刷新令牌
|
||||
func (h *AuthHandler) RefreshToken(c *gin.Context) {
|
||||
var req dto.RefreshTokenRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
loginResp, err := h.authService.RefreshToken(&req)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.Success(loginResp))
|
||||
}
|
||||
|
||||
// GetProfile 获取用户资料
|
||||
func (h *AuthHandler) GetProfile(c *gin.Context) {
|
||||
userID, exists := middleware.GetCurrentUser(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, "User not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.authService.GetUserByID(userID)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusInternalServerError, response.Error(http.StatusInternalServerError, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.Success(user))
|
||||
}
|
||||
|
||||
// UpdatePassword 更新密码
|
||||
func (h *AuthHandler) UpdatePassword(c *gin.Context) {
|
||||
userID, exists := middleware.GetCurrentUser(c)
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, response.Error(http.StatusUnauthorized, "User not authenticated"))
|
||||
return
|
||||
}
|
||||
|
||||
var req dto.ChangePasswordRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
if err := h.authService.UpdatePassword(userID, &req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error(http.StatusBadRequest, err.Error()))
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, response.Success(gin.H{"message": "Password updated successfully"}))
|
||||
}
|
||||
|
||||
// Logout 用户登出
|
||||
func (h *AuthHandler) Logout(c *gin.Context) {
|
||||
// 简单实现,实际应用中可能需要将token加入黑名单
|
||||
c.JSON(http.StatusOK, response.Success(gin.H{"message": "Logged out successfully"}))
|
||||
}
|
||||
430
backend-old/internal/api/handlers/category_handler.go
Normal file
430
backend-old/internal/api/handlers/category_handler.go
Normal file
@ -0,0 +1,430 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/service"
|
||||
"photography-backend/pkg/response"
|
||||
|
||||
"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} response.Error
|
||||
// @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, response.Error(http.StatusBadRequest, "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, response.Error(http.StatusInternalServerError, 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} response.Error
|
||||
// @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, response.Error{
|
||||
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} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @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, response.Error{
|
||||
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, response.Error{
|
||||
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, response.Error{
|
||||
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} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @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, response.Error{
|
||||
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, response.Error{
|
||||
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} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /categories [post]
|
||||
func (h *CategoryHandler) CreateCategory(c *gin.Context) {
|
||||
var req entity.CreateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求数据
|
||||
if err := h.validateCreateCategoryRequest(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
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, response.Error{
|
||||
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} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @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, response.Error{
|
||||
Error: "Invalid category ID",
|
||||
Message: "Category ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req entity.UpdateCategoryRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
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, response.Error{
|
||||
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, response.Error{
|
||||
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} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @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, response.Error{
|
||||
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, response.Error{
|
||||
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, response.Error{
|
||||
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} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /categories/reorder [post]
|
||||
func (h *CategoryHandler) ReorderCategories(c *gin.Context) {
|
||||
var req entity.ReorderCategoriesRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.CategoryIDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
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, response.Error{
|
||||
Error: "Failed to reorder categories",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, entity.SuccessResponse{
|
||||
Message: "Categories reordered successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetCategoryStats 获取分类统计信息
|
||||
// @Summary 获取分类统计信息
|
||||
// @Description 获取分类统计信息
|
||||
// @Tags categories
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.CategoryStats
|
||||
// @Failure 500 {object} response.Error
|
||||
// @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, response.Error{
|
||||
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} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /categories/generate-slug [post]
|
||||
func (h *CategoryHandler) GenerateSlug(c *gin.Context) {
|
||||
var req entity.GenerateSlugRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
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, response.Error{
|
||||
Error: "Failed to generate slug",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, entity.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
|
||||
}
|
||||
480
backend-old/internal/api/handlers/photo_handler.go
Normal file
480
backend-old/internal/api/handlers/photo_handler.go
Normal file
@ -0,0 +1,480 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type PhotoHandler struct {
|
||||
photoService *service.PhotoService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewPhotoHandler(photoService *service.PhotoService, logger *zap.Logger) *PhotoHandler {
|
||||
return &PhotoHandler{
|
||||
photoService: photoService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetPhotos 获取照片列表
|
||||
// @Summary 获取照片列表
|
||||
// @Description 获取照片列表,支持分页、搜索、过滤
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码"
|
||||
// @Param limit query int false "每页数量"
|
||||
// @Param search query string false "搜索关键词"
|
||||
// @Param status query string false "状态筛选"
|
||||
// @Param category_id query int false "分类ID"
|
||||
// @Param tags query string false "标签列表(逗号分隔)"
|
||||
// @Param start_date query string false "开始日期"
|
||||
// @Param end_date query string false "结束日期"
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向"
|
||||
// @Success 200 {object} service.PhotoListResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos [get]
|
||||
func (h *PhotoHandler) GetPhotos(c *gin.Context) {
|
||||
var params service.PhotoListParams
|
||||
|
||||
// 解析查询参数
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
h.logger.Error("Failed to bind query params", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid query parameters",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 解析标签参数
|
||||
if tagsStr := c.Query("tags"); tagsStr != "" {
|
||||
params.Tags = strings.Split(tagsStr, ",")
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
result, err := h.photoService.GetPhotos(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get photos", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get photos",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetPhoto 获取照片详情
|
||||
// @Summary 获取照片详情
|
||||
// @Description 根据ID获取照片详情
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "照片ID"
|
||||
// @Success 200 {object} models.Photo
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos/{id} [get]
|
||||
func (h *PhotoHandler) GetPhoto(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid photo ID",
|
||||
Message: "Photo ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
photo, err := h.photoService.GetPhotoByID(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
if err.Error() == "photo not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "Photo not found",
|
||||
Message: "The requested photo does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to get photo", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get photo",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, photo)
|
||||
}
|
||||
|
||||
// CreatePhoto 创建照片
|
||||
// @Summary 创建照片
|
||||
// @Description 创建新的照片记录
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param photo body models.CreatePhotoRequest true "照片信息"
|
||||
// @Success 201 {object} models.Photo
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos [post]
|
||||
func (h *PhotoHandler) CreatePhoto(c *gin.Context) {
|
||||
var req models.CreatePhotoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求数据
|
||||
if err := h.validateCreatePhotoRequest(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request data",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
photo, err := h.photoService.CreatePhoto(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create photo", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to create photo",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, photo)
|
||||
}
|
||||
|
||||
// UpdatePhoto 更新照片
|
||||
// @Summary 更新照片
|
||||
// @Description 更新照片信息
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "照片ID"
|
||||
// @Param photo body models.UpdatePhotoRequest true "照片信息"
|
||||
// @Success 200 {object} models.Photo
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos/{id} [put]
|
||||
func (h *PhotoHandler) UpdatePhoto(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid photo ID",
|
||||
Message: "Photo ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdatePhotoRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
photo, err := h.photoService.UpdatePhoto(c.Request.Context(), uint(id), &req)
|
||||
if err != nil {
|
||||
if err.Error() == "photo not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "Photo not found",
|
||||
Message: "The requested photo does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to update photo", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to update photo",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, photo)
|
||||
}
|
||||
|
||||
// DeletePhoto 删除照片
|
||||
// @Summary 删除照片
|
||||
// @Description 删除照片
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "照片ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos/{id} [delete]
|
||||
func (h *PhotoHandler) DeletePhoto(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid photo ID",
|
||||
Message: "Photo ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.photoService.DeletePhoto(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
if err.Error() == "photo not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "Photo not found",
|
||||
Message: "The requested photo does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to delete photo", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to delete photo",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// UploadPhoto 上传照片
|
||||
// @Summary 上传照片
|
||||
// @Description 上传照片文件并创建记录
|
||||
// @Tags photos
|
||||
// @Accept multipart/form-data
|
||||
// @Produce json
|
||||
// @Param file formData file true "照片文件"
|
||||
// @Param title formData string false "标题"
|
||||
// @Param description formData string false "描述"
|
||||
// @Param status formData string false "状态"
|
||||
// @Param category_ids formData string false "分类ID列表(逗号分隔)"
|
||||
// @Param tag_ids formData string false "标签ID列表(逗号分隔)"
|
||||
// @Success 201 {object} models.Photo
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos/upload [post]
|
||||
func (h *PhotoHandler) UploadPhoto(c *gin.Context) {
|
||||
// 获取上传的文件
|
||||
file, header, err := c.Request.FormFile("file")
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "No file uploaded",
|
||||
Message: "Please select a file to upload",
|
||||
})
|
||||
return
|
||||
}
|
||||
defer file.Close()
|
||||
|
||||
// 构建创建请求
|
||||
req := &models.CreatePhotoRequest{
|
||||
Title: c.PostForm("title"),
|
||||
Description: c.PostForm("description"),
|
||||
Status: c.PostForm("status"),
|
||||
}
|
||||
|
||||
// 如果未指定状态,默认为草稿
|
||||
if req.Status == "" {
|
||||
req.Status = "draft"
|
||||
}
|
||||
|
||||
// 解析分类ID
|
||||
if categoryIDsStr := c.PostForm("category_ids"); categoryIDsStr != "" {
|
||||
categoryIDStrs := strings.Split(categoryIDsStr, ",")
|
||||
for _, idStr := range categoryIDStrs {
|
||||
if id, err := strconv.ParseUint(strings.TrimSpace(idStr), 10, 32); err == nil {
|
||||
req.CategoryIDs = append(req.CategoryIDs, uint(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 解析标签ID
|
||||
if tagIDsStr := c.PostForm("tag_ids"); tagIDsStr != "" {
|
||||
tagIDStrs := strings.Split(tagIDsStr, ",")
|
||||
for _, idStr := range tagIDStrs {
|
||||
if id, err := strconv.ParseUint(strings.TrimSpace(idStr), 10, 32); err == nil {
|
||||
req.TagIDs = append(req.TagIDs, uint(id))
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 上传照片
|
||||
photo, err := h.photoService.UploadPhoto(c.Request.Context(), file, header, req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to upload photo", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to upload photo",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, photo)
|
||||
}
|
||||
|
||||
// BatchUpdatePhotos 批量更新照片
|
||||
// @Summary 批量更新照片
|
||||
// @Description 批量更新照片信息
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.BatchUpdatePhotosRequest true "批量更新请求"
|
||||
// @Success 200 {object} models.SuccessResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos/batch/update [post]
|
||||
func (h *PhotoHandler) BatchUpdatePhotos(c *gin.Context) {
|
||||
var req models.BatchUpdatePhotosRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request",
|
||||
Message: "No photo IDs provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.photoService.BatchUpdatePhotos(c.Request.Context(), req.IDs, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to batch update photos", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to batch update photos",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.SuccessResponse{
|
||||
Message: "Photos updated successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// BatchDeletePhotos 批量删除照片
|
||||
// @Summary 批量删除照片
|
||||
// @Description 批量删除照片
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.BatchDeleteRequest true "批量删除请求"
|
||||
// @Success 200 {object} models.SuccessResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos/batch/delete [post]
|
||||
func (h *PhotoHandler) BatchDeletePhotos(c *gin.Context) {
|
||||
var req models.BatchDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request",
|
||||
Message: "No photo IDs provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.photoService.BatchDeletePhotos(c.Request.Context(), req.IDs)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to batch delete photos", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to batch delete photos",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.SuccessResponse{
|
||||
Message: "Photos deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetPhotoStats 获取照片统计信息
|
||||
// @Summary 获取照片统计信息
|
||||
// @Description 获取照片统计信息
|
||||
// @Tags photos
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.PhotoStats
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /photos/stats [get]
|
||||
func (h *PhotoHandler) GetPhotoStats(c *gin.Context) {
|
||||
stats, err := h.photoService.GetPhotoStats(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get photo stats", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get photo stats",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// validateCreatePhotoRequest 验证创建照片请求
|
||||
func (h *PhotoHandler) validateCreatePhotoRequest(req *models.CreatePhotoRequest) error {
|
||||
if req.Title == "" {
|
||||
return errors.New("title is required")
|
||||
}
|
||||
|
||||
if req.Status == "" {
|
||||
req.Status = "draft"
|
||||
}
|
||||
|
||||
// 验证状态值
|
||||
validStatuses := []string{"draft", "published", "archived", "processing"}
|
||||
isValidStatus := false
|
||||
for _, status := range validStatuses {
|
||||
if req.Status == status {
|
||||
isValidStatus = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidStatus {
|
||||
return errors.New("invalid status value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
536
backend-old/internal/api/handlers/tag_handler.go
Normal file
536
backend-old/internal/api/handlers/tag_handler.go
Normal file
@ -0,0 +1,536 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type TagHandler struct {
|
||||
tagService *service.TagService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewTagHandler(tagService *service.TagService, logger *zap.Logger) *TagHandler {
|
||||
return &TagHandler{
|
||||
tagService: tagService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetTags 获取标签列表
|
||||
// @Summary 获取标签列表
|
||||
// @Description 获取标签列表,支持分页、搜索、过滤
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码"
|
||||
// @Param limit query int false "每页数量"
|
||||
// @Param search query string false "搜索关键词"
|
||||
// @Param is_active query bool false "是否激活"
|
||||
// @Param sort_by query string false "排序字段"
|
||||
// @Param sort_order query string false "排序方向"
|
||||
// @Success 200 {object} service.TagListResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags [get]
|
||||
func (h *TagHandler) GetTags(c *gin.Context) {
|
||||
var params service.TagListParams
|
||||
|
||||
// 解析查询参数
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
h.logger.Error("Failed to bind query params", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid query parameters",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 调用服务层
|
||||
result, err := h.tagService.GetTags(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get tags", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get tags",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetAllTags 获取所有活跃标签
|
||||
// @Summary 获取所有活跃标签
|
||||
// @Description 获取所有活跃标签(用于选择器)
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.Tag
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/all [get]
|
||||
func (h *TagHandler) GetAllTags(c *gin.Context) {
|
||||
tags, err := h.tagService.GetAllTags(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get all tags", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get all tags",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// GetTag 获取标签详情
|
||||
// @Summary 获取标签详情
|
||||
// @Description 根据ID获取标签详情
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "标签ID"
|
||||
// @Success 200 {object} models.Tag
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/{id} [get]
|
||||
func (h *TagHandler) GetTag(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid tag ID",
|
||||
Message: "Tag ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.tagService.GetTagByID(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
if err.Error() == "tag not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "Tag not found",
|
||||
Message: "The requested tag does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to get tag", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get tag",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tag)
|
||||
}
|
||||
|
||||
// GetTagBySlug 根据slug获取标签
|
||||
// @Summary 根据slug获取标签
|
||||
// @Description 根据slug获取标签详情
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param slug path string true "标签slug"
|
||||
// @Success 200 {object} models.Tag
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/slug/{slug} [get]
|
||||
func (h *TagHandler) GetTagBySlug(c *gin.Context) {
|
||||
slug := c.Param("slug")
|
||||
|
||||
tag, err := h.tagService.GetTagBySlug(c.Request.Context(), slug)
|
||||
if err != nil {
|
||||
if err.Error() == "tag not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "Tag not found",
|
||||
Message: "The requested tag does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to get tag by slug", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get tag",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tag)
|
||||
}
|
||||
|
||||
// CreateTag 创建标签
|
||||
// @Summary 创建标签
|
||||
// @Description 创建新的标签
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param tag body models.CreateTagRequest true "标签信息"
|
||||
// @Success 201 {object} models.Tag
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags [post]
|
||||
func (h *TagHandler) CreateTag(c *gin.Context) {
|
||||
var req models.CreateTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求数据
|
||||
if err := h.validateCreateTagRequest(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request data",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.tagService.CreateTag(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create tag", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to create tag",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, tag)
|
||||
}
|
||||
|
||||
// UpdateTag 更新标签
|
||||
// @Summary 更新标签
|
||||
// @Description 更新标签信息
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "标签ID"
|
||||
// @Param tag body models.UpdateTagRequest true "标签信息"
|
||||
// @Success 200 {object} models.Tag
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/{id} [put]
|
||||
func (h *TagHandler) UpdateTag(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid tag ID",
|
||||
Message: "Tag ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateTagRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
tag, err := h.tagService.UpdateTag(c.Request.Context(), uint(id), &req)
|
||||
if err != nil {
|
||||
if err.Error() == "tag not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "Tag not found",
|
||||
Message: "The requested tag does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to update tag", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to update tag",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tag)
|
||||
}
|
||||
|
||||
// DeleteTag 删除标签
|
||||
// @Summary 删除标签
|
||||
// @Description 删除标签
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "标签ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/{id} [delete]
|
||||
func (h *TagHandler) DeleteTag(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid tag ID",
|
||||
Message: "Tag ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.tagService.DeleteTag(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
if err.Error() == "tag not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "Tag not found",
|
||||
Message: "The requested tag does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to delete tag", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to delete tag",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// BatchDeleteTags 批量删除标签
|
||||
// @Summary 批量删除标签
|
||||
// @Description 批量删除标签
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.BatchDeleteRequest true "批量删除请求"
|
||||
// @Success 200 {object} models.SuccessResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/batch/delete [post]
|
||||
func (h *TagHandler) BatchDeleteTags(c *gin.Context) {
|
||||
var req models.BatchDeleteRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if len(req.IDs) == 0 {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request",
|
||||
Message: "No tag IDs provided",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err := h.tagService.BatchDeleteTags(c.Request.Context(), req.IDs)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to batch delete tags", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to batch delete tags",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.SuccessResponse{
|
||||
Message: "Tags deleted successfully",
|
||||
})
|
||||
}
|
||||
|
||||
// GetPopularTags 获取热门标签
|
||||
// @Summary 获取热门标签
|
||||
// @Description 获取热门标签(按使用次数排序)
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param limit query int false "限制数量"
|
||||
// @Success 200 {array} models.TagWithCount
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/popular [get]
|
||||
func (h *TagHandler) GetPopularTags(c *gin.Context) {
|
||||
limit := 10
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := h.tagService.GetPopularTags(c.Request.Context(), limit)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get popular tags", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get popular tags",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// GetTagCloud 获取标签云
|
||||
// @Summary 获取标签云
|
||||
// @Description 获取标签云数据
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {array} models.TagCloudItem
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/cloud [get]
|
||||
func (h *TagHandler) GetTagCloud(c *gin.Context) {
|
||||
cloud, err := h.tagService.GetTagCloud(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get tag cloud", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get tag cloud",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, cloud)
|
||||
}
|
||||
|
||||
// GetTagStats 获取标签统计信息
|
||||
// @Summary 获取标签统计信息
|
||||
// @Description 获取标签统计信息
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.TagStats
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/stats [get]
|
||||
func (h *TagHandler) GetTagStats(c *gin.Context) {
|
||||
stats, err := h.tagService.GetTagStats(c.Request.Context())
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get tag stats", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get tag stats",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, stats)
|
||||
}
|
||||
|
||||
// SearchTags 搜索标签
|
||||
// @Summary 搜索标签
|
||||
// @Description 搜索标签(用于自动完成)
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param q query string true "搜索关键词"
|
||||
// @Param limit query int false "限制数量"
|
||||
// @Success 200 {array} models.Tag
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/search [get]
|
||||
func (h *TagHandler) SearchTags(c *gin.Context) {
|
||||
query := c.Query("q")
|
||||
if query == "" {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid query",
|
||||
Message: "Search query is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
limit := 10
|
||||
if limitStr := c.Query("limit"); limitStr != "" {
|
||||
if parsedLimit, err := strconv.Atoi(limitStr); err == nil && parsedLimit > 0 {
|
||||
limit = parsedLimit
|
||||
}
|
||||
}
|
||||
|
||||
tags, err := h.tagService.SearchTags(c.Request.Context(), query, limit)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to search tags", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to search tags",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, tags)
|
||||
}
|
||||
|
||||
// GenerateSlug 生成标签slug
|
||||
// @Summary 生成标签slug
|
||||
// @Description 根据标签名称生成唯一的slug
|
||||
// @Tags tags
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param request body models.GenerateSlugRequest true "生成slug请求"
|
||||
// @Success 200 {object} models.GenerateSlugResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /tags/generate-slug [post]
|
||||
func (h *TagHandler) 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, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
if req.Name == "" {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request",
|
||||
Message: "Name is required",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
slug, err := h.tagService.GenerateSlug(c.Request.Context(), req.Name)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to generate slug", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to generate slug",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, models.GenerateSlugResponse{
|
||||
Slug: slug,
|
||||
})
|
||||
}
|
||||
|
||||
// validateCreateTagRequest 验证创建标签请求
|
||||
func (h *TagHandler) validateCreateTagRequest(req *models.CreateTagRequest) error {
|
||||
if req.Name == "" {
|
||||
return errors.New("name is required")
|
||||
}
|
||||
|
||||
if req.Slug == "" {
|
||||
return errors.New("slug is required")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
410
backend-old/internal/api/handlers/user_handler.go
Normal file
410
backend-old/internal/api/handlers/user_handler.go
Normal file
@ -0,0 +1,410 @@
|
||||
package handlers
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
"photography-backend/internal/model/entity"
|
||||
"photography-backend/internal/model/dto"
|
||||
"photography-backend/internal/service"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type UserHandler struct {
|
||||
userService *service.UserService
|
||||
logger *zap.Logger
|
||||
}
|
||||
|
||||
func NewUserHandler(userService *service.UserService, logger *zap.Logger) *UserHandler {
|
||||
return &UserHandler{
|
||||
userService: userService,
|
||||
logger: logger,
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentUser 获取当前用户信息
|
||||
// @Summary 获取当前用户信息
|
||||
// @Description 获取当前登录用户的详细信息
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Success 200 {object} models.UserResponse
|
||||
// @Failure 401 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /me [get]
|
||||
func (h *UserHandler) GetCurrentUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
user, err := h.userService.GetUserByID(c.Request.Context(), userID)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get current user", zap.Error(err), zap.Uint("user_id", userID))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get user information",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userResponse := &models.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, userResponse)
|
||||
}
|
||||
|
||||
// UpdateCurrentUser 更新当前用户信息
|
||||
// @Summary 更新当前用户信息
|
||||
// @Description 更新当前登录用户的个人信息
|
||||
// @Tags users
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user body models.UpdateCurrentUserRequest true "用户信息"
|
||||
// @Success 200 {object} models.UserResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /me [put]
|
||||
func (h *UserHandler) UpdateCurrentUser(c *gin.Context) {
|
||||
userID := c.GetUint("user_id")
|
||||
|
||||
var req models.UpdateCurrentUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.UpdateCurrentUser(c.Request.Context(), userID, &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to update current user", zap.Error(err), zap.Uint("user_id", userID))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to update user information",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userResponse := &models.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, userResponse)
|
||||
}
|
||||
|
||||
// GetUsers 获取用户列表 (管理员功能)
|
||||
// @Summary 获取用户列表
|
||||
// @Description 获取系统中所有用户列表
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param page query int false "页码"
|
||||
// @Param limit query int false "每页数量"
|
||||
// @Param search query string false "搜索关键词"
|
||||
// @Success 200 {object} service.UserListResponse
|
||||
// @Failure 403 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /admin/users [get]
|
||||
func (h *UserHandler) GetUsers(c *gin.Context) {
|
||||
var params service.UserListParams
|
||||
|
||||
// 解析查询参数
|
||||
if err := c.ShouldBindQuery(¶ms); err != nil {
|
||||
h.logger.Error("Failed to bind query params", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid query parameters",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
result, err := h.userService.GetUsers(c.Request.Context(), params)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to get users", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get users",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, result)
|
||||
}
|
||||
|
||||
// GetUser 获取用户详情 (管理员功能)
|
||||
// @Summary 获取用户详情
|
||||
// @Description 根据ID获取用户详情
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "用户ID"
|
||||
// @Success 200 {object} models.UserResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /admin/users/{id} [get]
|
||||
func (h *UserHandler) GetUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid user ID",
|
||||
Message: "User ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.GetUserByID(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
if err.Error() == "user not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "User not found",
|
||||
Message: "The requested user does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to get user", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to get user",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userResponse := &models.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, userResponse)
|
||||
}
|
||||
|
||||
// CreateUser 创建用户 (管理员功能)
|
||||
// @Summary 创建用户
|
||||
// @Description 创建新用户
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param user body models.CreateUserRequest true "用户信息"
|
||||
// @Success 201 {object} models.UserResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /admin/users [post]
|
||||
func (h *UserHandler) CreateUser(c *gin.Context) {
|
||||
var req models.CreateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 验证请求数据
|
||||
if err := h.validateCreateUserRequest(&req); err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request data",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.CreateUser(c.Request.Context(), &req)
|
||||
if err != nil {
|
||||
h.logger.Error("Failed to create user", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to create user",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userResponse := &models.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusCreated, userResponse)
|
||||
}
|
||||
|
||||
// UpdateUser 更新用户 (管理员功能)
|
||||
// @Summary 更新用户
|
||||
// @Description 更新用户信息
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "用户ID"
|
||||
// @Param user body models.UpdateUserRequest true "用户信息"
|
||||
// @Success 200 {object} models.UserResponse
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /admin/users/{id} [put]
|
||||
func (h *UserHandler) UpdateUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid user ID",
|
||||
Message: "User ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
var req models.UpdateUserRequest
|
||||
if err := c.ShouldBindJSON(&req); err != nil {
|
||||
h.logger.Error("Failed to bind JSON", zap.Error(err))
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid request body",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.userService.UpdateUser(c.Request.Context(), uint(id), &req)
|
||||
if err != nil {
|
||||
if err.Error() == "user not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "User not found",
|
||||
Message: "The requested user does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to update user", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to update user",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
userResponse := &models.UserResponse{
|
||||
ID: user.ID,
|
||||
Username: user.Username,
|
||||
Email: user.Email,
|
||||
Role: user.Role,
|
||||
IsActive: user.IsActive,
|
||||
CreatedAt: user.CreatedAt,
|
||||
UpdatedAt: user.UpdatedAt,
|
||||
}
|
||||
|
||||
c.JSON(http.StatusOK, userResponse)
|
||||
}
|
||||
|
||||
// DeleteUser 删除用户 (管理员功能)
|
||||
// @Summary 删除用户
|
||||
// @Description 删除用户
|
||||
// @Tags admin
|
||||
// @Accept json
|
||||
// @Produce json
|
||||
// @Param id path int true "用户ID"
|
||||
// @Success 204 "No Content"
|
||||
// @Failure 400 {object} response.Error
|
||||
// @Failure 404 {object} response.Error
|
||||
// @Failure 500 {object} response.Error
|
||||
// @Router /admin/users/{id} [delete]
|
||||
func (h *UserHandler) DeleteUser(c *gin.Context) {
|
||||
idStr := c.Param("id")
|
||||
id, err := strconv.ParseUint(idStr, 10, 32)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Invalid user ID",
|
||||
Message: "User ID must be a valid number",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
// 防止删除自己
|
||||
currentUserID := c.GetUint("user_id")
|
||||
if uint(id) == currentUserID {
|
||||
c.JSON(http.StatusBadRequest, response.Error{
|
||||
Error: "Cannot delete yourself",
|
||||
Message: "You cannot delete your own account",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
err = h.userService.DeleteUser(c.Request.Context(), uint(id))
|
||||
if err != nil {
|
||||
if err.Error() == "user not found" {
|
||||
c.JSON(http.StatusNotFound, response.Error{
|
||||
Error: "User not found",
|
||||
Message: "The requested user does not exist",
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
h.logger.Error("Failed to delete user", zap.Error(err))
|
||||
c.JSON(http.StatusInternalServerError, response.Error{
|
||||
Error: "Failed to delete user",
|
||||
Message: err.Error(),
|
||||
})
|
||||
return
|
||||
}
|
||||
|
||||
c.Status(http.StatusNoContent)
|
||||
}
|
||||
|
||||
// validateCreateUserRequest 验证创建用户请求
|
||||
func (h *UserHandler) validateCreateUserRequest(req *models.CreateUserRequest) error {
|
||||
if req.Username == "" {
|
||||
return errors.New("username is required")
|
||||
}
|
||||
|
||||
if req.Email == "" {
|
||||
return errors.New("email is required")
|
||||
}
|
||||
|
||||
if req.Password == "" {
|
||||
return errors.New("password is required")
|
||||
}
|
||||
|
||||
if req.Role == "" {
|
||||
req.Role = "user"
|
||||
}
|
||||
|
||||
// 验证角色
|
||||
validRoles := []string{"user", "editor", "admin"}
|
||||
isValidRole := false
|
||||
for _, role := range validRoles {
|
||||
if req.Role == role {
|
||||
isValidRole = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if !isValidRole {
|
||||
return errors.New("invalid role value")
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
217
backend-old/internal/api/middleware/auth.go
Normal file
217
backend-old/internal/api/middleware/auth.go
Normal file
@ -0,0 +1,217 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"github.com/gin-gonic/gin"
|
||||
"photography-backend/internal/service/auth"
|
||||
"photography-backend/internal/model/entity"
|
||||
)
|
||||
|
||||
// AuthMiddleware 认证中间件
|
||||
type AuthMiddleware struct {
|
||||
jwtService *auth.JWTService
|
||||
}
|
||||
|
||||
// NewAuthMiddleware 创建认证中间件
|
||||
func NewAuthMiddleware(jwtService *auth.JWTService) *AuthMiddleware {
|
||||
return &AuthMiddleware{
|
||||
jwtService: jwtService,
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAuth 需要认证的中间件
|
||||
func (m *AuthMiddleware) RequireAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
// 从Header中获取Authorization
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Authorization header is required",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查Bearer前缀
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid authorization header format",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 提取token
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
|
||||
// 验证token
|
||||
claims, err := m.jwtService.ValidateToken(token)
|
||||
if err != nil {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid or expired token",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存入上下文
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("user_role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireRole 需要特定角色的中间件
|
||||
func (m *AuthMiddleware) RequireRole(requiredRole string) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
userRole, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "User role not found in context",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
roleStr, ok := userRole.(string)
|
||||
if !ok {
|
||||
c.JSON(http.StatusUnauthorized, gin.H{
|
||||
"error": "Invalid user role",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
// 检查角色权限
|
||||
if !m.hasPermission(roleStr, requiredRole) {
|
||||
c.JSON(http.StatusForbidden, gin.H{
|
||||
"error": "Insufficient permissions",
|
||||
})
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// RequireAdmin 需要管理员权限的中间件
|
||||
func (m *AuthMiddleware) RequireAdmin() gin.HandlerFunc {
|
||||
return m.RequireRole(string(entity.UserRoleAdmin))
|
||||
}
|
||||
|
||||
// RequirePhotographer 需要摄影师权限的中间件
|
||||
func (m *AuthMiddleware) RequirePhotographer() gin.HandlerFunc {
|
||||
return m.RequireRole(string(entity.UserRolePhotographer))
|
||||
}
|
||||
|
||||
// OptionalAuth 可选认证中间件
|
||||
func (m *AuthMiddleware) OptionalAuth() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
authHeader := c.GetHeader("Authorization")
|
||||
if authHeader == "" {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
if !strings.HasPrefix(authHeader, "Bearer ") {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
token := strings.TrimPrefix(authHeader, "Bearer ")
|
||||
claims, err := m.jwtService.ValidateToken(token)
|
||||
if err != nil {
|
||||
c.Next()
|
||||
return
|
||||
}
|
||||
|
||||
// 将用户信息存入上下文
|
||||
c.Set("user_id", claims.UserID)
|
||||
c.Set("username", claims.Username)
|
||||
c.Set("user_role", claims.Role)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// GetCurrentUser 获取当前用户ID
|
||||
func GetCurrentUser(c *gin.Context) (uint, bool) {
|
||||
userID, exists := c.Get("user_id")
|
||||
if !exists {
|
||||
return 0, false
|
||||
}
|
||||
|
||||
id, ok := userID.(uint)
|
||||
return id, ok
|
||||
}
|
||||
|
||||
// GetCurrentUserRole 获取当前用户角色
|
||||
func GetCurrentUserRole(c *gin.Context) (string, bool) {
|
||||
userRole, exists := c.Get("user_role")
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
|
||||
role, ok := userRole.(string)
|
||||
return role, ok
|
||||
}
|
||||
|
||||
// GetCurrentUsername 获取当前用户名
|
||||
func GetCurrentUsername(c *gin.Context) (string, bool) {
|
||||
username, exists := c.Get("username")
|
||||
if !exists {
|
||||
return "", false
|
||||
}
|
||||
|
||||
name, ok := username.(string)
|
||||
return name, ok
|
||||
}
|
||||
|
||||
// IsAuthenticated 检查是否已认证
|
||||
func IsAuthenticated(c *gin.Context) bool {
|
||||
_, exists := c.Get("user_id")
|
||||
return exists
|
||||
}
|
||||
|
||||
// IsAdmin 检查是否为管理员
|
||||
func IsAdmin(c *gin.Context) bool {
|
||||
role, exists := GetCurrentUserRole(c)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return role == string(entity.UserRoleAdmin)
|
||||
}
|
||||
|
||||
// IsPhotographer 检查是否为摄影师或以上
|
||||
func IsPhotographer(c *gin.Context) bool {
|
||||
role, exists := GetCurrentUserRole(c)
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
return role == string(entity.UserRolePhotographer) || role == string(entity.UserRoleAdmin)
|
||||
}
|
||||
|
||||
// hasPermission 检查权限
|
||||
func (m *AuthMiddleware) hasPermission(userRole, requiredRole string) bool {
|
||||
roleLevel := map[string]int{
|
||||
string(entity.UserRoleUser): 1,
|
||||
string(entity.UserRolePhotographer): 2,
|
||||
string(entity.UserRoleAdmin): 3,
|
||||
}
|
||||
|
||||
userLevel, exists := roleLevel[userRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
requiredLevel, exists := roleLevel[requiredRole]
|
||||
if !exists {
|
||||
return false
|
||||
}
|
||||
|
||||
return userLevel >= requiredLevel
|
||||
}
|
||||
58
backend-old/internal/api/middleware/cors.go
Normal file
58
backend-old/internal/api/middleware/cors.go
Normal file
@ -0,0 +1,58 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"github.com/gin-gonic/gin"
|
||||
"photography-backend/internal/config"
|
||||
)
|
||||
|
||||
// CORSMiddleware CORS中间件
|
||||
func CORSMiddleware(cfg *config.CORSConfig) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
origin := c.GetHeader("Origin")
|
||||
|
||||
// 检查是否允许的来源
|
||||
allowed := false
|
||||
for _, allowedOrigin := range cfg.AllowedOrigins {
|
||||
if allowedOrigin == "*" || allowedOrigin == origin {
|
||||
allowed = true
|
||||
break
|
||||
}
|
||||
}
|
||||
|
||||
if allowed {
|
||||
c.Header("Access-Control-Allow-Origin", origin)
|
||||
}
|
||||
|
||||
// 设置其他CORS头
|
||||
c.Header("Access-Control-Allow-Methods", joinStrings(cfg.AllowedMethods, ", "))
|
||||
c.Header("Access-Control-Allow-Headers", joinStrings(cfg.AllowedHeaders, ", "))
|
||||
c.Header("Access-Control-Max-Age", "86400")
|
||||
|
||||
if cfg.AllowCredentials {
|
||||
c.Header("Access-Control-Allow-Credentials", "true")
|
||||
}
|
||||
|
||||
// 处理预检请求
|
||||
if c.Request.Method == "OPTIONS" {
|
||||
c.AbortWithStatus(http.StatusNoContent)
|
||||
return
|
||||
}
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// joinStrings 连接字符串数组
|
||||
func joinStrings(strs []string, sep string) string {
|
||||
if len(strs) == 0 {
|
||||
return ""
|
||||
}
|
||||
|
||||
result := strs[0]
|
||||
for i := 1; i < len(strs); i++ {
|
||||
result += sep + strs[i]
|
||||
}
|
||||
|
||||
return result
|
||||
}
|
||||
74
backend-old/internal/api/middleware/logger.go
Normal file
74
backend-old/internal/api/middleware/logger.go
Normal file
@ -0,0 +1,74 @@
|
||||
package middleware
|
||||
|
||||
import (
|
||||
"time"
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
// LoggerMiddleware 日志中间件
|
||||
func LoggerMiddleware(logger *zap.Logger) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
start := time.Now()
|
||||
path := c.Request.URL.Path
|
||||
raw := c.Request.URL.RawQuery
|
||||
|
||||
// 处理请求
|
||||
c.Next()
|
||||
|
||||
// 计算延迟
|
||||
latency := time.Since(start)
|
||||
|
||||
// 获取请求信息
|
||||
clientIP := c.ClientIP()
|
||||
method := c.Request.Method
|
||||
statusCode := c.Writer.Status()
|
||||
bodySize := c.Writer.Size()
|
||||
|
||||
if raw != "" {
|
||||
path = path + "?" + raw
|
||||
}
|
||||
|
||||
// 记录日志
|
||||
logger.Info("HTTP Request",
|
||||
zap.String("method", method),
|
||||
zap.String("path", path),
|
||||
zap.String("client_ip", clientIP),
|
||||
zap.Int("status_code", statusCode),
|
||||
zap.Int("body_size", bodySize),
|
||||
zap.Duration("latency", latency),
|
||||
zap.String("user_agent", c.Request.UserAgent()),
|
||||
)
|
||||
}
|
||||
}
|
||||
|
||||
// RequestIDMiddleware 请求ID中间件
|
||||
func RequestIDMiddleware() gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
requestID := c.GetHeader("X-Request-ID")
|
||||
if requestID == "" {
|
||||
requestID = generateRequestID()
|
||||
}
|
||||
|
||||
c.Set("request_id", requestID)
|
||||
c.Header("X-Request-ID", requestID)
|
||||
|
||||
c.Next()
|
||||
}
|
||||
}
|
||||
|
||||
// generateRequestID 生成请求ID
|
||||
func generateRequestID() string {
|
||||
// 简单实现,实际应用中可能需要更复杂的ID生成逻辑
|
||||
return time.Now().Format("20060102150405") + "-" + randomString(8)
|
||||
}
|
||||
|
||||
// randomString 生成随机字符串
|
||||
func randomString(length int) string {
|
||||
const charset = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"
|
||||
b := make([]byte, length)
|
||||
for i := range b {
|
||||
b[i] = charset[time.Now().UnixNano()%int64(len(charset))]
|
||||
}
|
||||
return string(b)
|
||||
}
|
||||
131
backend-old/internal/api/routes/routes.go
Normal file
131
backend-old/internal/api/routes/routes.go
Normal file
@ -0,0 +1,131 @@
|
||||
package routes
|
||||
|
||||
import (
|
||||
"photography-backend/internal/api/handlers"
|
||||
"photography-backend/internal/api/middleware"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
)
|
||||
|
||||
type Handlers struct {
|
||||
AuthHandler *handlers.AuthHandler
|
||||
UserHandler *handlers.UserHandler
|
||||
PhotoHandler *handlers.PhotoHandler
|
||||
CategoryHandler *handlers.CategoryHandler
|
||||
TagHandler *handlers.TagHandler
|
||||
}
|
||||
|
||||
func SetupRoutes(r *gin.Engine, h *Handlers, authMiddleware *middleware.AuthMiddleware, logger *zap.Logger) {
|
||||
// 健康检查
|
||||
r.GET("/health", func(c *gin.Context) {
|
||||
c.JSON(200, gin.H{"status": "ok"})
|
||||
})
|
||||
|
||||
// API 路由组
|
||||
api := r.Group("/api")
|
||||
{
|
||||
// 公开路由
|
||||
public := api.Group("")
|
||||
{
|
||||
// 认证相关
|
||||
auth := public.Group("/auth")
|
||||
{
|
||||
auth.POST("/login", h.AuthHandler.Login)
|
||||
auth.POST("/refresh", h.AuthHandler.RefreshToken)
|
||||
}
|
||||
}
|
||||
|
||||
// 需要认证的路由
|
||||
protected := api.Group("")
|
||||
protected.Use(authMiddleware.RequireAuth())
|
||||
{
|
||||
// 当前用户信息
|
||||
protected.GET("/me", h.UserHandler.GetCurrentUser)
|
||||
protected.PUT("/me", h.UserHandler.UpdateCurrentUser)
|
||||
protected.POST("/auth/logout", h.AuthHandler.Logout)
|
||||
|
||||
// 照片管理
|
||||
photos := protected.Group("/photos")
|
||||
{
|
||||
photos.GET("", h.PhotoHandler.GetPhotos)
|
||||
photos.POST("", h.PhotoHandler.CreatePhoto)
|
||||
photos.GET("/stats", h.PhotoHandler.GetPhotoStats)
|
||||
photos.POST("/upload", h.PhotoHandler.UploadPhoto)
|
||||
photos.POST("/batch/update", h.PhotoHandler.BatchUpdatePhotos)
|
||||
photos.POST("/batch/delete", h.PhotoHandler.BatchDeletePhotos)
|
||||
photos.GET("/:id", h.PhotoHandler.GetPhoto)
|
||||
photos.PUT("/:id", h.PhotoHandler.UpdatePhoto)
|
||||
photos.DELETE("/:id", h.PhotoHandler.DeletePhoto)
|
||||
}
|
||||
|
||||
// 分类管理
|
||||
categories := protected.Group("/categories")
|
||||
{
|
||||
categories.GET("", h.CategoryHandler.GetCategories)
|
||||
categories.POST("", h.CategoryHandler.CreateCategory)
|
||||
categories.GET("/tree", h.CategoryHandler.GetCategoryTree)
|
||||
categories.GET("/stats", h.CategoryHandler.GetCategoryStats)
|
||||
categories.POST("/reorder", h.CategoryHandler.ReorderCategories)
|
||||
categories.POST("/generate-slug", h.CategoryHandler.GenerateSlug)
|
||||
categories.GET("/:id", h.CategoryHandler.GetCategory)
|
||||
categories.PUT("/:id", h.CategoryHandler.UpdateCategory)
|
||||
categories.DELETE("/:id", h.CategoryHandler.DeleteCategory)
|
||||
categories.GET("/slug/:slug", h.CategoryHandler.GetCategoryBySlug)
|
||||
}
|
||||
|
||||
// 标签管理
|
||||
tags := protected.Group("/tags")
|
||||
{
|
||||
tags.GET("", h.TagHandler.GetTags)
|
||||
tags.POST("", h.TagHandler.CreateTag)
|
||||
tags.GET("/all", h.TagHandler.GetAllTags)
|
||||
tags.GET("/popular", h.TagHandler.GetPopularTags)
|
||||
tags.GET("/cloud", h.TagHandler.GetTagCloud)
|
||||
tags.GET("/stats", h.TagHandler.GetTagStats)
|
||||
tags.GET("/search", h.TagHandler.SearchTags)
|
||||
tags.POST("/batch/delete", h.TagHandler.BatchDeleteTags)
|
||||
tags.POST("/generate-slug", h.TagHandler.GenerateSlug)
|
||||
tags.GET("/:id", h.TagHandler.GetTag)
|
||||
tags.PUT("/:id", h.TagHandler.UpdateTag)
|
||||
tags.DELETE("/:id", h.TagHandler.DeleteTag)
|
||||
tags.GET("/slug/:slug", h.TagHandler.GetTagBySlug)
|
||||
}
|
||||
|
||||
// 用户管理 (需要管理员权限)
|
||||
admin := protected.Group("/admin")
|
||||
admin.Use(authMiddleware.RequireRole("admin"))
|
||||
{
|
||||
users := admin.Group("/users")
|
||||
{
|
||||
users.GET("", h.UserHandler.GetUsers)
|
||||
users.POST("", h.UserHandler.CreateUser)
|
||||
users.GET("/:id", h.UserHandler.GetUser)
|
||||
users.PUT("/:id", h.UserHandler.UpdateUser)
|
||||
users.DELETE("/:id", h.UserHandler.DeleteUser)
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// 前端公共 API (无需认证)
|
||||
frontend := api.Group("/public")
|
||||
{
|
||||
// 公开的照片接口
|
||||
frontend.GET("/photos", h.PhotoHandler.GetPhotos)
|
||||
frontend.GET("/photos/:id", h.PhotoHandler.GetPhoto)
|
||||
|
||||
// 公开的分类接口
|
||||
frontend.GET("/categories", h.CategoryHandler.GetCategories)
|
||||
frontend.GET("/categories/tree", h.CategoryHandler.GetCategoryTree)
|
||||
frontend.GET("/categories/:id", h.CategoryHandler.GetCategory)
|
||||
frontend.GET("/categories/slug/:slug", h.CategoryHandler.GetCategoryBySlug)
|
||||
|
||||
// 公开的标签接口
|
||||
frontend.GET("/tags", h.TagHandler.GetTags)
|
||||
frontend.GET("/tags/popular", h.TagHandler.GetPopularTags)
|
||||
frontend.GET("/tags/cloud", h.TagHandler.GetTagCloud)
|
||||
frontend.GET("/tags/:id", h.TagHandler.GetTag)
|
||||
frontend.GET("/tags/slug/:slug", h.TagHandler.GetTagBySlug)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user