解决合并后出现的问题,为swagger提供禁用选项,暂时移除wiki
This commit is contained in:
@@ -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) {
|
||||
|
||||
@@ -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,
|
||||
}))
|
||||
}
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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) {
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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 == "" {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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:"-"`
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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"`
|
||||
|
||||
@@ -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:"-"` // 加密后的密码,不返回给前端
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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: "无效Token(accessToken不存在)",
|
||||
accessToken: "invalid-access-token",
|
||||
clientToken: "",
|
||||
wantValid: false,
|
||||
},
|
||||
{
|
||||
name: "无效Token(clientToken不匹配)",
|
||||
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)
|
||||
}
|
||||
}
|
||||
@@ -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 {
|
||||
|
||||
@@ -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"`
|
||||
|
||||
Reference in New Issue
Block a user