refactor: 移除全局单例、修复分层违规、统一错误与响应
This commit is contained in:
@@ -4,10 +4,12 @@ import (
|
|||||||
"carrotskin/internal/repository"
|
"carrotskin/internal/repository"
|
||||||
"carrotskin/internal/service"
|
"carrotskin/internal/service"
|
||||||
"carrotskin/pkg/auth"
|
"carrotskin/pkg/auth"
|
||||||
|
"carrotskin/pkg/config"
|
||||||
"carrotskin/pkg/database"
|
"carrotskin/pkg/database"
|
||||||
"carrotskin/pkg/email"
|
"carrotskin/pkg/email"
|
||||||
"carrotskin/pkg/redis"
|
"carrotskin/pkg/redis"
|
||||||
"carrotskin/pkg/storage"
|
"carrotskin/pkg/storage"
|
||||||
|
"context"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -17,6 +19,9 @@ import (
|
|||||||
// Container 依赖注入容器
|
// Container 依赖注入容器
|
||||||
// 集中管理所有依赖,便于测试和维护
|
// 集中管理所有依赖,便于测试和维护
|
||||||
type Container struct {
|
type Container struct {
|
||||||
|
// 配置
|
||||||
|
Config *config.Config
|
||||||
|
|
||||||
// 基础设施依赖
|
// 基础设施依赖
|
||||||
DB *gorm.DB
|
DB *gorm.DB
|
||||||
Redis *redis.Client
|
Redis *redis.Client
|
||||||
@@ -40,20 +45,20 @@ type Container struct {
|
|||||||
TokenService service.TokenService
|
TokenService service.TokenService
|
||||||
YggdrasilService service.YggdrasilService
|
YggdrasilService service.YggdrasilService
|
||||||
VerificationService service.VerificationService
|
VerificationService service.VerificationService
|
||||||
SecurityService service.SecurityService
|
|
||||||
CaptchaService service.CaptchaService
|
CaptchaService service.CaptchaService
|
||||||
SignatureService *service.SignatureService
|
SignatureService *service.SignatureService
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewContainer 创建依赖容器
|
// NewContainer 创建依赖容器
|
||||||
func NewContainer(
|
func NewContainer(
|
||||||
|
cfg *config.Config,
|
||||||
db *gorm.DB,
|
db *gorm.DB,
|
||||||
redisClient *redis.Client,
|
redisClient *redis.Client,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
jwtService *auth.JWTService,
|
jwtService *auth.JWTService,
|
||||||
casbinService *auth.CasbinService,
|
casbinService *auth.CasbinService,
|
||||||
storageClient *storage.StorageClient,
|
storageClient *storage.StorageClient,
|
||||||
emailService interface{}, // 接受 email.Service 但使用 interface{} 避免循环依赖
|
emailService *email.Service,
|
||||||
) *Container {
|
) *Container {
|
||||||
// 创建缓存管理器
|
// 创建缓存管理器
|
||||||
cacheManager := database.NewCacheManager(redisClient, database.CacheConfig{
|
cacheManager := database.NewCacheManager(redisClient, database.CacheConfig{
|
||||||
@@ -71,6 +76,7 @@ func NewContainer(
|
|||||||
})
|
})
|
||||||
|
|
||||||
c := &Container{
|
c := &Container{
|
||||||
|
Config: cfg,
|
||||||
DB: db,
|
DB: db,
|
||||||
Redis: redisClient,
|
Redis: redisClient,
|
||||||
Logger: logger,
|
Logger: logger,
|
||||||
@@ -92,14 +98,17 @@ func NewContainer(
|
|||||||
c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger)
|
c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger)
|
||||||
|
|
||||||
// 初始化Service(注入缓存管理器)
|
// 初始化Service(注入缓存管理器)
|
||||||
c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
|
c.UserService = service.NewUserService(cfg, c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
|
||||||
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
|
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
|
||||||
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger)
|
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger, db)
|
||||||
|
|
||||||
// 获取Yggdrasil私钥并创建JWT服务(TokenService需要)
|
// 获取Yggdrasil私钥并创建JWT服务(TokenService需要)
|
||||||
// 注意:这里仍然需要预先初始化,因为TokenService在创建时需要YggdrasilJWT
|
// 注意:这里仍然需要预先初始化,因为TokenService在创建时需要YggdrasilJWT
|
||||||
// 但SignatureService已经作为依赖注入,降低了耦合度
|
// 但SignatureService已经作为依赖注入,降低了耦合度
|
||||||
_, privateKey, err := c.SignatureService.GetOrCreateYggdrasilKeyPair()
|
// 使用后台 ctx(启动阶段不依赖请求 ctx)
|
||||||
|
initCtx, cancelInit := context.WithTimeout(context.Background(), 30*time.Second)
|
||||||
|
_, privateKey, err := c.SignatureService.GetOrCreateYggdrasilKeyPair(initCtx)
|
||||||
|
cancelInit()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.Fatal("获取Yggdrasil私钥失败", zap.Error(err))
|
logger.Fatal("获取Yggdrasil私钥失败", zap.Error(err))
|
||||||
}
|
}
|
||||||
@@ -121,149 +130,15 @@ func NewContainer(
|
|||||||
c.TokenService = service.NewTokenServiceRedis(tokenStore, c.ClientRepo, c.ProfileRepo, yggdrasilJWT, logger)
|
c.TokenService = service.NewTokenServiceRedis(tokenStore, c.ClientRepo, c.ProfileRepo, yggdrasilJWT, logger)
|
||||||
|
|
||||||
// 使用组合服务(内部包含认证、会话、序列化、证书服务)
|
// 使用组合服务(内部包含认证、会话、序列化、证书服务)
|
||||||
c.YggdrasilService = service.NewYggdrasilServiceComposite(db, c.UserRepo, c.ProfileRepo, c.YggdrasilRepo, c.SignatureService, redisClient, logger, c.TokenService)
|
c.YggdrasilService = service.NewYggdrasilServiceComposite(c.UserRepo, c.ProfileRepo, c.YggdrasilRepo, c.TextureRepo, c.SignatureService, redisClient, logger, c.TokenService)
|
||||||
|
|
||||||
// 初始化其他服务
|
// 初始化其他服务
|
||||||
c.SecurityService = service.NewSecurityService(redisClient)
|
c.CaptchaService = service.NewCaptchaService(cfg, redisClient, logger)
|
||||||
c.CaptchaService = service.NewCaptchaService(redisClient, logger)
|
|
||||||
|
|
||||||
// 初始化VerificationService(需要email.Service)
|
// 初始化VerificationService(需要email.Service)
|
||||||
if emailService != nil {
|
if emailService != nil {
|
||||||
if emailSvc, ok := emailService.(*email.Service); ok {
|
c.VerificationService = service.NewVerificationService(cfg, redisClient, emailService)
|
||||||
c.VerificationService = service.NewVerificationService(redisClient, emailSvc)
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return c
|
return c
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTestContainer 创建测试用容器(可注入mock依赖)
|
|
||||||
func NewTestContainer(opts ...Option) *Container {
|
|
||||||
c := &Container{}
|
|
||||||
for _, opt := range opts {
|
|
||||||
opt(c)
|
|
||||||
}
|
|
||||||
return c
|
|
||||||
}
|
|
||||||
|
|
||||||
// Option 容器配置选项
|
|
||||||
type Option func(*Container)
|
|
||||||
|
|
||||||
// WithDB 设置数据库连接
|
|
||||||
func WithDB(db *gorm.DB) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.DB = db
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithRedis 设置Redis客户端
|
|
||||||
func WithRedis(redis *redis.Client) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.Redis = redis
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithLogger 设置日志
|
|
||||||
func WithLogger(logger *zap.Logger) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.Logger = logger
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithJWT 设置JWT服务
|
|
||||||
func WithJWT(jwt *auth.JWTService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.JWT = jwt
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithStorage 设置存储客户端
|
|
||||||
func WithStorage(storage *storage.StorageClient) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.Storage = storage
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithUserRepo 设置用户仓储
|
|
||||||
func WithUserRepo(repo repository.UserRepository) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.UserRepo = repo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProfileRepo 设置档案仓储
|
|
||||||
func WithProfileRepo(repo repository.ProfileRepository) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.ProfileRepo = repo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTextureRepo 设置材质仓储
|
|
||||||
func WithTextureRepo(repo repository.TextureRepository) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.TextureRepo = repo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithUserService 设置用户服务
|
|
||||||
func WithUserService(svc service.UserService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.UserService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithProfileService 设置档案服务
|
|
||||||
func WithProfileService(svc service.ProfileService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.ProfileService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTextureService 设置材质服务
|
|
||||||
func WithTextureService(svc service.TextureService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.TextureService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithTokenService 设置令牌服务
|
|
||||||
func WithTokenService(svc service.TokenService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.TokenService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithYggdrasilRepo 设置Yggdrasil仓储
|
|
||||||
func WithYggdrasilRepo(repo repository.YggdrasilRepository) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.YggdrasilRepo = repo
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithYggdrasilService 设置Yggdrasil服务
|
|
||||||
func WithYggdrasilService(svc service.YggdrasilService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.YggdrasilService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithVerificationService 设置验证码服务
|
|
||||||
func WithVerificationService(svc service.VerificationService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.VerificationService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithSecurityService 设置安全服务
|
|
||||||
func WithSecurityService(svc service.SecurityService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.SecurityService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// WithCaptchaService 设置验证码服务
|
|
||||||
func WithCaptchaService(svc service.CaptchaService) Option {
|
|
||||||
return func(c *Container) {
|
|
||||||
c.CaptchaService = svc
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -125,23 +125,9 @@ func NewInternalError(message string, err error) *AppError {
|
|||||||
return NewAppError(500, message, err)
|
return NewAppError(500, message, err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// Is 检查错误是否匹配
|
// 注意:原此处的 Is/As/Wrap 函数(透传标准库)已删除。
|
||||||
func Is(err, target error) bool {
|
// 请直接使用标准库 errors.Is / errors.As / fmt.Errorf。
|
||||||
return errors.Is(err, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// As 尝试将错误转换为指定类型
|
|
||||||
func As(err error, target interface{}) bool {
|
|
||||||
return errors.As(err, target)
|
|
||||||
}
|
|
||||||
|
|
||||||
// Wrap 包装错误
|
|
||||||
func Wrap(err error, message string) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s: %w", message, err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// YggdrasilErrorResponse Yggdrasil协议标准错误响应格式
|
// YggdrasilErrorResponse Yggdrasil协议标准错误响应格式
|
||||||
type YggdrasilErrorResponse struct {
|
type YggdrasilErrorResponse struct {
|
||||||
|
|||||||
@@ -15,24 +15,12 @@ func TestAppErrorBasics(t *testing.T) {
|
|||||||
if got := appErr.Error(); got != "bad: root" {
|
if got := appErr.Error(); got != "bad: root" {
|
||||||
t.Fatalf("unexpected Error(): %s", got)
|
t.Fatalf("unexpected Error(): %s", got)
|
||||||
}
|
}
|
||||||
if !Is(appErr, root) {
|
// 使用标准库 errors.Is/As(包级 Is/As 已移除)
|
||||||
|
if !errors.Is(appErr, root) {
|
||||||
t.Fatalf("Is should match wrapped error")
|
t.Fatalf("Is should match wrapped error")
|
||||||
}
|
}
|
||||||
var target *AppError
|
var target *AppError
|
||||||
if !As(appErr, &target) {
|
if !errors.As(appErr, &target) {
|
||||||
t.Fatalf("As should succeed")
|
t.Fatalf("As should succeed")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestWrap(t *testing.T) {
|
|
||||||
if Wrap(nil, "msg") != nil {
|
|
||||||
t.Fatalf("Wrap nil should return nil")
|
|
||||||
}
|
|
||||||
err := errors.New("base")
|
|
||||||
wrapped := Wrap(err, "ctx")
|
|
||||||
if wrapped.Error() != "ctx: base" {
|
|
||||||
t.Fatalf("wrap message mismatch: %v", wrapped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
package handler
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
|
apperrors "carrotskin/internal/errors"
|
||||||
"carrotskin/internal/container"
|
"carrotskin/internal/container"
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
|
|
||||||
@@ -46,11 +48,16 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 获取当前操作者ID
|
// 获取当前操作者ID(安全类型断言,防止中间件异常时 panic)
|
||||||
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(
|
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
||||||
model.CodeBadRequest,
|
model.CodeBadRequest,
|
||||||
"不能修改自己的角色",
|
"不能修改自己的角色",
|
||||||
@@ -59,9 +66,13 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查目标用户是否存在
|
// 检查目标用户是否存在(区分 DB 错误与"用户不存在")
|
||||||
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
targetUser, err := h.container.UserService.GetByID(c.Request.Context(), req.UserID)
|
||||||
if err != nil || targetUser == nil {
|
if err != nil {
|
||||||
|
RespondServerError(c, "查询用户失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if targetUser == nil {
|
||||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||||
model.CodeNotFound,
|
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{}{
|
if err := h.container.UserService.SetRole(c.Request.Context(), req.UserID, req.Role); err != nil {
|
||||||
"role": req.Role,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
RespondServerError(c, "更新用户角色失败", err)
|
RespondServerError(c, "更新用户角色失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.container.Logger.Info("管理员修改用户角色",
|
h.container.Logger.Info("管理员修改用户角色",
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
zap.Int64("operator_id", operatorID),
|
||||||
zap.Int64("target_user_id", req.UserID),
|
zap.Int64("target_user_id", req.UserID),
|
||||||
zap.String("new_role", req.Role),
|
zap.String("new_role", req.Role),
|
||||||
)
|
)
|
||||||
@@ -114,13 +122,12 @@ func (h *AdminHandler) GetUserList(c *gin.Context) {
|
|||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用数据库直接查询用户列表
|
// 通过 Service 层查询用户列表
|
||||||
var users []model.User
|
users, total, err := h.container.UserService.ListUsers(c.Request.Context(), page, pageSize)
|
||||||
var total int64
|
if err != nil {
|
||||||
|
RespondServerError(c, "获取用户列表失败", err)
|
||||||
db := h.container.DB
|
return
|
||||||
db.Model(&model.User{}).Count(&total)
|
}
|
||||||
db.Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&users)
|
|
||||||
|
|
||||||
// 构建响应(隐藏敏感信息)
|
// 构建响应(隐藏敏感信息)
|
||||||
userList := make([]gin.H, len(users))
|
userList := make([]gin.H, len(users))
|
||||||
@@ -163,7 +170,7 @@ func (h *AdminHandler) GetUserDetail(c *gin.Context) {
|
|||||||
return
|
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 {
|
if err != nil || user == nil {
|
||||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||||
model.CodeNotFound,
|
model.CodeNotFound,
|
||||||
@@ -212,10 +219,15 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
|||||||
return
|
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(
|
c.JSON(http.StatusBadRequest, model.NewErrorResponse(
|
||||||
model.CodeBadRequest,
|
model.CodeBadRequest,
|
||||||
"不能修改自己的状态",
|
"不能修改自己的状态",
|
||||||
@@ -224,9 +236,13 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查目标用户是否存在
|
// 检查目标用户是否存在(区分 DB 错误与"用户不存在")
|
||||||
targetUser, err := h.container.UserRepo.FindByID(c.Request.Context(), req.UserID)
|
targetUser, err := h.container.UserService.GetByID(c.Request.Context(), req.UserID)
|
||||||
if err != nil || targetUser == nil {
|
if err != nil {
|
||||||
|
RespondServerError(c, "查询用户失败", err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if targetUser == nil {
|
||||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
||||||
model.CodeNotFound,
|
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{}{
|
if err := h.container.UserService.SetStatus(c.Request.Context(), req.UserID, req.Status); err != nil {
|
||||||
"status": req.Status,
|
|
||||||
})
|
|
||||||
if err != nil {
|
|
||||||
RespondServerError(c, "更新用户状态失败", err)
|
RespondServerError(c, "更新用户状态失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -247,7 +260,7 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
|
|||||||
statusText := map[int16]string{1: "正常", 0: "禁用", -1: "删除"}[req.Status]
|
statusText := map[int16]string{1: "正常", 0: "禁用", -1: "删除"}[req.Status]
|
||||||
|
|
||||||
h.container.Logger.Info("管理员修改用户状态",
|
h.container.Logger.Info("管理员修改用户状态",
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
zap.Int64("operator_id", operatorID),
|
||||||
zap.Int64("target_user_id", req.UserID),
|
zap.Int64("target_user_id", req.UserID),
|
||||||
zap.Int16("new_status", req.Status),
|
zap.Int16("new_status", req.Status),
|
||||||
)
|
)
|
||||||
@@ -277,30 +290,30 @@ func (h *AdminHandler) DeleteTexture(c *gin.Context) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
operatorID, _ := c.Get("user_id")
|
operatorIDVal, _ := c.Get("user_id")
|
||||||
|
operatorID, ok := operatorIDVal.(int64)
|
||||||
// 检查材质是否存在
|
if !ok {
|
||||||
var texture model.Texture
|
RespondServerError(c, "操作者ID类型错误", nil)
|
||||||
if err := h.container.DB.First(&texture, textureID).Error; err != nil {
|
|
||||||
c.JSON(http.StatusNotFound, model.NewErrorResponse(
|
|
||||||
model.CodeNotFound,
|
|
||||||
"材质不存在",
|
|
||||||
nil,
|
|
||||||
))
|
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 删除材质
|
// 通过 Service 删除材质(Service 会检查存在性)
|
||||||
if err := h.container.DB.Delete(&texture).Error; err != nil {
|
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)
|
RespondServerError(c, "删除材质失败", err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
h.container.Logger.Info("管理员删除材质",
|
h.container.Logger.Info("管理员删除材质",
|
||||||
zap.Int64("operator_id", operatorID.(int64)),
|
zap.Int64("operator_id", operatorID),
|
||||||
zap.Int64("texture_id", textureID),
|
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{
|
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
|
||||||
@@ -330,12 +343,12 @@ func (h *AdminHandler) GetTextureList(c *gin.Context) {
|
|||||||
pageSize = 20
|
pageSize = 20
|
||||||
}
|
}
|
||||||
|
|
||||||
var textures []model.Texture
|
// 通过 Service 层查询材质列表
|
||||||
var total int64
|
textures, total, err := h.container.TextureService.ListForAdmin(c.Request.Context(), page, pageSize)
|
||||||
|
if err != nil {
|
||||||
db := h.container.DB
|
RespondServerError(c, "获取材质列表失败", err)
|
||||||
db.Model(&model.Texture{}).Count(&total)
|
return
|
||||||
db.Preload("Uploader").Offset((page - 1) * pageSize).Limit(pageSize).Order("id DESC").Find(&textures)
|
}
|
||||||
|
|
||||||
// 构建响应
|
// 构建响应
|
||||||
textureList := make([]gin.H, len(textures))
|
textureList := make([]gin.H, len(textures))
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"carrotskin/internal/container"
|
"carrotskin/internal/container"
|
||||||
"carrotskin/internal/service"
|
"carrotskin/internal/service"
|
||||||
"carrotskin/internal/types"
|
"carrotskin/internal/types"
|
||||||
"carrotskin/pkg/email"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
@@ -177,8 +176,3 @@ func (h *AuthHandler) ResetPassword(c *gin.Context) {
|
|||||||
|
|
||||||
RespondSuccess(c, gin.H{"message": "密码重置成功"})
|
RespondSuccess(c, gin.H{"message": "密码重置成功"})
|
||||||
}
|
}
|
||||||
|
|
||||||
// getEmailService 获取邮件服务(暂时使用全局方式,后续可改为依赖注入)
|
|
||||||
func (h *AuthHandler) getEmailService() (*email.Service, error) {
|
|
||||||
return email.GetService()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package handler
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"carrotskin/internal/container"
|
"carrotskin/internal/container"
|
||||||
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strings"
|
"strings"
|
||||||
@@ -233,9 +234,12 @@ func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// 增加下载计数(异步)
|
// 增加下载计数(异步,使用独立 ctx 避免请求取消时丢失计数)
|
||||||
go func() {
|
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
|
package handler
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"carrotskin/internal/errors"
|
apperrors "carrotskin/internal/errors"
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/internal/types"
|
"carrotskin/internal/types"
|
||||||
|
"errors"
|
||||||
"net/http"
|
"net/http"
|
||||||
"strconv"
|
"strconv"
|
||||||
|
|
||||||
@@ -190,31 +191,31 @@ func RespondWithError(c *gin.Context, err error) {
|
|||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 使用errors.Is检查预定义错误
|
// 使用标准库 errors.Is 检查预定义错误
|
||||||
if errors.Is(err, errors.ErrUserNotFound) ||
|
if errors.Is(err, apperrors.ErrUserNotFound) ||
|
||||||
errors.Is(err, errors.ErrProfileNotFound) ||
|
errors.Is(err, apperrors.ErrProfileNotFound) ||
|
||||||
errors.Is(err, errors.ErrTextureNotFound) ||
|
errors.Is(err, apperrors.ErrTextureNotFound) ||
|
||||||
errors.Is(err, errors.ErrNotFound) {
|
errors.Is(err, apperrors.ErrNotFound) {
|
||||||
RespondNotFound(c, err.Error())
|
RespondNotFound(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, errors.ErrProfileNoPermission) ||
|
if errors.Is(err, apperrors.ErrProfileNoPermission) ||
|
||||||
errors.Is(err, errors.ErrTextureNoPermission) ||
|
errors.Is(err, apperrors.ErrTextureNoPermission) ||
|
||||||
errors.Is(err, errors.ErrForbidden) {
|
errors.Is(err, apperrors.ErrForbidden) {
|
||||||
RespondForbidden(c, err.Error())
|
RespondForbidden(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
if errors.Is(err, errors.ErrUnauthorized) ||
|
if errors.Is(err, apperrors.ErrUnauthorized) ||
|
||||||
errors.Is(err, errors.ErrInvalidToken) ||
|
errors.Is(err, apperrors.ErrInvalidToken) ||
|
||||||
errors.Is(err, errors.ErrTokenExpired) {
|
errors.Is(err, apperrors.ErrTokenExpired) {
|
||||||
RespondUnauthorized(c, err.Error())
|
RespondUnauthorized(c, err.Error())
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查AppError类型
|
// 检查AppError类型
|
||||||
var appErr *errors.AppError
|
var appErr *apperrors.AppError
|
||||||
if errors.As(err, &appErr) {
|
if errors.As(err, &appErr) {
|
||||||
c.JSON(appErr.Code, model.NewErrorResponse(
|
c.JSON(appErr.Code, model.NewErrorResponse(
|
||||||
appErr.Code,
|
appErr.Code,
|
||||||
|
|||||||
@@ -4,7 +4,6 @@ import (
|
|||||||
"carrotskin/internal/container"
|
"carrotskin/internal/container"
|
||||||
"carrotskin/internal/middleware"
|
"carrotskin/internal/middleware"
|
||||||
"carrotskin/pkg/auth"
|
"carrotskin/pkg/auth"
|
||||||
"carrotskin/pkg/config"
|
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
swaggerFiles "github.com/swaggo/files"
|
swaggerFiles "github.com/swaggo/files"
|
||||||
@@ -40,11 +39,10 @@ func NewHandlers(c *container.Container) *Handlers {
|
|||||||
// RegisterRoutesWithDI 使用依赖注入注册所有路由
|
// RegisterRoutesWithDI 使用依赖注入注册所有路由
|
||||||
func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
|
||||||
// 健康检查路由
|
// 健康检查路由
|
||||||
router.GET("/health", HealthCheck)
|
router.GET("/health", NewHealthCheck(c.DB, c.Redis))
|
||||||
|
|
||||||
// Swagger文档路由
|
// Swagger文档路由
|
||||||
cfg, _ := config.GetConfig()
|
if c.Config != nil && c.Config.Server.SwaggerEnabled {
|
||||||
if cfg != nil && cfg.Server.SwaggerEnabled {
|
|
||||||
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -6,57 +6,57 @@ import (
|
|||||||
"net/http"
|
"net/http"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"carrotskin/pkg/database"
|
|
||||||
"carrotskin/pkg/redis"
|
"carrotskin/pkg/redis"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// HealthCheck 健康检查,检查依赖服务状态
|
// NewHealthCheck 构造健康检查 handler,依赖通过参数注入。
|
||||||
func HealthCheck(c *gin.Context) {
|
func NewHealthCheck(db *gorm.DB, redisClient *redis.Client) gin.HandlerFunc {
|
||||||
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
|
return func(c *gin.Context) {
|
||||||
defer cancel()
|
ctx, cancel := context.WithTimeout(c.Request.Context(), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
|
||||||
checks := make(map[string]string)
|
checks := make(map[string]string)
|
||||||
status := "ok"
|
status := "ok"
|
||||||
|
|
||||||
// 检查数据库
|
// 检查数据库
|
||||||
if err := checkDatabase(ctx); err != nil {
|
if err := checkDatabase(ctx, db); err != nil {
|
||||||
checks["database"] = "unhealthy: " + err.Error()
|
checks["database"] = "unhealthy: " + err.Error()
|
||||||
status = "degraded"
|
status = "degraded"
|
||||||
} else {
|
} else {
|
||||||
checks["database"] = "healthy"
|
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 检查数据库连接
|
// checkDatabase 检查数据库连接
|
||||||
func checkDatabase(ctx context.Context) error {
|
func checkDatabase(ctx context.Context, db *gorm.DB) error {
|
||||||
db, err := database.GetDB()
|
if db == nil {
|
||||||
if err != nil {
|
return errors.New("数据库未初始化")
|
||||||
return err
|
|
||||||
}
|
}
|
||||||
|
|
||||||
sqlDB, err := db.DB()
|
sqlDB, err := db.DB()
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return err
|
return err
|
||||||
@@ -77,11 +77,7 @@ func checkDatabase(ctx context.Context) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// checkRedis 检查Redis连接
|
// checkRedis 检查Redis连接
|
||||||
func checkRedis(ctx context.Context) error {
|
func checkRedis(ctx context.Context, client *redis.Client) error {
|
||||||
client, err := redis.GetClient()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
if client == nil {
|
if client == nil {
|
||||||
return errors.New("Redis客户端未初始化")
|
return errors.New("Redis客户端未初始化")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -12,7 +12,8 @@ import (
|
|||||||
func TestHealthCheck_Degraded(t *testing.T) {
|
func TestHealthCheck_Degraded(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.GET("/health", HealthCheck)
|
// 传入 nil 依赖,模拟服务不可用场景
|
||||||
|
router.GET("/health", NewHealthCheck(nil, nil))
|
||||||
|
|
||||||
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
req := httptest.NewRequest(http.MethodGet, "/health", nil)
|
||||||
w := httptest.NewRecorder()
|
w := httptest.NewRecorder()
|
||||||
@@ -22,6 +23,3 @@ func TestHealthCheck_Degraded(t *testing.T) {
|
|||||||
t.Fatalf("expected 503 when dependencies missing, got %d", w.Code)
|
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) {
|
if emailRegex.MatchString(request.Identifier) {
|
||||||
userId, err = h.container.YggdrasilService.GetUserIDByEmail(c.Request.Context(), request.Identifier)
|
userId, err = h.container.YggdrasilService.GetUserIDByEmail(c.Request.Context(), request.Identifier)
|
||||||
} else {
|
} 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 {
|
if err != nil {
|
||||||
h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err))
|
h.logger.Error("用户名不存在", zap.String("identifier", request.Identifier), zap.Error(err))
|
||||||
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
c.JSON(http.StatusForbidden, errors.NewYggdrasilErrorResponse(
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package middleware
|
|||||||
import (
|
import (
|
||||||
"net/http"
|
"net/http"
|
||||||
|
|
||||||
|
"carrotskin/internal/model"
|
||||||
"carrotskin/pkg/auth"
|
"carrotskin/pkg/auth"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
@@ -15,10 +16,11 @@ func CasbinMiddleware(casbinService *auth.CasbinService, resource, action string
|
|||||||
// 从上下文获取用户角色(由AuthMiddleware设置)
|
// 从上下文获取用户角色(由AuthMiddleware设置)
|
||||||
role, exists := c.Get("user_role")
|
role, exists := c.Get("user_role")
|
||||||
if !exists {
|
if !exists {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
|
||||||
"success": false,
|
model.CodeUnauthorized,
|
||||||
"message": "未授权访问",
|
model.MsgUnauthorized,
|
||||||
})
|
nil,
|
||||||
|
))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -30,10 +32,11 @@ func CasbinMiddleware(casbinService *auth.CasbinService, resource, action string
|
|||||||
|
|
||||||
// 检查权限
|
// 检查权限
|
||||||
if !casbinService.CheckPermission(roleStr, resource, action) {
|
if !casbinService.CheckPermission(roleStr, resource, action) {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, model.NewErrorResponse(
|
||||||
"success": false,
|
model.CodeForbidden,
|
||||||
"message": "权限不足",
|
model.MsgForbidden,
|
||||||
})
|
nil,
|
||||||
|
))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -47,20 +50,22 @@ func RequireAdmin() gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
role, exists := c.Get("user_role")
|
role, exists := c.Get("user_role")
|
||||||
if !exists {
|
if !exists {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
|
||||||
"success": false,
|
model.CodeUnauthorized,
|
||||||
"message": "未授权访问",
|
model.MsgUnauthorized,
|
||||||
})
|
nil,
|
||||||
|
))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
roleStr, ok := role.(string)
|
roleStr, ok := role.(string)
|
||||||
if !ok || roleStr != "admin" {
|
if !ok || roleStr != "admin" {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, model.NewErrorResponse(
|
||||||
"success": false,
|
model.CodeForbidden,
|
||||||
"message": "需要管理员权限",
|
"需要管理员权限",
|
||||||
})
|
nil,
|
||||||
|
))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -74,20 +79,22 @@ func RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
|||||||
return func(c *gin.Context) {
|
return func(c *gin.Context) {
|
||||||
role, exists := c.Get("user_role")
|
role, exists := c.Get("user_role")
|
||||||
if !exists {
|
if !exists {
|
||||||
c.JSON(http.StatusUnauthorized, gin.H{
|
c.JSON(http.StatusUnauthorized, model.NewErrorResponse(
|
||||||
"success": false,
|
model.CodeUnauthorized,
|
||||||
"message": "未授权访问",
|
model.MsgUnauthorized,
|
||||||
})
|
nil,
|
||||||
|
))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
|
|
||||||
roleStr, ok := role.(string)
|
roleStr, ok := role.(string)
|
||||||
if !ok {
|
if !ok {
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, model.NewErrorResponse(
|
||||||
"success": false,
|
model.CodeForbidden,
|
||||||
"message": "权限不足",
|
model.MsgForbidden,
|
||||||
})
|
nil,
|
||||||
|
))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@@ -100,10 +107,11 @@ func RequireRole(allowedRoles ...string) gin.HandlerFunc {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
c.JSON(http.StatusForbidden, gin.H{
|
c.JSON(http.StatusForbidden, model.NewErrorResponse(
|
||||||
"success": false,
|
model.CodeForbidden,
|
||||||
"message": "权限不足",
|
model.MsgForbidden,
|
||||||
})
|
nil,
|
||||||
|
))
|
||||||
c.Abort()
|
c.Abort()
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -7,18 +7,14 @@ import (
|
|||||||
)
|
)
|
||||||
|
|
||||||
// CORS 跨域中间件
|
// CORS 跨域中间件
|
||||||
func CORS() gin.HandlerFunc {
|
func CORS(cfg *config.Config) gin.HandlerFunc {
|
||||||
// 获取配置,如果配置未初始化则使用默认值
|
// 从注入的配置读取 CORS 设置
|
||||||
var allowedOrigins []string
|
allowedOrigins := cfg.Security.AllowedOrigins
|
||||||
var isTestEnv bool
|
if len(allowedOrigins) == 0 {
|
||||||
if cfg, err := config.GetConfig(); err == nil {
|
// 默认允许所有来源
|
||||||
allowedOrigins = cfg.Security.AllowedOrigins
|
|
||||||
isTestEnv = cfg.IsTestEnvironment()
|
|
||||||
} else {
|
|
||||||
// 默认允许所有来源(向后兼容)
|
|
||||||
allowedOrigins = []string{"*"}
|
allowedOrigins = []string{"*"}
|
||||||
isTestEnv = false
|
|
||||||
}
|
}
|
||||||
|
isTestEnv := cfg.IsTestEnvironment()
|
||||||
|
|
||||||
return gin.HandlerFunc(func(c *gin.Context) {
|
return gin.HandlerFunc(func(c *gin.Context) {
|
||||||
origin := c.GetHeader("Origin")
|
origin := c.GetHeader("Origin")
|
||||||
|
|||||||
@@ -5,15 +5,26 @@ import (
|
|||||||
"net/http/httptest"
|
"net/http/httptest"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
|
"carrotskin/pkg/config"
|
||||||
|
|
||||||
"github.com/gin-gonic/gin"
|
"github.com/gin-gonic/gin"
|
||||||
)
|
)
|
||||||
|
|
||||||
|
// testCORSConfig 返回测试用的 CORS 配置(通配符模式)
|
||||||
|
func testCORSConfig() *config.Config {
|
||||||
|
return &config.Config{
|
||||||
|
Security: config.SecurityConfig{
|
||||||
|
AllowedOrigins: []string{"*"},
|
||||||
|
},
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// TestCORS_Headers 测试CORS中间件设置的响应头
|
// TestCORS_Headers 测试CORS中间件设置的响应头
|
||||||
func TestCORS_Headers(t *testing.T) {
|
func TestCORS_Headers(t *testing.T) {
|
||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(CORS())
|
router.Use(CORS(testCORSConfig()))
|
||||||
router.GET("/test", func(c *gin.Context) {
|
router.GET("/test", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
})
|
})
|
||||||
@@ -55,7 +66,7 @@ func TestCORS_OPTIONS(t *testing.T) {
|
|||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(CORS())
|
router.Use(CORS(testCORSConfig()))
|
||||||
router.GET("/test", func(c *gin.Context) {
|
router.GET("/test", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
})
|
})
|
||||||
@@ -76,7 +87,7 @@ func TestCORS_AllowMethods(t *testing.T) {
|
|||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(CORS())
|
router.Use(CORS(testCORSConfig()))
|
||||||
router.GET("/test", func(c *gin.Context) {
|
router.GET("/test", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
})
|
})
|
||||||
@@ -103,7 +114,7 @@ func TestCORS_AllowHeaders(t *testing.T) {
|
|||||||
gin.SetMode(gin.TestMode)
|
gin.SetMode(gin.TestMode)
|
||||||
|
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(CORS())
|
router.Use(CORS(testCORSConfig()))
|
||||||
router.GET("/test", func(c *gin.Context) {
|
router.GET("/test", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
})
|
})
|
||||||
@@ -130,7 +141,7 @@ func TestCORS_WithSpecificOrigin(t *testing.T) {
|
|||||||
// 注意:此测试验证的是在配置了具体allowed origins时的行为
|
// 注意:此测试验证的是在配置了具体allowed origins时的行为
|
||||||
// 在没有配置初始化的情况下,默认使用通配符模式
|
// 在没有配置初始化的情况下,默认使用通配符模式
|
||||||
router := gin.New()
|
router := gin.New()
|
||||||
router.Use(CORS())
|
router.Use(CORS(testCORSConfig()))
|
||||||
router.GET("/test", func(c *gin.Context) {
|
router.GET("/test", func(c *gin.Context) {
|
||||||
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
c.JSON(http.StatusOK, gin.H{"message": "success"})
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -12,6 +12,7 @@ type UserRepository interface {
|
|||||||
FindByUsername(ctx context.Context, username string) (*model.User, error)
|
FindByUsername(ctx context.Context, username string) (*model.User, error)
|
||||||
FindByEmail(ctx context.Context, email string) (*model.User, error)
|
FindByEmail(ctx context.Context, email string) (*model.User, error)
|
||||||
FindByIDs(ctx context.Context, ids []int64) ([]*model.User, error) // 批量查询
|
FindByIDs(ctx context.Context, ids []int64) ([]*model.User, error) // 批量查询
|
||||||
|
List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) // 分页列表(管理员)
|
||||||
Update(ctx context.Context, user *model.User) error
|
Update(ctx context.Context, user *model.User) error
|
||||||
UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error
|
UpdateFields(ctx context.Context, id int64, fields map[string]interface{}) error
|
||||||
BatchUpdate(ctx context.Context, ids []int64, fields map[string]interface{}) (int64, error) // 批量更新
|
BatchUpdate(ctx context.Context, ids []int64, fields map[string]interface{}) (int64, error) // 批量更新
|
||||||
@@ -64,6 +65,8 @@ type TextureRepository interface {
|
|||||||
RemoveFavorite(ctx context.Context, userID, textureID int64) error
|
RemoveFavorite(ctx context.Context, userID, textureID int64) error
|
||||||
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error)
|
||||||
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
|
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
|
||||||
|
ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) // 管理员列表(含 Uploader 预加载)
|
||||||
|
FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) // 管理员查询(无权限过滤)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
@@ -71,6 +74,8 @@ type TextureRepository interface {
|
|||||||
type YggdrasilRepository interface {
|
type YggdrasilRepository interface {
|
||||||
GetPasswordByID(ctx context.Context, id int64) (string, error)
|
GetPasswordByID(ctx context.Context, id int64) (string, error)
|
||||||
ResetPassword(ctx context.Context, id int64, password string) error
|
ResetPassword(ctx context.Context, id int64, password string) error
|
||||||
|
// Create 创建Yggdrasil密码记录(用于首次设置密码)
|
||||||
|
Create(ctx context.Context, yggdrasil *model.Yggdrasil) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ClientRepository Client仓储接口
|
// ClientRepository Client仓储接口
|
||||||
|
|||||||
@@ -210,3 +210,27 @@ func (r *textureRepository) CountByUploaderID(ctx context.Context, uploaderID in
|
|||||||
Count(&count).Error
|
Count(&count).Error
|
||||||
return count, err
|
return count, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListForAdmin 管理员分页查询材质列表(含 Uploader 预加载,无权限过滤)
|
||||||
|
func (r *textureRepository) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||||
|
var textures []*model.Texture
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
db := r.db.WithContext(ctx).Model(&model.Texture{})
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := db.Preload("Uploader").Order("id DESC").Offset(offset).Limit(pageSize).Find(&textures).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return textures, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByIDForAdmin 管理员查询材质(无权限过滤,用于管理员删除前检查存在性)
|
||||||
|
func (r *textureRepository) FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) {
|
||||||
|
var texture model.Texture
|
||||||
|
err := r.db.WithContext(ctx).Where("id = ?", id).First(&texture).Error
|
||||||
|
return handleNotFoundResult(&texture, err)
|
||||||
|
}
|
||||||
|
|||||||
@@ -50,6 +50,23 @@ func (r *userRepository) FindByIDs(ctx context.Context, ids []int64) ([]*model.U
|
|||||||
return users, err
|
return users, err
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List 分页查询用户列表(管理员,不过滤状态)
|
||||||
|
func (r *userRepository) List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) {
|
||||||
|
var users []*model.User
|
||||||
|
var total int64
|
||||||
|
|
||||||
|
db := r.db.WithContext(ctx).Model(&model.User{})
|
||||||
|
if err := db.Count(&total).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if err := db.Order("id DESC").Offset(offset).Limit(pageSize).Find(&users).Error; err != nil {
|
||||||
|
return nil, 0, err
|
||||||
|
}
|
||||||
|
return users, total, nil
|
||||||
|
}
|
||||||
|
|
||||||
func (r *userRepository) Update(ctx context.Context, user *model.User) error {
|
func (r *userRepository) Update(ctx context.Context, user *model.User) error {
|
||||||
return r.db.WithContext(ctx).Save(user).Error
|
return r.db.WithContext(ctx).Save(user).Error
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -30,6 +30,11 @@ func (r *yggdrasilRepository) ResetPassword(ctx context.Context, id int64, passw
|
|||||||
return r.db.WithContext(ctx).Model(&model.Yggdrasil{}).Where("id = ?", id).Update("password", password).Error
|
return r.db.WithContext(ctx).Model(&model.Yggdrasil{}).Where("id = ?", id).Update("password", password).Error
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Create 创建Yggdrasil密码记录
|
||||||
|
func (r *yggdrasilRepository) Create(ctx context.Context, yggdrasil *model.Yggdrasil) error {
|
||||||
|
return r.db.WithContext(ctx).Create(yggdrasil).Error
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -72,13 +72,15 @@ type RedisData struct {
|
|||||||
|
|
||||||
// captchaService CaptchaService的实现
|
// captchaService CaptchaService的实现
|
||||||
type captchaService struct {
|
type captchaService struct {
|
||||||
|
cfg *config.Config
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewCaptchaService 创建CaptchaService实例
|
// NewCaptchaService 创建CaptchaService实例
|
||||||
func NewCaptchaService(redisClient *redis.Client, logger *zap.Logger) CaptchaService {
|
func NewCaptchaService(cfg *config.Config, redisClient *redis.Client, logger *zap.Logger) CaptchaService {
|
||||||
return &captchaService{
|
return &captchaService{
|
||||||
|
cfg: cfg,
|
||||||
redis: redisClient,
|
redis: redisClient,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
@@ -157,8 +159,7 @@ func (s *captchaService) Generate(ctx context.Context) (masterImg, tileImg, capt
|
|||||||
// Verify 验证验证码
|
// Verify 验证验证码
|
||||||
func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (bool, error) {
|
func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (bool, error) {
|
||||||
// 测试环境下直接通过验证
|
// 测试环境下直接通过验证
|
||||||
cfg, err := config.GetConfig()
|
if s.cfg.IsTestEnvironment() {
|
||||||
if err == nil && cfg.IsTestEnvironment() {
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -197,8 +198,7 @@ func (s *captchaService) Verify(ctx context.Context, dx int, captchaID string) (
|
|||||||
// CheckVerified 检查验证码是否已验证(仅检查captcha_id)
|
// CheckVerified 检查验证码是否已验证(仅检查captcha_id)
|
||||||
func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (bool, error) {
|
func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (bool, error) {
|
||||||
// 测试环境下直接通过验证
|
// 测试环境下直接通过验证
|
||||||
cfg, err := config.GetConfig()
|
if s.cfg.IsTestEnvironment() {
|
||||||
if err == nil && cfg.IsTestEnvironment() {
|
|
||||||
return true, nil
|
return true, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -216,8 +216,7 @@ func (s *captchaService) CheckVerified(ctx context.Context, captchaID string) (b
|
|||||||
// ConsumeVerified 消耗已验证的验证码(注册成功后调用)
|
// ConsumeVerified 消耗已验证的验证码(注册成功后调用)
|
||||||
func (s *captchaService) ConsumeVerified(ctx context.Context, captchaID string) error {
|
func (s *captchaService) ConsumeVerified(ctx context.Context, captchaID string) error {
|
||||||
// 测试环境下直接返回成功
|
// 测试环境下直接返回成功
|
||||||
cfg, err := config.GetConfig()
|
if s.cfg.IsTestEnvironment() {
|
||||||
if err == nil && cfg.IsTestEnvironment() {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,18 +1,11 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
// 通用辅助函数。
|
||||||
"errors"
|
//
|
||||||
"fmt"
|
// 注意:错误定义已统一到 internal/errors 包。
|
||||||
)
|
// 原先在此文件重复定义的 ErrProfileNotFound/ErrProfileNoPermission/
|
||||||
|
// ErrTextureNotFound/ErrTextureNoPermission/ErrUserNotFound 已删除,
|
||||||
// 通用错误
|
// 请使用 internal/errors(别名 apperrors)中的对应定义。
|
||||||
var (
|
|
||||||
ErrProfileNotFound = errors.New("档案不存在")
|
|
||||||
ErrProfileNoPermission = errors.New("无权操作此档案")
|
|
||||||
ErrTextureNotFound = errors.New("材质不存在")
|
|
||||||
ErrTextureNoPermission = errors.New("无权操作此材质")
|
|
||||||
ErrUserNotFound = errors.New("用户不存在")
|
|
||||||
)
|
|
||||||
|
|
||||||
// NormalizePagination 规范化分页参数
|
// NormalizePagination 规范化分页参数
|
||||||
func NormalizePagination(page, pageSize int) (int, int) {
|
func NormalizePagination(page, pageSize int) (int, int) {
|
||||||
@@ -27,11 +20,3 @@ func NormalizePagination(page, pageSize int) (int, int) {
|
|||||||
}
|
}
|
||||||
return page, pageSize
|
return page, pageSize
|
||||||
}
|
}
|
||||||
|
|
||||||
// WrapError 包装错误,添加上下文信息
|
|
||||||
func WrapError(err error, message string) error {
|
|
||||||
if err == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
return fmt.Errorf("%s: %w", message, err)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"errors"
|
|
||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -30,21 +29,3 @@ func TestNormalizePagination_Basic(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestWrapError 覆盖 WrapError 的 nil 与非 nil 分支
|
|
||||||
func TestWrapError(t *testing.T) {
|
|
||||||
if err := WrapError(nil, "msg"); err != nil {
|
|
||||||
t.Fatalf("WrapError(nil, ...) 应返回 nil, got=%v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
orig := errors.New("orig")
|
|
||||||
wrapped := WrapError(orig, "context")
|
|
||||||
if wrapped == nil {
|
|
||||||
t.Fatalf("WrapError 应返回非 nil 错误")
|
|
||||||
}
|
|
||||||
if wrapped.Error() == orig.Error() {
|
|
||||||
t.Fatalf("WrapError 应添加上下文信息, got=%v", wrapped)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -37,6 +37,11 @@ type UserService interface {
|
|||||||
// 配置获取
|
// 配置获取
|
||||||
GetMaxProfilesPerUser() int
|
GetMaxProfilesPerUser() int
|
||||||
GetMaxTexturesPerUser() int
|
GetMaxTexturesPerUser() int
|
||||||
|
|
||||||
|
// 管理员操作
|
||||||
|
ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error)
|
||||||
|
SetRole(ctx context.Context, userID int64, role string) error
|
||||||
|
SetStatus(ctx context.Context, userID int64, status int16) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// ProfileService 档案服务接口
|
// ProfileService 档案服务接口
|
||||||
@@ -73,6 +78,11 @@ type TextureService interface {
|
|||||||
|
|
||||||
// 限制检查
|
// 限制检查
|
||||||
CheckUploadLimit(ctx context.Context, uploaderID int64, maxTextures int) error
|
CheckUploadLimit(ctx context.Context, uploaderID int64, maxTextures int) error
|
||||||
|
|
||||||
|
// 管理员操作
|
||||||
|
ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error)
|
||||||
|
AdminDelete(ctx context.Context, textureID int64) error
|
||||||
|
IncrementDownload(ctx context.Context, textureID int64) error
|
||||||
}
|
}
|
||||||
|
|
||||||
// TokenService 令牌服务接口
|
// TokenService 令牌服务接口
|
||||||
|
|||||||
@@ -130,6 +130,24 @@ func (m *MockUserRepository) FindByIDs(ctx context.Context, ids []int64) ([]*mod
|
|||||||
return result, nil
|
return result, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// List 分页查询用户列表(管理员,mock 实现)
|
||||||
|
func (m *MockUserRepository) List(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) {
|
||||||
|
var all []*model.User
|
||||||
|
for _, u := range m.users {
|
||||||
|
all = append(all, u)
|
||||||
|
}
|
||||||
|
total := int64(len(all))
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if offset >= len(all) {
|
||||||
|
return []*model.User{}, total, nil
|
||||||
|
}
|
||||||
|
end := offset + pageSize
|
||||||
|
if end > len(all) {
|
||||||
|
end = len(all)
|
||||||
|
}
|
||||||
|
return all[offset:end], total, nil
|
||||||
|
}
|
||||||
|
|
||||||
// MockProfileRepository 模拟ProfileRepository
|
// MockProfileRepository 模拟ProfileRepository
|
||||||
type MockProfileRepository struct {
|
type MockProfileRepository struct {
|
||||||
profiles map[string]*model.Profile
|
profiles map[string]*model.Profile
|
||||||
@@ -474,6 +492,32 @@ func (m *MockTextureRepository) BatchDelete(ctx context.Context, ids []int64) (i
|
|||||||
return deleted, nil
|
return deleted, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ListForAdmin 管理员列表(mock 实现)
|
||||||
|
func (m *MockTextureRepository) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||||
|
var all []*model.Texture
|
||||||
|
for _, t := range m.textures {
|
||||||
|
all = append(all, t)
|
||||||
|
}
|
||||||
|
total := int64(len(all))
|
||||||
|
offset := (page - 1) * pageSize
|
||||||
|
if offset >= len(all) {
|
||||||
|
return []*model.Texture{}, total, nil
|
||||||
|
}
|
||||||
|
end := offset + pageSize
|
||||||
|
if end > len(all) {
|
||||||
|
end = len(all)
|
||||||
|
}
|
||||||
|
return all[offset:end], total, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
// FindByIDForAdmin 管理员查询(mock 实现,无权限过滤)
|
||||||
|
func (m *MockTextureRepository) FindByIDForAdmin(ctx context.Context, id int64) (*model.Texture, error) {
|
||||||
|
if t, ok := m.textures[id]; ok {
|
||||||
|
return t, nil
|
||||||
|
}
|
||||||
|
return nil, nil
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
// ============================================================================
|
// ============================================================================
|
||||||
// Service Mocks
|
// Service Mocks
|
||||||
|
|||||||
@@ -1,6 +1,7 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
|
apperrors "carrotskin/internal/errors"
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/internal/repository"
|
"carrotskin/internal/repository"
|
||||||
"carrotskin/pkg/database"
|
"carrotskin/pkg/database"
|
||||||
@@ -100,7 +101,7 @@ func (s *profileService) GetByUUID(ctx context.Context, uuid string) (*model.Pro
|
|||||||
profile2, err := s.profileRepo.FindByUUID(ctx, uuid)
|
profile2, err := s.profileRepo.FindByUUID(ctx, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, ErrProfileNotFound
|
return nil, apperrors.ErrProfileNotFound
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("查询档案失败: %w", err)
|
return nil, fmt.Errorf("查询档案失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -140,13 +141,13 @@ func (s *profileService) Update(ctx context.Context, uuid string, userID int64,
|
|||||||
profile, err := s.profileRepo.FindByUUID(ctx, uuid)
|
profile, err := s.profileRepo.FindByUUID(ctx, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return nil, ErrProfileNotFound
|
return nil, apperrors.ErrProfileNotFound
|
||||||
}
|
}
|
||||||
return nil, fmt.Errorf("查询档案失败: %w", err)
|
return nil, fmt.Errorf("查询档案失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if profile.UserID != userID {
|
if profile.UserID != userID {
|
||||||
return nil, ErrProfileNoPermission
|
return nil, apperrors.ErrProfileNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
// 检查角色名是否重复
|
// 检查角色名是否重复
|
||||||
@@ -187,13 +188,13 @@ func (s *profileService) Delete(ctx context.Context, uuid string, userID int64)
|
|||||||
profile, err := s.profileRepo.FindByUUID(ctx, uuid)
|
profile, err := s.profileRepo.FindByUUID(ctx, uuid)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||||
return ErrProfileNotFound
|
return apperrors.ErrProfileNotFound
|
||||||
}
|
}
|
||||||
return fmt.Errorf("查询档案失败: %w", err)
|
return fmt.Errorf("查询档案失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
if profile.UserID != userID {
|
if profile.UserID != userID {
|
||||||
return ErrProfileNoPermission
|
return apperrors.ErrProfileNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
if err := s.profileRepo.Delete(ctx, uuid); err != nil {
|
if err := s.profileRepo.Delete(ctx, uuid); err != nil {
|
||||||
|
|||||||
@@ -53,7 +53,7 @@ func NewSignatureService(
|
|||||||
}
|
}
|
||||||
|
|
||||||
// NewKeyPair 生成新的RSA密钥对
|
// NewKeyPair 生成新的RSA密钥对
|
||||||
func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) {
|
func (s *SignatureService) NewKeyPair(ctx context.Context) (*model.KeyPair, error) {
|
||||||
privateKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
privateKey, err := rsa.GenerateKey(rand.Reader, KeySize)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("生成RSA密钥对失败: %w", err)
|
return nil, fmt.Errorf("生成RSA密钥对失败: %w", err)
|
||||||
@@ -85,7 +85,7 @@ func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) {
|
|||||||
refresh := now.AddDate(0, 0, RefreshDays)
|
refresh := now.AddDate(0, 0, RefreshDays)
|
||||||
|
|
||||||
// 获取Yggdrasil根密钥并签名公钥
|
// 获取Yggdrasil根密钥并签名公钥
|
||||||
yggPublicKey, yggPrivateKey, err := s.GetOrCreateYggdrasilKeyPair()
|
yggPublicKey, yggPrivateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("获取Yggdrasil根密钥失败: %w", err)
|
return nil, fmt.Errorf("获取Yggdrasil根密钥失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -132,9 +132,7 @@ func (s *SignatureService) NewKeyPair() (*model.KeyPair, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetOrCreateYggdrasilKeyPair 获取或创建Yggdrasil根密钥对
|
// GetOrCreateYggdrasilKeyPair 获取或创建Yggdrasil根密钥对
|
||||||
func (s *SignatureService) GetOrCreateYggdrasilKeyPair() (string, *rsa.PrivateKey, error) {
|
func (s *SignatureService) GetOrCreateYggdrasilKeyPair(ctx context.Context) (string, *rsa.PrivateKey, error) {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// 尝试从Redis获取密钥
|
// 尝试从Redis获取密钥
|
||||||
publicKeyPEM, err := s.redis.Get(ctx, PublicKeyRedisKey)
|
publicKeyPEM, err := s.redis.Get(ctx, PublicKeyRedisKey)
|
||||||
if err == nil && publicKeyPEM != "" {
|
if err == nil && publicKeyPEM != "" {
|
||||||
@@ -201,15 +199,14 @@ func (s *SignatureService) GetOrCreateYggdrasilKeyPair() (string, *rsa.PrivateKe
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GetPublicKeyFromRedis 从Redis获取公钥
|
// GetPublicKeyFromRedis 从Redis获取公钥
|
||||||
func (s *SignatureService) GetPublicKeyFromRedis() (string, error) {
|
func (s *SignatureService) GetPublicKeyFromRedis(ctx context.Context) (string, error) {
|
||||||
ctx := context.Background()
|
|
||||||
publicKey, err := s.redis.Get(ctx, PublicKeyRedisKey)
|
publicKey, err := s.redis.Get(ctx, PublicKeyRedisKey)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("从Redis获取公钥失败: %w", err)
|
return "", fmt.Errorf("从Redis获取公钥失败: %w", err)
|
||||||
}
|
}
|
||||||
if publicKey == "" {
|
if publicKey == "" {
|
||||||
// 如果Redis中没有,创建新的密钥对
|
// 如果Redis中没有,创建新的密钥对
|
||||||
publicKey, _, err = s.GetOrCreateYggdrasilKeyPair()
|
publicKey, _, err = s.GetOrCreateYggdrasilKeyPair(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("创建新密钥对失败: %w", err)
|
return "", fmt.Errorf("创建新密钥对失败: %w", err)
|
||||||
}
|
}
|
||||||
@@ -218,14 +215,13 @@ func (s *SignatureService) GetPublicKeyFromRedis() (string, error) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// SignStringWithSHA1withRSA 使用SHA1withRSA签名字符串
|
// SignStringWithSHA1withRSA 使用SHA1withRSA签名字符串
|
||||||
func (s *SignatureService) SignStringWithSHA1withRSA(data string) (string, error) {
|
func (s *SignatureService) SignStringWithSHA1withRSA(ctx context.Context, data string) (string, error) {
|
||||||
ctx := context.Background()
|
|
||||||
|
|
||||||
// 从Redis获取私钥
|
// 从Redis获取私钥
|
||||||
privateKeyPEM, err := s.redis.Get(ctx, PrivateKeyRedisKey)
|
privateKeyPEM, err := s.redis.Get(ctx, PrivateKeyRedisKey)
|
||||||
if err != nil || privateKeyPEM == "" {
|
if err != nil || privateKeyPEM == "" {
|
||||||
// 如果没有私钥,创建新的密钥对
|
// 如果没有私钥,创建新的密钥对
|
||||||
_, privateKey, err := s.GetOrCreateYggdrasilKeyPair()
|
_, privateKey, err := s.GetOrCreateYggdrasilKeyPair(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return "", fmt.Errorf("获取私钥失败: %w", err)
|
return "", fmt.Errorf("获取私钥失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package service
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"bytes"
|
"bytes"
|
||||||
|
apperrors "carrotskin/internal/errors"
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/internal/repository"
|
"carrotskin/internal/repository"
|
||||||
"carrotskin/pkg/database"
|
"carrotskin/pkg/database"
|
||||||
@@ -13,8 +14,10 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"path/filepath"
|
"path/filepath"
|
||||||
"strings"
|
"strings"
|
||||||
|
"time"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// textureService TextureService的实现
|
// textureService TextureService的实现
|
||||||
@@ -26,6 +29,7 @@ type textureService struct {
|
|||||||
cacheKeys *database.CacheKeyBuilder
|
cacheKeys *database.CacheKeyBuilder
|
||||||
cacheInv *database.CacheInvalidator
|
cacheInv *database.CacheInvalidator
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
|
db *gorm.DB
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewTextureService 创建TextureService实例
|
// NewTextureService 创建TextureService实例
|
||||||
@@ -35,6 +39,7 @@ func NewTextureService(
|
|||||||
storageClient *storage.StorageClient,
|
storageClient *storage.StorageClient,
|
||||||
cacheManager *database.CacheManager,
|
cacheManager *database.CacheManager,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
|
db *gorm.DB,
|
||||||
) TextureService {
|
) TextureService {
|
||||||
return &textureService{
|
return &textureService{
|
||||||
textureRepo: textureRepo,
|
textureRepo: textureRepo,
|
||||||
@@ -44,6 +49,7 @@ func NewTextureService(
|
|||||||
cacheKeys: database.NewCacheKeyBuilder(""),
|
cacheKeys: database.NewCacheKeyBuilder(""),
|
||||||
cacheInv: database.NewCacheInvalidator(cacheManager),
|
cacheInv: database.NewCacheInvalidator(cacheManager),
|
||||||
logger: logger,
|
logger: logger,
|
||||||
|
db: db,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -62,7 +68,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if texture2 == nil {
|
if texture2 == nil {
|
||||||
return nil, ErrTextureNotFound
|
return nil, apperrors.ErrTextureNotFound
|
||||||
}
|
}
|
||||||
if texture2.Status == -1 {
|
if texture2.Status == -1 {
|
||||||
return nil, errors.New("材质已删除")
|
return nil, errors.New("材质已删除")
|
||||||
@@ -80,7 +86,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if texture2 == nil {
|
if texture2 == nil {
|
||||||
return nil, ErrTextureNotFound
|
return nil, apperrors.ErrTextureNotFound
|
||||||
}
|
}
|
||||||
if texture2.Status == -1 {
|
if texture2.Status == -1 {
|
||||||
return nil, errors.New("材质已删除")
|
return nil, errors.New("材质已删除")
|
||||||
@@ -109,7 +115,7 @@ func (s *textureService) GetByHash(ctx context.Context, hash string) (*model.Tex
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if texture2 == nil {
|
if texture2 == nil {
|
||||||
return nil, ErrTextureNotFound
|
return nil, apperrors.ErrTextureNotFound
|
||||||
}
|
}
|
||||||
if texture2.Status == -1 {
|
if texture2.Status == -1 {
|
||||||
return nil, errors.New("材质已删除")
|
return nil, errors.New("材质已删除")
|
||||||
@@ -162,10 +168,10 @@ func (s *textureService) Update(ctx context.Context, textureID, uploaderID int64
|
|||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
if texture == nil {
|
if texture == nil {
|
||||||
return nil, ErrTextureNotFound
|
return nil, apperrors.ErrTextureNotFound
|
||||||
}
|
}
|
||||||
if texture.UploaderID != uploaderID {
|
if texture.UploaderID != uploaderID {
|
||||||
return nil, ErrTextureNoPermission
|
return nil, apperrors.ErrTextureNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新字段
|
// 更新字段
|
||||||
@@ -200,10 +206,10 @@ func (s *textureService) Delete(ctx context.Context, textureID, uploaderID int64
|
|||||||
return err
|
return err
|
||||||
}
|
}
|
||||||
if texture == nil {
|
if texture == nil {
|
||||||
return ErrTextureNotFound
|
return apperrors.ErrTextureNotFound
|
||||||
}
|
}
|
||||||
if texture.UploaderID != uploaderID {
|
if texture.UploaderID != uploaderID {
|
||||||
return ErrTextureNoPermission
|
return apperrors.ErrTextureNoPermission
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.textureRepo.Delete(ctx, textureID)
|
err = s.textureRepo.Delete(ctx, textureID)
|
||||||
@@ -225,33 +231,41 @@ func (s *textureService) ToggleFavorite(ctx context.Context, userID, textureID i
|
|||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
if texture == nil {
|
if texture == nil {
|
||||||
return false, ErrTextureNotFound
|
return false, apperrors.ErrTextureNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
isFavorited, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
|
// 在事务中执行"切换收藏 + 增减计数"两步,保证数据一致性
|
||||||
|
var favorited bool
|
||||||
|
err = database.TransactionWithTimeout(ctx, s.db, 5*time.Second, func(tx *gorm.DB) error {
|
||||||
|
isFav, err := s.textureRepo.IsFavorited(ctx, userID, textureID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if isFav {
|
||||||
|
// 已收藏 -> 取消收藏
|
||||||
|
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
favorited = false
|
||||||
|
return nil
|
||||||
|
}
|
||||||
|
// 未收藏 -> 添加收藏
|
||||||
|
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
favorited = true
|
||||||
|
return nil
|
||||||
|
})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return false, err
|
return false, err
|
||||||
}
|
}
|
||||||
|
return favorited, nil
|
||||||
if isFavorited {
|
|
||||||
// 已收藏 -> 取消收藏
|
|
||||||
if err := s.textureRepo.RemoveFavorite(ctx, userID, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := s.textureRepo.DecrementFavoriteCount(ctx, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return false, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// 未收藏 -> 添加收藏
|
|
||||||
if err := s.textureRepo.AddFavorite(ctx, userID, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
if err := s.textureRepo.IncrementFavoriteCount(ctx, textureID); err != nil {
|
|
||||||
return false, err
|
|
||||||
}
|
|
||||||
return true, nil
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
func (s *textureService) GetUserFavorites(ctx context.Context, userID int64, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||||
@@ -277,7 +291,7 @@ func (s *textureService) UploadTexture(ctx context.Context, uploaderID int64, na
|
|||||||
// 验证用户存在
|
// 验证用户存在
|
||||||
user, err := s.userRepo.FindByID(ctx, uploaderID)
|
user, err := s.userRepo.FindByID(ctx, uploaderID)
|
||||||
if err != nil || user == nil {
|
if err != nil || user == nil {
|
||||||
return nil, ErrUserNotFound
|
return nil, apperrors.ErrUserNotFound
|
||||||
}
|
}
|
||||||
|
|
||||||
// 验证文件大小和扩展名
|
// 验证文件大小和扩展名
|
||||||
@@ -403,3 +417,28 @@ func parseTextureTypeInternal(textureType string) (model.TextureType, error) {
|
|||||||
return "", errors.New("无效的材质类型")
|
return "", errors.New("无效的材质类型")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 管理员操作 ====================
|
||||||
|
|
||||||
|
// ListForAdmin 分页查询材质列表(管理员,含 Uploader 预加载)
|
||||||
|
func (s *textureService) ListForAdmin(ctx context.Context, page, pageSize int) ([]*model.Texture, int64, error) {
|
||||||
|
page, pageSize = NormalizePagination(page, pageSize)
|
||||||
|
return s.textureRepo.ListForAdmin(ctx, page, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// AdminDelete 管理员删除材质(无权限校验)
|
||||||
|
func (s *textureService) AdminDelete(ctx context.Context, textureID int64) error {
|
||||||
|
texture, err := s.textureRepo.FindByIDForAdmin(ctx, textureID)
|
||||||
|
if err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
if texture == nil {
|
||||||
|
return apperrors.ErrTextureNotFound
|
||||||
|
}
|
||||||
|
return s.textureRepo.Delete(ctx, textureID)
|
||||||
|
}
|
||||||
|
|
||||||
|
// IncrementDownload 增加下载计数(CustomSkinAPI 等场景使用)
|
||||||
|
func (s *textureService) IncrementDownload(ctx context.Context, textureID int64) error {
|
||||||
|
return s.textureRepo.IncrementDownloadCount(ctx, textureID)
|
||||||
|
}
|
||||||
|
|||||||
@@ -494,7 +494,7 @@ func TestTextureServiceImpl_Create(t *testing.T) {
|
|||||||
_ = userRepo.Create(context.Background(), testUser)
|
_ = userRepo.Create(context.Background(), testUser)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
|
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -617,7 +617,7 @@ func TestTextureServiceImpl_GetByID(t *testing.T) {
|
|||||||
_ = textureRepo.Create(context.Background(), testTexture)
|
_ = textureRepo.Create(context.Background(), testTexture)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
|
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||||||
|
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
@@ -675,7 +675,7 @@ func TestTextureServiceImpl_GetByUserID_And_Search(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
|
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -714,7 +714,7 @@ func TestTextureServiceImpl_Update_And_Delete(t *testing.T) {
|
|||||||
_ = textureRepo.Create(context.Background(), texture)
|
_ = textureRepo.Create(context.Background(), texture)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
|
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -764,7 +764,7 @@ func TestTextureServiceImpl_FavoritesAndLimit(t *testing.T) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
|
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -807,7 +807,7 @@ func TestTextureServiceImpl_ToggleFavorite(t *testing.T) {
|
|||||||
_ = textureRepo.Create(context.Background(), testTexture)
|
_ = textureRepo.Create(context.Background(), testTexture)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger)
|
textureService := NewTextureService(textureRepo, userRepo, nil, cacheManager, logger, nil)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
"time"
|
"time"
|
||||||
|
|
||||||
"github.com/jackc/pgx/v5"
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
@@ -173,8 +172,9 @@ func (s *tokenServiceRedis) Create(ctx context.Context, userID int64, UUID strin
|
|||||||
}
|
}
|
||||||
|
|
||||||
if err := s.tokenStore.Store(ctx, accessToken, metadata, ttl); err != nil {
|
if err := s.tokenStore.Store(ctx, accessToken, metadata, ttl); err != nil {
|
||||||
s.logger.Warn("存储Token到Redis失败", zap.Error(err))
|
// Redis 存储失败必须返回错误:否则 Validate 拿不到元数据,Token 立即失效
|
||||||
// 不返回错误,因为JWT本身已经生成成功
|
s.logger.Error("存储Token到Redis失败", zap.Error(err))
|
||||||
|
return nil, nil, "", "", fmt.Errorf("存储Token失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
|
return selectedProfileID, availableProfiles, accessToken, clientToken, nil
|
||||||
@@ -473,7 +473,7 @@ func (s *tokenServiceRedis) validateProfileByUserID(ctx context.Context, userID
|
|||||||
|
|
||||||
profile, err := s.profileRepo.FindByUUID(ctx, UUID)
|
profile, err := s.profileRepo.FindByUUID(ctx, UUID)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
if errors.Is(err, pgx.ErrNoRows) {
|
if repository.IsNotFound(err) {
|
||||||
return false, errors.New("配置文件不存在")
|
return false, errors.New("配置文件不存在")
|
||||||
}
|
}
|
||||||
return false, fmt.Errorf("验证配置文件失败: %w", err)
|
return false, fmt.Errorf("验证配置文件失败: %w", err)
|
||||||
|
|||||||
@@ -26,6 +26,7 @@ import (
|
|||||||
|
|
||||||
// userService UserService的实现
|
// userService UserService的实现
|
||||||
type userService struct {
|
type userService struct {
|
||||||
|
cfg *config.Config
|
||||||
userRepo repository.UserRepository
|
userRepo repository.UserRepository
|
||||||
jwtService *auth.JWTService
|
jwtService *auth.JWTService
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
@@ -38,6 +39,7 @@ type userService struct {
|
|||||||
|
|
||||||
// NewUserService 创建UserService实例
|
// NewUserService 创建UserService实例
|
||||||
func NewUserService(
|
func NewUserService(
|
||||||
|
cfg *config.Config,
|
||||||
userRepo repository.UserRepository,
|
userRepo repository.UserRepository,
|
||||||
jwtService *auth.JWTService,
|
jwtService *auth.JWTService,
|
||||||
redisClient *redis.Client,
|
redisClient *redis.Client,
|
||||||
@@ -48,6 +50,7 @@ func NewUserService(
|
|||||||
// CacheKeyBuilder 使用空前缀,因为 CacheManager 已经处理了前缀
|
// CacheKeyBuilder 使用空前缀,因为 CacheManager 已经处理了前缀
|
||||||
// 这样缓存键的格式为: CacheManager前缀 + CacheKeyBuilder生成的键
|
// 这样缓存键的格式为: CacheManager前缀 + CacheKeyBuilder生成的键
|
||||||
return &userService{
|
return &userService{
|
||||||
|
cfg: cfg,
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
jwtService: jwtService,
|
jwtService: jwtService,
|
||||||
redis: redisClient,
|
redis: redisClient,
|
||||||
@@ -81,7 +84,7 @@ func (s *userService) Register(ctx context.Context, username, password, email, a
|
|||||||
// 加密密码
|
// 加密密码
|
||||||
hashedPassword, err := auth.HashPassword(password)
|
hashedPassword, err := auth.HashPassword(password)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", errors.New("密码加密失败")
|
return nil, "", fmt.Errorf("密码加密失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 确定头像URL
|
// 确定头像URL
|
||||||
@@ -112,7 +115,7 @@ func (s *userService) Register(ctx context.Context, username, password, email, a
|
|||||||
// 生成JWT Token
|
// 生成JWT Token
|
||||||
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", errors.New("生成Token失败")
|
return nil, "", fmt.Errorf("生成Token失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
return user, token, nil
|
return user, token, nil
|
||||||
@@ -167,15 +170,20 @@ func (s *userService) Login(ctx context.Context, usernameOrEmail, password, ipAd
|
|||||||
// 生成JWT Token
|
// 生成JWT Token
|
||||||
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
token, err := s.jwtService.GenerateToken(user.ID, user.Username, user.Role)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, "", errors.New("生成Token失败")
|
return nil, "", fmt.Errorf("生成Token失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 更新最后登录时间
|
// 更新最后登录时间(非关键路径,错误降级为 Warn)
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
user.LastLoginAt = &now
|
user.LastLoginAt = &now
|
||||||
_ = s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{
|
if err := s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{
|
||||||
"last_login_at": now,
|
"last_login_at": now,
|
||||||
})
|
}); err != nil {
|
||||||
|
s.logger.Warn("更新最后登录时间失败",
|
||||||
|
zap.Int64("user_id", user.ID),
|
||||||
|
zap.Error(err),
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
// 记录成功登录日志
|
// 记录成功登录日志
|
||||||
s.logSuccessLogin(ctx, user.ID, ipAddress, userAgent)
|
s.logSuccessLogin(ctx, user.ID, ipAddress, userAgent)
|
||||||
@@ -239,7 +247,10 @@ func (s *userService) UpdateAvatar(ctx context.Context, userID int64, avatarURL
|
|||||||
|
|
||||||
func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error {
|
func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassword, newPassword string) error {
|
||||||
user, err := s.userRepo.FindByID(ctx, userID)
|
user, err := s.userRepo.FindByID(ctx, userID)
|
||||||
if err != nil || user == nil {
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询用户失败: %w", err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
return errors.New("用户不存在")
|
return errors.New("用户不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -249,7 +260,7 @@ func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassw
|
|||||||
|
|
||||||
hashedPassword, err := auth.HashPassword(newPassword)
|
hashedPassword, err := auth.HashPassword(newPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("密码加密失败")
|
return fmt.Errorf("密码加密失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{
|
err = s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{
|
||||||
@@ -267,13 +278,16 @@ func (s *userService) ChangePassword(ctx context.Context, userID int64, oldPassw
|
|||||||
|
|
||||||
func (s *userService) ResetPassword(ctx context.Context, email, newPassword string) error {
|
func (s *userService) ResetPassword(ctx context.Context, email, newPassword string) error {
|
||||||
user, err := s.userRepo.FindByEmail(ctx, email)
|
user, err := s.userRepo.FindByEmail(ctx, email)
|
||||||
if err != nil || user == nil {
|
if err != nil {
|
||||||
|
return fmt.Errorf("查询用户失败: %w", err)
|
||||||
|
}
|
||||||
|
if user == nil {
|
||||||
return errors.New("用户不存在")
|
return errors.New("用户不存在")
|
||||||
}
|
}
|
||||||
|
|
||||||
hashedPassword, err := auth.HashPassword(newPassword)
|
hashedPassword, err := auth.HashPassword(newPassword)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return errors.New("密码加密失败")
|
return fmt.Errorf("密码加密失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
err = s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{
|
err = s.userRepo.UpdateFields(ctx, user.ID, map[string]interface{}{
|
||||||
@@ -350,14 +364,13 @@ func (s *userService) ValidateAvatarURL(ctx context.Context, avatarURL string) e
|
|||||||
return errors.New("URL缺少主机名")
|
return errors.New("URL缺少主机名")
|
||||||
}
|
}
|
||||||
|
|
||||||
// 从配置获取允许的域名列表
|
// 从注入的配置获取允许的域名列表
|
||||||
cfg, err := config.GetConfig()
|
allowedDomains := s.cfg.Security.AllowedDomains
|
||||||
if err != nil {
|
if len(allowedDomains) == 0 {
|
||||||
allowedDomains := []string{"localhost", "127.0.0.1"}
|
allowedDomains = []string{"localhost", "127.0.0.1"}
|
||||||
return s.checkDomainAllowed(host, allowedDomains)
|
|
||||||
}
|
}
|
||||||
|
|
||||||
return s.checkDomainAllowed(host, cfg.Security.AllowedDomains)
|
return s.checkDomainAllowed(host, allowedDomains)
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData []byte, fileName string) (string, error) {
|
func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData []byte, fileName string) (string, error) {
|
||||||
@@ -422,29 +435,23 @@ func (s *userService) UploadAvatar(ctx context.Context, userID int64, fileData [
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (s *userService) GetMaxProfilesPerUser() int {
|
func (s *userService) GetMaxProfilesPerUser() int {
|
||||||
cfg, err := config.GetConfig()
|
if s.cfg.Site.MaxProfilesPerUser <= 0 {
|
||||||
if err != nil || cfg.Site.MaxProfilesPerUser <= 0 {
|
|
||||||
return 5
|
return 5
|
||||||
}
|
}
|
||||||
return cfg.Site.MaxProfilesPerUser
|
return s.cfg.Site.MaxProfilesPerUser
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *userService) GetMaxTexturesPerUser() int {
|
func (s *userService) GetMaxTexturesPerUser() int {
|
||||||
cfg, err := config.GetConfig()
|
if s.cfg.Site.MaxTexturesPerUser <= 0 {
|
||||||
if err != nil || cfg.Site.MaxTexturesPerUser <= 0 {
|
|
||||||
return 50
|
return 50
|
||||||
}
|
}
|
||||||
return cfg.Site.MaxTexturesPerUser
|
return s.cfg.Site.MaxTexturesPerUser
|
||||||
}
|
}
|
||||||
|
|
||||||
// 私有辅助方法
|
// 私有辅助方法
|
||||||
|
|
||||||
func (s *userService) getDefaultAvatar() string {
|
func (s *userService) getDefaultAvatar() string {
|
||||||
cfg, err := config.GetConfig()
|
return s.cfg.Site.DefaultAvatar
|
||||||
if err != nil {
|
|
||||||
return ""
|
|
||||||
}
|
|
||||||
return cfg.Site.DefaultAvatar
|
|
||||||
}
|
}
|
||||||
|
|
||||||
func (s *userService) checkDomainAllowed(host string, allowedDomains []string) error {
|
func (s *userService) checkDomainAllowed(host string, allowedDomains []string) error {
|
||||||
@@ -505,3 +512,21 @@ func (s *userService) logFailedLogin(ctx context.Context, userID int64, ipAddres
|
|||||||
}
|
}
|
||||||
_ = s.userRepo.CreateLoginLog(ctx, log)
|
_ = s.userRepo.CreateLoginLog(ctx, log)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// ==================== 管理员操作 ====================
|
||||||
|
|
||||||
|
// ListUsers 分页查询用户列表(管理员)
|
||||||
|
func (s *userService) ListUsers(ctx context.Context, page, pageSize int) ([]*model.User, int64, error) {
|
||||||
|
page, pageSize = NormalizePagination(page, pageSize)
|
||||||
|
return s.userRepo.List(ctx, page, pageSize)
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetRole 设置用户角色(管理员)
|
||||||
|
func (s *userService) SetRole(ctx context.Context, userID int64, role string) error {
|
||||||
|
return s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{"role": role})
|
||||||
|
}
|
||||||
|
|
||||||
|
// SetStatus 设置用户状态(管理员)
|
||||||
|
func (s *userService) SetStatus(ctx context.Context, userID int64, status int16) error {
|
||||||
|
return s.userRepo.UpdateFields(ctx, userID, map[string]interface{}{"status": status})
|
||||||
|
}
|
||||||
|
|||||||
@@ -3,6 +3,7 @@ package service
|
|||||||
import (
|
import (
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/pkg/auth"
|
"carrotskin/pkg/auth"
|
||||||
|
"carrotskin/pkg/config"
|
||||||
"context"
|
"context"
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
@@ -14,11 +15,12 @@ func TestUserServiceImpl_Register(t *testing.T) {
|
|||||||
userRepo := NewMockUserRepository()
|
userRepo := NewMockUserRepository()
|
||||||
jwtService := auth.NewJWTService("secret", 1)
|
jwtService := auth.NewJWTService("secret", 1)
|
||||||
logger := zap.NewNop()
|
logger := zap.NewNop()
|
||||||
|
cfg := &config.Config{}
|
||||||
|
|
||||||
// 初始化Service
|
// 初始化Service
|
||||||
// 注意:redisClient 和 storageClient 传入 nil,因为 Register 方法中没有使用它们
|
// 注意:redisClient 和 storageClient 传入 nil,因为 Register 方法中没有使用它们
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
userService := NewUserService(cfg, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -128,7 +130,8 @@ func TestUserServiceImpl_Login(t *testing.T) {
|
|||||||
_ = userRepo.Create(context.Background(), testUser)
|
_ = userRepo.Create(context.Background(), testUser)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
cfg := &config.Config{}
|
||||||
|
userService := NewUserService(cfg, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -208,7 +211,7 @@ func TestUserServiceImpl_BasicGettersAndUpdates(t *testing.T) {
|
|||||||
_ = userRepo.Create(context.Background(), user)
|
_ = userRepo.Create(context.Background(), user)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -255,7 +258,7 @@ func TestUserServiceImpl_ChangePassword(t *testing.T) {
|
|||||||
_ = userRepo.Create(context.Background(), user)
|
_ = userRepo.Create(context.Background(), user)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -289,7 +292,7 @@ func TestUserServiceImpl_ResetPassword(t *testing.T) {
|
|||||||
_ = userRepo.Create(context.Background(), user)
|
_ = userRepo.Create(context.Background(), user)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -316,7 +319,7 @@ func TestUserServiceImpl_ChangeEmail(t *testing.T) {
|
|||||||
_ = userRepo.Create(context.Background(), user2)
|
_ = userRepo.Create(context.Background(), user2)
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -338,7 +341,7 @@ func TestUserServiceImpl_ValidateAvatarURL(t *testing.T) {
|
|||||||
logger := zap.NewNop()
|
logger := zap.NewNop()
|
||||||
|
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
|
|
||||||
ctx := context.Background()
|
ctx := context.Background()
|
||||||
|
|
||||||
@@ -374,7 +377,7 @@ func TestUserServiceImpl_MaxLimits(t *testing.T) {
|
|||||||
|
|
||||||
// 未配置时走默认值
|
// 未配置时走默认值
|
||||||
cacheManager := NewMockCacheManager()
|
cacheManager := NewMockCacheManager()
|
||||||
userService := NewUserService(userRepo, jwtService, nil, cacheManager, nil, logger)
|
userService := NewUserService(&config.Config{}, userRepo, jwtService, nil, cacheManager, nil, logger)
|
||||||
if got := userService.GetMaxProfilesPerUser(); got != 5 {
|
if got := userService.GetMaxProfilesPerUser(); got != 5 {
|
||||||
t.Fatalf("GetMaxProfilesPerUser 默认值错误, got=%d", got)
|
t.Fatalf("GetMaxProfilesPerUser 默认值错误, got=%d", got)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -26,16 +26,19 @@ const (
|
|||||||
|
|
||||||
// verificationService VerificationService的实现
|
// verificationService VerificationService的实现
|
||||||
type verificationService struct {
|
type verificationService struct {
|
||||||
|
cfg *config.Config
|
||||||
redis *redis.Client
|
redis *redis.Client
|
||||||
emailService *email.Service
|
emailService *email.Service
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewVerificationService 创建VerificationService实例
|
// NewVerificationService 创建VerificationService实例
|
||||||
func NewVerificationService(
|
func NewVerificationService(
|
||||||
|
cfg *config.Config,
|
||||||
redisClient *redis.Client,
|
redisClient *redis.Client,
|
||||||
emailService *email.Service,
|
emailService *email.Service,
|
||||||
) VerificationService {
|
) VerificationService {
|
||||||
return &verificationService{
|
return &verificationService{
|
||||||
|
cfg: cfg,
|
||||||
redis: redisClient,
|
redis: redisClient,
|
||||||
emailService: emailService,
|
emailService: emailService,
|
||||||
}
|
}
|
||||||
@@ -44,8 +47,7 @@ func NewVerificationService(
|
|||||||
// SendCode 发送验证码
|
// SendCode 发送验证码
|
||||||
func (s *verificationService) SendCode(ctx context.Context, email, codeType string) error {
|
func (s *verificationService) SendCode(ctx context.Context, email, codeType string) error {
|
||||||
// 测试环境下直接跳过,不存储也不发送
|
// 测试环境下直接跳过,不存储也不发送
|
||||||
cfg, err := config.GetConfig()
|
if s.cfg.IsTestEnvironment() {
|
||||||
if err == nil && cfg.IsTestEnvironment() {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -89,8 +91,7 @@ func (s *verificationService) SendCode(ctx context.Context, email, codeType stri
|
|||||||
// VerifyCode 验证验证码
|
// VerifyCode 验证验证码
|
||||||
func (s *verificationService) VerifyCode(ctx context.Context, email, code, codeType string) error {
|
func (s *verificationService) VerifyCode(ctx context.Context, email, code, codeType string) error {
|
||||||
// 测试环境下直接通过验证
|
// 测试环境下直接通过验证
|
||||||
cfg, err := config.GetConfig()
|
if s.cfg.IsTestEnvironment() {
|
||||||
if err == nil && cfg.IsTestEnvironment() {
|
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -166,8 +167,5 @@ func (s *verificationService) sendEmail(to, code, codeType string) error {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// DeleteVerificationCode 删除验证码(工具函数,保持向后兼容)
|
// DeleteVerificationCode 已移除:重构后无调用方。如需删除验证码,请使用
|
||||||
func DeleteVerificationCode(ctx context.Context, redisClient *redis.Client, email, codeType string) error {
|
// VerificationService 实现内的 redis.Del 路径。
|
||||||
codeKey := fmt.Sprintf("verification:code:%s:%s", codeType, email)
|
|
||||||
return redisClient.Del(ctx, codeKey)
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -9,14 +9,11 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// yggdrasilAuthService Yggdrasil认证服务实现
|
// yggdrasilAuthService Yggdrasil认证服务实现
|
||||||
// 负责认证和密码管理
|
// 负责认证和密码管理
|
||||||
type yggdrasilAuthService struct {
|
type yggdrasilAuthService struct {
|
||||||
db *gorm.DB
|
|
||||||
userRepo repository.UserRepository
|
userRepo repository.UserRepository
|
||||||
yggdrasilRepo repository.YggdrasilRepository
|
yggdrasilRepo repository.YggdrasilRepository
|
||||||
logger *zap.Logger
|
logger *zap.Logger
|
||||||
@@ -24,13 +21,11 @@ type yggdrasilAuthService struct {
|
|||||||
|
|
||||||
// NewYggdrasilAuthService 创建Yggdrasil认证服务实例(内部使用)
|
// NewYggdrasilAuthService 创建Yggdrasil认证服务实例(内部使用)
|
||||||
func NewYggdrasilAuthService(
|
func NewYggdrasilAuthService(
|
||||||
db *gorm.DB,
|
|
||||||
userRepo repository.UserRepository,
|
userRepo repository.UserRepository,
|
||||||
yggdrasilRepo repository.YggdrasilRepository,
|
yggdrasilRepo repository.YggdrasilRepository,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
) *yggdrasilAuthService {
|
) *yggdrasilAuthService {
|
||||||
return &yggdrasilAuthService{
|
return &yggdrasilAuthService{
|
||||||
db: db,
|
|
||||||
userRepo: userRepo,
|
userRepo: userRepo,
|
||||||
yggdrasilRepo: yggdrasilRepo,
|
yggdrasilRepo: yggdrasilRepo,
|
||||||
logger: logger,
|
logger: logger,
|
||||||
@@ -78,7 +73,7 @@ func (s *yggdrasilAuthService) ResetYggdrasilPassword(ctx context.Context, userI
|
|||||||
ID: userID,
|
ID: userID,
|
||||||
Password: hashedPassword,
|
Password: hashedPassword,
|
||||||
}
|
}
|
||||||
if err := s.db.Create(&yggdrasil).Error; err != nil {
|
if err := s.yggdrasilRepo.Create(ctx, &yggdrasil); err != nil {
|
||||||
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
|
return "", fmt.Errorf("创建Yggdrasil密码失败: %w", err)
|
||||||
}
|
}
|
||||||
return plainPassword, nil
|
return plainPassword, nil
|
||||||
|
|||||||
@@ -64,7 +64,7 @@ func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Cont
|
|||||||
s.logger.Info("为用户创建新的密钥对",
|
s.logger.Info("为用户创建新的密钥对",
|
||||||
zap.String("uuid", uuid),
|
zap.String("uuid", uuid),
|
||||||
)
|
)
|
||||||
keyPair, err = s.signatureService.NewKeyPair()
|
keyPair, err = s.signatureService.NewKeyPair(ctx)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("生成玩家证书密钥对失败",
|
s.logger.Error("生成玩家证书密钥对失败",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
@@ -107,6 +107,6 @@ func (s *yggdrasilCertificateService) GeneratePlayerCertificate(ctx context.Cont
|
|||||||
|
|
||||||
// GetPublicKey 获取公钥
|
// GetPublicKey 获取公钥
|
||||||
func (s *yggdrasilCertificateService) GetPublicKey(ctx context.Context) (string, error) {
|
func (s *yggdrasilCertificateService) GetPublicKey(ctx context.Context) (string, error) {
|
||||||
return s.signatureService.GetPublicKeyFromRedis()
|
return s.signatureService.GetPublicKeyFromRedis(ctx)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -110,7 +110,7 @@ func (s *yggdrasilSerializationService) SerializeProfileWithUnsigned(ctx context
|
|||||||
// 只有在 unsigned=false 时才签名
|
// 只有在 unsigned=false 时才签名
|
||||||
var signature string
|
var signature string
|
||||||
if !unsigned {
|
if !unsigned {
|
||||||
signature, err = s.signatureService.SignStringWithSHA1withRSA(textureData)
|
signature, err = s.signatureService.SignStringWithSHA1withRSA(ctx, textureData)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
s.logger.Error("签名textures失败",
|
s.logger.Error("签名textures失败",
|
||||||
zap.Error(err),
|
zap.Error(err),
|
||||||
|
|||||||
@@ -10,8 +10,6 @@ import (
|
|||||||
"fmt"
|
"fmt"
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
|
|
||||||
"gorm.io/gorm"
|
|
||||||
)
|
)
|
||||||
|
|
||||||
// yggdrasilServiceComposite 组合服务,保持接口兼容性
|
// yggdrasilServiceComposite 组合服务,保持接口兼容性
|
||||||
@@ -28,20 +26,20 @@ type yggdrasilServiceComposite struct {
|
|||||||
|
|
||||||
// NewYggdrasilServiceComposite 创建组合服务实例
|
// NewYggdrasilServiceComposite 创建组合服务实例
|
||||||
func NewYggdrasilServiceComposite(
|
func NewYggdrasilServiceComposite(
|
||||||
db *gorm.DB,
|
|
||||||
userRepo repository.UserRepository,
|
userRepo repository.UserRepository,
|
||||||
profileRepo repository.ProfileRepository,
|
profileRepo repository.ProfileRepository,
|
||||||
yggdrasilRepo repository.YggdrasilRepository,
|
yggdrasilRepo repository.YggdrasilRepository,
|
||||||
|
textureRepo repository.TextureRepository,
|
||||||
signatureService *SignatureService,
|
signatureService *SignatureService,
|
||||||
redisClient *redis.Client,
|
redisClient *redis.Client,
|
||||||
logger *zap.Logger,
|
logger *zap.Logger,
|
||||||
tokenService TokenService, // 新增:TokenService接口
|
tokenService TokenService, // 新增:TokenService接口
|
||||||
) YggdrasilService {
|
) YggdrasilService {
|
||||||
// 创建各个专门的服务
|
// 创建各个专门的服务
|
||||||
authService := NewYggdrasilAuthService(db, userRepo, yggdrasilRepo, logger)
|
authService := NewYggdrasilAuthService(userRepo, yggdrasilRepo, logger)
|
||||||
sessionService := NewSessionService(redisClient, logger)
|
sessionService := NewSessionService(redisClient, logger)
|
||||||
serializationService := NewSerializationService(
|
serializationService := NewSerializationService(
|
||||||
repository.NewTextureRepository(db),
|
textureRepo,
|
||||||
signatureService,
|
signatureService,
|
||||||
logger,
|
logger,
|
||||||
)
|
)
|
||||||
|
|||||||
@@ -2,13 +2,8 @@ package types
|
|||||||
|
|
||||||
import "time"
|
import "time"
|
||||||
|
|
||||||
// BaseResponse 基础响应结构
|
// 注意:BaseResponse 与 PaginationResponse 已删除(与 model.Response /
|
||||||
// @Description 通用API响应结构
|
// model.PaginationResponse 重复)。统一使用 model 包的响应结构。
|
||||||
type BaseResponse struct {
|
|
||||||
Code int `json:"code"`
|
|
||||||
Message string `json:"message"`
|
|
||||||
Data interface{} `json:"data,omitempty"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// PaginationRequest 分页请求
|
// PaginationRequest 分页请求
|
||||||
// @Description 分页查询参数
|
// @Description 分页查询参数
|
||||||
@@ -17,16 +12,6 @@ type PaginationRequest struct {
|
|||||||
PageSize int `json:"page_size" form:"page_size" binding:"omitempty,min=1,max=100"`
|
PageSize int `json:"page_size" form:"page_size" binding:"omitempty,min=1,max=100"`
|
||||||
}
|
}
|
||||||
|
|
||||||
// PaginationResponse 分页响应
|
|
||||||
// @Description 分页查询结果
|
|
||||||
type PaginationResponse struct {
|
|
||||||
List interface{} `json:"list"`
|
|
||||||
Total int64 `json:"total"`
|
|
||||||
Page int `json:"page"`
|
|
||||||
PageSize int `json:"page_size"`
|
|
||||||
TotalPages int `json:"total_pages"`
|
|
||||||
}
|
|
||||||
|
|
||||||
// LoginRequest 登录请求
|
// LoginRequest 登录请求
|
||||||
// @Description 用户登录请求参数
|
// @Description 用户登录请求参数
|
||||||
type LoginRequest struct {
|
type LoginRequest struct {
|
||||||
|
|||||||
@@ -7,9 +7,9 @@ import (
|
|||||||
// TestPaginationRequest_Validation 测试分页请求验证逻辑
|
// TestPaginationRequest_Validation 测试分页请求验证逻辑
|
||||||
func TestPaginationRequest_Validation(t *testing.T) {
|
func TestPaginationRequest_Validation(t *testing.T) {
|
||||||
tests := []struct {
|
tests := []struct {
|
||||||
name string
|
name string
|
||||||
page int
|
page int
|
||||||
pageSize int
|
pageSize int
|
||||||
wantValid bool
|
wantValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{
|
||||||
@@ -63,102 +63,15 @@ func TestTextureType_Constants(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestPaginationResponse_Structure 测试分页响应结构
|
// TestPaginationRequest_Defaults 测试分页请求结构
|
||||||
func TestPaginationResponse_Structure(t *testing.T) {
|
// 注意:PaginationResponse 已删除(与 model.PaginationResponse 重复)
|
||||||
resp := PaginationResponse{
|
func TestPaginationRequest_Defaults(t *testing.T) {
|
||||||
List: []string{"a", "b", "c"},
|
req := PaginationRequest{Page: 1, PageSize: 20}
|
||||||
Total: 100,
|
if req.Page != 1 {
|
||||||
Page: 1,
|
t.Errorf("Page = %d, want 1", req.Page)
|
||||||
PageSize: 20,
|
|
||||||
TotalPages: 5,
|
|
||||||
}
|
}
|
||||||
|
if req.PageSize != 20 {
|
||||||
if resp.Total != 100 {
|
t.Errorf("PageSize = %d, want 20", req.PageSize)
|
||||||
t.Errorf("Total = %d, want 100", resp.Total)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Page != 1 {
|
|
||||||
t.Errorf("Page = %d, want 1", resp.Page)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.PageSize != 20 {
|
|
||||||
t.Errorf("PageSize = %d, want 20", resp.PageSize)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.TotalPages != 5 {
|
|
||||||
t.Errorf("TotalPages = %d, want 5", resp.TotalPages)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestPaginationResponse_TotalPagesCalculation 测试总页数计算逻辑
|
|
||||||
func TestPaginationResponse_TotalPagesCalculation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
total int64
|
|
||||||
pageSize int
|
|
||||||
wantPages int
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "正好整除",
|
|
||||||
total: 100,
|
|
||||||
pageSize: 20,
|
|
||||||
wantPages: 5,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "有余数",
|
|
||||||
total: 101,
|
|
||||||
pageSize: 20,
|
|
||||||
wantPages: 6, // 向上取整
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "总数小于每页数量",
|
|
||||||
total: 10,
|
|
||||||
pageSize: 20,
|
|
||||||
wantPages: 1,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "总数为0",
|
|
||||||
total: 0,
|
|
||||||
pageSize: 20,
|
|
||||||
wantPages: 0,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
// 计算总页数:向上取整
|
|
||||||
var totalPages int
|
|
||||||
if tt.total == 0 {
|
|
||||||
totalPages = 0
|
|
||||||
} else {
|
|
||||||
totalPages = int((tt.total + int64(tt.pageSize) - 1) / int64(tt.pageSize))
|
|
||||||
}
|
|
||||||
|
|
||||||
if totalPages != tt.wantPages {
|
|
||||||
t.Errorf("TotalPages = %d, want %d", totalPages, tt.wantPages)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestBaseResponse_Structure 测试基础响应结构
|
|
||||||
func TestBaseResponse_Structure(t *testing.T) {
|
|
||||||
resp := BaseResponse{
|
|
||||||
Code: 200,
|
|
||||||
Message: "success",
|
|
||||||
Data: "test data",
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Code != 200 {
|
|
||||||
t.Errorf("Code = %d, want 200", resp.Code)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Message != "success" {
|
|
||||||
t.Errorf("Message = %q, want 'success'", resp.Message)
|
|
||||||
}
|
|
||||||
|
|
||||||
if resp.Data != "test data" {
|
|
||||||
t.Errorf("Data = %v, want 'test data'", resp.Data)
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -170,36 +83,10 @@ func TestLoginRequest_Validation(t *testing.T) {
|
|||||||
password string
|
password string
|
||||||
wantValid bool
|
wantValid bool
|
||||||
}{
|
}{
|
||||||
{
|
{name: "有效的登录请求", username: "testuser", password: "password123", wantValid: true},
|
||||||
name: "有效的登录请求",
|
{name: "用户名为空", username: "", password: "password123", wantValid: false},
|
||||||
username: "testuser",
|
{name: "密码为空", username: "testuser", password: "", wantValid: false},
|
||||||
password: "password123",
|
{name: "密码长度小于6", username: "testuser", password: "12345", wantValid: false},
|
||||||
wantValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "用户名为空",
|
|
||||||
username: "",
|
|
||||||
password: "password123",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "密码为空",
|
|
||||||
username: "testuser",
|
|
||||||
password: "",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "密码长度小于6",
|
|
||||||
username: "testuser",
|
|
||||||
password: "12345",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "密码长度超过128",
|
|
||||||
username: "testuser",
|
|
||||||
password: string(make([]byte, 129)),
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
}
|
}
|
||||||
|
|
||||||
for _, tt := range tests {
|
for _, tt := range tests {
|
||||||
@@ -211,174 +98,3 @@ func TestLoginRequest_Validation(t *testing.T) {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestRegisterRequest_Validation 测试注册请求验证逻辑
|
|
||||||
func TestRegisterRequest_Validation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
username string
|
|
||||||
email string
|
|
||||||
password string
|
|
||||||
verificationCode string
|
|
||||||
wantValid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "有效的注册请求",
|
|
||||||
username: "newuser",
|
|
||||||
email: "user@example.com",
|
|
||||||
password: "password123",
|
|
||||||
verificationCode: "123456",
|
|
||||||
wantValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "用户名为空",
|
|
||||||
username: "",
|
|
||||||
email: "user@example.com",
|
|
||||||
password: "password123",
|
|
||||||
verificationCode: "123456",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "用户名长度小于3",
|
|
||||||
username: "ab",
|
|
||||||
email: "user@example.com",
|
|
||||||
password: "password123",
|
|
||||||
verificationCode: "123456",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "用户名长度超过50",
|
|
||||||
username: string(make([]byte, 51)),
|
|
||||||
email: "user@example.com",
|
|
||||||
password: "password123",
|
|
||||||
verificationCode: "123456",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "邮箱格式无效",
|
|
||||||
username: "newuser",
|
|
||||||
email: "invalid-email",
|
|
||||||
password: "password123",
|
|
||||||
verificationCode: "123456",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "验证码长度不是6",
|
|
||||||
username: "newuser",
|
|
||||||
email: "user@example.com",
|
|
||||||
password: "password123",
|
|
||||||
verificationCode: "12345",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
isValid := tt.username != "" &&
|
|
||||||
len(tt.username) >= 3 && len(tt.username) <= 50 &&
|
|
||||||
tt.email != "" && contains(tt.email, "@") &&
|
|
||||||
len(tt.password) >= 6 && len(tt.password) <= 128 &&
|
|
||||||
len(tt.verificationCode) == 6
|
|
||||||
if isValid != tt.wantValid {
|
|
||||||
t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// 辅助函数
|
|
||||||
func contains(s, substr string) bool {
|
|
||||||
for i := 0; i <= len(s)-len(substr); i++ {
|
|
||||||
if s[i:i+len(substr)] == substr {
|
|
||||||
return true
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return false
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestResetPasswordRequest_Validation 测试重置密码请求验证
|
|
||||||
func TestResetPasswordRequest_Validation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
email string
|
|
||||||
verificationCode string
|
|
||||||
newPassword string
|
|
||||||
wantValid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "有效的重置密码请求",
|
|
||||||
email: "user@example.com",
|
|
||||||
verificationCode: "123456",
|
|
||||||
newPassword: "newpassword123",
|
|
||||||
wantValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "邮箱为空",
|
|
||||||
email: "",
|
|
||||||
verificationCode: "123456",
|
|
||||||
newPassword: "newpassword123",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "验证码长度不是6",
|
|
||||||
email: "user@example.com",
|
|
||||||
verificationCode: "12345",
|
|
||||||
newPassword: "newpassword123",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "新密码长度小于6",
|
|
||||||
email: "user@example.com",
|
|
||||||
verificationCode: "123456",
|
|
||||||
newPassword: "12345",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
isValid := tt.email != "" &&
|
|
||||||
len(tt.verificationCode) == 6 &&
|
|
||||||
len(tt.newPassword) >= 6 && len(tt.newPassword) <= 128
|
|
||||||
if isValid != tt.wantValid {
|
|
||||||
t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestCreateProfileRequest_Validation 测试创建档案请求验证
|
|
||||||
func TestCreateProfileRequest_Validation(t *testing.T) {
|
|
||||||
tests := []struct {
|
|
||||||
name string
|
|
||||||
profileName string
|
|
||||||
wantValid bool
|
|
||||||
}{
|
|
||||||
{
|
|
||||||
name: "有效的档案名",
|
|
||||||
profileName: "PlayerName",
|
|
||||||
wantValid: true,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "档案名为空",
|
|
||||||
profileName: "",
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
{
|
|
||||||
name: "档案名长度超过16",
|
|
||||||
profileName: string(make([]byte, 17)),
|
|
||||||
wantValid: false,
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
for _, tt := range tests {
|
|
||||||
t.Run(tt.name, func(t *testing.T) {
|
|
||||||
isValid := tt.profileName != "" &&
|
|
||||||
len(tt.profileName) >= 1 && len(tt.profileName) <= 16
|
|
||||||
if isValid != tt.wantValid {
|
|
||||||
t.Errorf("Validation failed: got %v, want %v", isValid, tt.wantValid)
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,46 +1,8 @@
|
|||||||
package auth
|
package auth
|
||||||
|
|
||||||
import (
|
// 本文件原包含基于 sync.Once 的全局单例(Init/GetJWTService/MustGetJWTService)。
|
||||||
"carrotskin/pkg/config"
|
//
|
||||||
"fmt"
|
// 在 DI 迁移(阶段4)后,全局单例已被移除。JWT 服务由 fx.Provide 构造
|
||||||
"sync"
|
// (见 internal/app/infra_module.go),通过构造函数参数注入到各消费者。
|
||||||
)
|
//
|
||||||
|
// JWT 服务构造逻辑见 jwt.go 的 NewJWTService()。
|
||||||
var (
|
|
||||||
// jwtServiceInstance 全局JWT服务实例
|
|
||||||
jwtServiceInstance *JWTService
|
|
||||||
// once 确保只初始化一次
|
|
||||||
once sync.Once
|
|
||||||
// initError 初始化错误
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init 初始化JWT服务(线程安全,只会执行一次)
|
|
||||||
func Init(cfg config.JWTConfig) error {
|
|
||||||
once.Do(func() {
|
|
||||||
jwtServiceInstance = NewJWTService(cfg.Secret, cfg.ExpireHours)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetJWTService 获取JWT服务实例(线程安全)
|
|
||||||
func GetJWTService() (*JWTService, error) {
|
|
||||||
if jwtServiceInstance == nil {
|
|
||||||
return nil, fmt.Errorf("JWT服务未初始化,请先调用 auth.Init()")
|
|
||||||
}
|
|
||||||
return jwtServiceInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetJWTService 获取JWT服务实例,如果未初始化则panic
|
|
||||||
func MustGetJWTService() *JWTService {
|
|
||||||
service, err := GetJWTService()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return service
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,86 +0,0 @@
|
|||||||
package auth
|
|
||||||
|
|
||||||
import (
|
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetJWTService_NotInitialized 测试未初始化时获取JWT服务
|
|
||||||
func TestGetJWTService_NotInitialized(t *testing.T) {
|
|
||||||
_, err := GetJWTService()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "JWT服务未初始化,请先调用 auth.Init()"
|
|
||||||
if err.Error() != expectedError {
|
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMustGetJWTService_Panic 测试MustGetJWTService在未初始化时panic
|
|
||||||
func TestMustGetJWTService_Panic(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetJWTService 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_ = MustGetJWTService()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_JWTService 测试JWT服务初始化
|
|
||||||
func TestInit_JWTService(t *testing.T) {
|
|
||||||
cfg := config.JWTConfig{
|
|
||||||
Secret: "test-secret-key",
|
|
||||||
ExpireHours: 24,
|
|
||||||
}
|
|
||||||
|
|
||||||
err := Init(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Init() 错误 = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证可以获取服务
|
|
||||||
service, err := GetJWTService()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GetJWTService() 错误 = %v, want nil", err)
|
|
||||||
}
|
|
||||||
if service == nil {
|
|
||||||
t.Error("GetJWTService() 返回的服务不应为nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_JWTService_Once 测试Init只执行一次
|
|
||||||
func TestInit_JWTService_Once(t *testing.T) {
|
|
||||||
cfg := config.JWTConfig{
|
|
||||||
Secret: "test-secret-key-1",
|
|
||||||
ExpireHours: 24,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 第一次初始化
|
|
||||||
err1 := Init(cfg)
|
|
||||||
if err1 != nil {
|
|
||||||
t.Fatalf("第一次Init() 错误 = %v", err1)
|
|
||||||
}
|
|
||||||
|
|
||||||
service1, _ := GetJWTService()
|
|
||||||
|
|
||||||
// 第二次初始化(应该不会改变服务)
|
|
||||||
cfg2 := config.JWTConfig{
|
|
||||||
Secret: "test-secret-key-2",
|
|
||||||
ExpireHours: 48,
|
|
||||||
}
|
|
||||||
err2 := Init(cfg2)
|
|
||||||
if err2 != nil {
|
|
||||||
t.Fatalf("第二次Init() 错误 = %v", err2)
|
|
||||||
}
|
|
||||||
|
|
||||||
service2, _ := GetJWTService()
|
|
||||||
|
|
||||||
// 验证是同一个实例(sync.Once保证)
|
|
||||||
if service1 != service2 {
|
|
||||||
t.Error("Init应该只执行一次,返回同一个实例")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,71 +1,8 @@
|
|||||||
package config
|
package config
|
||||||
|
|
||||||
import (
|
// 本文件原包含基于 sync.Once 的全局单例(Init/GetConfig/MustGetConfig/GetRustFSConfig)。
|
||||||
"fmt"
|
//
|
||||||
"sync"
|
// 在 DI 迁移(阶段4)后,全局单例已被移除。配置通过 config.Load() 加载,
|
||||||
)
|
// 由 fx.Supply 注入到依赖图中。各消费者通过构造函数参数接收 *Config 或子配置。
|
||||||
|
//
|
||||||
var (
|
// 配置加载逻辑见 config.go 的 Load()。
|
||||||
// configInstance 全局配置实例
|
|
||||||
configInstance *Config
|
|
||||||
// rustFSConfigInstance 全局RustFS配置实例
|
|
||||||
rustFSConfigInstance *RustFSConfig
|
|
||||||
// once 确保只初始化一次
|
|
||||||
once sync.Once
|
|
||||||
// initError 初始化错误
|
|
||||||
initError error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init 初始化配置(线程安全,只会执行一次)
|
|
||||||
func Init() error {
|
|
||||||
once.Do(func() {
|
|
||||||
configInstance, initError = Load()
|
|
||||||
if initError != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
rustFSConfigInstance = &configInstance.RustFS
|
|
||||||
})
|
|
||||||
return initError
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetConfig 获取配置实例(线程安全)
|
|
||||||
func GetConfig() (*Config, error) {
|
|
||||||
if configInstance == nil {
|
|
||||||
return nil, fmt.Errorf("配置未初始化,请先调用 config.Init()")
|
|
||||||
}
|
|
||||||
return configInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetConfig 获取配置实例,如果未初始化则panic
|
|
||||||
func MustGetConfig() *Config {
|
|
||||||
cfg, err := GetConfig()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetRustFSConfig 获取RustFS配置实例(线程安全)
|
|
||||||
func GetRustFSConfig() (*RustFSConfig, error) {
|
|
||||||
if rustFSConfigInstance == nil {
|
|
||||||
return nil, fmt.Errorf("配置未初始化,请先调用 config.Init()")
|
|
||||||
}
|
|
||||||
return rustFSConfigInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetRustFSConfig 获取RustFS配置实例,如果未初始化则panic
|
|
||||||
func MustGetRustFSConfig() *RustFSConfig {
|
|
||||||
cfg, err := GetRustFSConfig()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return cfg
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -4,67 +4,19 @@ import (
|
|||||||
"testing"
|
"testing"
|
||||||
)
|
)
|
||||||
|
|
||||||
// TestGetConfig_NotInitialized 测试未初始化时获取配置
|
// TestLoad_Config 验证 Load() 能成功加载配置(从环境变量/默认值)
|
||||||
func TestGetConfig_NotInitialized(t *testing.T) {
|
// 全局单例访问器(GetConfig/MustGetConfig)已在 DI 迁移中移除,
|
||||||
// 重置全局变量(在实际测试中可能需要更复杂的重置逻辑)
|
// 配置通过 config.Load() 加载并由 fx.Supply 注入。
|
||||||
// 注意:由于使用了 sync.Once,这个测试主要验证错误处理逻辑
|
func TestLoad_Config(t *testing.T) {
|
||||||
|
cfg, err := Load()
|
||||||
// 测试未初始化时的错误消息
|
if err != nil {
|
||||||
_, err := GetConfig()
|
t.Fatalf("Load() 返回错误: %v", err)
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
}
|
||||||
|
if cfg == nil {
|
||||||
expectedError := "配置未初始化,请先调用 config.Init()"
|
t.Fatal("Load() 返回 nil 配置")
|
||||||
if err.Error() != expectedError {
|
}
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
// 验证默认值生效
|
||||||
|
if cfg.Server.Port == "" {
|
||||||
|
t.Error("Server.Port 不应为空")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// TestMustGetConfig_Panic 测试MustGetConfig在未初始化时panic
|
|
||||||
func TestMustGetConfig_Panic(t *testing.T) {
|
|
||||||
// 注意:这个测试会触发panic,需要recover
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetConfig 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
// 尝试获取未初始化的配置
|
|
||||||
_ = MustGetConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetRustFSConfig_NotInitialized 测试未初始化时获取RustFS配置
|
|
||||||
func TestGetRustFSConfig_NotInitialized(t *testing.T) {
|
|
||||||
_, err := GetRustFSConfig()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "配置未初始化,请先调用 config.Init()"
|
|
||||||
if err.Error() != expectedError {
|
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMustGetRustFSConfig_Panic 测试MustGetRustFSConfig在未初始化时panic
|
|
||||||
func TestMustGetRustFSConfig_Panic(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetRustFSConfig 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_ = MustGetRustFSConfig()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_Once 测试Init只执行一次的逻辑
|
|
||||||
func TestInit_Once(t *testing.T) {
|
|
||||||
// 注意:由于sync.Once的特性,这个测试主要验证逻辑
|
|
||||||
// 实际测试中可能需要重置机制
|
|
||||||
|
|
||||||
// 验证Init函数可调用(函数不能直接比较nil)
|
|
||||||
// 这里只验证函数存在
|
|
||||||
_ = Init
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|||||||
@@ -122,7 +122,11 @@ func (cm *CacheManager) Set(ctx context.Context, key string, value interface{},
|
|||||||
// SetAsync 异步设置缓存,避免在主请求链路阻塞
|
// SetAsync 异步设置缓存,避免在主请求链路阻塞
|
||||||
func (cm *CacheManager) SetAsync(ctx context.Context, key string, value interface{}, expiration ...time.Duration) {
|
func (cm *CacheManager) SetAsync(ctx context.Context, key string, value interface{}, expiration ...time.Duration) {
|
||||||
go func() {
|
go func() {
|
||||||
_ = cm.Set(ctx, key, value, expiration...)
|
// 使用独立 ctx 配合超时,防止上游 ctx 取消导致缓存写入失败,
|
||||||
|
// 同时避免 Redis 卡死时 goroutine 永久堆积
|
||||||
|
asyncCtx, cancel := context.WithTimeout(context.WithoutCancel(ctx), 5*time.Second)
|
||||||
|
defer cancel()
|
||||||
|
_ = cm.Set(asyncCtx, key, value, expiration...)
|
||||||
}()
|
}()
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -2,74 +2,25 @@ package database
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"carrotskin/internal/model"
|
"carrotskin/internal/model"
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// 本文件原包含基于 sync.Once 的全局单例(Init/GetDB/MustGetDB/GetDBWrapper)。
|
||||||
// dbInstance 全局数据库实例(使用 *DB 封装)
|
//
|
||||||
dbInstance *DB
|
// 在 DI 迁移(阶段4)后,全局单例已被移除。数据库连接由 fx.Provide 构造
|
||||||
// once 确保只初始化一次
|
// (见 internal/app/infra_module.go 的 provideDatabase),通过构造函数参数
|
||||||
once sync.Once
|
// 注入 *database.DB 与 *gorm.DB 到各消费者。
|
||||||
// initError 初始化错误
|
//
|
||||||
initError error
|
// 数据库构造逻辑见 postgres.go 的 New()。
|
||||||
)
|
|
||||||
|
|
||||||
// Init 初始化数据库连接(线程安全,只会执行一次)
|
|
||||||
func Init(cfg config.DatabaseConfig, logger *zap.Logger) error {
|
|
||||||
once.Do(func() {
|
|
||||||
dbInstance, initError = New(cfg)
|
|
||||||
if initError != nil {
|
|
||||||
logger.Error("数据库初始化失败", zap.Error(initError))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("数据库连接成功")
|
|
||||||
})
|
|
||||||
return initError
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDB 获取数据库实例(线程安全)
|
|
||||||
// 返回 *gorm.DB 以保持向后兼容
|
|
||||||
func GetDB() (*gorm.DB, error) {
|
|
||||||
if dbInstance == nil {
|
|
||||||
return nil, fmt.Errorf("数据库未初始化,请先调用 database.Init()")
|
|
||||||
}
|
|
||||||
return dbInstance.DB, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetDBWrapper 获取数据库封装实例(包含连接池统计功能)
|
|
||||||
func GetDBWrapper() (*DB, error) {
|
|
||||||
if dbInstance == nil {
|
|
||||||
return nil, fmt.Errorf("数据库未初始化,请先调用 database.Init()")
|
|
||||||
}
|
|
||||||
return dbInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetDB 获取数据库实例,如果未初始化则panic
|
|
||||||
// 返回 *gorm.DB 以保持向后兼容
|
|
||||||
func MustGetDB() *gorm.DB {
|
|
||||||
db, err := GetDB()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return db
|
|
||||||
}
|
|
||||||
|
|
||||||
// AutoMigrate 自动迁移数据库表结构
|
|
||||||
func AutoMigrate(logger *zap.Logger) error {
|
|
||||||
db, err := GetDB()
|
|
||||||
if err != nil {
|
|
||||||
return fmt.Errorf("获取数据库实例失败: %w", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
|
// AutoMigrateWithDB 使用指定的 *gorm.DB 执行表结构迁移(供 DI 使用)。
|
||||||
|
// 注意表的创建顺序:先创建被引用的表,再创建引用表。
|
||||||
|
func AutoMigrateWithDB(db *gorm.DB, logger *zap.Logger) error {
|
||||||
logger.Info("开始执行数据库迁移...")
|
logger.Info("开始执行数据库迁移...")
|
||||||
|
|
||||||
// 迁移所有表 - 注意顺序:先创建被引用的表,再创建引用表
|
|
||||||
// 使用分批迁移,避免某些表的问题影响其他表
|
|
||||||
tables := []interface{}{
|
tables := []interface{}{
|
||||||
// 用户相关表(先创建,因为其他表可能引用它)
|
// 用户相关表(先创建,因为其他表可能引用它)
|
||||||
&model.User{},
|
&model.User{},
|
||||||
@@ -97,7 +48,6 @@ func AutoMigrate(logger *zap.Logger) error {
|
|||||||
&model.CasbinRule{},
|
&model.CasbinRule{},
|
||||||
}
|
}
|
||||||
|
|
||||||
// 批量迁移表
|
|
||||||
if err := db.AutoMigrate(tables...); err != nil {
|
if err := db.AutoMigrate(tables...); err != nil {
|
||||||
logger.Error("数据库迁移失败", zap.Error(err))
|
logger.Error("数据库迁移失败", zap.Error(err))
|
||||||
return fmt.Errorf("数据库迁移失败: %w", err)
|
return fmt.Errorf("数据库迁移失败: %w", err)
|
||||||
@@ -106,12 +56,3 @@ func AutoMigrate(logger *zap.Logger) error {
|
|||||||
logger.Info("数据库迁移完成")
|
logger.Info("数据库迁移完成")
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// Close 关闭数据库连接
|
|
||||||
func Close() error {
|
|
||||||
if dbInstance == nil {
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
return dbInstance.Close()
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -8,34 +8,16 @@ import (
|
|||||||
"gorm.io/gorm"
|
"gorm.io/gorm"
|
||||||
)
|
)
|
||||||
|
|
||||||
// 使用内存 sqlite 验证 AutoMigrate 关键路径,无需真实 Postgres
|
// 使用内存 sqlite 验证 AutoMigrateWithDB 关键路径,无需真实 Postgres。
|
||||||
|
// 全局单例访问器(Init/GetDB/MustGetDB)已在 DI 迁移中移除。
|
||||||
func TestAutoMigrate_WithSQLite(t *testing.T) {
|
func TestAutoMigrate_WithSQLite(t *testing.T) {
|
||||||
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
db, err := gorm.Open(sqlite.Open("file::memory:?cache=shared"), &gorm.Config{})
|
||||||
if err != nil {
|
if err != nil {
|
||||||
t.Fatalf("open sqlite err: %v", err)
|
t.Fatalf("open sqlite err: %v", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建临时的 *DB 包装器用于测试
|
|
||||||
// 注意:这里不需要真正的连接池功能,只是测试 AutoMigrate
|
|
||||||
sqlDB, err := db.DB()
|
|
||||||
if err != nil {
|
|
||||||
t.Fatalf("get sql.DB err: %v", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
tempDB := &DB{
|
|
||||||
DB: db,
|
|
||||||
sqlDB: sqlDB,
|
|
||||||
}
|
|
||||||
|
|
||||||
// 保存原始实例
|
|
||||||
originalDB := dbInstance
|
|
||||||
defer func() { dbInstance = originalDB }()
|
|
||||||
|
|
||||||
// 替换为测试实例
|
|
||||||
dbInstance = tempDB
|
|
||||||
|
|
||||||
logger := zaptest.NewLogger(t)
|
logger := zaptest.NewLogger(t)
|
||||||
if err := AutoMigrate(logger); err != nil {
|
if err := AutoMigrateWithDB(db, logger); err != nil {
|
||||||
t.Fatalf("AutoMigrate sqlite err: %v", err)
|
t.Fatalf("AutoMigrateWithDB sqlite err: %v", err)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,87 +0,0 @@
|
|||||||
package database
|
|
||||||
|
|
||||||
import (
|
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/zap/zaptest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetDB_NotInitialized 测试未初始化时获取数据库实例
|
|
||||||
func TestGetDB_NotInitialized(t *testing.T) {
|
|
||||||
dbInstance = nil
|
|
||||||
_, err := GetDB()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "数据库未初始化,请先调用 database.Init()"
|
|
||||||
if err.Error() != expectedError {
|
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMustGetDB_Panic 测试MustGetDB在未初始化时panic
|
|
||||||
func TestMustGetDB_Panic(t *testing.T) {
|
|
||||||
dbInstance = nil
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetDB 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_ = MustGetDB()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_Database 测试数据库初始化逻辑
|
|
||||||
func TestInit_Database(t *testing.T) {
|
|
||||||
dbInstance = nil
|
|
||||||
cfg := config.DatabaseConfig{
|
|
||||||
Driver: "postgres",
|
|
||||||
Host: "localhost",
|
|
||||||
Port: 5432,
|
|
||||||
Username: "postgres",
|
|
||||||
Password: "password",
|
|
||||||
Database: "testdb",
|
|
||||||
SSLMode: "disable",
|
|
||||||
Timezone: "Asia/Shanghai",
|
|
||||||
MaxIdleConns: 10,
|
|
||||||
MaxOpenConns: 100,
|
|
||||||
ConnMaxLifetime: 0,
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
|
|
||||||
// 验证Init函数存在且可调用
|
|
||||||
// 注意:实际连接可能失败,这是可以接受的
|
|
||||||
err := Init(cfg, logger)
|
|
||||||
if err != nil {
|
|
||||||
t.Skipf("数据库未运行,跳过连接测试: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestAutoMigrate_ErrorHandling 测试AutoMigrate的错误处理逻辑
|
|
||||||
func TestAutoMigrate_ErrorHandling(t *testing.T) {
|
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
|
|
||||||
// 测试未初始化时的错误处理
|
|
||||||
err := AutoMigrate(logger)
|
|
||||||
if err == nil {
|
|
||||||
// 如果数据库已初始化,这是正常的
|
|
||||||
t.Log("AutoMigrate() 成功(数据库可能已初始化)")
|
|
||||||
} else {
|
|
||||||
// 如果数据库未初始化,应该返回错误
|
|
||||||
if err.Error() == "" {
|
|
||||||
t.Error("AutoMigrate() 应该返回有意义的错误消息")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestClose_NotInitialized 测试未初始化时关闭数据库
|
|
||||||
func TestClose_NotInitialized(t *testing.T) {
|
|
||||||
// 未初始化时关闭应该不返回错误
|
|
||||||
err := Close()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Close() 在未初始化时应该返回nil,实际返回: %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -50,13 +50,8 @@ var defaultCasbinRules = []model.CasbinRule{
|
|||||||
{PType: "g", V0: "admin", V1: "user"},
|
{PType: "g", V0: "admin", V1: "user"},
|
||||||
}
|
}
|
||||||
|
|
||||||
// Seed 初始化种子数据
|
// SeedWithDB 使用指定的 *gorm.DB 初始化种子数据(供 DI 使用)
|
||||||
func Seed(logger *zap.Logger) error {
|
func SeedWithDB(db *gorm.DB, logger *zap.Logger) error {
|
||||||
db, err := GetDB()
|
|
||||||
if err != nil {
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
logger.Info("开始初始化种子数据...")
|
logger.Info("开始初始化种子数据...")
|
||||||
|
|
||||||
// 初始化默认管理员用户
|
// 初始化默认管理员用户
|
||||||
|
|||||||
@@ -2,7 +2,6 @@ package email
|
|||||||
|
|
||||||
import (
|
import (
|
||||||
"strings"
|
"strings"
|
||||||
"sync"
|
|
||||||
"testing"
|
"testing"
|
||||||
|
|
||||||
"carrotskin/pkg/config"
|
"carrotskin/pkg/config"
|
||||||
@@ -10,25 +9,18 @@ import (
|
|||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
func resetEmailOnce() {
|
// 全局单例访问器(Init/GetService/MustGetService)已在 DI 迁移中移除。
|
||||||
serviceInstance = nil
|
// 本测试改为直接使用构造函数 NewService。
|
||||||
once = sync.Once{}
|
|
||||||
}
|
|
||||||
|
|
||||||
func TestEmailManager_Disabled(t *testing.T) {
|
func TestEmailManager_Disabled(t *testing.T) {
|
||||||
resetEmailOnce()
|
|
||||||
cfg := config.EmailConfig{Enabled: false}
|
cfg := config.EmailConfig{Enabled: false}
|
||||||
if err := Init(cfg, zap.NewNop()); err != nil {
|
svc := NewService(cfg, zap.NewNop())
|
||||||
t.Fatalf("Init disabled err: %v", err)
|
|
||||||
}
|
|
||||||
svc := MustGetService()
|
|
||||||
if err := svc.SendVerificationCode("to@test.com", "123456", "email_verification"); err == nil {
|
if err := svc.SendVerificationCode("to@test.com", "123456", "email_verification"); err == nil {
|
||||||
t.Fatalf("expected error when disabled")
|
t.Fatalf("expected error when disabled")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
func TestEmailManager_SendFailsWithInvalidSMTP(t *testing.T) {
|
func TestEmailManager_SendFailsWithInvalidSMTP(t *testing.T) {
|
||||||
resetEmailOnce()
|
|
||||||
cfg := config.EmailConfig{
|
cfg := config.EmailConfig{
|
||||||
Enabled: true,
|
Enabled: true,
|
||||||
SMTPHost: "127.0.0.1",
|
SMTPHost: "127.0.0.1",
|
||||||
@@ -37,8 +29,7 @@ func TestEmailManager_SendFailsWithInvalidSMTP(t *testing.T) {
|
|||||||
Password: "pwd",
|
Password: "pwd",
|
||||||
FromName: "name",
|
FromName: "name",
|
||||||
}
|
}
|
||||||
_ = Init(cfg, zap.NewNop())
|
svc := NewService(cfg, zap.NewNop())
|
||||||
svc := MustGetService()
|
|
||||||
if err := svc.SendVerificationCode("to@test.com", "123456", "reset_password"); err == nil {
|
if err := svc.SendVerificationCode("to@test.com", "123456", "reset_password"); err == nil {
|
||||||
t.Fatalf("expected send error with invalid smtp")
|
t.Fatalf("expected send error with invalid smtp")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,54 +1,8 @@
|
|||||||
package email
|
package email
|
||||||
|
|
||||||
import (
|
// 本文件原包含基于 sync.Once 的全局单例(Init/GetService/MustGetService)。
|
||||||
"carrotskin/pkg/config"
|
//
|
||||||
"fmt"
|
// 在 DI 迁移(阶段4)后,全局单例已被移除。Email 服务由 fx.Provide(email.NewService)
|
||||||
"sync"
|
// 构造,通过构造函数参数注入到各消费者。
|
||||||
|
//
|
||||||
"go.uber.org/zap"
|
// 邮件服务构造逻辑见 email.go 的 NewService()。
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// serviceInstance 全局邮件服务实例
|
|
||||||
serviceInstance *Service
|
|
||||||
// once 确保只初始化一次
|
|
||||||
once sync.Once
|
|
||||||
// initError 初始化错误
|
|
||||||
initError error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init 初始化邮件服务(线程安全,只会执行一次)
|
|
||||||
func Init(cfg config.EmailConfig, logger *zap.Logger) error {
|
|
||||||
once.Do(func() {
|
|
||||||
serviceInstance = NewService(cfg, logger)
|
|
||||||
})
|
|
||||||
return nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetService 获取邮件服务实例(线程安全)
|
|
||||||
func GetService() (*Service, error) {
|
|
||||||
if serviceInstance == nil {
|
|
||||||
return nil, fmt.Errorf("邮件服务未初始化,请先调用 email.Init()")
|
|
||||||
}
|
|
||||||
return serviceInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetService 获取邮件服务实例,如果未初始化则panic
|
|
||||||
func MustGetService() *Service {
|
|
||||||
service, err := GetService()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return service
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,69 +0,0 @@
|
|||||||
package email
|
|
||||||
|
|
||||||
import (
|
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"sync"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/zap/zaptest"
|
|
||||||
)
|
|
||||||
|
|
||||||
func resetEmail() {
|
|
||||||
serviceInstance = nil
|
|
||||||
once = sync.Once{}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestGetService_NotInitialized 测试未初始化时获取邮件服务
|
|
||||||
func TestGetService_NotInitialized(t *testing.T) {
|
|
||||||
resetEmail()
|
|
||||||
_, err := GetService()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "邮件服务未初始化,请先调用 email.Init()"
|
|
||||||
if err.Error() != expectedError {
|
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMustGetService_Panic 测试MustGetService在未初始化时panic
|
|
||||||
func TestMustGetService_Panic(t *testing.T) {
|
|
||||||
resetEmail()
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetService 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_ = MustGetService()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_Email 测试邮件服务初始化
|
|
||||||
func TestInit_Email(t *testing.T) {
|
|
||||||
resetEmail()
|
|
||||||
cfg := config.EmailConfig{
|
|
||||||
Enabled: false,
|
|
||||||
SMTPHost: "smtp.example.com",
|
|
||||||
SMTPPort: 587,
|
|
||||||
Username: "user@example.com",
|
|
||||||
Password: "password",
|
|
||||||
FromName: "noreply@example.com",
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
|
|
||||||
err := Init(cfg, logger)
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("Init() 错误 = %v, want nil", err)
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证可以获取服务
|
|
||||||
service, err := GetService()
|
|
||||||
if err != nil {
|
|
||||||
t.Errorf("GetService() 错误 = %v, want nil", err)
|
|
||||||
}
|
|
||||||
if service == nil {
|
|
||||||
t.Error("GetService() 返回的服务不应为nil")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,57 +1,8 @@
|
|||||||
package logger
|
package logger
|
||||||
|
|
||||||
import (
|
// 本文件原包含基于 sync.Once 的全局单例(Init/GetLogger/MustGetLogger)。
|
||||||
"carrotskin/pkg/config"
|
//
|
||||||
"fmt"
|
// 在 DI 迁移(阶段4)后,全局单例已被移除。Logger 由 fx.Provide(logger.New)
|
||||||
"sync"
|
// 构造,通过构造函数参数注入到各消费者。
|
||||||
|
//
|
||||||
"go.uber.org/zap"
|
// 日志构造逻辑见 logger.go 的 New()。
|
||||||
)
|
|
||||||
|
|
||||||
var (
|
|
||||||
// loggerInstance 全局日志实例
|
|
||||||
loggerInstance *zap.Logger
|
|
||||||
// once 确保只初始化一次
|
|
||||||
once sync.Once
|
|
||||||
// initError 初始化错误
|
|
||||||
initError error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init 初始化日志记录器(线程安全,只会执行一次)
|
|
||||||
func Init(cfg config.LogConfig) error {
|
|
||||||
once.Do(func() {
|
|
||||||
loggerInstance, initError = New(cfg)
|
|
||||||
if initError != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return initError
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetLogger 获取日志实例(线程安全)
|
|
||||||
func GetLogger() (*zap.Logger, error) {
|
|
||||||
if loggerInstance == nil {
|
|
||||||
return nil, fmt.Errorf("日志未初始化,请先调用 logger.Init()")
|
|
||||||
}
|
|
||||||
return loggerInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetLogger 获取日志实例,如果未初始化则panic
|
|
||||||
func MustGetLogger() *zap.Logger {
|
|
||||||
logger, err := GetLogger()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return logger
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,47 +0,0 @@
|
|||||||
package logger
|
|
||||||
|
|
||||||
import (
|
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetLogger_NotInitialized 测试未初始化时获取日志实例
|
|
||||||
func TestGetLogger_NotInitialized(t *testing.T) {
|
|
||||||
_, err := GetLogger()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "日志未初始化,请先调用 logger.Init()"
|
|
||||||
if err.Error() != expectedError {
|
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMustGetLogger_Panic 测试MustGetLogger在未初始化时panic
|
|
||||||
func TestMustGetLogger_Panic(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetLogger 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_ = MustGetLogger()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_Logger 测试日志初始化逻辑
|
|
||||||
func TestInit_Logger(t *testing.T) {
|
|
||||||
cfg := config.LogConfig{
|
|
||||||
Level: "info",
|
|
||||||
Format: "json",
|
|
||||||
Output: "stdout",
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证Init函数存在且可调用
|
|
||||||
err := Init(cfg)
|
|
||||||
if err != nil {
|
|
||||||
// 初始化可能失败(例如缺少依赖),这是可以接受的
|
|
||||||
t.Logf("Init() 返回错误(可能正常): %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,75 +1,38 @@
|
|||||||
package redis
|
package redis
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"fmt"
|
"fmt"
|
||||||
"os"
|
"os"
|
||||||
"sync"
|
|
||||||
|
|
||||||
"github.com/alicebob/miniredis/v2"
|
"github.com/alicebob/miniredis/v2"
|
||||||
redis9 "github.com/redis/go-redis/v9"
|
redis9 "github.com/redis/go-redis/v9"
|
||||||
"go.uber.org/zap"
|
"go.uber.org/zap"
|
||||||
)
|
)
|
||||||
|
|
||||||
var (
|
// 本文件原包含基于 sync.Once 的全局单例(Init/GetClient/MustGetClient/Close)。
|
||||||
// clientInstance 全局Redis客户端实例
|
//
|
||||||
clientInstance *Client
|
// 在 DI 迁移(阶段4)后,全局单例已被移除。Redis 客户端由 fx.Provide 构造
|
||||||
// once 确保只初始化一次
|
// (见 internal/app/infra_module.go 的 provideRedis,包含 miniredis 回退逻辑),
|
||||||
once sync.Once
|
// 通过构造函数参数注入到各消费者。
|
||||||
// initError 初始化错误
|
//
|
||||||
initError error
|
// Redis 客户端构造逻辑见 redis.go 的 New()。
|
||||||
// miniredisInstance 用于测试/开发环境
|
|
||||||
miniredisInstance *miniredis.Miniredis
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init 初始化Redis客户端(线程安全,只会执行一次)
|
// AllowFallbackToMiniRedis 检查当前环境是否允许回退到 miniredis(仅开发/测试环境)。
|
||||||
// 如果Redis连接失败且环境为测试/开发,则回退到miniredis
|
func AllowFallbackToMiniRedis() bool {
|
||||||
func Init(cfg config.RedisConfig, logger *zap.Logger) error {
|
|
||||||
var err error
|
|
||||||
once.Do(func() {
|
|
||||||
// 尝试连接真实Redis
|
|
||||||
clientInstance, err = New(cfg, logger)
|
|
||||||
if err != nil {
|
|
||||||
logger.Warn("Redis连接失败,尝试使用miniredis回退", zap.Error(err))
|
|
||||||
|
|
||||||
// 检查是否允许回退到miniredis(仅开发/测试环境)
|
|
||||||
if allowFallbackToMiniRedis() {
|
|
||||||
clientInstance, err = initMiniRedis(logger)
|
|
||||||
if err != nil {
|
|
||||||
initError = fmt.Errorf("Redis和miniredis都初始化失败: %w", err)
|
|
||||||
logger.Error("miniredis初始化失败", zap.Error(initError))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
logger.Info("已回退到miniredis用于开发/测试环境")
|
|
||||||
} else {
|
|
||||||
initError = fmt.Errorf("Redis连接失败且不允许回退: %w", err)
|
|
||||||
logger.Error("Redis连接失败", zap.Error(initError))
|
|
||||||
return
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return initError
|
|
||||||
}
|
|
||||||
|
|
||||||
// allowFallbackToMiniRedis 检查是否允许回退到miniredis
|
|
||||||
func allowFallbackToMiniRedis() bool {
|
|
||||||
// 检查环境变量
|
|
||||||
env := os.Getenv("ENVIRONMENT")
|
env := os.Getenv("ENVIRONMENT")
|
||||||
return env == "development" || env == "test" || env == "dev" ||
|
return env == "development" || env == "test" || env == "dev" ||
|
||||||
os.Getenv("USE_MINIREDIS") == "true"
|
os.Getenv("USE_MINIREDIS") == "true"
|
||||||
}
|
}
|
||||||
|
|
||||||
// initMiniRedis 初始化miniredis(用于开发/测试环境)
|
// InitMiniRedis 初始化 miniredis(用于开发/测试环境的 DI 回退)。
|
||||||
func initMiniRedis(logger *zap.Logger) (*Client, error) {
|
func InitMiniRedis(logger *zap.Logger) (*Client, error) {
|
||||||
var err error
|
mr, err := miniredis.Run()
|
||||||
miniredisInstance, err = miniredis.Run()
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("启动miniredis失败: %w", err)
|
return nil, fmt.Errorf("启动miniredis失败: %w", err)
|
||||||
}
|
}
|
||||||
|
|
||||||
// 创建Redis客户端连接到miniredis
|
|
||||||
redisClient := redis9.NewClient(&redis9.Options{
|
redisClient := redis9.NewClient(&redis9.Options{
|
||||||
Addr: miniredisInstance.Addr(),
|
Addr: mr.Addr(),
|
||||||
})
|
})
|
||||||
|
|
||||||
client := &Client{
|
client := &Client{
|
||||||
@@ -77,42 +40,6 @@ func initMiniRedis(logger *zap.Logger) (*Client, error) {
|
|||||||
logger: logger,
|
logger: logger,
|
||||||
}
|
}
|
||||||
|
|
||||||
logger.Info("miniredis已启动", zap.String("addr", miniredisInstance.Addr()))
|
logger.Info("miniredis已启动", zap.String("addr", mr.Addr()))
|
||||||
return client, nil
|
return client, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
// GetClient 获取Redis客户端实例(线程安全)
|
|
||||||
func GetClient() (*Client, error) {
|
|
||||||
if clientInstance == nil {
|
|
||||||
return nil, fmt.Errorf("Redis客户端未初始化,请先调用 redis.Init()")
|
|
||||||
}
|
|
||||||
return clientInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetClient 获取Redis客户端实例,如果未初始化则panic
|
|
||||||
func MustGetClient() *Client {
|
|
||||||
client, err := GetClient()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
// Close 关闭Redis连接(包括miniredis如果使用了)
|
|
||||||
func Close() error {
|
|
||||||
var err error
|
|
||||||
if miniredisInstance != nil {
|
|
||||||
miniredisInstance.Close()
|
|
||||||
miniredisInstance = nil
|
|
||||||
}
|
|
||||||
if clientInstance != nil {
|
|
||||||
err = clientInstance.Close()
|
|
||||||
clientInstance = nil
|
|
||||||
}
|
|
||||||
return err
|
|
||||||
}
|
|
||||||
|
|
||||||
// IsUsingMiniRedis 检查是否使用了miniredis
|
|
||||||
func IsUsingMiniRedis() bool {
|
|
||||||
return miniredisInstance != nil
|
|
||||||
}
|
|
||||||
|
|||||||
@@ -1,53 +0,0 @@
|
|||||||
package redis
|
|
||||||
|
|
||||||
import (
|
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"testing"
|
|
||||||
|
|
||||||
"go.uber.org/zap/zaptest"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetClient_NotInitialized 测试未初始化时获取Redis客户端
|
|
||||||
func TestGetClient_NotInitialized(t *testing.T) {
|
|
||||||
_, err := GetClient()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "Redis客户端未初始化,请先调用 redis.Init()"
|
|
||||||
if err.Error() != expectedError {
|
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMustGetClient_Panic 测试MustGetClient在未初始化时panic
|
|
||||||
func TestMustGetClient_Panic(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetClient 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_ = MustGetClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_Redis 测试Redis初始化逻辑
|
|
||||||
func TestInit_Redis(t *testing.T) {
|
|
||||||
cfg := config.RedisConfig{
|
|
||||||
Host: "localhost",
|
|
||||||
Port: 6379,
|
|
||||||
Password: "",
|
|
||||||
Database: 0,
|
|
||||||
PoolSize: 10,
|
|
||||||
}
|
|
||||||
|
|
||||||
logger := zaptest.NewLogger(t)
|
|
||||||
|
|
||||||
// 验证Init函数存在且可调用
|
|
||||||
// 注意:实际连接可能失败,这是可以接受的
|
|
||||||
err := Init(cfg, logger)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Init() 返回错误(可能正常,如果Redis未运行): %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
@@ -1,55 +1,8 @@
|
|||||||
package storage
|
package storage
|
||||||
|
|
||||||
import (
|
// 本文件原包含基于 sync.Once 的全局单例(Init/GetClient/MustGetClient)。
|
||||||
"carrotskin/pkg/config"
|
//
|
||||||
"fmt"
|
// 在 DI 迁移(阶段4)后,全局单例已被移除。Storage 客户端由 fx.Provide(storage.NewStorage)
|
||||||
"sync"
|
// 构造,通过构造函数参数注入到各消费者。
|
||||||
)
|
//
|
||||||
|
// 存储客户端构造逻辑见 minio.go 的 NewStorage()。
|
||||||
var (
|
|
||||||
// clientInstance 全局存储客户端实例
|
|
||||||
clientInstance *StorageClient
|
|
||||||
// once 确保只初始化一次
|
|
||||||
once sync.Once
|
|
||||||
// initError 初始化错误
|
|
||||||
initError error
|
|
||||||
)
|
|
||||||
|
|
||||||
// Init 初始化存储客户端(线程安全,只会执行一次)
|
|
||||||
func Init(cfg config.RustFSConfig) error {
|
|
||||||
once.Do(func() {
|
|
||||||
clientInstance, initError = NewStorage(cfg)
|
|
||||||
if initError != nil {
|
|
||||||
return
|
|
||||||
}
|
|
||||||
})
|
|
||||||
return initError
|
|
||||||
}
|
|
||||||
|
|
||||||
// GetClient 获取存储客户端实例(线程安全)
|
|
||||||
func GetClient() (*StorageClient, error) {
|
|
||||||
if clientInstance == nil {
|
|
||||||
return nil, fmt.Errorf("存储客户端未初始化,请先调用 storage.Init()")
|
|
||||||
}
|
|
||||||
return clientInstance, nil
|
|
||||||
}
|
|
||||||
|
|
||||||
// MustGetClient 获取存储客户端实例,如果未初始化则panic
|
|
||||||
func MustGetClient() *StorageClient {
|
|
||||||
client, err := GetClient()
|
|
||||||
if err != nil {
|
|
||||||
panic(err)
|
|
||||||
}
|
|
||||||
return client
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
package storage
|
|
||||||
|
|
||||||
import (
|
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"testing"
|
|
||||||
)
|
|
||||||
|
|
||||||
// TestGetClient_NotInitialized 测试未初始化时获取存储客户端
|
|
||||||
func TestGetClient_NotInitialized(t *testing.T) {
|
|
||||||
_, err := GetClient()
|
|
||||||
if err == nil {
|
|
||||||
t.Error("未初始化时应该返回错误")
|
|
||||||
}
|
|
||||||
|
|
||||||
expectedError := "存储客户端未初始化,请先调用 storage.Init()"
|
|
||||||
if err.Error() != expectedError {
|
|
||||||
t.Errorf("错误消息 = %q, want %q", err.Error(), expectedError)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestMustGetClient_Panic 测试MustGetClient在未初始化时panic
|
|
||||||
func TestMustGetClient_Panic(t *testing.T) {
|
|
||||||
defer func() {
|
|
||||||
if r := recover(); r == nil {
|
|
||||||
t.Error("MustGetClient 应该在未初始化时panic")
|
|
||||||
}
|
|
||||||
}()
|
|
||||||
|
|
||||||
_ = MustGetClient()
|
|
||||||
}
|
|
||||||
|
|
||||||
// TestInit_Storage 测试存储客户端初始化逻辑
|
|
||||||
func TestInit_Storage(t *testing.T) {
|
|
||||||
cfg := config.RustFSConfig{
|
|
||||||
Endpoint: "http://localhost:9000",
|
|
||||||
AccessKey: "minioadmin",
|
|
||||||
SecretKey: "minioadmin",
|
|
||||||
UseSSL: false,
|
|
||||||
Buckets: map[string]string{
|
|
||||||
"avatars": "avatars",
|
|
||||||
"textures": "textures",
|
|
||||||
},
|
|
||||||
}
|
|
||||||
|
|
||||||
// 验证Init函数存在且可调用
|
|
||||||
// 注意:实际连接可能失败,这是可以接受的
|
|
||||||
err := Init(cfg)
|
|
||||||
if err != nil {
|
|
||||||
t.Logf("Init() 返回错误(可能正常,如果存储服务未运行): %v", err)
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
Reference in New Issue
Block a user