refactor: 移除不必要的配置依赖,简化上传URL生成逻辑并添加公开访问URL支持
This commit is contained in:
76
.dockerignore
Normal file
76
.dockerignore
Normal file
@@ -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/
|
||||
|
||||
47
.env.docker.example
Normal file
47
.env.docker.example
Normal file
@@ -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=
|
||||
79
.gitea/workflows/docker.yml
Normal file
79
.gitea/workflows/docker.yml
Normal file
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -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
|
||||
|
||||
61
Dockerfile
Normal file
61
Dockerfile
Normal file
@@ -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"]
|
||||
|
||||
177
docker-compose.yml
Normal file
177
docker-compose.yml
Normal file
@@ -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
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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),
|
||||
|
||||
@@ -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)
|
||||
|
||||
@@ -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")
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user