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/model"
|
||||||
"carrotskin/internal/service"
|
"carrotskin/internal/service"
|
||||||
"carrotskin/internal/types"
|
"carrotskin/internal/types"
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"carrotskin/pkg/database"
|
"carrotskin/pkg/database"
|
||||||
"carrotskin/pkg/logger"
|
"carrotskin/pkg/logger"
|
||||||
"carrotskin/pkg/storage"
|
"carrotskin/pkg/storage"
|
||||||
@@ -38,11 +37,9 @@ func GenerateTextureUploadURL(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
storageClient := storage.MustGetClient()
|
storageClient := storage.MustGetClient()
|
||||||
cfg := *config.MustGetRustFSConfig()
|
|
||||||
result, err := service.GenerateTextureUploadURL(
|
result, err := service.GenerateTextureUploadURL(
|
||||||
c.Request.Context(),
|
c.Request.Context(),
|
||||||
storageClient,
|
storageClient,
|
||||||
cfg,
|
|
||||||
userID,
|
userID,
|
||||||
req.FileName,
|
req.FileName,
|
||||||
string(req.TextureType),
|
string(req.TextureType),
|
||||||
|
|||||||
@@ -3,7 +3,6 @@ package handler
|
|||||||
import (
|
import (
|
||||||
"carrotskin/internal/service"
|
"carrotskin/internal/service"
|
||||||
"carrotskin/internal/types"
|
"carrotskin/internal/types"
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"carrotskin/pkg/database"
|
"carrotskin/pkg/database"
|
||||||
"carrotskin/pkg/logger"
|
"carrotskin/pkg/logger"
|
||||||
"carrotskin/pkg/redis"
|
"carrotskin/pkg/redis"
|
||||||
@@ -140,8 +139,7 @@ func GenerateAvatarUploadURL(c *gin.Context) {
|
|||||||
}
|
}
|
||||||
|
|
||||||
storageClient := storage.MustGetClient()
|
storageClient := storage.MustGetClient()
|
||||||
cfg := *config.MustGetRustFSConfig()
|
result, err := service.GenerateAvatarUploadURL(c.Request.Context(), storageClient, userID, req.FileName)
|
||||||
result, err := service.GenerateAvatarUploadURL(c.Request.Context(), storageClient, cfg, userID, req.FileName)
|
|
||||||
if err != nil {
|
if err != nil {
|
||||||
logger.MustGetLogger().Error("生成头像上传URL失败",
|
logger.MustGetLogger().Error("生成头像上传URL失败",
|
||||||
zap.Int64("user_id", userID),
|
zap.Int64("user_id", userID),
|
||||||
|
|||||||
@@ -1,7 +1,6 @@
|
|||||||
package service
|
package service
|
||||||
|
|
||||||
import (
|
import (
|
||||||
"carrotskin/pkg/config"
|
|
||||||
"carrotskin/pkg/storage"
|
"carrotskin/pkg/storage"
|
||||||
"context"
|
"context"
|
||||||
"fmt"
|
"fmt"
|
||||||
@@ -76,7 +75,7 @@ func ValidateFileName(fileName string, fileType FileType) error {
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateAvatarUploadURL 生成头像上传URL
|
// 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. 验证文件名
|
// 1. 验证文件名
|
||||||
if err := ValidateFileName(fileName, FileTypeAvatar); err != nil {
|
if err := ValidateFileName(fileName, FileTypeAvatar); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -96,7 +95,7 @@ func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.Storage
|
|||||||
timestamp := time.Now().Format("20060102150405")
|
timestamp := time.Now().Format("20060102150405")
|
||||||
objectName := fmt.Sprintf("user_%d/%s_%s", userID, timestamp, fileName)
|
objectName := fmt.Sprintf("user_%d/%s_%s", userID, timestamp, fileName)
|
||||||
|
|
||||||
// 5. 生成预签名POST URL
|
// 5. 生成预签名POST URL (使用存储客户端内置的 PublicURL)
|
||||||
result, err := storageClient.GeneratePresignedPostURL(
|
result, err := storageClient.GeneratePresignedPostURL(
|
||||||
ctx,
|
ctx,
|
||||||
bucketName,
|
bucketName,
|
||||||
@@ -104,8 +103,6 @@ func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.Storage
|
|||||||
uploadConfig.MinSize,
|
uploadConfig.MinSize,
|
||||||
uploadConfig.MaxSize,
|
uploadConfig.MaxSize,
|
||||||
uploadConfig.Expires,
|
uploadConfig.Expires,
|
||||||
cfg.UseSSL,
|
|
||||||
cfg.Endpoint,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("生成上传URL失败: %w", err)
|
return nil, fmt.Errorf("生成上传URL失败: %w", err)
|
||||||
@@ -115,7 +112,7 @@ func GenerateAvatarUploadURL(ctx context.Context, storageClient *storage.Storage
|
|||||||
}
|
}
|
||||||
|
|
||||||
// GenerateTextureUploadURL 生成材质上传URL
|
// 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. 验证文件名
|
// 1. 验证文件名
|
||||||
if err := ValidateFileName(fileName, FileTypeTexture); err != nil {
|
if err := ValidateFileName(fileName, FileTypeTexture); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
@@ -141,7 +138,7 @@ func GenerateTextureUploadURL(ctx context.Context, storageClient *storage.Storag
|
|||||||
textureTypeFolder := strings.ToLower(textureType)
|
textureTypeFolder := strings.ToLower(textureType)
|
||||||
objectName := fmt.Sprintf("user_%d/%s/%s_%s", userID, textureTypeFolder, timestamp, fileName)
|
objectName := fmt.Sprintf("user_%d/%s/%s_%s", userID, textureTypeFolder, timestamp, fileName)
|
||||||
|
|
||||||
// 6. 生成预签名POST URL
|
// 6. 生成预签名POST URL (使用存储客户端内置的 PublicURL)
|
||||||
result, err := storageClient.GeneratePresignedPostURL(
|
result, err := storageClient.GeneratePresignedPostURL(
|
||||||
ctx,
|
ctx,
|
||||||
bucketName,
|
bucketName,
|
||||||
@@ -149,8 +146,6 @@ func GenerateTextureUploadURL(ctx context.Context, storageClient *storage.Storag
|
|||||||
uploadConfig.MinSize,
|
uploadConfig.MinSize,
|
||||||
uploadConfig.MaxSize,
|
uploadConfig.MaxSize,
|
||||||
uploadConfig.Expires,
|
uploadConfig.Expires,
|
||||||
cfg.UseSSL,
|
|
||||||
cfg.Endpoint,
|
|
||||||
)
|
)
|
||||||
if err != nil {
|
if err != nil {
|
||||||
return nil, fmt.Errorf("生成上传URL失败: %w", err)
|
return nil, fmt.Errorf("生成上传URL失败: %w", err)
|
||||||
|
|||||||
@@ -59,6 +59,7 @@ type RedisConfig struct {
|
|||||||
// RustFSConfig RustFS对象存储配置 (S3兼容)
|
// RustFSConfig RustFS对象存储配置 (S3兼容)
|
||||||
type RustFSConfig struct {
|
type RustFSConfig struct {
|
||||||
Endpoint string `mapstructure:"endpoint"`
|
Endpoint string `mapstructure:"endpoint"`
|
||||||
|
PublicURL string `mapstructure:"public_url"` // 公开访问URL (用于生成文件访问链接)
|
||||||
AccessKey string `mapstructure:"access_key"`
|
AccessKey string `mapstructure:"access_key"`
|
||||||
SecretKey string `mapstructure:"secret_key"`
|
SecretKey string `mapstructure:"secret_key"`
|
||||||
UseSSL bool `mapstructure:"use_ssl"`
|
UseSSL bool `mapstructure:"use_ssl"`
|
||||||
@@ -159,6 +160,7 @@ func setDefaults() {
|
|||||||
|
|
||||||
// RustFS默认配置
|
// RustFS默认配置
|
||||||
viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000")
|
viper.SetDefault("rustfs.endpoint", "127.0.0.1:9000")
|
||||||
|
viper.SetDefault("rustfs.public_url", "") // 为空时使用 endpoint 构建 URL
|
||||||
viper.SetDefault("rustfs.use_ssl", false)
|
viper.SetDefault("rustfs.use_ssl", false)
|
||||||
|
|
||||||
// JWT默认配置
|
// JWT默认配置
|
||||||
@@ -214,6 +216,7 @@ func setupEnvMappings() {
|
|||||||
|
|
||||||
// RustFS配置
|
// RustFS配置
|
||||||
viper.BindEnv("rustfs.endpoint", "RUSTFS_ENDPOINT")
|
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.access_key", "RUSTFS_ACCESS_KEY")
|
||||||
viper.BindEnv("rustfs.secret_key", "RUSTFS_SECRET_KEY")
|
viper.BindEnv("rustfs.secret_key", "RUSTFS_SECRET_KEY")
|
||||||
viper.BindEnv("rustfs.use_ssl", "RUSTFS_USE_SSL")
|
viper.BindEnv("rustfs.use_ssl", "RUSTFS_USE_SSL")
|
||||||
|
|||||||
@@ -13,8 +13,9 @@ import (
|
|||||||
|
|
||||||
// StorageClient S3兼容对象存储客户端包装 (支持RustFS、MinIO等)
|
// StorageClient S3兼容对象存储客户端包装 (支持RustFS、MinIO等)
|
||||||
type StorageClient struct {
|
type StorageClient struct {
|
||||||
client *minio.Client
|
client *minio.Client
|
||||||
buckets map[string]string
|
buckets map[string]string
|
||||||
|
publicURL string // 公开访问URL前缀
|
||||||
}
|
}
|
||||||
|
|
||||||
// NewStorage 创建新的对象存储客户端 (S3兼容,支持RustFS)
|
// 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{
|
storageClient := &StorageClient{
|
||||||
client: client,
|
client: client,
|
||||||
buckets: cfg.Buckets,
|
buckets: cfg.Buckets,
|
||||||
|
publicURL: publicURL,
|
||||||
}
|
}
|
||||||
|
|
||||||
return storageClient, nil
|
return storageClient, nil
|
||||||
@@ -81,7 +94,7 @@ type PresignedPostPolicyResult struct {
|
|||||||
|
|
||||||
// GeneratePresignedPostURL 生成预签名POST URL (支持表单上传)
|
// GeneratePresignedPostURL 生成预签名POST URL (支持表单上传)
|
||||||
// 注意:使用时必须确保file字段是表单的最后一个字段
|
// 注意:使用时必须确保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()
|
policy := minio.NewPostPolicy()
|
||||||
|
|
||||||
@@ -105,12 +118,8 @@ func (s *StorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName
|
|||||||
// 注意:在Go中直接delete不存在的key是安全的
|
// 注意:在Go中直接delete不存在的key是安全的
|
||||||
delete(formData, "bucket")
|
delete(formData, "bucket")
|
||||||
|
|
||||||
// 构造文件的永久访问URL
|
// 使用配置的公开访问URL构造文件的永久访问URL
|
||||||
protocol := "http"
|
fileURL := s.BuildFileURL(bucketName, objectName)
|
||||||
if useSSL {
|
|
||||||
protocol = "https"
|
|
||||||
}
|
|
||||||
fileURL := fmt.Sprintf("%s://%s/%s/%s", protocol, endpoint, bucketName, objectName)
|
|
||||||
|
|
||||||
return &PresignedPostPolicyResult{
|
return &PresignedPostPolicyResult{
|
||||||
PostURL: postURL.String(),
|
PostURL: postURL.String(),
|
||||||
@@ -118,3 +127,13 @@ func (s *StorageClient) GeneratePresignedPostURL(ctx context.Context, bucketName
|
|||||||
FileURL: fileURL,
|
FileURL: fileURL,
|
||||||
}, nil
|
}, 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