feat: Add public user information retrieval endpoint

- Introduced a new endpoint to fetch public user information without authentication.
- Implemented UserToPublicUserInfo function to format user data for the response.
- Updated UserService interface and user service implementation to support fetching users by username.
- Enhanced user handler to validate input parameters and check user status before responding.
This commit is contained in:
lafay
2026-01-10 03:52:35 +08:00
parent 22142db782
commit 06539dc086
7 changed files with 93 additions and 0 deletions

View File

@@ -32,6 +32,8 @@ import (
"github.com/gin-gonic/gin" "github.com/gin-gonic/gin"
"go.uber.org/zap" "go.uber.org/zap"
_ "carrotskin/docs"
) )
func main() { func main() {

View File

@@ -62,6 +62,19 @@ func UserToUserInfo(user *model.User) *types.UserInfo {
} }
} }
// UserToPublicUserInfo 将 User 模型转换为 PublicUserInfo 响应
func UserToPublicUserInfo(user *model.User) *types.PublicUserInfo {
return &types.PublicUserInfo{
ID: user.ID,
Username: user.Username,
Avatar: user.Avatar,
Points: user.Points,
Role: user.Role,
Status: user.Status,
CreatedAt: user.CreatedAt,
}
}
// ProfileToProfileInfo 将 Profile 模型转换为 ProfileInfo 响应 // ProfileToProfileInfo 将 Profile 模型转换为 ProfileInfo 响应
func ProfileToProfileInfo(profile *model.Profile) *types.ProfileInfo { func ProfileToProfileInfo(profile *model.Profile) *types.ProfileInfo {
return &types.ProfileInfo{ return &types.ProfileInfo{

View File

@@ -93,6 +93,10 @@ func registerAuthRoutes(v1 *gin.RouterGroup, h *AuthHandler) {
// registerUserRoutes 注册用户路由 // registerUserRoutes 注册用户路由
func registerUserRoutes(v1 *gin.RouterGroup, h *UserHandler, jwtService *auth.JWTService) { func registerUserRoutes(v1 *gin.RouterGroup, h *UserHandler, jwtService *auth.JWTService) {
// 公开用户信息路由(无需认证)
v1.GET("/users/public", h.GetPublicInfo)
// 需要认证的用户路由
userGroup := v1.Group("/user") userGroup := v1.Group("/user")
userGroup.Use(middleware.AuthMiddleware(jwtService)) userGroup.Use(middleware.AuthMiddleware(jwtService))
{ {

View File

@@ -2,6 +2,7 @@ package handler
import ( import (
"carrotskin/internal/container" "carrotskin/internal/container"
"carrotskin/internal/model"
"carrotskin/internal/service" "carrotskin/internal/service"
"carrotskin/internal/types" "carrotskin/internal/types"
@@ -315,3 +316,55 @@ func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) {
h.logger.Info("Yggdrasil密码重置成功", zap.Int64("userId", userID)) h.logger.Info("Yggdrasil密码重置成功", zap.Int64("userId", userID))
RespondSuccess(c, gin.H{"password": newPassword}) RespondSuccess(c, gin.H{"password": newPassword})
} }
// GetPublicInfo 获取用户公开信息
// @Summary 获取用户公开信息
// @Description 根据用户名或用户ID获取用户的公开信息不包含敏感信息如邮箱
// @Tags user
// @Accept json
// @Produce json
// @Param username query string false "用户名"
// @Param id query int false "用户ID"
// @Success 200 {object} model.Response{data=types.PublicUserInfo} "获取成功"
// @Failure 400 {object} model.ErrorResponse "参数错误"
// @Failure 404 {object} model.ErrorResponse "用户不存在"
// @Router /api/v1/users/public [get]
func (h *UserHandler) GetPublicInfo(c *gin.Context) {
username := c.Query("username")
idStr := c.Query("id")
// 至少需要提供一个参数
if username == "" && idStr == "" {
RespondBadRequest(c, "必须提供用户名或用户ID", nil)
return
}
var user *model.User
var err error
// 优先使用用户名查询
if username != "" {
user, err = h.container.UserService.GetByUsername(c.Request.Context(), username)
} else {
// 使用用户ID查询
id := parseIntWithDefault(idStr, 0)
if id == 0 {
RespondBadRequest(c, "无效的用户ID", nil)
return
}
user, err = h.container.UserService.GetByID(c.Request.Context(), int64(id))
}
if err != nil || user == nil {
RespondNotFound(c, "用户不存在")
return
}
// 检查用户状态
if user.Status != 1 {
RespondNotFound(c, "用户不可用")
return
}
RespondSuccess(c, UserToPublicUserInfo(user))
}

View File

@@ -19,6 +19,7 @@ type UserService interface {
// 用户查询 // 用户查询
GetByID(ctx context.Context, id int64) (*model.User, error) GetByID(ctx context.Context, id int64) (*model.User, error)
GetByEmail(ctx context.Context, email string) (*model.User, error) GetByEmail(ctx context.Context, email string) (*model.User, error)
GetByUsername(ctx context.Context, username string) (*model.User, error)
// 用户更新 // 用户更新
UpdateInfo(ctx context.Context, user *model.User) error UpdateInfo(ctx context.Context, user *model.User) error

View File

@@ -199,6 +199,14 @@ func (s *userService) GetByEmail(ctx context.Context, email string) (*model.User
}, s.cache.Policy.UserEmailTTL) }, s.cache.Policy.UserEmailTTL)
} }
func (s *userService) GetByUsername(ctx context.Context, username string) (*model.User, error) {
// 使用 Cached 装饰器自动处理缓存
cacheKey := s.cacheKeys.UserByUsername(username)
return database.Cached(ctx, s.cache, cacheKey, func() (*model.User, error) {
return s.userRepo.FindByUsername(ctx, username)
}, s.cache.Policy.UserTTL)
}
func (s *userService) UpdateInfo(ctx context.Context, user *model.User) error { func (s *userService) UpdateInfo(ctx context.Context, user *model.User) error {
err := s.userRepo.Update(ctx, user) err := s.userRepo.Update(ctx, user)
if err != nil { if err != nil {

View File

@@ -110,6 +110,18 @@ type UserInfo struct {
UpdatedAt time.Time `json:"updated_at" example:"2025-10-01T10:00:00Z"` UpdatedAt time.Time `json:"updated_at" example:"2025-10-01T10:00:00Z"`
} }
// PublicUserInfo 用户公开信息
// @Description 用户公开信息(不包含敏感信息如邮箱)
type PublicUserInfo struct {
ID int64 `json:"id" example:"1"`
Username string `json:"username" example:"testuser"`
Avatar string `json:"avatar" example:"https://example.com/avatar.png"`
Points int `json:"points" example:"100"`
Role string `json:"role" example:"user"`
Status int16 `json:"status" example:"1"`
CreatedAt time.Time `json:"created_at" example:"2025-10-01T10:00:00Z"`
}
// TextureType 材质类型 // TextureType 材质类型
type TextureType string type TextureType string