diff --git a/README.md b/README.md index e0cf190..3bd37da 100644 --- a/README.md +++ b/README.md @@ -144,6 +144,140 @@ golangci-lint run (若已安装) - 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步 - 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露 +## 🔐 管理后台 API + +管理后台接口均需要管理员权限(`role=admin`),所有接口路径前缀为 `/api/v1/admin`。 + +### 📊 统计信息 + +| 接口 | 方法 | 说明 | +| --- | --- | --- | +| `/stats` | GET | 获取系统统计数据(用户数、材质数、下载量等) | + +**响应示例:** +```json +{ + "code": 0, + "message": "success", + "data": { + "total_users": 100, + "active_users": 80, + "banned_users": 5, + "admin_users": 3, + "total_textures": 500, + "public_textures": 300, + "pending_textures": 10, + "total_downloads": 1000, + "total_favorites": 500 + } +} +``` + +### 👥 角色管理 + +| 接口 | 方法 | 说明 | +| --- | --- | --- | +| `/roles` | GET | 获取所有可用角色列表 | + +**响应示例:** +```json +{ + "code": 0, + "message": "success", + "data": { + "roles": [ + { + "name": "user", + "display_name": "普通用户", + "description": "拥有基本用户权限" + }, + { + "name": "admin", + "display_name": "管理员", + "description": "拥有所有管理权限" + } + ] + } +} +``` + +### 👤 用户管理 + +| 接口 | 方法 | 说明 | +| --- | --- | --- | +| `/users` | GET | 获取用户列表(分页) | +| `/users/search` | GET | 搜索用户(支持关键词、角色、状态筛选、排序) | +| `/users/{id}` | GET | 获取用户详情 | +| `/users/{id}` | DELETE | 删除用户(软删除) | +| `/users/role` | PUT | 设置单个用户角色 | +| `/users/status` | PUT | 设置单个用户状态(封禁/解封) | +| `/users/batch-role` | PUT | 批量设置用户角色 | +| `/users/batch-delete` | DELETE | 批量删除用户 | + +**搜索用户请求参数:** +- `keyword` (string): 搜索关键词(用户名或邮箱) +- `role` (string): 角色筛选 +- `status` (int): 状态筛选(1=正常,0=禁用,-1=删除) +- `sort_by` (string): 排序字段 +- `sort_desc` (bool): 是否降序 +- `page` (int): 页码 +- `page_size` (int): 每页数量 + +**设置用户状态请求示例:** +```json +{ + "user_id": 123, + "status": 0 +} +``` +- `status`: 1=正常,0=禁用,-1=删除 + +**批量设置角色请求示例:** +```json +{ + "user_ids": [1, 2, 3], + "role": "admin" +} +``` + +### 🎨 材质管理 + +| 接口 | 方法 | 说明 | +| --- | --- | --- | +| `/textures` | GET | 获取材质列表(分页) | +| `/textures/search` | GET | 搜索材质(支持关键词、类型、状态、上传者筛选、排序) | +| `/textures/{id}` | PUT | 更新材质信息(名称、描述、公开状态、审核状态) | +| `/textures/{id}` | DELETE | 删除材质 | +| `/textures/batch-delete` | DELETE | 批量删除材质 | + +**搜索材质请求参数:** +- `keyword` (string): 搜索关键词 +- `type` (string): 材质类型(SKIN/CAPE) +- `status` (int): 状态筛选 +- `uploader_id` (int): 上传者ID筛选 +- `sort_by` (string): 排序字段 +- `sort_desc` (bool): 是否降序 +- `page` (int): 页码 +- `page_size` (int): 每页数量 + +**更新材质请求示例:** +```json +{ + "name": "新皮肤名称", + "description": "新描述", + "is_public": true, + "status": 1 +} +``` + +### 🔒 安全特性 + +1. **权限保护**:所有管理接口都需要管理员权限(`role=admin`) +2. **安全限制**:管理员不能修改/删除自己的角色和状态 +3. **批量操作**:支持批量设置角色和批量删除 +4. **操作日志**:所有管理操作都会记录日志 +5. **封禁功能**:通过 `/users/status` 接口可以封禁/解封用户 + ## 🤝 贡献指南 1. Fork & Clone diff --git a/go.mod b/go.mod index 683fd7f..067730c 100644 --- a/go.mod +++ b/go.mod @@ -112,7 +112,7 @@ require ( github.com/spf13/cast v1.10.0 // indirect github.com/spf13/pflag v1.0.10 // indirect github.com/subosito/gotenv v1.6.0 // indirect - github.com/swaggo/swag v1.16.6 + github.com/swaggo/swag v1.16.6 // indirect github.com/twitchyliquid64/golang-asm v0.15.1 // indirect github.com/ugorji/go/codec v1.3.1 // indirect go.uber.org/multierr v1.11.0 // indirect diff --git a/internal/handler/admin_handler.go b/internal/handler/admin_handler.go index 3a12d88..445eb3f 100644 --- a/internal/handler/admin_handler.go +++ b/internal/handler/admin_handler.go @@ -6,6 +6,7 @@ import ( "carrotskin/internal/container" "carrotskin/internal/model" + "carrotskin/internal/types" "github.com/gin-gonic/gin" "go.uber.org/zap" @@ -365,18 +366,527 @@ func (h *AdminHandler) GetTextureList(c *gin.Context) { })) } -// GetPermissions 获取权限列表 -// @Summary 获取权限列表 -// @Description 管理员获取所有Casbin权限规则 +// SearchUsers 搜索用户 +// @Summary 搜索用户 +// @Description 管理员根据条件搜索用户 // @Tags Admin // @Produce json -// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功" +// @Param keyword query string false "搜索关键词(用户名或邮箱)" +// @Param role query string false "角色筛选" +// @Param status query int false "状态筛选" +// @Param sort_by query string false "排序字段" +// @Param sort_desc query bool false "是否降序" +// @Param page query int false "页码" +// @Param page_size query int false "每页数量" +// @Success 200 {object} model.Response{data=map[string]interface{}} "搜索成功" // @Security BearerAuth -// @Router /api/v1/admin/permissions [get] -func (h *AdminHandler) GetPermissions(c *gin.Context) { - // 获取所有权限规则 - policies, _ := h.container.Casbin.GetEnforcer().GetPolicy() +// @Router /api/v1/admin/users/search [get] +func (h *AdminHandler) SearchUsers(c *gin.Context) { + var req types.AdminUserSearchRequest + if err := c.ShouldBindQuery(&req); err != nil { + RespondBadRequest(c, "参数错误", err) + return + } + + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + + db := h.container.DB.Model(&model.User{}) + + // 关键词搜索 + if req.Keyword != "" { + db = db.Where("username LIKE ? OR email LIKE ?", "%"+req.Keyword+"%", "%"+req.Keyword+"%") + } + + // 角色筛选 + if req.Role != "" { + db = db.Where("role = ?", req.Role) + } + + // 状态筛选 + if req.Status != nil { + db = db.Where("status = ?", *req.Status) + } + + // 排序 + sortBy := "id" + if req.SortBy != "" { + sortBy = req.SortBy + } + order := sortBy + if req.SortDesc { + order += " DESC" + } + db = db.Order(order) + + // 分页 + var total int64 + db.Count(&total) + + var users []model.User + db.Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&users) + + // 构建响应 + userList := make([]gin.H, len(users)) + for i, u := range users { + userList[i] = gin.H{ + "id": u.ID, + "username": u.Username, + "email": u.Email, + "avatar": u.Avatar, + "role": u.Role, + "status": u.Status, + "points": u.Points, + "last_login_at": u.LastLoginAt, + "created_at": u.CreatedAt, + } + } + c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ - "policies": policies, + "users": userList, + "total": total, + "page": req.Page, + "page_size": req.PageSize, })) } + +// DeleteUser 删除用户 +// @Summary 删除用户 +// @Description 管理员删除用户(软删除) +// @Tags Admin +// @Produce json +// @Param id path int true "用户ID" +// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功" +// @Failure 400 {object} model.ErrorResponse "不能删除自己" +// @Failure 404 {object} model.ErrorResponse "用户不存在" +// @Security BearerAuth +// @Router /api/v1/admin/users/{id} [delete] +func (h *AdminHandler) DeleteUser(c *gin.Context) { + userID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + RespondBadRequest(c, "无效的用户ID", err) + return + } + + operatorID, _ := c.Get("user_id") + + // 不能删除自己 + if userID == operatorID.(int64) { + c.JSON(http.StatusBadRequest, model.NewErrorResponse( + model.CodeBadRequest, + "不能删除自己", + nil, + )) + return + } + + // 检查用户是否存在 + user, err := h.container.UserRepo.FindByID(c.Request.Context(), userID) + if err != nil || user == nil { + c.JSON(http.StatusNotFound, model.NewErrorResponse( + model.CodeNotFound, + "用户不存在", + nil, + )) + return + } + + // 软删除 + if err := h.container.DB.Delete(user).Error; err != nil { + RespondServerError(c, "删除用户失败", err) + return + } + + h.container.Logger.Info("管理员删除用户", + zap.Int64("operator_id", operatorID.(int64)), + zap.Int64("deleted_user_id", userID), + zap.String("username", user.Username), + ) + + c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ + "message": "用户删除成功", + "user_id": userID, + })) +} + +// BatchSetUserRole 批量设置用户角色 +// @Summary 批量设置用户角色 +// @Description 管理员批量设置多个用户的角色 +// @Tags Admin +// @Accept json +// @Produce json +// @Param request body map[string]interface{} true "批量设置请求" +// @Success 200 {object} model.Response{data=map[string]interface{}} "设置成功" +// @Failure 400 {object} model.ErrorResponse "参数错误" +// @Security BearerAuth +// @Router /api/v1/admin/users/batch-role [put] +func (h *AdminHandler) BatchSetUserRole(c *gin.Context) { + var req struct { + UserIDs []int64 `json:"user_ids" binding:"required,min=1"` + Role string `json:"role" binding:"required,oneof=user admin"` + } + if err := c.ShouldBindJSON(&req); err != nil { + RespondBadRequest(c, "参数错误", err) + return + } + + operatorID, _ := c.Get("user_id") + + // 检查是否包含自己 + for _, uid := range req.UserIDs { + if uid == operatorID.(int64) { + c.JSON(http.StatusBadRequest, model.NewErrorResponse( + model.CodeBadRequest, + "不能修改自己的角色", + nil, + )) + return + } + } + + // 批量更新 + if err := h.container.DB.Model(&model.User{}). + Where("id IN ?", req.UserIDs). + Update("role", req.Role).Error; err != nil { + RespondServerError(c, "批量更新角色失败", err) + return + } + + h.container.Logger.Info("管理员批量设置用户角色", + zap.Int64("operator_id", operatorID.(int64)), + zap.Int("count", len(req.UserIDs)), + zap.String("role", req.Role), + ) + + c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ + "message": "批量设置角色成功", + "count": len(req.UserIDs), + "role": req.Role, + })) +} + +// BatchDeleteUsers 批量删除用户 +// @Summary 批量删除用户 +// @Description 管理员批量删除多个用户 +// @Tags Admin +// @Accept json +// @Produce json +// @Param request body map[string]interface{} true "批量删除请求" +// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功" +// @Failure 400 {object} model.ErrorResponse "参数错误" +// @Security BearerAuth +// @Router /api/v1/admin/users/batch-delete [delete] +func (h *AdminHandler) BatchDeleteUsers(c *gin.Context) { + var req struct { + UserIDs []int64 `json:"user_ids" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + RespondBadRequest(c, "参数错误", err) + return + } + + operatorID, _ := c.Get("user_id") + + // 检查是否包含自己 + for _, uid := range req.UserIDs { + if uid == operatorID.(int64) { + c.JSON(http.StatusBadRequest, model.NewErrorResponse( + model.CodeBadRequest, + "不能删除自己", + nil, + )) + return + } + } + + // 批量软删除 + if err := h.container.DB.Where("id IN ?", req.UserIDs).Delete(&model.User{}).Error; err != nil { + RespondServerError(c, "批量删除用户失败", err) + return + } + + h.container.Logger.Info("管理员批量删除用户", + zap.Int64("operator_id", operatorID.(int64)), + zap.Int("count", len(req.UserIDs)), + ) + + c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ + "message": "批量删除用户成功", + "count": len(req.UserIDs), + })) +} + +// GetRoles 获取角色列表 +// @Summary 获取角色列表 +// @Description 管理员获取所有可用角色 +// @Tags Admin +// @Produce json +// @Success 200 {object} model.Response{data=types.AdminRoleListResponse} "获取成功" +// @Security BearerAuth +// @Router /api/v1/admin/roles [get] +func (h *AdminHandler) GetRoles(c *gin.Context) { + roles := []types.RoleInfo{ + { + Name: "user", + DisplayName: "普通用户", + Description: "拥有基本用户权限", + }, + { + Name: "admin", + DisplayName: "管理员", + Description: "拥有所有管理权限", + }, + } + + c.JSON(http.StatusOK, model.NewSuccessResponse(types.AdminRoleListResponse{ + Roles: roles, + })) +} + +// SearchTextures 搜索材质 +// @Summary 搜索材质 +// @Description 管理员根据条件搜索材质 +// @Tags Admin +// @Produce json +// @Param keyword query string false "搜索关键词" +// @Param type query string false "材质类型" +// @Param status query int false "状态筛选" +// @Param uploader_id query int false "上传者ID" +// @Param sort_by query string false "排序字段" +// @Param sort_desc query bool false "是否降序" +// @Param page query int false "页码" +// @Param page_size query int false "每页数量" +// @Success 200 {object} model.Response{data=map[string]interface{}} "搜索成功" +// @Security BearerAuth +// @Router /api/v1/admin/textures/search [get] +func (h *AdminHandler) SearchTextures(c *gin.Context) { + var req types.AdminTextureSearchRequest + if err := c.ShouldBindQuery(&req); err != nil { + RespondBadRequest(c, "参数错误", err) + return + } + + if req.Page < 1 { + req.Page = 1 + } + if req.PageSize < 1 || req.PageSize > 100 { + req.PageSize = 20 + } + + db := h.container.DB.Model(&model.Texture{}) + + // 关键词搜索 + if req.Keyword != "" { + db = db.Where("name LIKE ?", "%"+req.Keyword+"%") + } + + // 类型筛选 + if req.Type != "" { + db = db.Where("type = ?", req.Type) + } + + // 状态筛选 + if req.Status != nil { + db = db.Where("status = ?", *req.Status) + } + + // 上传者筛选 + if req.UploaderID != nil { + db = db.Where("uploader_id = ?", *req.UploaderID) + } + + // 排序 + sortBy := "id" + if req.SortBy != "" { + sortBy = req.SortBy + } + order := sortBy + if req.SortDesc { + order += " DESC" + } + db = db.Order(order) + + // 分页 + var total int64 + db.Count(&total) + + var textures []model.Texture + db.Preload("Uploader").Offset((req.Page - 1) * req.PageSize).Limit(req.PageSize).Find(&textures) + + // 构建响应 + textureList := make([]gin.H, len(textures)) + for i, t := range textures { + uploaderName := "" + if t.Uploader != nil { + uploaderName = t.Uploader.Username + } + textureList[i] = gin.H{ + "id": t.ID, + "name": t.Name, + "type": t.Type, + "hash": t.Hash, + "uploader_id": t.UploaderID, + "uploader_name": uploaderName, + "is_public": t.IsPublic, + "download_count": t.DownloadCount, + "favorite_count": t.FavoriteCount, + "status": t.Status, + "created_at": t.CreatedAt, + } + } + + c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ + "textures": textureList, + "total": total, + "page": req.Page, + "page_size": req.PageSize, + })) +} + +// UpdateTexture 更新材质 +// @Summary 更新材质 +// @Description 管理员更新材质信息 +// @Tags Admin +// @Accept json +// @Produce json +// @Param id path int true "材质ID" +// @Param request body types.AdminTextureUpdateRequest true "更新材质请求" +// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功" +// @Failure 400 {object} model.ErrorResponse "参数错误" +// @Failure 404 {object} model.ErrorResponse "材质不存在" +// @Security BearerAuth +// @Router /api/v1/admin/textures/{id} [put] +func (h *AdminHandler) UpdateTexture(c *gin.Context) { + textureID, err := strconv.ParseInt(c.Param("id"), 10, 64) + if err != nil { + RespondBadRequest(c, "无效的材质ID", err) + return + } + + var req types.AdminTextureUpdateRequest + if err := c.ShouldBindJSON(&req); err != nil { + RespondBadRequest(c, "参数错误", err) + return + } + + // 检查材质是否存在 + var texture model.Texture + if err := h.container.DB.First(&texture, textureID).Error; err != nil { + c.JSON(http.StatusNotFound, model.NewErrorResponse( + model.CodeNotFound, + "材质不存在", + nil, + )) + return + } + + // 构建更新字段 + updates := make(map[string]interface{}) + + if req.Name != nil { + updates["name"] = *req.Name + } + + if req.Description != nil { + updates["description"] = *req.Description + } + + if req.IsPublic != nil { + updates["is_public"] = *req.IsPublic + } + + if req.Status != nil { + updates["status"] = *req.Status + } + + // 执行更新 + if len(updates) > 0 { + if err := h.container.DB.Model(&texture).Updates(updates).Error; err != nil { + RespondServerError(c, "更新材质失败", err) + return + } + } + + operatorID, _ := c.Get("user_id") + h.container.Logger.Info("管理员更新材质", + zap.Int64("operator_id", operatorID.(int64)), + zap.Int64("texture_id", textureID), + ) + + c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ + "message": "材质更新成功", + "texture_id": textureID, + })) +} + +// BatchDeleteTextures 批量删除材质 +// @Summary 批量删除材质 +// @Description 管理员批量删除多个材质 +// @Tags Admin +// @Accept json +// @Produce json +// @Param request body map[string]interface{} true "批量删除请求" +// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功" +// @Failure 400 {object} model.ErrorResponse "参数错误" +// @Security BearerAuth +// @Router /api/v1/admin/textures/batch-delete [delete] +func (h *AdminHandler) BatchDeleteTextures(c *gin.Context) { + var req struct { + TextureIDs []int64 `json:"texture_ids" binding:"required,min=1"` + } + if err := c.ShouldBindJSON(&req); err != nil { + RespondBadRequest(c, "参数错误", err) + return + } + + operatorID, _ := c.Get("user_id") + + // 批量删除 + if err := h.container.DB.Where("id IN ?", req.TextureIDs).Delete(&model.Texture{}).Error; err != nil { + RespondServerError(c, "批量删除材质失败", err) + return + } + + h.container.Logger.Info("管理员批量删除材质", + zap.Int64("operator_id", operatorID.(int64)), + zap.Int("count", len(req.TextureIDs)), + ) + + c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{ + "message": "批量删除材质成功", + "count": len(req.TextureIDs), + })) +} + +// GetStats 获取统计信息 +// @Summary 获取统计信息 +// @Description 管理员获取系统统计数据 +// @Tags Admin +// @Produce json +// @Success 200 {object} model.Response{data=types.AdminStatsResponse} "获取成功" +// @Security BearerAuth +// @Router /api/v1/admin/stats [get] +func (h *AdminHandler) GetStats(c *gin.Context) { + var stats types.AdminStatsResponse + + // 用户统计 + h.container.DB.Model(&model.User{}).Count(&stats.TotalUsers) + h.container.DB.Model(&model.User{}).Where("status = ?", 1).Count(&stats.ActiveUsers) + h.container.DB.Model(&model.User{}).Where("status = ?", 0).Count(&stats.BannedUsers) + h.container.DB.Model(&model.User{}).Where("role = ?", "admin").Count(&stats.AdminUsers) + + // 材质统计 + h.container.DB.Model(&model.Texture{}).Count(&stats.TotalTextures) + h.container.DB.Model(&model.Texture{}).Where("is_public = ?", true).Count(&stats.PublicTextures) + h.container.DB.Model(&model.Texture{}).Where("status = ?", 0).Count(&stats.PendingTextures) + + // 下载和收藏统计 + h.container.DB.Model(&model.Texture{}).Select("COALESCE(SUM(download_count), 0)").Scan(&stats.TotalDownloads) + h.container.DB.Model(&model.Texture{}).Select("COALESCE(SUM(favorite_count), 0)").Scan(&stats.TotalFavorites) + + c.JSON(http.StatusOK, model.NewSuccessResponse(stats)) +} diff --git a/internal/handler/routes.go b/internal/handler/routes.go index 6448362..c5479c7 100644 --- a/internal/handler/routes.go +++ b/internal/handler/routes.go @@ -203,18 +203,28 @@ func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHa admin.Use(middleware.RequireAdmin()) { + // 统计信息 + admin.GET("/stats", h.GetStats) + + // 角色管理 + admin.GET("/roles", h.GetRoles) + // 用户管理 admin.GET("/users", h.GetUserList) + admin.GET("/users/search", h.SearchUsers) admin.GET("/users/:id", h.GetUserDetail) + admin.DELETE("/users/:id", h.DeleteUser) admin.PUT("/users/role", h.SetUserRole) admin.PUT("/users/status", h.SetUserStatus) + admin.PUT("/users/batch-role", h.BatchSetUserRole) + admin.DELETE("/users/batch-delete", h.BatchDeleteUsers) // 材质管理(审核) admin.GET("/textures", h.GetTextureList) + admin.GET("/textures/search", h.SearchTextures) + admin.PUT("/textures/:id", h.UpdateTexture) admin.DELETE("/textures/:id", h.DeleteTexture) - - // 权限管理 - admin.GET("/permissions", h.GetPermissions) + admin.DELETE("/textures/batch-delete", h.BatchDeleteTextures) } } diff --git a/internal/types/common.go b/internal/types/common.go index dd8de31..788749b 100644 --- a/internal/types/common.go +++ b/internal/types/common.go @@ -206,3 +206,84 @@ type SystemConfigResponse struct { MaxTexturesPerUser int `json:"max_textures_per_user" example:"100"` MaxProfilesPerUser int `json:"max_profiles_per_user" example:"5"` } + +// AdminUserSearchRequest 管理员用户搜索请求 +// @Description 管理员搜索用户请求参数 +type AdminUserSearchRequest struct { + PaginationRequest + Keyword string `json:"keyword" form:"keyword" example:"testuser"` + Role string `json:"role" form:"role" binding:"omitempty,oneof=user admin"` + Status *int16 `json:"status" form:"status" binding:"omitempty,oneof=1 0 -1"` + SortBy string `json:"sort_by" form:"sort_by" binding:"omitempty,oneof=id username email points created_at"` + SortDesc bool `json:"sort_desc" form:"sort_desc"` +} + +// AdminUserCreateRequest 管理员创建用户请求 +// @Description 管理员创建用户请求参数 +type AdminUserCreateRequest struct { + Username string `json:"username" binding:"required,min=3,max=50" example:"newuser"` + Email string `json:"email" binding:"required,email" example:"user@example.com"` + Password string `json:"password" binding:"required,min=6,max=128" example:"password123"` + Role string `json:"role" binding:"required,oneof=user admin" example:"user"` + Points int `json:"points" binding:"omitempty,min=0" example:"0"` +} + +// AdminUserUpdateRequest 管理员更新用户请求 +// @Description 管理员更新用户请求参数 +type AdminUserUpdateRequest struct { + Username *string `json:"username" binding:"omitempty,min=3,max=50" example:"newusername"` + Email *string `json:"email" binding:"omitempty,email" example:"newemail@example.com"` + Password *string `json:"password" binding:"omitempty,min=6,max=128" example:"newpassword"` + Role *string `json:"role" binding:"omitempty,oneof=user admin" example:"admin"` + Points *int `json:"points" binding:"omitempty,min=0" example:"100"` + Status *int16 `json:"status" binding:"omitempty,oneof=1 0 -1"` +} + +// AdminTextureSearchRequest 管理员材质搜索请求 +// @Description 管理员搜索材质请求参数 +type AdminTextureSearchRequest struct { + PaginationRequest + Keyword string `json:"keyword" form:"keyword" example:"skin"` + Type TextureType `json:"type" form:"type" binding:"omitempty,oneof=SKIN CAPE"` + Status *int16 `json:"status" form:"status" binding:"omitempty,oneof=1 0 -1"` + UploaderID *int64 `json:"uploader_id" form:"uploader_id" example:"1"` + SortBy string `json:"sort_by" form:"sort_by" binding:"omitempty,oneof=id name download_count favorite_count created_at"` + SortDesc bool `json:"sort_desc" form:"sort_desc"` +} + +// AdminTextureUpdateRequest 管理员更新材质请求 +// @Description 管理员更新材质请求参数 +type AdminTextureUpdateRequest struct { + Name *string `json:"name" binding:"omitempty,min=1,max=100" example:"New Skin Name"` + Description *string `json:"description" binding:"omitempty,max=500" example:"New description"` + IsPublic *bool `json:"is_public" example:"true"` + Status *int16 `json:"status" binding:"omitempty,oneof=1 0 -1"` +} + +// AdminRoleListResponse 角色列表响应 +// @Description 角色列表响应数据 +type AdminRoleListResponse struct { + Roles []RoleInfo `json:"roles"` +} + +// RoleInfo 角色信息 +// @Description 角色详细信息 +type RoleInfo struct { + Name string `json:"name" example:"admin"` + DisplayName string `json:"display_name" example:"管理员"` + Description string `json:"description" example:"拥有所有管理权限"` +} + +// AdminStatsResponse 管理员统计信息响应 +// @Description 管理员统计信息 +type AdminStatsResponse struct { + TotalUsers int64 `json:"total_users" example:"100"` + ActiveUsers int64 `json:"active_users" example:"80"` + BannedUsers int64 `json:"banned_users" example:"5"` + AdminUsers int64 `json:"admin_users" example:"3"` + TotalTextures int64 `json:"total_textures" example:"500"` + PublicTextures int64 `json:"public_textures" example:"300"` + PendingTextures int64 `json:"pending_textures" example:"10"` + TotalDownloads int64 `json:"total_downloads" example:"1000"` + TotalFavorites int64 `json:"total_favorites" example:"500"` +}