2025-12-08 19:12:30 +08:00
|
|
|
package handler
|
|
|
|
|
|
|
|
|
|
import (
|
|
|
|
|
"net/http"
|
|
|
|
|
"strconv"
|
|
|
|
|
|
|
|
|
|
"carrotskin/internal/container"
|
|
|
|
|
"carrotskin/internal/model"
|
2026-01-23 00:32:27 +08:00
|
|
|
"carrotskin/internal/types"
|
2025-12-08 19:12:30 +08:00
|
|
|
|
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
|
"go.uber.org/zap"
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
// AdminHandler 管理员处理器
|
|
|
|
|
type AdminHandler struct {
|
|
|
|
|
container *container.Container
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// NewAdminHandler 创建管理员处理器
|
|
|
|
|
func NewAdminHandler(c *container.Container) *AdminHandler {
|
|
|
|
|
return &AdminHandler{container: c}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetUserRoleRequest 设置用户角色请求
|
|
|
|
|
type SetUserRoleRequest struct {
|
|
|
|
|
UserID int64 `json:"user_id" binding:"required"`
|
|
|
|
|
Role string `json:"role" binding:"required,oneof=user admin"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetUserRole 设置用户角色
|
|
|
|
|
// @Summary 设置用户角色
|
|
|
|
|
// @Description 管理员设置指定用户的角色
|
|
|
|
|
// @Tags Admin
|
|
|
|
|
// @Accept json
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Param request body SetUserRoleRequest true "设置角色请求"
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
|
|
|
|
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
|
|
|
|
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
2025-12-08 19:12:30 +08:00
|
|
|
// @Security BearerAuth
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Router /api/v1/admin/users/role [put]
|
2025-12-08 19:12:30 +08:00
|
|
|
func (h *AdminHandler) SetUserRole(c *gin.Context) {
|
|
|
|
|
var req SetUserRoleRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
RespondBadRequest(c, "参数错误", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 获取当前操作者ID
|
|
|
|
|
operatorID, _ := c.Get("user_id")
|
|
|
|
|
|
|
|
|
|
// 不能修改自己的角色
|
|
|
|
|
if req.UserID == operatorID.(int64) {
|
|
|
|
|
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
|
|
|
|
model.CodeBadRequest,
|
|
|
|
|
"不能修改自己的角色",
|
|
|
|
|
nil,
|
|
|
|
|
))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查目标用户是否存在
|
|
|
|
|
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
|
|
|
|
if err != nil || targetUser == nil {
|
|
|
|
|
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
|
|
|
|
model.CodeNotFound,
|
|
|
|
|
"用户不存在",
|
|
|
|
|
nil,
|
|
|
|
|
))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新用户角色
|
|
|
|
|
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
|
|
|
|
|
"role": req.Role,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
RespondServerError(c, "更新用户角色失败", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.container.Logger.Info("管理员修改用户角色",
|
|
|
|
|
zap.Int64("operator_id", operatorID.(int64)),
|
|
|
|
|
zap.Int64("target_user_id", req.UserID),
|
|
|
|
|
zap.String("new_role", req.Role),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
|
|
|
"message": "用户角色更新成功",
|
|
|
|
|
"user_id": req.UserID,
|
|
|
|
|
"role": req.Role,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUserList 获取用户列表
|
|
|
|
|
// @Summary 获取用户列表
|
|
|
|
|
// @Description 管理员获取所有用户列表
|
|
|
|
|
// @Tags Admin
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Param page query int false "页码" default(1)
|
|
|
|
|
// @Param page_size query int false "每页数量" default(20)
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
|
|
|
|
// @Failure 403 {object} model.ErrorResponse "无权操作"
|
2025-12-08 19:12:30 +08:00
|
|
|
// @Security BearerAuth
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Router /api/v1/admin/users [get]
|
2025-12-08 19:12:30 +08:00
|
|
|
func (h *AdminHandler) GetUserList(c *gin.Context) {
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
page = 1
|
|
|
|
|
}
|
|
|
|
|
if pageSize < 1 || pageSize > 100 {
|
|
|
|
|
pageSize = 20
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 使用数据库直接查询用户列表
|
|
|
|
|
var users []model.User
|
|
|
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
db := h.container.DB
|
|
|
|
|
db.Model(&model.User{}).Count(&total)
|
|
|
|
|
db.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").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{
|
|
|
|
|
"users": userList,
|
|
|
|
|
"total": total,
|
|
|
|
|
"page": page,
|
|
|
|
|
"page_size": pageSize,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetUserDetail 获取用户详情
|
|
|
|
|
// @Summary 获取用户详情
|
|
|
|
|
// @Description 管理员获取指定用户的详细信息
|
|
|
|
|
// @Tags Admin
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Param id path int true "用户ID"
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
|
|
|
|
// @Failure 404 {object} model.ErrorResponse "用户不存在"
|
2025-12-08 19:12:30 +08:00
|
|
|
// @Security BearerAuth
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Router /api/v1/admin/users/{id} [get]
|
2025-12-08 19:12:30 +08:00
|
|
|
func (h *AdminHandler) GetUserDetail(c *gin.Context) {
|
|
|
|
|
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
RespondBadRequest(c, "无效的用户ID", err)
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
|
|
|
"id": user.ID,
|
|
|
|
|
"username": user.Username,
|
|
|
|
|
"email": user.Email,
|
|
|
|
|
"avatar": user.Avatar,
|
|
|
|
|
"role": user.Role,
|
|
|
|
|
"status": user.Status,
|
|
|
|
|
"points": user.Points,
|
|
|
|
|
"properties": user.Properties,
|
|
|
|
|
"last_login_at": user.LastLoginAt,
|
|
|
|
|
"created_at": user.CreatedAt,
|
|
|
|
|
"updated_at": user.UpdatedAt,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetUserStatusRequest 设置用户状态请求
|
|
|
|
|
type SetUserStatusRequest struct {
|
|
|
|
|
UserID int64 `json:"user_id" binding:"required"`
|
|
|
|
|
Status int16 `json:"status" binding:"required,oneof=1 0 -1"` // 1:正常, 0:禁用, -1:删除
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// SetUserStatus 设置用户状态
|
|
|
|
|
// @Summary 设置用户状态
|
|
|
|
|
// @Description 管理员设置用户状态(启用/禁用)
|
|
|
|
|
// @Tags Admin
|
|
|
|
|
// @Accept json
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Param request body SetUserStatusRequest true "设置状态请求"
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
|
|
|
|
|
// @Failure 400 {object} model.ErrorResponse "参数错误"
|
2025-12-08 19:12:30 +08:00
|
|
|
// @Security BearerAuth
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Router /api/v1/admin/users/status [put]
|
2025-12-08 19:12:30 +08:00
|
|
|
func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
|
|
|
|
var req SetUserStatusRequest
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil {
|
|
|
|
|
RespondBadRequest(c, "参数错误", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
operatorID, _ := c.Get("user_id")
|
|
|
|
|
|
|
|
|
|
// 不能修改自己的状态
|
|
|
|
|
if req.UserID == operatorID.(int64) {
|
|
|
|
|
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
|
|
|
|
model.CodeBadRequest,
|
|
|
|
|
"不能修改自己的状态",
|
|
|
|
|
nil,
|
|
|
|
|
))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 检查目标用户是否存在
|
|
|
|
|
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
|
|
|
|
if err != nil || targetUser == nil {
|
|
|
|
|
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
|
|
|
|
model.CodeNotFound,
|
|
|
|
|
"用户不存在",
|
|
|
|
|
nil,
|
|
|
|
|
))
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 更新用户状态
|
|
|
|
|
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
|
|
|
|
|
"status": req.Status,
|
|
|
|
|
})
|
|
|
|
|
if err != nil {
|
|
|
|
|
RespondServerError(c, "更新用户状态失败", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
statusText := map[int16]string{1: "正常", 0: "禁用", -1: "删除"}[req.Status]
|
|
|
|
|
|
|
|
|
|
h.container.Logger.Info("管理员修改用户状态",
|
|
|
|
|
zap.Int64("operator_id", operatorID.(int64)),
|
|
|
|
|
zap.Int64("target_user_id", req.UserID),
|
|
|
|
|
zap.Int16("new_status", req.Status),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
|
|
|
"message": "用户状态更新成功",
|
|
|
|
|
"user_id": req.UserID,
|
|
|
|
|
"status": req.Status,
|
|
|
|
|
"status_text": statusText,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// DeleteTexture 管理员删除材质
|
|
|
|
|
// @Summary 管理员删除材质
|
|
|
|
|
// @Description 管理员可以删除任意材质(用于审核不当内容)
|
|
|
|
|
// @Tags Admin
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Param id path int true "材质ID"
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
|
|
|
|
|
// @Failure 404 {object} model.ErrorResponse "材质不存在"
|
2025-12-08 19:12:30 +08:00
|
|
|
// @Security BearerAuth
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Router /api/v1/admin/textures/{id} [delete]
|
2025-12-08 19:12:30 +08:00
|
|
|
func (h *AdminHandler) DeleteTexture(c *gin.Context) {
|
|
|
|
|
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
|
|
|
|
|
if err != nil {
|
|
|
|
|
RespondBadRequest(c, "无效的材质ID", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
operatorID, _ := c.Get("user_id")
|
|
|
|
|
|
|
|
|
|
// 检查材质是否存在
|
|
|
|
|
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
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 删除材质
|
|
|
|
|
if err := h.container.DB.Delete(&texture).Error; err != nil {
|
|
|
|
|
RespondServerError(c, "删除材质失败", err)
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
h.container.Logger.Info("管理员删除材质",
|
|
|
|
|
zap.Int64("operator_id", operatorID.(int64)),
|
|
|
|
|
zap.Int64("texture_id", textureID),
|
|
|
|
|
zap.Int64("uploader_id", texture.UploaderID),
|
|
|
|
|
zap.String("texture_name", texture.Name),
|
|
|
|
|
)
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
|
|
|
"message": "材质删除成功",
|
|
|
|
|
"texture_id": textureID,
|
|
|
|
|
}))
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// GetTextureList 管理员获取材质列表
|
|
|
|
|
// @Summary 管理员获取材质列表
|
|
|
|
|
// @Description 管理员获取所有材质列表(用于审核)
|
|
|
|
|
// @Tags Admin
|
|
|
|
|
// @Produce json
|
|
|
|
|
// @Param page query int false "页码" default(1)
|
|
|
|
|
// @Param page_size query int false "每页数量" default(20)
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
|
2025-12-08 19:12:30 +08:00
|
|
|
// @Security BearerAuth
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Router /api/v1/admin/textures [get]
|
2025-12-08 19:12:30 +08:00
|
|
|
func (h *AdminHandler) GetTextureList(c *gin.Context) {
|
|
|
|
|
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
|
|
|
|
|
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
|
|
|
|
|
|
|
|
|
|
if page < 1 {
|
|
|
|
|
page = 1
|
|
|
|
|
}
|
|
|
|
|
if pageSize < 1 || pageSize > 100 {
|
|
|
|
|
pageSize = 20
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
var textures []model.Texture
|
|
|
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
db := h.container.DB
|
|
|
|
|
db.Model(&model.Texture{}).Count(&total)
|
|
|
|
|
db.Preload("Uploader").Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").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,
|
|
|
|
|
"created_at": t.CreatedAt,
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
|
|
|
|
"textures": textureList,
|
|
|
|
|
"total": total,
|
|
|
|
|
"page": page,
|
|
|
|
|
"page_size": pageSize,
|
|
|
|
|
}))
|
|
|
|
|
}
|
2025-12-26 01:15:17 +08:00
|
|
|
|
2026-01-23 00:32:27 +08:00
|
|
|
// SearchUsers 搜索用户
|
|
|
|
|
// @Summary 搜索用户
|
|
|
|
|
// @Description 管理员根据条件搜索用户
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Tags Admin
|
|
|
|
|
// @Produce json
|
2026-01-23 00:32:27 +08:00
|
|
|
// @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/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{
|
|
|
|
|
"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 "材质不存在"
|
2025-12-26 01:15:17 +08:00
|
|
|
// @Security BearerAuth
|
2026-01-23 00:32:27 +08:00
|
|
|
// @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)),
|
|
|
|
|
)
|
|
|
|
|
|
2025-12-26 01:15:17 +08:00
|
|
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
2026-01-23 00:32:27 +08:00
|
|
|
"message": "批量删除材质成功",
|
|
|
|
|
"count": len(req.TextureIDs),
|
2025-12-26 01:15:17 +08:00
|
|
|
}))
|
|
|
|
|
}
|
2026-01-23 00:32:27 +08:00
|
|
|
|
|
|
|
|
// 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))
|
|
|
|
|
}
|