删除服务端材质渲染功能及system_config表,转为环境变量配置,初步配置管理员功能

This commit is contained in:
2025-12-08 19:12:30 +08:00
parent 399e6f096f
commit 9b0a60033e
37 changed files with 1135 additions and 3913 deletions

View File

@@ -0,0 +1,366 @@
package handler
import (
"net/http"
"strconv"
"carrotskin/internal/container"
"carrotskin/internal/model"
"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 "设置角色请求"
// @Success 200 {object} model.Response
// @Failure 400 {object} model.Response
// @Failure 403 {object} model.Response
// @Security BearerAuth
// @Router /admin/users/role [put]
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)
// @Success 200 {object} model.Response
// @Failure 403 {object} model.Response
// @Security BearerAuth
// @Router /admin/users [get]
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"
// @Success 200 {object} model.Response
// @Failure 404 {object} model.Response
// @Security BearerAuth
// @Router /admin/users/{id} [get]
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 "设置状态请求"
// @Success 200 {object} model.Response
// @Failure 400 {object} model.Response
// @Security BearerAuth
// @Router /admin/users/status [put]
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"
// @Success 200 {object} model.Response
// @Failure 404 {object} model.Response
// @Security BearerAuth
// @Router /admin/textures/{id} [delete]
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)
// @Success 200 {object} model.Response
// @Security BearerAuth
// @Router /admin/textures [get]
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,
}))
}

View File

@@ -5,6 +5,7 @@ import (
"carrotskin/internal/middleware"
"carrotskin/internal/model"
"carrotskin/pkg/auth"
"carrotskin/pkg/config"
"github.com/gin-gonic/gin"
swaggerFiles "github.com/swaggo/files"
@@ -20,6 +21,7 @@ type Handlers struct {
Captcha *CaptchaHandler
Yggdrasil *YggdrasilHandler
CustomSkin *CustomSkinHandler
Admin *AdminHandler
}
// NewHandlers 创建所有Handler实例
@@ -32,6 +34,7 @@ func NewHandlers(c *container.Container) *Handlers {
Captcha: NewCaptchaHandler(c),
Yggdrasil: NewYggdrasilHandler(c),
CustomSkin: NewCustomSkinHandler(c),
Admin: NewAdminHandler(c),
}
}
@@ -68,10 +71,13 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
registerYggdrasilRoutesWithDI(v1, h.Yggdrasil)
// 系统路由
registerSystemRoutes(v1)
registerSystemRoutes(v1, c)
// CustomSkinAPI 路由
registerCustomSkinRoutes(v1, h.CustomSkin)
// 管理员路由(需要管理员权限)
registerAdminRoutes(v1, c, h.Admin)
}
}
@@ -113,10 +119,6 @@ func registerTextureRoutes(v1 *gin.RouterGroup, h *TextureHandler, jwtService *a
// 公开路由(无需认证)
textureGroup.GET("", h.Search)
textureGroup.GET("/:id", h.Get)
textureGroup.GET("/:id/render", h.RenderTexture) // type/front/back/full/head/isometric
textureGroup.GET("/:id/avatar", h.RenderAvatar) // mode=2d/3d
textureGroup.GET("/:id/cape", h.RenderCape)
textureGroup.GET("/:id/preview", h.RenderPreview) // 自动根据类型预览
// 需要认证的路由
textureAuth := textureGroup.Group("")
@@ -192,17 +194,46 @@ func registerYggdrasilRoutesWithDI(v1 *gin.RouterGroup, h *YggdrasilHandler) {
}
// registerSystemRoutes 注册系统路由
func registerSystemRoutes(v1 *gin.RouterGroup) {
func registerSystemRoutes(v1 *gin.RouterGroup, c *container.Container) {
system := v1.Group("/system")
{
system.GET("/config", func(c *gin.Context) {
// TODO: 实现从数据库读取系统配置
c.JSON(200, model.NewSuccessResponse(gin.H{
"site_name": "CarrotSkin",
"site_description": "A Minecraft Skin Station",
"registration_enabled": true,
"max_textures_per_user": 100,
"max_profiles_per_user": 5,
// 公开配置(无需认证)
system.GET("/config", func(ctx *gin.Context) {
cfg, _ := config.GetConfig()
ctx.JSON(200, model.NewSuccessResponse(gin.H{
"site_name": cfg.Site.Name,
"site_description": cfg.Site.Description,
"registration_enabled": cfg.Site.RegistrationEnabled,
"max_textures_per_user": cfg.Site.MaxTexturesPerUser,
"max_profiles_per_user": cfg.Site.MaxProfilesPerUser,
}))
})
}
}
// registerAdminRoutes 注册管理员路由
func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHandler) {
admin := v1.Group("/admin")
admin.Use(middleware.AuthMiddleware(c.JWT))
admin.Use(middleware.RequireAdmin())
{
// 用户管理
admin.GET("/users", h.GetUserList)
admin.GET("/users/:id", h.GetUserDetail)
admin.PUT("/users/role", h.SetUserRole)
admin.PUT("/users/status", h.SetUserStatus)
// 材质管理(审核)
admin.GET("/textures", h.GetTextureList)
admin.DELETE("/textures/:id", h.DeleteTexture)
// 权限管理
admin.GET("/permissions", func(ctx *gin.Context) {
// 获取所有权限规则
policies, _ := c.Casbin.GetEnforcer().GetPolicy()
ctx.JSON(200, model.NewSuccessResponse(gin.H{
"policies": policies,
}))
})
}

View File

@@ -3,7 +3,6 @@ package handler
import (
"carrotskin/internal/container"
"carrotskin/internal/model"
"carrotskin/internal/service"
"carrotskin/internal/types"
"strconv"
@@ -85,98 +84,6 @@ func (h *TextureHandler) Search(c *gin.Context) {
})
}
// RenderTexture 渲染皮肤/披风预览
func (h *TextureHandler) RenderTexture(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
renderType := service.RenderType(c.DefaultQuery("type", string(service.RenderTypeIsometric)))
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderTexture(c.Request.Context(), textureID, renderType, size, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// RenderAvatar 渲染头像2D/3D
func (h *TextureHandler) RenderAvatar(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
mode := service.AvatarMode(c.DefaultQuery("mode", string(service.AvatarMode2D)))
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderAvatar(c.Request.Context(), textureID, size, mode, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// RenderCape 渲染披风
func (h *TextureHandler) RenderCape(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderCape(c.Request.Context(), textureID, size, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// RenderPreview 自动选择预览(皮肤走等距,披风走披风渲染)
func (h *TextureHandler) RenderPreview(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
RespondBadRequest(c, "无效的材质ID", err)
return
}
size := parseIntWithDefault(c.DefaultQuery("size", "256"), 256)
format := service.ImageFormat(c.DefaultQuery("format", string(service.ImageFormatPNG)))
result, err := h.container.TextureRenderService.RenderPreview(c.Request.Context(), textureID, size, format)
if err != nil {
RespondBadRequest(c, err.Error(), err)
return
}
RespondSuccess(c, toRenderResponse(result))
}
// toRenderResponse 转换为API响应
func toRenderResponse(r *service.RenderResult) *types.RenderResponse {
if r == nil {
return nil
}
resp := &types.RenderResponse{
URL: r.URL,
ContentType: r.ContentType,
ETag: r.ETag,
Size: r.Size,
}
if !r.LastModified.IsZero() {
t := r.LastModified
resp.LastModified = &t
}
return resp
}
// Update 更新材质
func (h *TextureHandler) Update(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)