添加了后台管理接口,包括用户、角色、材质管理,管理员可进行删除、查找、设定状态操作,详情在README
Some checks failed
Build / build (pull_request) Successful in 4m14s
Build / build-docker (pull_request) Failing after 3m11s

This commit is contained in:
WuYuuuub
2026-01-23 00:32:27 +08:00
parent 9219e8c6ea
commit 1ba0e6b2f0
5 changed files with 748 additions and 13 deletions

View File

@@ -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))
}