refactor: 移除不必要的配置依赖,简化上传URL生成逻辑并添加公开访问URL支持

This commit is contained in:
lan
2025-12-02 11:22:14 +08:00
parent 13bab28926
commit 23be1c563d
12 changed files with 478 additions and 173 deletions

76
.dockerignore Normal file
View 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
View 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=

View 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

View File

@@ -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

View File

@@ -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
View 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
View 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

View File

@@ -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),

View File

@@ -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),

View File

@@ -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)

View File

@@ -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")

View File

@@ -15,6 +15,7 @@ import (
type StorageClient struct {
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,
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
}