13 Commits

Author SHA1 Message Date
lafay
3e8b7d150d chore: Refactor Dockerfile and build workflow for improved efficiency
All checks were successful
Build / build (push) Successful in 4m4s
Build / build-docker (push) Successful in 1m16s
- Removed the build stage from the Dockerfile, simplifying the image creation process.
- Updated the Dockerfile to directly copy the pre-built binary instead of using a multi-stage build.
- Modified the build workflow to eliminate unnecessary build arguments, streamlining the configuration.
2026-01-10 05:21:45 +08:00
lafay
fd5a0e8405 chore: Update Docker image tags in build workflow
Some checks failed
Build / build (push) Successful in 3m57s
Build / build-docker (push) Has been cancelled
- Changed Docker image tags in the build workflow to reflect the new repository owner.
- Updated the image references to use 'carrotskin' instead of the previous owner for consistency.
2026-01-10 05:12:51 +08:00
lafay
573c10ed1d chore: Remove Swagger documentation generation from build workflow
Some checks failed
Build / build (push) Successful in 4m0s
Build / build-docker (push) Failing after 41s
- Eliminated the Swagger documentation generation step from the build process.
- Updated the main server file to remove the Swagger documentation import, streamlining the codebase.
2026-01-10 05:00:09 +08:00
lafay
3b8d8bd7a7 chore: Add Swagger documentation generation to build workflow
Some checks failed
Build / build (push) Failing after 55s
Build / build-docker (push) Has been skipped
- Included a step to generate Swagger documentation during the build process.
- This addition enhances API documentation and ensures it is up-to-date with the codebase.
2026-01-10 04:57:56 +08:00
lafay
6338592d27 chore: Remove Go proxy setup from build workflow
Some checks failed
Build / build (push) Failing after 3m8s
Build / build-docker (push) Has been skipped
- Eliminated the Go proxy configuration from the build workflow to streamline the setup process.
- This change simplifies the environment setup for dependency management.
2026-01-10 04:47:14 +08:00
lafay
ef460ec891 chore: Disable caching in Go setup for build workflow
Some checks failed
Build / build (push) Failing after 1m31s
Build / build-docker (push) Has been skipped
- Updated the build workflow to disable caching for the Go setup action.
- This change aims to ensure a clean build environment for better consistency.
2026-01-10 04:42:22 +08:00
lafay
62d9432a2d chore: Add Go proxy setup to build workflow
Some checks failed
Build / build-docker (push) Has been cancelled
Build / build (push) Has been cancelled
- Configured Go proxy settings for improved dependency management.
- Disabled Go checksum database for local development environments.
2026-01-10 04:39:11 +08:00
lafay
e1d79ed445 chore: Update build workflow to include 'dev' branch
Some checks failed
Build / build (push) Failing after 21m16s
Build / build-docker (push) Has been skipped
- Added 'dev' branch to the push and pull_request triggers in the build workflow configuration.
- Ensured that CI/CD processes are aligned for both master and dev branches.
2026-01-10 04:01:20 +08:00
lafay
c5d7e317a4 refactor: Streamline user information retrieval and validation
- Refactored the user information retrieval process to improve efficiency.
- Enhanced validation logic for input parameters in the user handler.
- Updated UserService interface to support new retrieval methods.
- Improved error handling for user status checks before responding.
2026-01-10 03:58:22 +08:00
lafay
06539dc086 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.
2026-01-10 03:52:35 +08:00
lafay
22142db782 fix: Improve texture upload handling and caching logic
- Simplified caching logic by removing unnecessary nil check before setting cache.
- Enhanced error handling in texture upload process to return the original texture object if fetching the uploader information fails or returns nil.
2026-01-10 03:23:26 +08:00
lafay
2c9c6ecfc0 Merge branch 'dev' of https://code.littlelan.cn/CarrotSkin/backend into dev 2026-01-10 03:17:39 +08:00
lafay
c5db489d72 refactor: Enhance texture handling and configuration
- Removed Swagger documentation import from the main server file.
- Updated TextureInfo struct to include UploaderUsername for better texture metadata.
- Modified texture repository methods to preload Uploader information when fetching textures by hash.
- Improved texture service to handle cases where Uploader information is missing, ensuring proper caching and retrieval.
- Added Redis configuration options in the environment variable setup for better flexibility.
2026-01-10 03:15:27 +08:00
13 changed files with 286 additions and 42 deletions

