refactor: 移除全局单例、修复分层违规、统一错误与响应
Some checks failed
Build / build (push) Successful in 7m52s
Build / build-docker (push) Has been cancelled

This commit is contained in:
lafay
2026-06-15 16:40:36 +08:00
parent d9de39a0a3
commit 7d1c78f965
55 changed files with 593 additions and 1744 deletions

View File

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

View File

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

View File

@@ -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)
}()
// 流式传输文件内容

View File

@@ -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,

View File

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

View File

@@ -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客户端未初始化")
}

View File

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

View File

@@ -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(