解决合并后出现的问题,为swagger提供禁用选项,暂时移除wiki

This commit is contained in:
2025-12-26 01:15:17 +08:00
parent 44f007936e
commit 85a9463913
90 changed files with 602 additions and 20104 deletions

View File

@@ -22,6 +22,7 @@ type Container struct {
Redis *redis.Client
Logger *zap.Logger
JWT *auth.JWTService
Casbin *auth.CasbinService
Storage *storage.StorageClient
CacheManager *database.CacheManager
@@ -30,7 +31,6 @@ type Container struct {
ProfileRepo repository.ProfileRepository
TextureRepo repository.TextureRepository
ClientRepo repository.ClientRepository
ConfigRepo repository.SystemConfigRepository
YggdrasilRepo repository.YggdrasilRepository
// Service层
@@ -40,7 +40,6 @@ type Container struct {
TokenService service.TokenService
YggdrasilService service.YggdrasilService
VerificationService service.VerificationService
UploadService service.UploadService
SecurityService service.SecurityService
CaptchaService service.CaptchaService
SignatureService *service.SignatureService
@@ -52,6 +51,7 @@ func NewContainer(
redisClient *redis.Client,
logger *zap.Logger,
jwtService *auth.JWTService,
casbinService *auth.CasbinService,
storageClient *storage.StorageClient,
emailService interface{}, // 接受 email.Service 但使用 interface{} 避免循环依赖
) *Container {
@@ -75,6 +75,7 @@ func NewContainer(
Redis: redisClient,
Logger: logger,
JWT: jwtService,
Casbin: casbinService,
Storage: storageClient,
CacheManager: cacheManager,
}
@@ -84,7 +85,6 @@ func NewContainer(
c.ProfileRepo = repository.NewProfileRepository(db)
c.TextureRepo = repository.NewTextureRepository(db)
c.ClientRepo = repository.NewClientRepository(db)
c.ConfigRepo = repository.NewSystemConfigRepository(db)
c.YggdrasilRepo = repository.NewYggdrasilRepository(db)
// 初始化SignatureService作为依赖注入避免在容器中创建并立即调用
@@ -92,7 +92,7 @@ func NewContainer(
c.SignatureService = service.NewSignatureService(c.ProfileRepo, redisClient, logger)
// 初始化Service注入缓存管理器
c.UserService = service.NewUserService(c.UserRepo, c.ConfigRepo, jwtService, redisClient, cacheManager, logger)
c.UserService = service.NewUserService(c.UserRepo, jwtService, redisClient, cacheManager, storageClient, logger)
c.ProfileService = service.NewProfileService(c.ProfileRepo, c.UserRepo, cacheManager, logger)
c.TextureService = service.NewTextureService(c.TextureRepo, c.UserRepo, storageClient, cacheManager, logger)
@@ -125,7 +125,6 @@ func NewContainer(
// 初始化其他服务
c.SecurityService = service.NewSecurityService(redisClient)
c.UploadService = service.NewUploadService(storageClient)
c.CaptchaService = service.NewCaptchaService(redisClient, logger)
// 初始化VerificationService需要email.Service
@@ -206,13 +205,6 @@ func WithTextureRepo(repo repository.TextureRepository) Option {
}
}
// WithConfigRepo 设置系统配置仓储
func WithConfigRepo(repo repository.SystemConfigRepository) Option {
return func(c *Container) {
c.ConfigRepo = repo
}
}
// WithUserService 设置用户服务
func WithUserService(svc service.UserService) Option {
return func(c *Container) {
@@ -262,13 +254,6 @@ func WithVerificationService(svc service.VerificationService) Option {
}
}
// WithUploadService 设置上传服务
func WithUploadService(svc service.UploadService) Option {
return func(c *Container) {
c.UploadService = svc
}
}
// WithSecurityService 设置安全服务
func WithSecurityService(svc service.SecurityService) Option {
return func(c *Container) {

View File

@@ -34,11 +34,11 @@ type SetUserRoleRequest struct {
// @Accept json
// @Produce json
// @Param request body SetUserRoleRequest true "设置角色请求"
// @Success 200 {object} model.Response
// @Failure 400 {object} model.Response
// @Failure 403 {object} model.Response
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Security BearerAuth
// @Router /admin/users/role [put]
// @Router /api/v1/admin/users/role [put]
func (h *AdminHandler) SetUserRole(c *gin.Context) {
var req SetUserRoleRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -99,10 +99,10 @@ func (h *AdminHandler) SetUserRole(c *gin.Context) {
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response
// @Failure 403 {object} model.Response
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Security BearerAuth
// @Router /admin/users [get]
// @Router /api/v1/admin/users [get]
func (h *AdminHandler) GetUserList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
@@ -152,10 +152,10 @@ func (h *AdminHandler) GetUserList(c *gin.Context) {
// @Tags Admin
// @Produce json
// @Param id path int true "用户ID"
// @Success 200 {object} model.Response
// @Failure 404 {object} model.Response
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Security BearerAuth
// @Router /admin/users/{id} [get]
// @Router /api/v1/admin/users/{id} [get]
func (h *AdminHandler) GetUserDetail(c *gin.Context) {
userID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
@@ -201,10 +201,10 @@ type SetUserStatusRequest struct {
// @Accept json
// @Produce json
// @Param request body SetUserStatusRequest true "设置状态请求"
// @Success 200 {object} model.Response
// @Failure 400 {object} model.Response
// @Success 200 {object} model.Response{data=map[string]interface{}} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Security BearerAuth
// @Router /admin/users/status [put]
// @Router /api/v1/admin/users/status [put]
func (h *AdminHandler) SetUserStatus(c *gin.Context) {
var req SetUserStatusRequest
if err := c.ShouldBindJSON(&req); err != nil {
@@ -266,10 +266,10 @@ func (h *AdminHandler) SetUserStatus(c *gin.Context) {
// @Tags Admin
// @Produce json
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response
// @Failure 404 {object} model.Response
// @Success 200 {object} model.Response{data=map[string]interface{}} "删除成功"
// @Failure 404 {object} model.ErrorResponse "材质不存在"
// @Security BearerAuth
// @Router /admin/textures/{id} [delete]
// @Router /api/v1/admin/textures/{id} [delete]
func (h *AdminHandler) DeleteTexture(c *gin.Context) {
textureID, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
@@ -316,9 +316,9 @@ func (h *AdminHandler) DeleteTexture(c *gin.Context) {
// @Produce json
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Security BearerAuth
// @Router /admin/textures [get]
// @Router /api/v1/admin/textures [get]
func (h *AdminHandler) GetTextureList(c *gin.Context) {
page, _ := strconv.Atoi(c.DefaultQuery("page", "1"))
pageSize, _ := strconv.Atoi(c.DefaultQuery("page_size", "20"))
@@ -364,3 +364,19 @@ func (h *AdminHandler) GetTextureList(c *gin.Context) {
"page_size": pageSize,
}))
}
// GetPermissions 获取权限列表
// @Summary 获取权限列表
// @Description 管理员获取所有Casbin权限规则
// @Tags Admin
// @Produce json
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Security BearerAuth
// @Router /api/v1/admin/permissions [get]
func (h *AdminHandler) GetPermissions(c *gin.Context) {
// 获取所有权限规则
policies, _ := h.container.Casbin.GetEnforcer().GetPolicy()
c.JSON(http.StatusOK, model.NewSuccessResponse(gin.H{
"policies": policies,
}))
}

View File

@@ -31,7 +31,7 @@ func NewAuthHandler(c *container.Container) *AuthHandler {
// @Accept json
// @Produce json
// @Param request body types.RegisterRequest true "注册信息"
// @Success 200 {object} model.Response "注册成功"
// @Success 200 {object} model.Response{data=types.LoginResponse} "注册成功"
// @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/auth/register [post]
func (h *AuthHandler) Register(c *gin.Context) {
@@ -107,7 +107,7 @@ func (h *AuthHandler) Login(c *gin.Context) {
// @Accept json
// @Produce json
// @Param request body types.SendVerificationCodeRequest true "发送验证码请求"
// @Success 200 {object} model.Response "发送成功"
// @Success 200 {object} model.Response{data=map[string]string} "发送成功"
// @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/auth/send-code [post]
func (h *AuthHandler) SendVerificationCode(c *gin.Context) {
@@ -137,7 +137,7 @@ func (h *AuthHandler) SendVerificationCode(c *gin.Context) {
// @Accept json
// @Produce json
// @Param request body types.ResetPasswordRequest true "重置密码请求"
// @Success 200 {object} model.Response "重置成功"
// @Success 200 {object} model.Response{data=map[string]string} "重置成功"
// @Failure 400 {object} model.ErrorResponse "请求参数错误"
// @Router /api/v1/auth/reset-password [post]
func (h *AuthHandler) ResetPassword(c *gin.Context) {

View File

@@ -34,7 +34,7 @@ type CaptchaVerifyRequest struct {
// @Tags captcha
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{} "生成成功"
// @Success 200 {object} map[string]interface{} "生成成功 {code: 200, data: {masterImage, tileImage, captchaId, y}}"
// @Failure 500 {object} map[string]interface{} "生成失败"
// @Router /api/v1/captcha/generate [get]
func (h *CaptchaHandler) Generate(c *gin.Context) {
@@ -66,7 +66,7 @@ func (h *CaptchaHandler) Generate(c *gin.Context) {
// @Accept json
// @Produce json
// @Param request body CaptchaVerifyRequest true "验证请求"
// @Success 200 {object} map[string]interface{} "验证结果"
// @Success 200 {object} map[string]interface{} "验证结果 {code: 200/400, msg: string}"
// @Failure 400 {object} map[string]interface{} "参数错误"
// @Router /api/v1/captcha/verify [post]
func (h *CaptchaHandler) Verify(c *gin.Context) {

View File

@@ -35,7 +35,16 @@ type CustomSkinAPIResponse struct {
}
// GetPlayerInfo 获取玩家信息
// GET {ROOT}/{USERNAME}.json
// @Summary 获取玩家信息
// @Description CustomSkinAPI: 获取玩家皮肤配置信息
// @Tags CustomSkinAPI
// @Accept json
// @Produce json
// @Param username path string true "玩家用户名"
// @Success 200 {object} CustomSkinAPIResponse
// @Failure 400 {object} map[string]string "用户名不能为空"
// @Failure 404 {object} map[string]string "玩家未找到"
// @Router /api/v1/csl/{username} [get]
func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
username := c.Param("username")
if username == "" {
@@ -136,7 +145,14 @@ func (h *CustomSkinHandler) GetPlayerInfo(c *gin.Context) {
}
// GetTexture 获取资源文件
// GET {ROOT}/textures/{hash}
// @Summary 获取资源文件
// @Description CustomSkinAPI: 获取材质图片文件
// @Tags CustomSkinAPI
// @Param hash path string true "材质Hash"
// @Success 200 {file} binary
// @Failure 400 {object} map[string]string "资源标识符不能为空"
// @Failure 404 {object} map[string]string "资源未找到或不可用"
// @Router /api/v1/csl/textures/{hash} [get]
func (h *CustomSkinHandler) GetTexture(c *gin.Context) {
hash := c.Param("hash")
if hash == "" {

View File

@@ -72,7 +72,8 @@ func (h *ProfileHandler) Create(c *gin.Context) {
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} model.Response "获取成功"
// @Success 200 {object} model.Response{data=[]types.ProfileInfo} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/profile [get]
func (h *ProfileHandler) List(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
@@ -100,7 +101,7 @@ func (h *ProfileHandler) List(c *gin.Context) {
// @Accept json
// @Produce json
// @Param uuid path string true "档案UUID"
// @Success 200 {object} model.Response "获取成功"
// @Success 200 {object} model.Response{data=types.ProfileInfo} "获取成功"
// @Failure 404 {object} model.ErrorResponse "档案不存在"
// @Router /api/v1/profile/{uuid} [get]
func (h *ProfileHandler) Get(c *gin.Context) {
@@ -132,7 +133,7 @@ func (h *ProfileHandler) Get(c *gin.Context) {
// @Security BearerAuth
// @Param uuid path string true "档案UUID"
// @Param request body types.UpdateProfileRequest true "更新信息"
// @Success 200 {object} model.Response "更新成功"
// @Success 200 {object} model.Response{data=types.ProfileInfo} "更新成功"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/profile/{uuid} [put]
func (h *ProfileHandler) Update(c *gin.Context) {
@@ -180,7 +181,7 @@ func (h *ProfileHandler) Update(c *gin.Context) {
// @Produce json
// @Security BearerAuth
// @Param uuid path string true "档案UUID"
// @Success 200 {object} model.Response "删除成功"
// @Success 200 {object} model.Response{data=map[string]string} "删除成功"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/profile/{uuid} [delete]
func (h *ProfileHandler) Delete(c *gin.Context) {

View File

@@ -3,7 +3,6 @@ package handler
import (
"carrotskin/internal/container"
"carrotskin/internal/middleware"
"carrotskin/internal/model"
"carrotskin/pkg/auth"
"carrotskin/pkg/config"
@@ -44,7 +43,10 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
router.GET("/health", HealthCheck)
// Swagger文档路由
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
cfg, _ := config.GetConfig()
if cfg != nil && cfg.Server.SwaggerEnabled {
router.GET("/swagger/*any", ginSwagger.WrapHandler(swaggerFiles.Handler))
}
// 创建Handler实例
h := NewHandlers(c)
@@ -70,9 +72,6 @@ func RegisterRoutesWithDI(router *gin.Engine, c *container.Container) {
// Yggdrasil API路由组
registerYggdrasilRoutesWithDI(v1, h.Yggdrasil)
// 系统路由
registerSystemRoutes(v1, c)
// CustomSkinAPI 路由
registerCustomSkinRoutes(v1, h.CustomSkin)
@@ -193,24 +192,6 @@ func registerYggdrasilRoutesWithDI(v1 *gin.RouterGroup, h *YggdrasilHandler) {
}
}
// registerSystemRoutes 注册系统路由
func registerSystemRoutes(v1 *gin.RouterGroup, c *container.Container) {
system := v1.Group("/system")
{
// 公开配置(无需认证)
system.GET("/config", func(ctx *gin.Context) {
cfg, _ := config.GetConfig()
ctx.JSON(200, model.NewSuccessResponse(gin.H{
"site_name": cfg.Site.Name,
"site_description": cfg.Site.Description,
"registration_enabled": cfg.Site.RegistrationEnabled,
"max_textures_per_user": cfg.Site.MaxTexturesPerUser,
"max_profiles_per_user": cfg.Site.MaxProfilesPerUser,
}))
})
}
}
// registerAdminRoutes 注册管理员路由
func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHandler) {
admin := v1.Group("/admin")
@@ -229,13 +210,7 @@ func registerAdminRoutes(v1 *gin.RouterGroup, c *container.Container, h *AdminHa
admin.DELETE("/textures/:id", h.DeleteTexture)
// 权限管理
admin.GET("/permissions", func(ctx *gin.Context) {
// 获取所有权限规则
policies, _ := c.Casbin.GetEnforcer().GetPolicy()
ctx.JSON(200, model.NewSuccessResponse(gin.H{
"policies": policies,
}))
})
admin.GET("/permissions", h.GetPermissions)
}
}

View File

@@ -25,6 +25,16 @@ func NewTextureHandler(c *container.Container) *TextureHandler {
}
// Get 获取材质详情
// @Summary 获取材质详情
// @Description 获取指定ID的材质详细信息
// @Tags texture
// @Accept json
// @Produce json
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response{data=types.TextureInfo} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "材质不存在"
// @Router /api/v1/texture/{id} [get]
func (h *TextureHandler) Get(c *gin.Context) {
id, err := strconv.ParseInt(c.Param("id"), 10, 64)
if err != nil {
@@ -42,6 +52,19 @@ func (h *TextureHandler) Get(c *gin.Context) {
}
// Search 搜索材质
// @Summary 搜索材质
// @Description 搜索材质列表,支持关键词、类型、公开性筛选和分页
// @Tags texture
// @Accept json
// @Produce json
// @Param keyword query string false "关键词"
// @Param type query string false "材质类型 (SKIN/CAPE)"
// @Param public_only query boolean false "仅显示公开材质"
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/texture [get]
func (h *TextureHandler) Search(c *gin.Context) {
keyword := c.Query("keyword")
textureTypeStr := c.Query("type")
@@ -85,6 +108,18 @@ func (h *TextureHandler) Search(c *gin.Context) {
}
// Update 更新材质
// @Summary 更新材质
// @Description 更新材质信息(名称、描述、公开性)
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "材质ID"
// @Param request body types.UpdateTextureRequest true "更新信息"
// @Success 200 {object} model.Response{data=types.TextureInfo} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/texture/{id} [put]
func (h *TextureHandler) Update(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -118,6 +153,17 @@ func (h *TextureHandler) Update(c *gin.Context) {
}
// Delete 删除材质
// @Summary 删除材质
// @Description 删除指定ID的材质
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response "删除成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 403 {object} model.ErrorResponse "无权操作"
// @Router /api/v1/texture/{id} [delete]
func (h *TextureHandler) Delete(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -144,6 +190,16 @@ func (h *TextureHandler) Delete(c *gin.Context) {
}
// ToggleFavorite 切换收藏状态
// @Summary 切换收藏状态
// @Description 收藏或取消收藏指定材质
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param id path int true "材质ID"
// @Success 200 {object} model.Response{data=map[string]bool} "操作成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Router /api/v1/texture/{id} [post]
func (h *TextureHandler) ToggleFavorite(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -171,6 +227,17 @@ func (h *TextureHandler) ToggleFavorite(c *gin.Context) {
}
// GetUserTextures 获取用户上传的材质列表
// @Summary 获取我的材质
// @Description 获取当前登录用户上传的材质列表
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/texture/my [get]
func (h *TextureHandler) GetUserTextures(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -196,6 +263,17 @@ func (h *TextureHandler) GetUserTextures(c *gin.Context) {
}
// GetUserFavorites 获取用户收藏的材质列表
// @Summary 获取我的收藏
// @Description 获取当前登录用户收藏的材质列表
// @Tags texture
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param page query int false "页码" default(1)
// @Param page_size query int false "每页数量" default(20)
// @Success 200 {object} model.Response{data=map[string]interface{}} "获取成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/texture/favorites [get]
func (h *TextureHandler) GetUserFavorites(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -221,6 +299,21 @@ func (h *TextureHandler) GetUserFavorites(c *gin.Context) {
}
// Upload 直接上传材质文件
// @Summary 上传材质
// @Description 上传图片文件创建新材质
// @Tags texture
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param file formData file true "材质文件 (PNG)"
// @Param name formData string true "材质名称"
// @Param description formData string false "材质描述"
// @Param type formData string false "材质类型 (SKIN/CAPE)" default(SKIN)
// @Param is_public formData boolean false "是否公开" default(false)
// @Param is_slim formData boolean false "是否为纤细模型 (仅SKIN有效)" default(false)
// @Success 200 {object} model.Response{data=types.TextureInfo} "上传成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Router /api/v1/texture/upload [post]
func (h *TextureHandler) Upload(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {

View File

@@ -24,6 +24,15 @@ func NewUserHandler(c *container.Container) *UserHandler {
}
// GetProfile 获取用户信息
// @Summary 获取用户信息
// @Description 获取当前登录用户的详细信息
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} model.Response{data=types.UserInfo} "获取成功"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/profile [get]
func (h *UserHandler) GetProfile(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -44,6 +53,17 @@ func (h *UserHandler) GetProfile(c *gin.Context) {
}
// UpdateProfile 更新用户信息
// @Summary 更新用户信息
// @Description 更新用户资料密码、头像URL如需上传头像请使用上传接口
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body types.UpdateUserRequest true "更新信息"
// @Success 200 {object} model.Response{data=types.UserInfo} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/profile [put]
func (h *UserHandler) UpdateProfile(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -103,6 +123,17 @@ func (h *UserHandler) UpdateProfile(c *gin.Context) {
}
// UploadAvatar 直接上传头像文件
// @Summary 上传头像
// @Description 上传图片文件作为用户头像
// @Tags user
// @Accept multipart/form-data
// @Produce json
// @Security BearerAuth
// @Param file formData file true "头像文件"
// @Success 200 {object} model.Response{data=map[string]interface{}} "上传成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/avatar/upload [post]
func (h *UserHandler) UploadAvatar(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -162,6 +193,17 @@ func (h *UserHandler) UploadAvatar(c *gin.Context) {
}
// UpdateAvatar 更新头像URL保留用于外部URL
// @Summary 更新头像URL
// @Description 更新用户头像为外部URL
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param avatar_url query string true "头像URL"
// @Success 200 {object} model.Response{data=types.UserInfo} "更新成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/avatar [put]
func (h *UserHandler) UpdateAvatar(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -199,6 +241,17 @@ func (h *UserHandler) UpdateAvatar(c *gin.Context) {
}
// ChangeEmail 更换邮箱
// @Summary 更换邮箱
// @Description 更换用户绑定的邮箱,需要验证码
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body types.ChangeEmailRequest true "更换邮箱请求"
// @Success 200 {object} model.Response{data=types.UserInfo} "更换成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/user/change-email [post]
func (h *UserHandler) ChangeEmail(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {
@@ -237,6 +290,15 @@ func (h *UserHandler) ChangeEmail(c *gin.Context) {
}
// ResetYggdrasilPassword 重置Yggdrasil密码
// @Summary 重置Yggdrasil密码
// @Description 重置用户的Yggdrasil API认证密码
// @Tags user
// @Accept json
// @Produce json
// @Security BearerAuth
// @Success 200 {object} model.Response{data=map[string]string} "重置成功"
// @Failure 500 {object} model.ErrorResponse "服务器错误"
// @Router /api/v1/user/yggdrasil-password/reset [post]
func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) {
userID, ok := GetUserIDFromContext(c)
if !ok {

View File

@@ -167,6 +167,15 @@ func NewYggdrasilHandler(c *container.Container) *YggdrasilHandler {
}
// Authenticate 用户认证
// @Summary Yggdrasil认证
// @Description Yggdrasil协议: 用户登录认证
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body AuthenticateRequest true "认证请求"
// @Success 200 {object} AuthenticateResponse
// @Failure 403 {object} map[string]string "认证失败"
// @Router /api/v1/yggdrasil/authserver/authenticate [post]
func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
rawData, err := io.ReadAll(c.Request.Body)
if err != nil {
@@ -248,6 +257,15 @@ func (h *YggdrasilHandler) Authenticate(c *gin.Context) {
}
// ValidToken 验证令牌
// @Summary Yggdrasil验证令牌
// @Description Yggdrasil协议: 验证AccessToken是否有效
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body ValidTokenRequest true "验证请求"
// @Success 204 "令牌有效"
// @Failure 403 {object} map[string]bool "令牌无效"
// @Router /api/v1/yggdrasil/authserver/validate [post]
func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -266,6 +284,15 @@ func (h *YggdrasilHandler) ValidToken(c *gin.Context) {
}
// RefreshToken 刷新令牌
// @Summary Yggdrasil刷新令牌
// @Description Yggdrasil协议: 刷新AccessToken
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body RefreshRequest true "刷新请求"
// @Success 200 {object} RefreshResponse
// @Failure 400 {object} map[string]string "刷新失败"
// @Router /api/v1/yggdrasil/authserver/refresh [post]
func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
var request RefreshRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -350,6 +377,14 @@ func (h *YggdrasilHandler) RefreshToken(c *gin.Context) {
}
// InvalidToken 使令牌失效
// @Summary Yggdrasil注销令牌
// @Description Yggdrasil协议: 使AccessToken失效
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body ValidTokenRequest true "失效请求"
// @Success 204 "操作成功"
// @Router /api/v1/yggdrasil/authserver/invalidate [post]
func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
var request ValidTokenRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -364,6 +399,15 @@ func (h *YggdrasilHandler) InvalidToken(c *gin.Context) {
}
// SignOut 用户登出
// @Summary Yggdrasil登出
// @Description Yggdrasil协议: 用户登出,使所有令牌失效
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body SignOutRequest true "登出请求"
// @Success 204 "操作成功"
// @Failure 400 {object} map[string]string "参数错误"
// @Router /api/v1/yggdrasil/authserver/signout [post]
func (h *YggdrasilHandler) SignOut(c *gin.Context) {
var request SignOutRequest
if err := c.ShouldBindJSON(&request); err != nil {
@@ -397,6 +441,15 @@ func (h *YggdrasilHandler) SignOut(c *gin.Context) {
}
// GetProfileByUUID 根据UUID获取档案
// @Summary Yggdrasil获取档案
// @Description Yggdrasil协议: 根据UUID获取用户档案信息
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param uuid path string true "用户UUID"
// @Success 200 {object} map[string]interface{} "档案信息"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/profile/{uuid} [get]
func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
uuid := utils.FormatUUID(c.Param("uuid"))
h.logger.Info("获取配置文件请求", zap.String("uuid", uuid))
@@ -413,6 +466,16 @@ func (h *YggdrasilHandler) GetProfileByUUID(c *gin.Context) {
}
// JoinServer 加入服务器
// @Summary Yggdrasil加入服务器
// @Description Yggdrasil协议: 客户端加入服务器
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body JoinServerRequest true "加入请求"
// @Success 204 "加入成功"
// @Failure 400 {object} APIResponse "参数错误"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/join [post]
func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
var request JoinServerRequest
clientIP := c.ClientIP()
@@ -449,6 +512,17 @@ func (h *YggdrasilHandler) JoinServer(c *gin.Context) {
}
// HasJoinedServer 验证玩家是否已加入服务器
// @Summary Yggdrasil验证加入
// @Description Yggdrasil协议: 服务端验证客户端是否已加入
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param username query string true "用户名"
// @Param serverId query string true "服务器ID"
// @Param ip query string false "客户端IP"
// @Success 200 {object} map[string]interface{} "验证成功,返回档案"
// @Failure 204 "验证失败"
// @Router /api/v1/yggdrasil/sessionserver/session/minecraft/hasJoined [get]
func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
clientIP, _ := c.GetQuery("ip")
@@ -499,6 +573,15 @@ func (h *YggdrasilHandler) HasJoinedServer(c *gin.Context) {
}
// GetProfilesByName 批量获取配置文件
// @Summary Yggdrasil批量获取档案
// @Description Yggdrasil协议: 根据名称批量获取用户档案
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param request body []string true "用户名列表"
// @Success 200 {array} model.Profile "档案列表"
// @Failure 400 {object} APIResponse "参数错误"
// @Router /api/v1/yggdrasil/api/profiles/minecraft [post]
func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
var names []string
@@ -520,6 +603,14 @@ func (h *YggdrasilHandler) GetProfilesByName(c *gin.Context) {
}
// GetMetaData 获取Yggdrasil元数据
// @Summary Yggdrasil元数据
// @Description Yggdrasil协议: 获取服务器元数据
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Success 200 {object} map[string]interface{} "元数据"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil [get]
func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
meta := gin.H{
"implementationName": "CellAuth",
@@ -550,6 +641,16 @@ func (h *YggdrasilHandler) GetMetaData(c *gin.Context) {
}
// GetPlayerCertificates 获取玩家证书
// @Summary Yggdrasil获取证书
// @Description Yggdrasil协议: 获取玩家证书
// @Tags Yggdrasil
// @Accept json
// @Produce json
// @Param Authorization header string true "Bearer {token}"
// @Success 200 {object} map[string]interface{} "证书信息"
// @Failure 401 {object} map[string]string "未授权"
// @Failure 500 {object} APIResponse "服务器错误"
// @Router /api/v1/yggdrasil/minecraftservices/player/certificates [post]
func (h *YggdrasilHandler) GetPlayerCertificates(c *gin.Context) {
authHeader := c.GetHeader("Authorization")
if authHeader == "" {

View File

@@ -5,6 +5,7 @@ import (
)
// AuditLog 审计日志模型
// @Description 系统操作审计日志记录
type AuditLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID *int64 `gorm:"column:user_id;type:bigint;index:idx_audit_logs_user_created,priority:1" json:"user_id,omitempty"`
@@ -27,6 +28,7 @@ func (AuditLog) TableName() string {
}
// CasbinRule Casbin 权限规则模型
// @Description Casbin权限控制规则数据
type CasbinRule struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
PType string `gorm:"column:ptype;type:varchar(100);not null;index:idx_casbin_ptype;uniqueIndex:uk_casbin_rule,priority:1" json:"ptype"`

View File

@@ -7,6 +7,7 @@ import (
)
// BaseModel 基础模型
// @Description 通用基础模型包含ID和时间戳字段
// 包含 uint 类型的 ID 和标准时间字段,但时间字段不通过 JSON 返回给前端
type BaseModel struct {
// ID 主键
@@ -21,11 +22,3 @@ type BaseModel struct {
// DeletedAt 删除时间 (软删除,不返回给前端)
DeletedAt gorm.DeletedAt `gorm:"index;column:deleted_at" json:"-"`
}

View File

@@ -3,12 +3,13 @@ package model
import "time"
// Client 客户端实体用于管理Token版本
// @Description Yggdrasil客户端Token管理数据
type Client struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` // Client UUID
ClientToken string `gorm:"column:client_token;type:varchar(64);not null;uniqueIndex" json:"client_token"` // 客户端Token
UserID int64 `gorm:"column:user_id;not null;index:idx_clients_user_id" json:"user_id"` // 用户ID
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"` // Client UUID
ClientToken string `gorm:"column:client_token;type:varchar(64);not null;uniqueIndex" json:"client_token"` // 客户端Token
UserID int64 `gorm:"column:user_id;not null;index:idx_clients_user_id" json:"user_id"` // 用户ID
ProfileID string `gorm:"column:profile_id;type:varchar(36);index:idx_clients_profile_id" json:"profile_id,omitempty"` // 选中的Profile
Version int `gorm:"column:version;not null;default:0;index:idx_clients_version" json:"version"` // 版本号
Version int `gorm:"column:version;not null;default:0;index:idx_clients_version" json:"version"` // 版本号
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
@@ -21,18 +22,3 @@ type Client struct {
func (Client) TableName() string {
return "clients"
}

View File

@@ -5,6 +5,7 @@ import (
)
// Profile Minecraft 档案模型
// @Description Minecraft角色档案数据模型
type Profile struct {
UUID string `gorm:"column:uuid;type:varchar(36);primaryKey" json:"uuid"`
UserID int64 `gorm:"column:user_id;not null;index:idx_profiles_user_created,priority:1" json:"user_id"`
@@ -28,6 +29,7 @@ func (Profile) TableName() string {
}
// ProfileResponse 档案响应(包含完整的皮肤/披风信息)
// @Description Minecraft档案完整响应数据
type ProfileResponse struct {
UUID string `json:"uuid"`
Name string `json:"name"`
@@ -37,22 +39,27 @@ type ProfileResponse struct {
}
// ProfileTexturesData Minecraft 材质数据结构
// @Description Minecraft档案材质数据
type ProfileTexturesData struct {
Skin *ProfileTexture `json:"SKIN,omitempty"`
Cape *ProfileTexture `json:"CAPE,omitempty"`
}
// ProfileTexture 单个材质信息
// @Description 单个材质的详细信息
type ProfileTexture struct {
URL string `json:"url"`
Metadata *ProfileTextureMetadata `json:"metadata,omitempty"`
}
// ProfileTextureMetadata 材质元数据
// @Description 材质的元数据信息
type ProfileTextureMetadata struct {
Model string `json:"model,omitempty"` // "slim" or "classic"
}
// KeyPair RSA密钥对
// @Description 用于Yggdrasil认证的RSA密钥对
type KeyPair struct {
PrivateKey string `json:"private_key" bson:"private_key"`
PublicKey string `json:"public_key" bson:"public_key"`

View File

@@ -3,6 +3,7 @@ package model
import "os"
// Response 通用API响应结构
// @Description 标准API响应格式
type Response struct {
Code int `json:"code"` // 业务状态码
Message string `json:"message"` // 响应消息
@@ -10,6 +11,7 @@ type Response struct {
}
// PaginationResponse 分页响应结构
// @Description 分页数据响应格式
type PaginationResponse struct {
Code int `json:"code"`
Message string `json:"message"`
@@ -20,6 +22,7 @@ type PaginationResponse struct {
}
// ErrorResponse 错误响应
// @Description API错误响应格式
type ErrorResponse struct {
Code int `json:"code"`
Message string `json:"message"`

View File

@@ -5,6 +5,7 @@ import (
)
// TextureType 材质类型
// @Description 材质类型枚举SKIN(皮肤)或CAPE(披风)
type TextureType string
const (
@@ -13,6 +14,7 @@ const (
)
// Texture 材质模型
// @Description Minecraft材质数据模型
type Texture struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UploaderID int64 `gorm:"column:uploader_id;not null;index:idx_textures_uploader_status,priority:1;index:idx_textures_uploader_created,priority:1" json:"uploader_id"`
@@ -40,6 +42,7 @@ func (Texture) TableName() string {
}
// UserTextureFavorite 用户材质收藏
// @Description 用户收藏材质关联表
type UserTextureFavorite struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;uniqueIndex:uk_user_texture,priority:1;index:idx_favorites_user_created,priority:1" json:"user_id"`
@@ -57,6 +60,7 @@ func (UserTextureFavorite) TableName() string {
}
// TextureDownloadLog 材质下载记录
// @Description 材质下载日志记录
type TextureDownloadLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
TextureID int64 `gorm:"column:texture_id;not null;index:idx_download_logs_texture_created,priority:1" json:"texture_id"`

View File

@@ -7,6 +7,7 @@ import (
)
// User 用户模型
// @Description 用户账户数据模型
type User struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
Username string `gorm:"column:username;type:varchar(255);not null;uniqueIndex:idx_user_username_status,priority:1" json:"username"`
@@ -16,7 +17,7 @@ type User struct {
Points int `gorm:"column:points;type:integer;not null;default:0;index:idx_user_points,sort:desc" json:"points"`
Role string `gorm:"column:role;type:varchar(50);not null;default:'user';index:idx_user_role_status,priority:1" json:"role"`
Status int16 `gorm:"column:status;type:smallint;not null;default:1;index:idx_user_username_status,priority:2;index:idx_user_email_status,priority:2;index:idx_user_role_status,priority:2" json:"status"` // 1:正常, 0:禁用, -1:删除
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty"` // JSON数据存储为PostgreSQL的JSONB类型
Properties *datatypes.JSON `gorm:"column:properties;type:jsonb" json:"properties,omitempty" swaggertype:"string"` // JSON数据存储为PostgreSQL的JSONB类型
LastLoginAt *time.Time `gorm:"column:last_login_at;type:timestamp;index:idx_user_last_login,sort:desc" json:"last_login_at,omitempty"`
CreatedAt time.Time `gorm:"column:created_at;type:timestamp;not null;default:CURRENT_TIMESTAMP;index:idx_user_created_at,sort:desc" json:"created_at"`
UpdatedAt time.Time `gorm:"column:updated_at;type:timestamp;not null;default:CURRENT_TIMESTAMP" json:"updated_at"`
@@ -28,6 +29,7 @@ func (User) TableName() string {
}
// UserPointLog 用户积分变更记录
// @Description 用户积分变动日志记录
type UserPointLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index:idx_point_logs_user_created,priority:1" json:"user_id"`
@@ -52,6 +54,7 @@ func (UserPointLog) TableName() string {
}
// UserLoginLog 用户登录日志
// @Description 用户登录历史记录
type UserLoginLog struct {
ID int64 `gorm:"column:id;primaryKey;autoIncrement" json:"id"`
UserID int64 `gorm:"column:user_id;not null;index:idx_login_logs_user_created,priority:1" json:"user_id"`

View File

@@ -13,6 +13,7 @@ import (
const passwordChars = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789"
// Yggdrasil ygg密码与用户id绑定
// @Description Yggdrasil认证密码数据模型
type Yggdrasil struct {
ID int64 `gorm:"column:id;primaryKey;not null" json:"id"`
Password string `gorm:"column:password;type:varchar(255);not null" json:"-"` // 加密后的密码,不返回给前端

View File

@@ -35,7 +35,6 @@ type ProfileRepository interface {
Delete(ctx context.Context, uuid string) error
BatchDelete(ctx context.Context, uuids []string) (int64, error) // 批量删除
CountByUserID(ctx context.Context, userID int64) (int64, error)
SetActive(ctx context.Context, uuid string, userID int64) error
UpdateLastUsedAt(ctx context.Context, uuid string) error
GetByNames(ctx context.Context, names []string) ([]*model.Profile, error)
GetKeyPair(ctx context.Context, profileId string) (*model.KeyPair, error)
@@ -67,14 +66,6 @@ type TextureRepository interface {
CountByUploaderID(ctx context.Context, uploaderID int64) (int64, error)
}
// SystemConfigRepository 系统配置仓储接口
type SystemConfigRepository interface {
GetByKey(ctx context.Context, key string) (*model.SystemConfig, error)
GetPublic(ctx context.Context) ([]model.SystemConfig, error)
GetAll(ctx context.Context) ([]model.SystemConfig, error)
Update(ctx context.Context, config *model.SystemConfig) error
UpdateValue(ctx context.Context, key, value string) error
}
// YggdrasilRepository Yggdrasil仓储接口
type YggdrasilRepository interface {

View File

@@ -83,7 +83,7 @@ func TestProfileRepository_Basic(t *testing.T) {
u := &model.User{Username: "u2", Email: "u2@test.com", Password: "pwd", Status: 1}
_ = userRepo.Create(ctx, u)
p := &model.Profile{UUID: "p-uuid", UserID: u.ID, Name: "hero", IsActive: false}
p := &model.Profile{UUID: "p-uuid", UserID: u.ID, Name: "hero"}
if err := profileRepo.Create(ctx, p); err != nil {
t.Fatalf("create profile err: %v", err)
}
@@ -98,9 +98,7 @@ func TestProfileRepository_Basic(t *testing.T) {
t.Fatalf("CountByUserID mismatch: %d err=%v", count, err)
}
if err := profileRepo.SetActive(ctx, "p-uuid", u.ID); err != nil {
t.Fatalf("SetActive err: %v", err)
}
if err := profileRepo.UpdateLastUsedAt(ctx, "p-uuid"); err != nil {
t.Fatalf("UpdateLastUsedAt err: %v", err)
}
@@ -208,29 +206,6 @@ func TestTextureRepository_Basic(t *testing.T) {
_ = textureRepo.Delete(ctx, tex.ID)
}
func TestSystemConfigRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)
repo := NewSystemConfigRepository(db)
ctx := context.Background()
cfg := &model.SystemConfig{Key: "site_name", Value: "Carrot", IsPublic: true}
if err := repo.Update(ctx, cfg); err != nil {
t.Fatalf("Update err: %v", err)
}
if v, err := repo.GetByKey(ctx, "site_name"); err != nil || v.Value != "Carrot" {
t.Fatalf("GetByKey mismatch")
}
_ = repo.UpdateValue(ctx, "site_name", "Carrot2")
if list, _ := repo.GetPublic(ctx); len(list) == 0 {
t.Fatalf("GetPublic expected entries")
}
if all, _ := repo.GetAll(ctx); len(all) == 0 {
t.Fatalf("GetAll expected entries")
}
if v, _ := repo.GetByKey(ctx, "site_name"); v.Value != "Carrot2" {
t.Fatalf("UpdateValue not applied")
}
}
func TestClientRepository_Basic(t *testing.T) {
db := testutil.NewTestDB(t)

View File

@@ -474,52 +474,6 @@ func (m *MockTextureRepository) BatchDelete(ctx context.Context, ids []int64) (i
return deleted, nil
}
// MockSystemConfigRepository 模拟SystemConfigRepository
type MockSystemConfigRepository struct {
configs map[string]*model.SystemConfig
}
func NewMockSystemConfigRepository() *MockSystemConfigRepository {
return &MockSystemConfigRepository{
configs: make(map[string]*model.SystemConfig),
}
}
func (m *MockSystemConfigRepository) GetByKey(ctx context.Context, key string) (*model.SystemConfig, error) {
if config, ok := m.configs[key]; ok {
return config, nil
}
return nil, nil
}
func (m *MockSystemConfigRepository) GetPublic(ctx context.Context) ([]model.SystemConfig, error) {
var result []model.SystemConfig
for _, v := range m.configs {
result = append(result, *v)
}
return result, nil
}
func (m *MockSystemConfigRepository) GetAll(ctx context.Context) ([]model.SystemConfig, error) {
var result []model.SystemConfig
for _, v := range m.configs {
result = append(result, *v)
}
return result, nil
}
func (m *MockSystemConfigRepository) Update(ctx context.Context, config *model.SystemConfig) error {
m.configs[config.Key] = config
return nil
}
func (m *MockSystemConfigRepository) UpdateValue(ctx context.Context, key, value string) error {
if config, ok := m.configs[key]; ok {
config.Value = value
return nil
}
return errors.New("config not found")
}
// ============================================================================
// Service Mocks

View File

@@ -563,15 +563,16 @@ func TestTextureServiceImpl_Create(t *testing.T) {
}
ctx := context.Background()
texture, err := textureService.Create(
// UploadTexture需要文件数据这里创建一个简单的测试数据
fileData := []byte("fake png data for testing")
texture, err := textureService.UploadTexture(
ctx,
tt.uploaderID,
tt.textureName,
"Test description",
tt.textureType,
"http://example.com/texture.png",
tt.hash,
512,
fileData,
"test.png",
true,
false,
)

View File

@@ -1,512 +0,0 @@
package service
import (
"carrotskin/internal/model"
"context"
"fmt"
"testing"
"go.uber.org/zap"
)
// TestTokenService_Constants 测试Token服务相关常量
func TestTokenService_Constants(t *testing.T) {
// 内部常量已私有化,通过服务行为间接测试
t.Skip("Token constants are now private - test through service behavior instead")
}
// TestTokenService_Validation 测试Token验证逻辑
func TestTokenService_Validation(t *testing.T) {
tests := []struct {
name string
accessToken string
wantValid bool
}{
{
name: "空token无效",
accessToken: "",
wantValid: false,
},
{
name: "非空token可能有效",
accessToken: "valid-token-string",
wantValid: true,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
// 测试空token检查逻辑
isValid := tt.accessToken != ""
if isValid != tt.wantValid {
t.Errorf("Token validation failed: got %v, want %v", isValid, tt.wantValid)
}
})
}
}
// TestTokenService_ClientTokenLogic 测试ClientToken逻辑
func TestTokenService_ClientTokenLogic(t *testing.T) {
tests := []struct {
name string
clientToken string
shouldGenerate bool
}{
{
name: "空的clientToken应该生成新的",
clientToken: "",
shouldGenerate: true,
},
{
name: "非空的clientToken应该使用提供的",
clientToken: "existing-client-token",
shouldGenerate: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
shouldGenerate := tt.clientToken == ""
if shouldGenerate != tt.shouldGenerate {
t.Errorf("ClientToken logic failed: got %v, want %v", shouldGenerate, tt.shouldGenerate)
}
})
}
}
// TestTokenService_ProfileSelection 测试Profile选择逻辑
func TestTokenService_ProfileSelection(t *testing.T) {
tests := []struct {
name string
profileCount int
shouldAutoSelect bool
}{
{
name: "只有一个profile时自动选择",
profileCount: 1,
shouldAutoSelect: true,
},
{
name: "多个profile时不自动选择",
profileCount: 2,
shouldAutoSelect: false,
},
{
name: "没有profile时不自动选择",
profileCount: 0,
shouldAutoSelect: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
shouldAutoSelect := tt.profileCount == 1
if shouldAutoSelect != tt.shouldAutoSelect {
t.Errorf("Profile selection logic failed: got %v, want %v", shouldAutoSelect, tt.shouldAutoSelect)
}
})
}
}
// TestTokenService_CleanupLogic 测试清理逻辑
func TestTokenService_CleanupLogic(t *testing.T) {
tests := []struct {
name string
tokenCount int
maxCount int
shouldCleanup bool
cleanupCount int
}{
{
name: "token数量未超过上限不需要清理",
tokenCount: 5,
maxCount: 10,
shouldCleanup: false,
cleanupCount: 0,
},
{
name: "token数量超过上限需要清理",
tokenCount: 15,
maxCount: 10,
shouldCleanup: true,
cleanupCount: 5,
},
{
name: "token数量等于上限不需要清理",
tokenCount: 10,
maxCount: 10,
shouldCleanup: false,
cleanupCount: 0,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
shouldCleanup := tt.tokenCount > tt.maxCount
if shouldCleanup != tt.shouldCleanup {
t.Errorf("Cleanup decision failed: got %v, want %v", shouldCleanup, tt.shouldCleanup)
}
if shouldCleanup {
expectedCleanupCount := tt.tokenCount - tt.maxCount
if expectedCleanupCount != tt.cleanupCount {
t.Errorf("Cleanup count failed: got %d, want %d", expectedCleanupCount, tt.cleanupCount)
}
}
})
}
}
// TestTokenService_UserIDValidation 测试UserID验证
func TestTokenService_UserIDValidation(t *testing.T) {
tests := []struct {
name string
userID int64
isValid bool
}{
{
name: "有效的UserID",
userID: 1,
isValid: true,
},
{
name: "UserID为0时无效",
userID: 0,
isValid: false,
},
{
name: "负数UserID无效",
userID: -1,
isValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
isValid := tt.userID > 0
if isValid != tt.isValid {
t.Errorf("UserID validation failed: got %v, want %v", isValid, tt.isValid)
}
})
}
}
// ============================================================================
// 使用 Mock 的集成测试
// ============================================================================
// TestTokenServiceImpl_Create 测试创建Token
func TestTokenServiceImpl_Create(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置Profile
testProfile := &model.Profile{
UUID: "test-profile-uuid",
UserID: 1,
Name: "TestProfile",
}
_ = profileRepo.Create(context.Background(), testProfile)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
tests := []struct {
name string
userID int64
uuid string
clientToken string
wantErr bool
}{
{
name: "正常创建Token指定UUID",
userID: 1,
uuid: "test-profile-uuid",
clientToken: "client-token-1",
wantErr: false,
},
{
name: "正常创建Token空clientToken",
userID: 1,
uuid: "test-profile-uuid",
clientToken: "",
wantErr: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
_, _, accessToken, clientToken, err := tokenService.Create(ctx, tt.userID, tt.uuid, tt.clientToken)
if tt.wantErr {
if err == nil {
t.Error("期望返回错误,但实际没有错误")
}
} else {
if err != nil {
t.Errorf("不期望返回错误: %v", err)
return
}
if accessToken == "" {
t.Error("accessToken不应为空")
}
if clientToken == "" {
t.Error("clientToken不应为空")
}
}
})
}
}
// TestTokenServiceImpl_Validate 测试验证Token
func TestTokenServiceImpl_Validate(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置Token
testToken := &model.Token{
AccessToken: "valid-access-token",
ClientToken: "valid-client-token",
UserID: 1,
ProfileId: "test-profile-uuid",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), testToken)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
tests := []struct {
name string
accessToken string
clientToken string
wantValid bool
}{
{
name: "有效Token完全匹配",
accessToken: "valid-access-token",
clientToken: "valid-client-token",
wantValid: true,
},
{
name: "有效Token只检查accessToken",
accessToken: "valid-access-token",
clientToken: "",
wantValid: true,
},
{
name: "无效TokenaccessToken不存在",
accessToken: "invalid-access-token",
clientToken: "",
wantValid: false,
},
{
name: "无效TokenclientToken不匹配",
accessToken: "valid-access-token",
clientToken: "wrong-client-token",
wantValid: false,
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
ctx := context.Background()
isValid := tokenService.Validate(ctx, tt.accessToken, tt.clientToken)
if isValid != tt.wantValid {
t.Errorf("Token验证结果不匹配: got %v, want %v", isValid, tt.wantValid)
}
})
}
}
// TestTokenServiceImpl_Invalidate 测试注销Token
func TestTokenServiceImpl_Invalidate(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置Token
testToken := &model.Token{
AccessToken: "token-to-invalidate",
ClientToken: "client-token",
UserID: 1,
ProfileId: "test-profile-uuid",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), testToken)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
// 验证Token存在
isValid := tokenService.Validate(ctx, "token-to-invalidate", "")
if !isValid {
t.Error("Token应该有效")
}
// 注销Token
tokenService.Invalidate(ctx, "token-to-invalidate")
// 验证Token已失效从repo中删除
_, err := tokenRepo.FindByAccessToken(context.Background(), "token-to-invalidate")
if err == nil {
t.Error("Token应该已被删除")
}
}
// TestTokenServiceImpl_InvalidateUserTokens 测试注销用户所有Token
func TestTokenServiceImpl_InvalidateUserTokens(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置多个Token
for i := 1; i <= 3; i++ {
_ = tokenRepo.Create(context.Background(), &model.Token{
AccessToken: fmt.Sprintf("user1-token-%d", i),
ClientToken: "client-token",
UserID: 1,
ProfileId: "test-profile-uuid",
Usable: true,
})
}
_ = tokenRepo.Create(context.Background(), &model.Token{
AccessToken: "user2-token-1",
ClientToken: "client-token",
UserID: 2,
ProfileId: "test-profile-uuid-2",
Usable: true,
})
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
// 注销用户1的所有Token
tokenService.InvalidateUserTokens(ctx, 1)
// 验证用户1的Token已失效
tokens, _ := tokenRepo.GetByUserID(context.Background(), 1)
if len(tokens) > 0 {
t.Errorf("用户1的Token应该全部被删除但还剩 %d 个", len(tokens))
}
// 验证用户2的Token仍然存在
tokens2, _ := tokenRepo.GetByUserID(context.Background(), 2)
if len(tokens2) != 1 {
t.Errorf("用户2的Token应该仍然存在期望1个实际 %d 个", len(tokens2))
}
}
// TestTokenServiceImpl_Refresh 覆盖 Refresh 的主要分支
func TestTokenServiceImpl_Refresh(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
// 预置 Profile 与 Token
profile := &model.Profile{
UUID: "profile-uuid",
UserID: 1,
}
_ = profileRepo.Create(context.Background(), profile)
oldToken := &model.Token{
AccessToken: "old-token",
ClientToken: "client-token",
UserID: 1,
ProfileId: "",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), oldToken)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
// 正常刷新,不指定 profile
newAccess, client, err := tokenService.Refresh(ctx, "old-token", "client-token", "")
if err != nil {
t.Fatalf("Refresh 正常情况失败: %v", err)
}
if newAccess == "" || client != "client-token" {
t.Fatalf("Refresh 返回值异常: access=%s, client=%s", newAccess, client)
}
// accessToken 为空
if _, _, err := tokenService.Refresh(ctx, "", "client-token", ""); err == nil {
t.Fatalf("Refresh 在 accessToken 为空时应返回错误")
}
}
// TestTokenServiceImpl_GetByAccessToken 封装 GetUUIDByAccessToken / GetUserIDByAccessToken
func TestTokenServiceImpl_GetByAccessToken(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
token := &model.Token{
AccessToken: "token-1",
UserID: 42,
ProfileId: "profile-42",
Usable: true,
}
_ = tokenRepo.Create(context.Background(), token)
tokenService := NewTokenService(tokenRepo, profileRepo, logger)
ctx := context.Background()
uuid, err := tokenService.GetUUIDByAccessToken(ctx, "token-1")
if err != nil || uuid != "profile-42" {
t.Fatalf("GetUUIDByAccessToken 返回错误: uuid=%s, err=%v", uuid, err)
}
uid, err := tokenService.GetUserIDByAccessToken(ctx, "token-1")
if err != nil || uid != 42 {
t.Fatalf("GetUserIDByAccessToken 返回错误: uid=%d, err=%v", uid, err)
}
}
// TestTokenServiceImpl_validateProfileByUserID 直接测试内部校验逻辑
func TestTokenServiceImpl_validateProfileByUserID(t *testing.T) {
tokenRepo := NewMockTokenRepository()
profileRepo := NewMockProfileRepository()
logger := zap.NewNop()
svc := &tokenService{
tokenRepo: tokenRepo,
profileRepo: profileRepo,
logger: logger,
}
// 预置 Profile
profile := &model.Profile{
UUID: "p-1",
UserID: 1,
}
_ = profileRepo.Create(context.Background(), profile)
// 参数非法
if ok, err := svc.validateProfileByUserID(context.Background(), 0, ""); err == nil || ok {
t.Fatalf("validateProfileByUserID 在参数非法时应返回错误")
}
// Profile 不存在
if ok, err := svc.validateProfileByUserID(context.Background(), 1, "not-exists"); err == nil || ok {
t.Fatalf("validateProfileByUserID 在 Profile 不存在时应返回错误")
}
// 用户与 Profile 匹配
if ok, err := svc.validateProfileByUserID(context.Background(), 1, "p-1"); err != nil || !ok {
t.Fatalf("validateProfileByUserID 匹配时应返回 true, err=%v", err)
}
// 用户与 Profile 不匹配
if ok, err := svc.validateProfileByUserID(context.Background(), 2, "p-1"); err != nil || ok {
t.Fatalf("validateProfileByUserID 不匹配时应返回 false, err=%v", err)
}
}

View File

@@ -31,7 +31,6 @@ func NewTestDB(t *testing.T) *gorm.DB {
&model.TextureDownloadLog{},
&model.Client{},
&model.Yggdrasil{},
&model.SystemConfig{},
&model.AuditLog{},
&model.CasbinRule{},
); err != nil {

View File

@@ -3,6 +3,7 @@ package types
import "time"
// BaseResponse 基础响应结构
// @Description 通用API响应结构
type BaseResponse struct {
Code int `json:"code"`
Message string `json:"message"`
@@ -10,12 +11,14 @@ type BaseResponse struct {
}
// PaginationRequest 分页请求
// @Description 分页查询参数
type PaginationRequest struct {
Page int `json:"page" form:"page" binding:"omitempty,min=1"`
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"`
@@ -25,12 +28,14 @@ type PaginationResponse struct {
}
// LoginRequest 登录请求
// @Description 用户登录请求参数
type LoginRequest struct {
Username string `json:"username" binding:"required" example:"testuser"` // 支持用户名或邮箱
Password string `json:"password" binding:"required,min=6,max=128" example:"password123"`
}
// RegisterRequest 注册请求
// @Description 用户注册请求参数
type RegisterRequest struct {
Username string `json:"username" binding:"required,min=3,max=50" example:"newuser"`
Email string `json:"email" binding:"required,email" example:"user@example.com"`
@@ -40,6 +45,7 @@ type RegisterRequest struct {
}
// UpdateUserRequest 更新用户请求
// @Description 更新用户信息请求参数
type UpdateUserRequest struct {
Avatar string `json:"avatar" binding:"omitempty,url" example:"https://example.com/new-avatar.png"`
OldPassword string `json:"old_password" binding:"omitempty,min=6,max=128" example:"oldpassword123"` // 修改密码时必需
@@ -47,12 +53,14 @@ type UpdateUserRequest struct {
}
// SendVerificationCodeRequest 发送验证码请求
// @Description 发送邮箱验证码请求参数
type SendVerificationCodeRequest struct {
Email string `json:"email" binding:"required,email" example:"user@example.com"`
Type string `json:"type" binding:"required,oneof=register reset_password change_email" example:"register"` // 类型: register/reset_password/change_email
}
// ResetPasswordRequest 重置密码请求
// @Description 重置密码请求参数
type ResetPasswordRequest struct {
Email string `json:"email" binding:"required,email" example:"user@example.com"`
VerificationCode string `json:"verification_code" binding:"required,len=6" example:"123456"`
@@ -60,17 +68,20 @@ type ResetPasswordRequest struct {
}
// ChangeEmailRequest 更换邮箱请求
// @Description 更换邮箱请求参数
type ChangeEmailRequest struct {
NewEmail string `json:"new_email" binding:"required,email" example:"newemail@example.com"`
VerificationCode string `json:"verification_code" binding:"required,len=6" example:"123456"`
}
// CreateProfileRequest 创建档案请求
// @Description 创建Minecraft档案请求参数
type CreateProfileRequest struct {
Name string `json:"name" binding:"required,min=1,max=16" example:"PlayerName"`
}
// UpdateTextureRequest 更新材质请求
// @Description 更新材质信息请求参数
type UpdateTextureRequest struct {
Name string `json:"name" binding:"omitempty,min=1,max=100" example:"My Skin"`
Description string `json:"description" binding:"omitempty,max=500" example:"A cool skin"`
@@ -78,12 +89,14 @@ type UpdateTextureRequest struct {
}
// LoginResponse 登录响应
// @Description 登录成功响应数据
type LoginResponse struct {
Token string `json:"token"`
UserInfo *UserInfo `json:"user_info"`
}
// UserInfo 用户信息
// @Description 用户详细信息
type UserInfo struct {
ID int64 `json:"id" example:"1"`
Username string `json:"username" example:"testuser"`
@@ -106,6 +119,7 @@ const (
)
// TextureInfo 材质信息
// @Description 材质详细信息
type TextureInfo struct {
ID int64 `json:"id" example:"1"`
UploaderID int64 `json:"uploader_id" example:"1"`
@@ -125,6 +139,7 @@ type TextureInfo struct {
}
// ProfileInfo 角色信息
// @Description Minecraft档案信息
type ProfileInfo struct {
UUID string `json:"uuid" example:"550e8400-e29b-41d4-a716-446655440000"`
UserID int64 `json:"user_id" example:"1"`
@@ -137,12 +152,14 @@ type ProfileInfo struct {
}
// UploadURLRequest 上传URL请求
// @Description 获取材质上传URL请求参数
type UploadURLRequest struct {
Type TextureType `json:"type" binding:"required,oneof=SKIN CAPE"`
Filename string `json:"filename" binding:"required"`
}
// UploadURLResponse 上传URL响应
// @Description 材质上传URL响应数据
type UploadURLResponse struct {
PostURL string `json:"post_url"`
FormData map[string]string `json:"form_data"`
@@ -151,6 +168,7 @@ type UploadURLResponse struct {
}
// SearchTextureRequest 搜索材质请求
// @Description 搜索材质请求参数
type SearchTextureRequest struct {
PaginationRequest
Keyword string `json:"keyword" form:"keyword"`
@@ -159,6 +177,7 @@ type SearchTextureRequest struct {
}
// UpdateProfileRequest 更新角色请求
// @Description 更新Minecraft档案请求参数
type UpdateProfileRequest struct {
Name string `json:"name" binding:"omitempty,min=1,max=16" example:"NewPlayerName"`
SkinID *int64 `json:"skin_id,omitempty" example:"1"`
@@ -166,6 +185,7 @@ type UpdateProfileRequest struct {
}
// SystemConfigResponse 基础系统配置响应
// @Description 系统配置信息
type SystemConfigResponse struct {
SiteName string `json:"site_name" example:"CarrotSkin"`
SiteDescription string `json:"site_description" example:"A Minecraft Skin Station"`