添加 HTML 转义,防止邮件内容中的 HTML 注入攻击 #3
134
README.md
134
README.md
@@ -144,6 +144,140 @@ golangci-lint run (若已安装)
|
|||||||
- 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步
|
- 若手动运行,需要保证 `docs/` 下的 `docs.go`、`swagger.json`、`swagger.yaml` 与代码同步
|
||||||
- 通过 `SERVER_SWAGGER_ENABLED=false` 可在生产环境关闭 Swagger UI 暴露
|
- 通过 `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
|
1. Fork & Clone
|
||||||
|
|||||||
2
go.mod
2
go.mod
@@ -112,7 +112,7 @@ require (
|
|||||||
github.com/spf13/cast v1.10.0 // indirect
|
github.com/spf13/cast v1.10.0 // indirect
|
||||||
github.com/spf13/pflag v1.0.10 // indirect
|
github.com/spf13/pflag v1.0.10 // indirect
|
||||||
github.com/subosito/gotenv v1.6.0 // 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/twitchyliquid64/golang-asm v0.15.1 // indirect
|
||||||
github.com/ugorji/go/codec v1.3.1 // indirect
|
github.com/ugorji/go/codec v1.3.1 // indirect
|
||||||
go.uber.org/multierr v1.11.0 // indirect
|
go.uber.org/multierr v1.11.0 // indirect
|
||||||
|
|||||||
@@ -6,6 +6,7 @@ import (
|
|||||||
|
|
||||||
"carrotskin/internal/container"
|
"carrotskin/internal/container"
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
|
"carrotskin/internal/types"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -365,18 +366,527 @@ func (h *AdminHandler) GetTextureList(c *gin.Context) {
|
|||||||
}))
|
}))
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetPermissions 获取权限列表
|
// SearchUsers 搜索用户
|
||||||
// @Summary 获取权限列表
|
// @Summary 搜索用户
|
||||||
// @Description 管理员获取所有Casbin权限规则
|
// @Description 管理员根据条件搜索用户
|
||||||
// @Tags Admin
|
// @Tags Admin
|
||||||
// @Produce json
|
// @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
|
// @Security BearerAuth
|
||||||
// @Router /api/v1/admin/permissions [get]
|
// @Router /api/v1/admin/users/search [get]
|
||||||
func (h *AdminHandler) GetPermissions(c *gin.Context) {
|
func (h *AdminHandler) SearchUsers(c *gin.Context) {
|
||||||
// 获取所有权限规则
|
var req types.AdminUserSearchRequest
|
||||||
policies, _ := h.container.Casbin.GetEnforcer().GetPolicy()
|
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{
|
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))
|
||||||
|
}
|
||||||
|
|||||||
@@ -203,18 +203,28 @@ func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHa
|
|||||||
admin.Use(middleware.RequireAdmin())
|
admin.Use(middleware.RequireAdmin())
|
||||||
{
|
{
|
||||||
|
|
||||||
|
// 统计信息
|
||||||
|
admin.GET("/stats", h.GetStats)
|
||||||
|
|
||||||
|
// 角色管理
|
||||||
|
admin.GET("/roles", h.GetRoles)
|
||||||
|
|
||||||
// 用户管理
|
// 用户管理
|
||||||
admin.GET("/users", h.GetUserList)
|
admin.GET("/users", h.GetUserList)
|
||||||
|
admin.GET("/users/search", h.SearchUsers)
|
||||||
admin.GET("/users/:id", h.GetUserDetail)
|
admin.GET("/users/:id", h.GetUserDetail)
|
||||||
|
admin.DELETE("/users/:id", h.DeleteUser)
|
||||||
admin.PUT("/users/role", h.SetUserRole)
|
admin.PUT("/users/role", h.SetUserRole)
|
||||||
admin.PUT("/users/status", h.SetUserStatus)
|
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", h.GetTextureList)
|
||||||
|
admin.GET("/textures/search", h.SearchTextures)
|
||||||
|
admin.PUT("/textures/:id", h.UpdateTexture)
|
||||||
admin.DELETE("/textures/:id", h.DeleteTexture)
|
admin.DELETE("/textures/:id", h.DeleteTexture)
|
||||||
|
admin.DELETE("/textures/batch-delete", h.BatchDeleteTextures)
|
||||||
// 权限管理
|
|
||||||
admin.GET("/permissions", h.GetPermissions)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -206,3 +206,84 @@ type SystemConfigResponse struct {
|
|||||||
MaxTexturesPerUser int `json:"max_textures_per_user" example:"100"`
|
MaxTexturesPerUser int `json:"max_textures_per_user" example:"100"`
|
||||||
MaxProfilesPerUser int `json:"max_profiles_per_user" example:"5"`
|
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"`
|
||||||
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user