refactor: 移除全局单例、修复分层违规、统一错误与响应
This commit is contained in:
@@ -1,9 +1,11 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/model"
|
||||
|
||||
@@ -46,11 +48,16 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 获取当前操作者ID
|
||||
operatorID, _ := c.Get("user_id")
|
||||
// 获取当前操作者ID(安全类型断言,防止中间件异常时 panic)
|
||||
operatorIDVal, _ := c.Get("user_id")
|
||||
operatorID, ok := operatorIDVal.(int64)
|
||||
if !ok {
|
||||
RespondServerError(c, "操作者ID类型错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 不能修改自己的角色
|
||||
if req.UserID == operatorID.(int64) {
|
||||
if req.UserID == operatorID {
|
||||
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
||||
model.CodeBadRequest,
|
||||
"不能修改自己的角色",
|
||||
@@ -59,9 +66,13 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查目标用户是否存在
|
||||
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
||||
if err != nil || targetUser == nil {
|
||||
// 检查目标用户是否存在(区分 DB 错误与"用户不存在")
|
||||
targetUser, err := h.container.UserService.GetByID(c.Request.Context(), req.UserID)
|
||||
if err != nil {
|
||||
RespondServerError(c, "查询用户失败", err)
|
||||
return
|
||||
}
|
||||
if targetUser == nil {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
"用户不存在",
|
||||
@@ -71,16 +82,13 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 更新用户角色
|
||||
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
|
||||
"role": req.Role,
|
||||
})
|
||||
if err != nil {
|
||||
if err := h.container.UserService.SetRole(c.Request.Context(), req.UserID, req.Role); err != nil {
|
||||
RespondServerError(c, "更新用户角色失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.container.Logger.Info("管理员修改用户角色",
|
||||
zap.Int64("operator_id", operatorID.(int64)),
|
||||
zap.Int64("operator_id", operatorID),
|
||||
zap.Int64("target_user_id", req.UserID),
|
||||
zap.String("new_role", req.Role),
|
||||
)
|
||||
@@ -114,13 +122,12 @@ func (h *AdminHandler) GetUserList(c *gin.Context) {
|
||||
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)
|
||||
// 通过 Service 层查询用户列表
|
||||
users, total, err := h.container.UserService.ListUsers(c.Request.Context(), page, pageSize)
|
||||
if err != nil {
|
||||
RespondServerError(c, "获取用户列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应(隐藏敏感信息)
|
||||
userList := make([]gin.H, len(users))
|
||||
@@ -163,7 +170,7 @@ func (h *AdminHandler) GetUserDetail(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
user, err := h.container.UserRepo.FindByID(c.Request.Context(), userID)
|
||||
user, err := h.container.UserService.GetByID(c.Request.Context(), userID)
|
||||
if err != nil || user == nil {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
@@ -212,10 +219,15 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
operatorID, _ := c.Get("user_id")
|
||||
operatorIDVal, _ := c.Get("user_id")
|
||||
operatorID, ok := operatorIDVal.(int64)
|
||||
if !ok {
|
||||
RespondServerError(c, "操作者ID类型错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 不能修改自己的状态
|
||||
if req.UserID == operatorID.(int64) {
|
||||
if req.UserID == operatorID {
|
||||
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
||||
model.CodeBadRequest,
|
||||
"不能修改自己的状态",
|
||||
@@ -224,9 +236,13 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 检查目标用户是否存在
|
||||
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
||||
if err != nil || targetUser == nil {
|
||||
// 检查目标用户是否存在(区分 DB 错误与"用户不存在")
|
||||
targetUser, err := h.container.UserService.GetByID(c.Request.Context(), req.UserID)
|
||||
if err != nil {
|
||||
RespondServerError(c, "查询用户失败", err)
|
||||
return
|
||||
}
|
||||
if targetUser == nil {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
"用户不存在",
|
||||
@@ -236,10 +252,7 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
||||
}
|
||||
|
||||
// 更新用户状态
|
||||
err = h.container.UserRepo.UpdateFields(c.Request.Context(), req.UserID, map[string]interface{}{
|
||||
"status": req.Status,
|
||||
})
|
||||
if err != nil {
|
||||
if err := h.container.UserService.SetStatus(c.Request.Context(), req.UserID, req.Status); err != nil {
|
||||
RespondServerError(c, "更新用户状态失败", err)
|
||||
return
|
||||
}
|
||||
@@ -247,7 +260,7 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
||||
statusText := map[int16]string{1: "正常", 0: "禁用", -1: "删除"}[req.Status]
|
||||
|
||||
h.container.Logger.Info("管理员修改用户状态",
|
||||
zap.Int64("operator_id", operatorID.(int64)),
|
||||
zap.Int64("operator_id", operatorID),
|
||||
zap.Int64("target_user_id", req.UserID),
|
||||
zap.Int16("new_status", req.Status),
|
||||
)
|
||||
@@ -277,30 +290,30 @@ func (h *AdminHandler) DeleteTexture(c *gin.Context) {
|
||||
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,
|
||||
))
|
||||
operatorIDVal, _ := c.Get("user_id")
|
||||
operatorID, ok := operatorIDVal.(int64)
|
||||
if !ok {
|
||||
RespondServerError(c, "操作者ID类型错误", nil)
|
||||
return
|
||||
}
|
||||
|
||||
// 删除材质
|
||||
if err := h.container.DB.Delete(&texture).Error; err != nil {
|
||||
// 通过 Service 删除材质(Service 会检查存在性)
|
||||
if err := h.container.TextureService.AdminDelete(c.Request.Context(), textureID); err != nil {
|
||||
if errors.Is(err, apperrors.ErrTextureNotFound) {
|
||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||
model.CodeNotFound,
|
||||
"材质不存在",
|
||||
nil,
|
||||
))
|
||||
return
|
||||
}
|
||||
RespondServerError(c, "删除材质失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
h.container.Logger.Info("管理员删除材质",
|
||||
zap.Int64("operator_id", operatorID.(int64)),
|
||||
zap.Int64("operator_id", operatorID),
|
||||
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{
|
||||
@@ -330,12 +343,12 @@ func (h *AdminHandler) GetTextureList(c *gin.Context) {
|
||||
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)
|
||||
// 通过 Service 层查询材质列表
|
||||
textures, total, err := h.container.TextureService.ListForAdmin(c.Request.Context(), page, pageSize)
|
||||
if err != nil {
|
||||
RespondServerError(c, "获取材质列表失败", err)
|
||||
return
|
||||
}
|
||||
|
||||
// 构建响应
|
||||
textureList := make([]gin.H, len(textures))
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/service"
|
||||
"carrotskin/internal/types"
|
||||
"carrotskin/pkg/email"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"go.uber.org/zap"
|
||||
@@ -177,8 +176,3 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
||||
|
||||
RespondSuccess(c, gin.H{"message": "密码重置成功"})
|
||||
}
|
||||
|
||||
// getEmailService 获取邮件服务(暂时使用全局方式,后续可改为依赖注入)
|
||||
func (h *AuthHandler) getEmailService() (*email.Service, error) {
|
||||
return email.GetService()
|
||||
}
|
||||
|
||||
@@ -2,6 +2,7 @@ package handler
|
||||
|
||||
import (
|
||||
"carrotskin/internal/container"
|
||||
"context"
|
||||
"fmt"
|
||||
"net/http"
|
||||
"strings"
|
||||
@@ -233,9 +234,12 @@ func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
|
||||
}
|
||||
}
|
||||
|
||||
// 增加下载计数(异步)
|
||||
// 增加下载计数(异步,使用独立 ctx 避免请求取消时丢失计数)
|
||||
go func() {
|
||||
_ = h.container.TextureRepo.IncrementDownloadCount(ctx, texture.ID)
|
||||
// 复制 ctx 但不传播取消,配合超时防止 Redis 永久阻塞
|
||||
asyncCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||
defer cancel()
|
||||
_ = h.container.TextureService.IncrementDownload(asyncCtx, texture.ID)
|
||||
}()
|
||||
|
||||
// 流式传输文件内容
|
||||
|
||||
@@ -1,9 +1,10 @@
|
||||
package handler
|
||||
|
||||
import (
|
||||
"carrotskin/internal/errors"
|
||||
apperrors "carrotskin/internal/errors"
|
||||
"carrotskin/internal/model"
|
||||
"carrotskin/internal/types"
|
||||
"errors"
|
||||
"net/http"
|
||||
"strconv"
|
||||
|
||||
@@ -190,31 +191,31 @@ func RespondWithError(c *gin.Context, err error) {
|
||||
return
|
||||
}
|
||||
|
||||
// 使用errors.Is检查预定义错误
|
||||
if errors.Is(err, errors.ErrUserNotFound) ||
|
||||
errors.Is(err, errors.ErrProfileNotFound) ||
|
||||
errors.Is(err, errors.ErrTextureNotFound) ||
|
||||
errors.Is(err, errors.ErrNotFound) {
|
||||
// 使用标准库 errors.Is 检查预定义错误
|
||||
if errors.Is(err, apperrors.ErrUserNotFound) ||
|
||||
errors.Is(err, apperrors.ErrProfileNotFound) ||
|
||||
errors.Is(err, apperrors.ErrTextureNotFound) ||
|
||||
errors.Is(err, apperrors.ErrNotFound) {
|
||||
RespondNotFound(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, errors.ErrProfileNoPermission) ||
|
||||
errors.Is(err, errors.ErrTextureNoPermission) ||
|
||||
errors.Is(err, errors.ErrForbidden) {
|
||||
if errors.Is(err, apperrors.ErrProfileNoPermission) ||
|
||||
errors.Is(err, apperrors.ErrTextureNoPermission) ||
|
||||
errors.Is(err, apperrors.ErrForbidden) {
|
||||
RespondForbidden(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
if errors.Is(err, errors.ErrUnauthorized) ||
|
||||
errors.Is(err, errors.ErrInvalidToken) ||
|
||||
errors.Is(err, errors.ErrTokenExpired) {
|
||||
if errors.Is(err, apperrors.ErrUnauthorized) ||
|
||||
errors.Is(err, apperrors.ErrInvalidToken) ||
|
||||
errors.Is(err, apperrors.ErrTokenExpired) {
|
||||
RespondUnauthorized(c, err.Error())
|
||||
return
|
||||
}
|
||||
|
||||
// 检查AppError类型
|
||||
var appErr *errors.AppError
|
||||
var appErr *apperrors.AppError
|
||||
if errors.As(err, &appErr) {
|
||||
c.JSON(appErr.Code, model.NewErrorResponse(
|
||||
appErr.Code,
|
||||
|
||||
@@ -4,7 +4,6 @@ import (
|
||||
"carrotskin/internal/container"
|
||||
"carrotskin/internal/middleware"
|
||||
"carrotskin/pkg/auth"
|
||||
"carrotskin/pkg/config"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
swaggerFiles "github.com/swaggo/files"
|
||||
@@ -40,11 +39,10 @@ func NewHandlers(c *container.Container) *Handlers {
|
||||
// RegisterRoutesWithDI 使用依赖注入注册所有路由
|
||||
func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||
// 健康检查路由
|
||||
router.GET("/health", HealthCheck)
|
||||
router.GET("/health", NewHealthCheck(c.DB, c.Redis))
|
||||
|
||||
// Swagger文档路由
|
||||
cfg, _ := config.GetConfig()
|
||||
if cfg != nil && cfg.Server.SwaggerEnabled {
|
||||
if c.Config != nil && c.Config.Server.SwaggerEnabled {
|
||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||
}
|
||||
|
||||
|
||||
@@ -6,57 +6,57 @@ import (
|
||||
"net/http"
|
||||
"time"
|
||||
|
||||
"carrotskin/pkg/database"
|
||||
"carrotskin/pkg/redis"
|
||||
|
||||
"github.com/gin-gonic/gin"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// HealthCheck 健康检查,检查依赖服务状态
|
||||
func HealthCheck(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
// NewHealthCheck 构造健康检查 handler,依赖通过参数注入。
|
||||
func NewHealthCheck(db *gorm.DB, redisClient *redis.Client) gin.HandlerFunc {
|
||||
return func(c *gin.Context) {
|
||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
|
||||
defer cancel()
|
||||
|
||||
checks := make(map[string]string)
|
||||
status := "ok"
|
||||
checks := make(map[string]string)
|
||||
status := "ok"
|
||||
|
||||
// 检查数据库
|
||||
if err := checkDatabase(ctx); err != nil {
|
||||
checks["database"] = "unhealthy: " + err.Error()
|
||||
status = "degraded"
|
||||
} else {
|
||||
checks["database"] = "healthy"
|
||||
// 检查数据库
|
||||
if err := checkDatabase(ctx, db); err != nil {
|
||||
checks["database"] = "unhealthy: " + err.Error()
|
||||
status = "degraded"
|
||||
} else {
|
||||
checks["database"] = "healthy"
|
||||
}
|
||||
|
||||
// 检查Redis
|
||||
if err := checkRedis(ctx, redisClient); err != nil {
|
||||
checks["redis"] = "unhealthy: " + err.Error()
|
||||
status = "degraded"
|
||||
} else {
|
||||
checks["redis"] = "healthy"
|
||||
}
|
||||
|
||||
// 根据状态返回相应的HTTP状态码
|
||||
httpStatus := http.StatusOK
|
||||
if status == "degraded" {
|
||||
httpStatus = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
c.JSON(httpStatus, gin.H{
|
||||
"status": status,
|
||||
"message": "CarrotSkin API health check",
|
||||
"checks": checks,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// 检查Redis
|
||||
if err := checkRedis(ctx); err != nil {
|
||||
checks["redis"] = "unhealthy: " + err.Error()
|
||||
status = "degraded"
|
||||
} else {
|
||||
checks["redis"] = "healthy"
|
||||
}
|
||||
|
||||
// 根据状态返回相应的HTTP状态码
|
||||
httpStatus := http.StatusOK
|
||||
if status == "degraded" {
|
||||
httpStatus = http.StatusServiceUnavailable
|
||||
}
|
||||
|
||||
c.JSON(httpStatus, gin.H{
|
||||
"status": status,
|
||||
"message": "CarrotSkin API health check",
|
||||
"checks": checks,
|
||||
"timestamp": time.Now().Unix(),
|
||||
})
|
||||
}
|
||||
|
||||
// checkDatabase 检查数据库连接
|
||||
func checkDatabase(ctx context.Context) error {
|
||||
db, err := database.GetDB()
|
||||
if err != nil {
|
||||
return err
|
||||
func checkDatabase(ctx context.Context, db *gorm.DB) error {
|
||||
if db == nil {
|
||||
return errors.New("数据库未初始化")
|
||||
}
|
||||
|
||||
sqlDB, err := db.DB()
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -77,11 +77,7 @@ func checkDatabase(ctx context.Context) error {
|
||||
}
|
||||
|
||||
// checkRedis 检查Redis连接
|
||||
func checkRedis(ctx context.Context) error {
|
||||
client, err := redis.GetClient()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
func checkRedis(ctx context.Context, client *redis.Client) error {
|
||||
if client == nil {
|
||||
return errors.New("Redis客户端未初始化")
|
||||
}
|
||||
|
||||
@@ -12,7 +12,8 @@ import (
|
||||
func TestHealthCheck_Degraded(t *testing.T) {
|
||||
gin.SetMode(gin.TestMode)
|
||||
router := gin.New()
|
||||
router.GET("/health", HealthCheck)
|
||||
// 传入 nil 依赖,模拟服务不可用场景
|
||||
router.GET("/health", NewHealthCheck(nil, nil))
|
||||
|
||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||
w := httptest.NewRecorder()
|
||||
@@ -22,6 +23,3 @@ func TestHealthCheck_Degraded(t *testing.T) {
|
||||
t.Fatalf("expected 503 when dependencies missing, got %d", w.Code)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -201,7 +201,7 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
|
||||
if emailRegex.MatchString(request.Identifier) {
|
||||
userId, err = h.container.YggdrasilService.GetUserIDByEmail(c.Request.Context(), request.Identifier)
|
||||
} else {
|
||||
profile, err = h.container.ProfileRepo.FindByName(c.Request.Context(), request.Identifier)
|
||||
profile, err = h.container.ProfileService.GetByProfileName(c.Request.Context(), request.Identifier)
|
||||
if err != nil {
|
||||
h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err))
|
||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||
|
||||
Reference in New Issue
Block a user