From 23be1c563d852c5a736e90193752834ba8f76366 Mon Sep 17 00:00:00 2001 From: lan Date: Tue, 2 Dec 2025 11:22:14 +0800 Subject: [PATCH] =?UTF-8?q?refactor:=20=E7=A7=BB=E9=99=A4=E4=B8=8D?= =?UTF-8?q?=E5=BF=85=E8=A6=81=E7=9A=84=E9=85=8D=E7=BD=AE=E4=BE=9D=E8=B5=96?= =?UTF-8?q?=EF=BC=8C=E7=AE=80=E5=8C=96=E4=B8=8A=E4=BC=A0URL=E7=94=9F?= =?UTF-8?q?=E6=88=90=E9=80=BB=E8=BE=91=E5=B9=B6=E6=B7=BB=E5=8A=A0=E5=85=AC?= =?UTF-8?q?=E5=BC=80=E8=AE=BF=E9=97=AEURL=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .dockerignore | 76 ++++++++++++ .env.docker.example | 47 ++++++++ .gitea/workflows/docker.yml | 79 +++++++++++++ .gitea/workflows/sonarqube.yml | 43 ------- .gitea/workflows/test.yml | 104 ---------------- Dockerfile | 61 ++++++++++ docker-compose.yml | 177 ++++++++++++++++++++++++++++ internal/handler/texture_handler.go | 3 - internal/handler/user_handler.go | 4 +- internal/service/upload_service.go | 13 +- pkg/config/config.go | 3 + pkg/storage/minio.go | 41 +++++-- 12 files changed, 478 insertions(+), 173 deletions(-) create mode 100644 .dockerignore create mode 100644 .env.docker.example create mode 100644 .gitea/workflows/docker.yml delete mode 100644 .gitea/workflows/sonarqube.yml delete mode 100644 .gitea/workflows/test.yml create mode 100644 Dockerfile create mode 100644 docker-compose.yml diff --git a/.dockerignore b/.dockerignore new file mode 100644 index 0000000..e375c38 --- /dev/null +++ b/.dockerignore @@ -0,0 +1,76 @@ +# Git +.git +.gitignore +.gitea + +# IDE +.vscode +.idea +*.swp +*.swo + +# 构建产物 +bin/ +dist/ +build/ +server +*.exe + +# 测试和覆盖率 +*.test +coverage.out +coverage.html +coverage.txt +test_results/ +test_coverage/ + +# 日志 +*.log +logs/ +log/ + +# 临时文件 +tmp/ +temp/ +.tmp/ + +# 本地配置 +.env +.env.local +.env.development +.env.test +.env.production +configs/config.yaml + +# 文档 (可选保留) +# docs/ + +# 数据库文件 +*.db +*.sqlite +*.sqlite3 + +# 备份 +*.bak +*.backup + +# OS 文件 +.DS_Store +Thumbs.db + +# Docker +docker-compose*.yml +Dockerfile* +!Dockerfile + +# README 和脚本 +README.md +*.sh +*.bat +scripts/ + +# 本地开发 +local/ +dev/ +minio-data/ + diff --git a/.env.docker.example b/.env.docker.example new file mode 100644 index 0000000..6ce89f2 --- /dev/null +++ b/.env.docker.example @@ -0,0 +1,47 @@ +# ==================== CarrotSkin Docker 环境配置示例 ==================== +# 复制此文件为 .env 后修改配置值 + +# ==================== 服务配置 ==================== +# 应用端口 +APP_PORT=8080 +# 运行模式: debug, release, test +SERVER_MODE=release +# API 根路径 (用于反向代理,如 /api) +SERVER_BASE_PATH= +# 公开访问地址 (用于生成回调URL、邮件链接等) +PUBLIC_URL=http://localhost:8080 + +# ==================== 数据库配置 ==================== +DB_PASSWORD=carrotskin123 + +# ==================== Redis 配置 ==================== +# 留空表示不设置密码 +REDIS_PASSWORD= + +# ==================== JWT 配置 ==================== +# 生产环境务必修改此密钥! +JWT_SECRET=your-super-secret-jwt-key-change-in-production + +# ==================== 存储配置 (RustFS S3兼容) ==================== +# 内部访问地址 (容器间通信) +RUSTFS_ENDPOINT=rustfs:9000 +RUSTFS_ACCESS_KEY=rustfsadmin +RUSTFS_SECRET_KEY=rustfsadmin123 +RUSTFS_USE_SSL=false + +# 存储桶配置 +RUSTFS_BUCKET_TEXTURES=carrotskin +RUSTFS_BUCKET_AVATARS=carrotskin + +# 公开访问地址 (用于生成文件URL,供外部浏览器访问) +# 示例: +# 直接访问: http://localhost:9000 +# 反向代理: https://example.com/storage +RUSTFS_PUBLIC_URL=http://localhost:9000 + +# ==================== 邮件配置 (可选) ==================== +SMTP_HOST= +SMTP_PORT=587 +SMTP_USER= +SMTP_PASSWORD= +SMTP_FROM= diff --git a/.gitea/workflows/docker.yml b/.gitea/workflows/docker.yml new file mode 100644 index 0000000..e2ac875 --- /dev/null +++ b/.gitea/workflows/docker.yml @@ -0,0 +1,79 @@ +name: Build and Push Docker Image + +on: + push: + branches: + - main + - master + tags: + - 'v*' + workflow_dispatch: + +env: + REGISTRY: code.littlelan.cn + IMAGE_NAME: carrotskin/backend + +jobs: + build-and-push: + runs-on: ubuntu-latest + + steps: + - name: Checkout code + uses: actions/checkout@v4 + with: + fetch-depth: 0 # 获取完整历史以支持 git describe + + - name: Set up Docker Buildx + uses: docker/setup-buildx-action@v3 + + - name: Log in to Container Registry + uses: docker/login-action@v3 + with: + registry: ${{ env.REGISTRY }} + username: ${{ secrets.REGISTRY_USERNAME }} + password: ${{ secrets.REGISTRY_PASSWORD }} + + - name: Extract metadata for Docker + id: meta + uses: docker/metadata-action@v5 + with: + images: ${{ env.REGISTRY }}/${{ env.IMAGE_NAME }} + tags: | + type=ref,event=branch + type=ref,event=tag + type=sha,prefix=sha- + type=raw,value=latest,enable=${{ github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' }} + + - name: Build and push Docker image + uses: docker/build-push-action@v5 + with: + context: . + file: ./Dockerfile + push: true + tags: ${{ steps.meta.outputs.tags }} + labels: ${{ steps.meta.outputs.labels }} + cache-from: type=gha + cache-to: type=gha,mode=max + platforms: linux/amd64 + + - name: Image digest + run: echo "Image pushed with digest ${{ steps.build.outputs.digest }}" + + # 可选:部署到服务器 + deploy: + runs-on: ubuntu-latest + needs: build-and-push + if: github.ref == 'refs/heads/main' || github.ref == 'refs/heads/master' + + steps: + - name: Deploy notification + run: | + echo "## 🚀 Docker 镜像构建完成" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "镜像已推送到: \`${{ env.REGISTRY }}/${{ env.IMAGE_NAME }}\`" >> $GITHUB_STEP_SUMMARY + echo "" >> $GITHUB_STEP_SUMMARY + echo "可用标签:" >> $GITHUB_STEP_SUMMARY + echo "- \`latest\`" >> $GITHUB_STEP_SUMMARY + echo "- \`${{ github.ref_name }}\`" >> $GITHUB_STEP_SUMMARY + echo "- \`sha-${{ github.sha }}\`" >> $GITHUB_STEP_SUMMARY + diff --git a/.gitea/workflows/sonarqube.yml b/.gitea/workflows/sonarqube.yml deleted file mode 100644 index 5bb158d..0000000 --- a/.gitea/workflows/sonarqube.yml +++ /dev/null @@ -1,43 +0,0 @@ -name: SonarQube Analysis - -on: - push: - pull_request: - -jobs: - sonarqube: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - with: - fetch-depth: 0 # Shallow clones should be disabled for better analysis - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.23' - - - name: Download and extract SonarQube Scanner - run: | - export SONAR_SCANNER_VERSION=7.2.0.5079 - export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux-x64 - curl --create-dirs -sSLo $HOME/.sonar/sonar-scanner.zip https://binaries.sonarsource.com/Distribution/sonar-scanner-cli/sonar-scanner-cli-$SONAR_SCANNER_VERSION-linux-x64.zip - unzip -o $HOME/.sonar/sonar-scanner.zip -d $HOME/.sonar/ - export PATH=$SONAR_SCANNER_HOME/bin:$PATH - echo "SONAR_SCANNER_HOME=$SONAR_SCANNER_HOME" >> $GITHUB_ENV - echo "$SONAR_SCANNER_HOME/bin" >> $GITHUB_PATH - - - name: Run SonarQube Scanner - env: - SONAR_TOKEN: sqp_b8a64837bd9e967b6876166e9ba27f0bc88626ed - run: | - export SONAR_SCANNER_VERSION=7.2.0.5079 - export SONAR_SCANNER_HOME=$HOME/.sonar/sonar-scanner-$SONAR_SCANNER_VERSION-linux-x64 - export PATH=$SONAR_SCANNER_HOME/bin:$PATH - sonar-scanner \ - -Dsonar.projectKey=CarrotSkin \ - -Dsonar.sources=. \ - -Dsonar.host.url=https://sonar.littlelan.cn - diff --git a/.gitea/workflows/test.yml b/.gitea/workflows/test.yml deleted file mode 100644 index 5ad23b0..0000000 --- a/.gitea/workflows/test.yml +++ /dev/null @@ -1,104 +0,0 @@ -name: Test - -on: - push: - branches: - - main - - master - - develop - - 'feature/**' - pull_request: - branches: - - main - - master - - develop - -jobs: - test: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.23' - cache-dependency-path: go.sum - - - name: Download dependencies - run: go mod download - - - name: Run tests - run: go test -v -race -coverprofile=coverage.out -covermode=atomic ./... - - - name: Generate coverage report - run: | - go tool cover -html=coverage.out -o coverage.html - go tool cover -func=coverage.out -o coverage.txt - - - name: Upload coverage reports - uses: actions/upload-artifact@v3 - with: - name: coverage-reports - path: | - coverage.out - coverage.html - coverage.txt - - - name: Display coverage summary - run: | - echo "## Test Coverage Summary" >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - cat coverage.txt >> $GITHUB_STEP_SUMMARY - echo '```' >> $GITHUB_STEP_SUMMARY - - lint: - runs-on: ubuntu-latest - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.23' - cache-dependency-path: go.sum - - - name: Download dependencies - run: go mod download - - - name: Run golangci-lint - uses: golangci/golangci-lint-action@v3 - with: - version: latest - args: --timeout=5m - - build: - runs-on: ubuntu-latest - needs: [test, lint] - - steps: - - name: Checkout code - uses: actions/checkout@v4 - - - name: Set up Go - uses: actions/setup-go@v5 - with: - go-version: '1.23' - cache-dependency-path: go.sum - - - name: Download dependencies - run: go mod download - - - name: Build - run: go build -v -o server ./cmd/server - - - name: Upload build artifacts - uses: actions/upload-artifact@v3 - with: - name: build-artifacts - path: server - diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..118c7a4 --- /dev/null +++ b/Dockerfile @@ -0,0 +1,61 @@ +# ==================== 构建阶段 ==================== +FROM golang:latest AS builder + +# 安装构建依赖 +RUN apk add --no-cache git ca-certificates tzdata + +# 设置工作目录 +WORKDIR /build + +# 复制依赖文件 +COPY go.mod go.sum ./ + +# 配置 Go 代理并下载依赖 +ENV GOPROXY=https://goproxy.cn,direct +RUN go mod download + +# 复制源代码 +COPY . . + +# 构建应用 +RUN CGO_ENABLED=0 GOOS=linux GOARCH=amd64 go build \ + -ldflags="-w -s -X main.Version=$(git describe --tags --always --dirty 2>/dev/null || echo 'dev')" \ + -o server ./cmd/server + +# ==================== 运行阶段 ==================== +FROM alpine:3.19 + +# 安装运行时依赖 +RUN apk add --no-cache ca-certificates tzdata + +# 设置时区 +ENV TZ=Asia/Shanghai + +# 创建非 root 用户 +RUN adduser -D -g '' appuser + +# 设置工作目录 +WORKDIR /app + +# 从构建阶段复制二进制文件 +COPY --from=builder /build/server . + +# 复制配置文件目录结构 +COPY --from=builder /build/configs ./configs + +# 设置文件权限 +RUN chown -R appuser:appuser /app + +# 切换到非 root 用户 +USER appuser + +# 暴露端口 +EXPOSE 8080 + +# 健康检查 +HEALTHCHECK --interval=30s --timeout=10s --start-period=5s --retries=3 \ + CMD wget --no-verbose --tries=1 --spider http://localhost:8080/api/health || exit 1 + +# 启动应用 +ENTRYPOINT ["./server"] + diff --git a/docker-compose.yml b/docker-compose.yml new file mode 100644 index 0000000..843629b --- /dev/null +++ b/docker-compose.yml @@ -0,0 +1,177 @@ +version: '3.8' + +services: + # ==================== 应用服务 ==================== + app: + build: + context: . + dockerfile: Dockerfile + image: carrotskin/backend:latest + container_name: carrotskin-backend + restart: unless-stopped + ports: + - "${APP_PORT:-8080}:8080" + environment: + # 服务器配置 + - SERVER_PORT=8080 + - SERVER_MODE=${SERVER_MODE:-release} + - SERVER_BASE_PATH=${SERVER_BASE_PATH:-} + # 公开访问地址 (用于生成回调URL、邮件链接等) + - PUBLIC_URL=${PUBLIC_URL:-http://localhost:8080} + # 数据库配置 + - DB_HOST=postgres + - DB_PORT=5432 + - DB_USER=carrotskin + - DB_PASSWORD=${DB_PASSWORD:-carrotskin123} + - DB_NAME=carrotskin + - DB_SSLMODE=disable + # Redis 配置 + - REDIS_HOST=redis + - REDIS_PORT=6379 + - REDIS_PASSWORD=${REDIS_PASSWORD:-} + - REDIS_DB=0 + # JWT 配置 + - JWT_SECRET=${JWT_SECRET:-your-super-secret-jwt-key-change-in-production} + - JWT_EXPIRE_HOURS=24 + # 存储配置 (RustFS S3兼容) + - RUSTFS_ENDPOINT=${RUSTFS_ENDPOINT:-rustfs:9000} + - RUSTFS_PUBLIC_URL=${RUSTFS_PUBLIC_URL:-http://localhost:9000} + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin123} + - RUSTFS_USE_SSL=${RUSTFS_USE_SSL:-false} + - RUSTFS_BUCKET_TEXTURES=${RUSTFS_BUCKET_TEXTURES:-carrotskin} + - RUSTFS_BUCKET_AVATARS=${RUSTFS_BUCKET_AVATARS:-carrotskin} + # 邮件配置 (可选) + - SMTP_HOST=${SMTP_HOST:-} + - SMTP_PORT=${SMTP_PORT:-587} + - SMTP_USER=${SMTP_USER:-} + - SMTP_PASSWORD=${SMTP_PASSWORD:-} + - SMTP_FROM=${SMTP_FROM:-} + depends_on: + postgres: + condition: service_healthy + redis: + condition: service_healthy + networks: + - carrotskin-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:8080/api/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + + # ==================== PostgreSQL 数据库 ==================== + postgres: + image: postgres:16-alpine + container_name: carrotskin-postgres + restart: unless-stopped + environment: + - POSTGRES_USER=carrotskin + - POSTGRES_PASSWORD=${DB_PASSWORD:-carrotskin123} + - POSTGRES_DB=carrotskin + - PGDATA=/var/lib/postgresql/data/pgdata + volumes: + - postgres-data:/var/lib/postgresql/data + ports: + - "5432:5432" + networks: + - carrotskin-network + healthcheck: + test: ["CMD-SHELL", "pg_isready -U carrotskin -d carrotskin"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 10s + + # ==================== Redis 缓存 ==================== + redis: + image: redis:7-alpine + container_name: carrotskin-redis + restart: unless-stopped + command: > + redis-server + --appendonly yes + --maxmemory 256mb + --maxmemory-policy allkeys-lru + ${REDIS_PASSWORD:+--requirepass ${REDIS_PASSWORD}} + volumes: + - redis-data:/data + ports: + - "6379:6379" + networks: + - carrotskin-network + healthcheck: + test: ["CMD", "redis-cli", "ping"] + interval: 10s + timeout: 5s + retries: 5 + start_period: 5s + + # ==================== RustFS 对象存储 (可选) ==================== + rustfs: + image: ghcr.io/rustfs/rustfs:latest + container_name: carrotskin-rustfs + restart: unless-stopped + command: > + server + --address 0.0.0.0:9000 + --console-address 0.0.0.0:9001 + --access-key ${RUSTFS_ACCESS_KEY:-rustfsadmin} + --secret-key ${RUSTFS_SECRET_KEY:-rustfsadmin123} + --data /data + volumes: + - rustfs-data:/data + ports: + - "9000:9000" # S3 API 端口 + - "9001:9001" # 控制台端口 + networks: + - carrotskin-network + healthcheck: + test: ["CMD", "wget", "--no-verbose", "--tries=1", "--spider", "http://localhost:9000/health"] + interval: 30s + timeout: 10s + retries: 3 + start_period: 10s + profiles: + - storage # 使用 --profile storage 启动 + + # RustFS 初始化服务 - 自动创建存储桶 + rustfs-init: + image: minio/mc:latest + container_name: carrotskin-rustfs-init + depends_on: + rustfs: + condition: service_healthy + entrypoint: > + /bin/sh -c " + echo '等待 RustFS 启动...'; + sleep 5; + mc alias set myrustfs http://rustfs:9000 $${RUSTFS_ACCESS_KEY} $${RUSTFS_SECRET_KEY}; + mc mb myrustfs/$${RUSTFS_BUCKET} --ignore-existing; + mc anonymous set download myrustfs/$${RUSTFS_BUCKET}; + echo '存储桶 $${RUSTFS_BUCKET} 创建完成,已设置公开读取权限'; + " + environment: + - RUSTFS_ACCESS_KEY=${RUSTFS_ACCESS_KEY:-rustfsadmin} + - RUSTFS_SECRET_KEY=${RUSTFS_SECRET_KEY:-rustfsadmin123} + - RUSTFS_BUCKET=${RUSTFS_BUCKET_TEXTURES:-carrotskin} + networks: + - carrotskin-network + profiles: + - storage + +# ==================== 数据卷 ==================== +volumes: + postgres-data: + driver: local + redis-data: + driver: local + rustfs-data: + driver: local + +# ==================== 网络 ==================== +networks: + carrotskin-network: + driver: bridge + diff --git a/internal/handler/texture_handler.go b/internal/handler/texture_handler.go index e352c30..c7a5184 100644 --- a/internal/handler/texture_handler.go +++ b/internal/handler/texture_handler.go @@ -4,7 +4,6 @@ import ( "carrotskin/internal/model" "carrotskin/internal/service" "carrotskin/internal/types" - "carrotskin/pkg/config" "carrotskin/pkg/database" "carrotskin/pkg/logger" "carrotskin/pkg/storage" @@ -38,11 +37,9 @@ func GenerateTextureUploadURL(c *gin.Context) { } storageClient := storage.MustGetClient() - cfg := *config.MustGetRustFSConfig() result, err := service.GenerateTextureUploadURL( c.Request.Context(), storageClient, - cfg, userID, req.FileName, string(req.TextureType), diff --git a/internal/handler/user_handler.go b/internal/handler/user_handler.go index 0feb627..c6144a4 100644 --- a/internal/handler/user_handler.go +++ b/internal/handler/user_handler.go @@ -3,7 +3,6 @@ package handler import ( "carrotskin/internal/service" "carrotskin/internal/types" - "carrotskin/pkg/config" "carrotskin/pkg/database" "carrotskin/pkg/logger" "carrotskin/pkg/redis" @@ -140,8 +139,7 @@ func GenerateAvatarUploadURL(c *gin.Context) { } storageClient := storage.MustGetClient() - cfg := *config.MustGetRustFSConfig() - result, err := service.GenerateAvatarUploadURL(c.Request.Context(), storageClient, cfg, userID, req.FileName) + result, err := service.GenerateAvatarUploadURL(c.Request.Context(), storageClient, userID, req.FileName) if err != nil { logger.MustGetLogger().Error("生成头像上传URL失败", zap.Int64("user_id", userID), diff --git a/internal/service/upload_service.go b/internal/service/upload_service.go index 3ebaeaa..4678872 100644 --- a/internal/service/upload_service.go +++ b/internal/service/upload_service.go @@ -1,7 +1,6 @@ package service import ( - "carrotskin/pkg/config" "carrotskin/pkg/storage" "context" "fmt" @@ -76,7 +75,7 @@ func ValidateFileName(fileName string, fileType FileType) error { } // GenerateAvatarUploadURL 生成头像上传URL -func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.StorageClient, cfg config.RustFSConfig, userID int64, fileName string) (*storage.PresignedPostPolicyResult, error) { +func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.StorageClient, userID int64, fileName string) (*storage.PresignedPostPolicyResult, error) { // 1. 验证文件名 if err := ValidateFileName(fileName, FileTypeAvatar); err != nil { return nil, err @@ -96,7 +95,7 @@ func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.Storage timestamp := time.Now().Format("20060102150405") objectName := fmt.Sprintf("user_%d/%s_%s", userID, timestamp, fileName) - // 5. 生成预签名POST URL + // 5. 生成预签名POST URL (使用存储客户端内置的 PublicURL) result, err := storageClient.GeneratePresignedPostURL( ctx, bucketName, @@ -104,8 +103,6 @@ func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.Storage uploadConfig.MinSize, uploadConfig.MaxSize, uploadConfig.Expires, - cfg.UseSSL, - cfg.Endpoint, ) if err != nil { return nil, fmt.Errorf("生成上传URL失败: %w", err) @@ -115,7 +112,7 @@ func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.Storage } // GenerateTextureUploadURL 生成材质上传URL -func GenerateTextureUploadURL(ctx context.Context, storageClient *storage.StorageClient, cfg config.RustFSConfig, userID int64, fileName, textureType string) (*storage.PresignedPostPolicyResult, error) { +func GenerateTextureUploadURL(ctx context.Context, storageClient *storage.StorageClient, userID int64, fileName, textureType string) (*storage.PresignedPostPolicyResult, error) { // 1. 验证文件名 if err := ValidateFileName(fileName, FileTypeTexture); err != nil { return nil, err @@ -141,7 +138,7 @@ func GenerateTextureUploadURL(ctx context.Context, storageClient *storage.Storag textureTypeFolder := strings.ToLower(textureType) objectName := fmt.Sprintf("user_%d/%s/%s_%s", userID, textureTypeFolder, timestamp, fileName) - // 6. 生成预签名POST URL + // 6. 生成预签名POST URL (使用存储客户端内置的 PublicURL) result, err := storageClient.GeneratePresignedPostURL( ctx, bucketName, @@ -149,8 +146,6 @@ func GenerateTextureUploadURL(ctx context.Context, storageClient *storage.Storag uploadConfig.MinSize, uploadConfig.MaxSize, uploadConfig.Expires, - cfg.UseSSL, - cfg.Endpoint, ) if err != nil { return nil, fmt.Errorf("生成上传URL失败: %w", err) diff --git a/pkg/config/config.go b/pkg/config/config.go index 1cf5dd8..919e4c3 100644 --- a/pkg/config/config.go +++ b/pkg/config/config.go @@ -59,6 +59,7 @@ type RedisConfig struct { // RustFSConfig RustFS对象存储配置 (S3兼容) type RustFSConfig struct { Endpoint string `mapstructure:"endpoint"` + PublicURL string `mapstructure:"public_url"` // 公开访问URL (用于生成文件访问链接) AccessKey string `mapstructure:"access_key"` SecretKey string `mapstructure:"secret_key"` UseSSL bool `mapstructure:"use_ssl"` @@ -159,6 +160,7 @@ func setDefaults() { // RustFS默认配置 viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000") + viper.SetDefault("rustfs.public_url", "") // 为空时使用 endpoint 构建 URL viper.SetDefault("rustfs.use_ssl", false) // JWT默认配置 @@ -214,6 +216,7 @@ func setupEnvMappings() { // RustFS配置 viper.BindEnv("rustfs.endpoint", "RUSTFS_ENDPOINT") + viper.BindEnv("rustfs.public_url", "RUSTFS_PUBLIC_URL") viper.BindEnv("rustfs.access_key", "RUSTFS_ACCESS_KEY") viper.BindEnv("rustfs.secret_key", "RUSTFS_SECRET_KEY") viper.BindEnv("rustfs.use_ssl", "RUSTFS_USE_SSL") diff --git a/pkg/storage/minio.go b/pkg/storage/minio.go index a3277f2..01755b9 100644 --- a/pkg/storage/minio.go +++ b/pkg/storage/minio.go @@ -13,8 +13,9 @@ import ( // StorageClient S3兼容对象存储客户端包装 (支持RustFS、MinIO等) type StorageClient struct { - client *minio.Client - buckets map[string]string + client *minio.Client + buckets map[string]string + publicURL string // 公开访问URL前缀 } // NewStorage 创建新的对象存储客户端 (S3兼容,支持RustFS) @@ -41,9 +42,21 @@ func NewStorage(cfg config.RustFSConfig) (*StorageClient, error) { } } + // 构建公开访问URL + publicURL := cfg.PublicURL + if publicURL == "" { + // 如果未配置 PublicURL,使用 Endpoint 构建 + protocol := "http" + if cfg.UseSSL { + protocol = "https" + } + publicURL = fmt.Sprintf("%s://%s", protocol, cfg.Endpoint) + } + storageClient := &StorageClient{ - client: client, - buckets: cfg.Buckets, + client: client, + buckets: cfg.Buckets, + publicURL: publicURL, } return storageClient, nil @@ -81,7 +94,7 @@ type PresignedPostPolicyResult struct { // GeneratePresignedPostURL 生成预签名POST URL (支持表单上传) // 注意:使用时必须确保file字段是表单的最后一个字段 -func (s *StorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration, useSSL bool, endpoint string) (*PresignedPostPolicyResult, error) { +func (s *StorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName, objectName string, minSize, maxSize int64, expires time.Duration) (*PresignedPostPolicyResult, error) { // 创建上传策略 policy := minio.NewPostPolicy() @@ -105,12 +118,8 @@ func (s *StorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName // 注意:在Go中直接delete不存在的key是安全的 delete(formData, "bucket") - // 构造文件的永久访问URL - protocol := "http" - if useSSL { - protocol = "https" - } - fileURL := fmt.Sprintf("%s://%s/%s/%s", protocol, endpoint, bucketName, objectName) + // 使用配置的公开访问URL构造文件的永久访问URL + fileURL := s.BuildFileURL(bucketName, objectName) return &PresignedPostPolicyResult{ PostURL: postURL.String(), @@ -118,3 +127,13 @@ func (s *StorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName FileURL: fileURL, }, nil } + +// BuildFileURL 构建文件的公开访问URL +func (s *StorageClient) BuildFileURL(bucketName, objectName string) string { + return fmt.Sprintf("%s/%s/%s", s.publicURL, bucketName, objectName) +} + +// GetPublicURL 获取公开访问URL前缀 +func (s *StorageClient) GetPublicURL() string { + return s.publicURL +}