View File

@@ -0,0 +1,73 @@
name: Build
on:
push:
branches:
- master
- dev
pull_request:
branches:
- master
- dev
jobs:
build:
runs-on: ubuntu-latest
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Go
uses: actions/setup-go@v5
with:
go-version: '1.25'
cache: false
- name: Download dependencies
run: go mod download
- name: Build
env:
GOOS: linux
GOARCH: amd64
CGO_ENABLED: 0
run: go build -v -o mcauth-linux-amd64 ./cmd/server
- name: Upload artifacts
uses: actions/upload-artifact@v3
with:
name: mcauth-linux-amd64
path: mcauth-linux-amd64
build-docker:
runs-on: ubuntu-latest
needs: build
steps:
- name: Checkout
uses: actions/checkout@v4
- name: Set up Docker Buildx
uses: docker/setup-buildx-action@v3
- name: Login to Gitea Container Registry
uses: docker/login-action@v3
with:
registry: code.littlelan.cn
username: ${{ secrets.GIT_USERNAME }}
password: ${{ secrets.GIT_TOKEN }}
- name: Download artifact
uses: actions/download-artifact@v3
with:
name: mcauth-linux-amd64
- name: Build and push Docker image
uses: docker/build-push-action@v5
with:
context: .
push: true
tags: |
code.littlelan.cn/carrotskin/mcauth:latest
code.littlelan.cn/carrotskin/mcauth:${{ github.sha }}
platforms: linux/amd64

35
Dockerfile Normal file
View File

@@ -0,0 +1,35 @@
# 运行阶段
FROM alpine:latest
# 安装必要的运行时依赖
RUN apk add --no-cache ca-certificates tzdata wget
# 创建非 root 用户
RUN addgroup -g 1000 appuser && \
adduser -D -u 1000 -G appuser appuser
# 设置工作目录
WORKDIR /app
# 复制已经编译好的二进制文件
ARG BINARY_NAME=mcauth-linux-amd64
COPY ${BINARY_NAME} /app/server
# 复制配置文件(如果需要)
COPY configs/ /app/configs/
# 设置权限
RUN chown -R appuser:appuser /app
# 切换到非 root 用户
USER appuser
# 暴露端口
EXPOSE 8080
# 健康检查
HEALTHCHECK --interval=30s --timeout=3s --start-period=5s --retries=3 \
CMD wget --no-verbose --tries=1 --spider http://localhost:8080/health || exit 1
# 启动应用
ENTRYPOINT ["/app/server"]

View File

@@ -32,7 +32,6 @@ import (
"github.com/gin-gonic/gin"
"go.uber.org/zap"
// _ "carrotskin/docs" // Swagger docs
)
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 响应
func ProfileToProfileInfo(profile *model.Profile) *types.ProfileInfo {
return &types.ProfileInfo{
@@ -87,22 +100,28 @@ func ProfilesToProfileInfos(profiles []*model.Profile) []*types.ProfileInfo {
// TextureToTextureInfo 将 Texture 模型转换为 TextureInfo 响应
func TextureToTextureInfo(texture *model.Texture) *types.TextureInfo {
uploaderUsername := ""
if texture.Uploader != nil {
uploaderUsername = texture.Uploader.Username
}
return &types.TextureInfo{
ID: texture.ID,
UploaderID: texture.UploaderID,
Name: texture.Name,
Description: texture.Description,
Type: types.TextureType(texture.Type),
URL: texture.URL,
Hash: texture.Hash,
Size: texture.Size,
IsPublic: texture.IsPublic,
DownloadCount: texture.DownloadCount,
FavoriteCount: texture.FavoriteCount,
IsSlim: texture.IsSlim,
Status: texture.Status,
CreatedAt: texture.CreatedAt,
UpdatedAt: texture.UpdatedAt,
ID: texture.ID,
UploaderID: texture.UploaderID,
UploaderUsername: uploaderUsername,
Name: texture.Name,
Description: texture.Description,
Type: types.TextureType(texture.Type),
URL: texture.URL,
Hash: texture.Hash,
Size: texture.Size,
IsPublic: texture.IsPublic,
DownloadCount: texture.DownloadCount,
FavoriteCount: texture.FavoriteCount,
IsSlim: texture.IsSlim,
Status: texture.Status,
CreatedAt: texture.CreatedAt,
UpdatedAt: texture.UpdatedAt,
}
}

View File

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

View File

@@ -2,6 +2,7 @@ package handler
import (
"carrotskin/internal/container"
"carrotskin/internal/model"
"carrotskin/internal/service"
"carrotskin/internal/types"
@@ -315,3 +316,55 @@ func (h *UserHandler) ResetYggdrasilPassword(c *gin.Context) {
h.logger.Info("Yggdrasil密码重置成功", zap.Int64("userId", userID))
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

@@ -29,13 +29,13 @@ func (r *textureRepository) FindByID(ctx context.Context, id int64) (*model.Text
func (r *textureRepository) FindByHash(ctx context.Context, hash string) (*model.Texture, error) {
var texture model.Texture
err := r.db.WithContext(ctx).Where("hash = ?", hash).First(&texture).Error
err := r.db.WithContext(ctx).Preload("Uploader").Where("hash = ?", hash).First(&texture).Error
return handleNotFoundResult(&texture, err)
}
func (r *textureRepository) FindByHashAndUploaderID(ctx context.Context, hash string, uploaderID int64) (*model.Texture, error) {
var texture model.Texture
err := r.db.WithContext(ctx).Where("hash = ? AND uploader_id = ?", hash, uploaderID).First(&texture).Error
err := r.db.WithContext(ctx).Preload("Uploader").Where("hash = ? AND uploader_id = ?", hash, uploaderID).First(&texture).Error
return handleNotFoundResult(&texture, err)
}

View File

@@ -19,6 +19,7 @@ type UserService interface {
// 用户查询
GetByID(ctx context.Context, id int64) (*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

View File

@@ -55,6 +55,22 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
if texture.Status == -1 {
return nil, errors.New("材质已删除")
}
// 如果缓存中没有 Uploader 信息,重新查询数据库
if texture.Uploader == nil {
texture2, err := s.textureRepo.FindByID(ctx, id)
if err != nil {
return nil, err
}
if texture2 == nil {
return nil, ErrTextureNotFound
}
if texture2.Status == -1 {
return nil, errors.New("材质已删除")
}
// 更新缓存
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
return texture2, nil
}
return &texture, nil
}
@@ -71,9 +87,7 @@ func (s *textureService) GetByID(ctx context.Context, id int64) (*model.Texture,
}
// 存入缓存(异步)
if texture2 != nil {
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
}
s.cache.SetAsync(context.Background(), cacheKey, texture2, s.cache.Policy.TextureTTL)
return texture2, nil
}
@@ -365,7 +379,17 @@ func (s *textureService) UploadTexture(ctx context.Context, uploaderID int64, na
// 清除用户的 texture 列表缓存(所有分页)
s.cacheInv.BatchInvalidate(ctx, fmt.Sprintf("texture:user:%d:*", uploaderID))
return texture, nil
// 重新查询以预加载 Uploader 关联
textureWithUploader, err := s.textureRepo.FindByID(ctx, texture.ID)
if err != nil {
// 如果查询失败,返回原始创建的 texture 对象(虽然可能没有 Uploader 信息)
return texture, nil
}
if textureWithUploader == nil {
// 如果查询返回 nil极端情况如数据库复制延迟返回原始创建的 texture 对象
return texture, nil
}
return textureWithUploader, nil
}
// parseTextureTypeInternal 解析材质类型

View File

@@ -199,6 +199,14 @@ func (s *userService) GetByEmail(ctx context.Context, email string) (*model.User
}, 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 {
err := s.userRepo.Update(ctx, user)
if err != nil {

View File

@@ -110,6 +110,18 @@ type UserInfo struct {
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 材质类型
type TextureType string
@@ -121,21 +133,22 @@ const (
// TextureInfo 材质信息
// @Description 材质详细信息
type TextureInfo struct {
ID int64 `json:"id" example:"1"`
UploaderID int64 `json:"uploader_id" example:"1"`
Name string `json:"name" example:"My Skin"`
Description string `json:"description,omitempty" example:"A cool skin"`
Type TextureType `json:"type" example:"SKIN"`
URL string `json:"url" example:"https://rustfs.example.com/textures/xxx.png"`
Hash string `json:"hash" example:"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`
Size int `json:"size" example:"2048"`
IsPublic bool `json:"is_public" example:"true"`
DownloadCount int `json:"download_count" example:"100"`
FavoriteCount int `json:"favorite_count" example:"50"`
IsSlim bool `json:"is_slim" example:"false"`
Status int16 `json:"status" example:"1"`
CreatedAt time.Time `json:"created_at" example:"2025-10-01T10:00:00Z"`
UpdatedAt time.Time `json:"updated_at" example:"2025-10-01T10:00:00Z"`
ID int64 `json:"id" example:"1"`
UploaderID int64 `json:"uploader_id" example:"1"`
UploaderUsername string `json:"uploader_username" example:"testuser"`
Name string `json:"name" example:"My Skin"`
Description string `json:"description,omitempty" example:"A cool skin"`
Type TextureType `json:"type" example:"SKIN"`
URL string `json:"url" example:"https://rustfs.example.com/textures/xxx.png"`
Hash string `json:"hash" example:"e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855"`
Size int `json:"size" example:"2048"`
IsPublic bool `json:"is_public" example:"true"`
DownloadCount int `json:"download_count" example:"100"`
FavoriteCount int `json:"favorite_count" example:"50"`
IsSlim bool `json:"is_slim" example:"false"`
Status int16 `json:"status" example:"1"`
CreatedAt time.Time `json:"created_at" example:"2025-10-01T10:00:00Z"`
UpdatedAt time.Time `json:"updated_at" example:"2025-10-01T10:00:00Z"`
}
// ProfileInfo 角色信息

View File

@@ -140,8 +140,7 @@ func Load() (*Config, error) {
// 设置默认值
setDefaults()
// 设置环境变量前缀
viper.SetEnvPrefix("CARROTSKIN")
// 自动读取环境变量(不设置前缀,因为 BindEnv 已经明确指定了变量名)
viper.AutomaticEnv()
// 手动设置环境变量映射
@@ -320,6 +319,7 @@ func setupEnvMappings() {
// overrideFromEnv 从环境变量中覆盖配置
func overrideFromEnv(config *Config) {
// 处理RustFS存储桶配置
if texturesBucket := os.Getenv("RUSTFS_BUCKET_TEXTURES"); texturesBucket != "" {
if config.RustFS.Buckets == nil {
@@ -360,6 +360,24 @@ func overrideFromEnv(config *Config) {
}
}
// 处理Redis基本配置
if host := os.Getenv("REDIS_HOST"); host != "" {
config.Redis.Host = host
}
if port := os.Getenv("REDIS_PORT"); port != "" {
if val, err := strconv.Atoi(port); err == nil {
config.Redis.Port = val
}
}
if password := os.Getenv("REDIS_PASSWORD"); password != "" {
config.Redis.Password = password
}
if database := os.Getenv("REDIS_DATABASE"); database != "" {
if val, err := strconv.Atoi(database); err == nil {
config.Redis.Database = val
}
}
// 处理Redis连接池配置
if poolSize := os.Getenv("REDIS_POOL_SIZE"); poolSize != "" {
if val, err := strconv.Atoi(poolSize); err == nil {

View File

@@ -1,9 +1,7 @@
package storage
import (
// "context"
"testing"
// "time"
"carrotskin/pkg/config"
@@ -41,4 +39,3 @@ func TestNewStorage_SkipConnectWhenNoCreds(t *testing.T) {
t.Fatalf("NewStorage should not error when creds empty: %v", err)
}
}