feat(m8): P1 MinIO 对象存储与文件管理

- 迁移 000023 gateway.file_objects(personal/system 归属隔离 + 部分索引)
- internal/platform/storage:minio-go 适配(端点 scheme 剥离、流式 PutObject/Open/Delete)
- internal/workbench/files.go:FileService(sha256 校验、PutObject-then-insert 回滚、delete 先删行再删对象)
- admin /api/v1/admin/files + portal /api/v1/portal/files 处理器(流式上传下载、Content-Disposition)
- RBAC file:read/file:manage;菜单加文件管理 + 门户文件仓库
- compose 增 minio 服务(S3_* anchor、不暴露端口);nginx client_max_body_size 32m→256m
- 管理端文件管理页 + 门户个人文件仓;集成测试 TestFileObjectLifecycle 连真 MinIO 通过
- healthz object_storage:true;README/PRODUCTION/进展文档同步

Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
ben
2026-08-12 14:08:38 +08:00
parent 5759c1862e
commit 6708c226a5
26 changed files with 1180 additions and 40 deletions
+47 -12
View File
@@ -11,18 +11,19 @@ import (
)
type Config struct {
Environment string
Server Server
Database Database
Redis Redis
Security Security
Auth Auth
Credentials Credentials
Upstream Upstream
Audit Audit
Outbox Outbox
RuntimeData RuntimeData
Shadow Shadow
Environment string
Server Server
Database Database
Redis Redis
Security Security
Auth Auth
Credentials Credentials
Upstream Upstream
Audit Audit
Outbox Outbox
RuntimeData RuntimeData
Shadow Shadow
ObjectStorage ObjectStorage
}
type Server struct {
@@ -114,6 +115,16 @@ type Shadow struct {
MaxConcurrent int
}
type ObjectStorage struct {
Endpoint string
AccessKeyID string
SecretAccessKey string
Bucket string
Region string
UseSSL bool
MaxFileBytes int64
}
func Load() (Config, error) {
cfg := Config{
Environment: env("APP_ENV", "local"),
@@ -182,6 +193,15 @@ func Load() (Config, error) {
Timeout: duration("SHADOW_TIMEOUT", 20*time.Second), MaxBodyBytes: int64Value("SHADOW_MAX_BODY_BYTES", 2<<20),
MaxConcurrent: intValue("SHADOW_MAX_CONCURRENT", 16),
},
ObjectStorage: ObjectStorage{
Endpoint: strings.TrimRight(env("S3_ENDPOINT", "http://minio:9000"), "/"),
AccessKeyID: env("S3_ACCESS_KEY_ID", "gateway"),
SecretAccessKey: env("S3_SECRET_ACCESS_KEY", "gateway-secret"),
Bucket: env("S3_BUCKET", "gateway-files"),
Region: env("S3_REGION", "us-east-1"),
UseSSL: boolValue("S3_USE_SSL", false),
MaxFileBytes: int64Value("S3_MAX_FILE_BYTES", 128<<20),
},
}
return cfg, cfg.Validate()
@@ -236,6 +256,21 @@ func (c Config) Validate() error {
errs = append(errs, errors.New("SHADOW_API_KEY is required when SHADOW_BASE_URL is set"))
}
}
if err := validateHTTPURL(c.ObjectStorage.Endpoint); err != nil {
errs = append(errs, fmt.Errorf("S3_ENDPOINT: %w", err))
} else if endpointURL, parseErr := url.Parse(c.ObjectStorage.Endpoint); parseErr == nil && endpointURL.Path != "" {
// minio-go 的 Endpoint 不接受带路径的完整 URL(报 "fully qualified paths")。
errs = append(errs, errors.New("S3_ENDPOINT must not contain a path"))
}
if c.ObjectStorage.AccessKeyID == "" || c.ObjectStorage.SecretAccessKey == "" {
errs = append(errs, errors.New("S3_ACCESS_KEY_ID and S3_SECRET_ACCESS_KEY are required"))
}
if c.ObjectStorage.Bucket == "" || len(c.ObjectStorage.Bucket) > 63 {
errs = append(errs, errors.New("S3_BUCKET must be a non-empty bucket name of at most 63 characters"))
}
if c.ObjectStorage.MaxFileBytes < 1<<20 || c.ObjectStorage.MaxFileBytes > 512<<20 {
errs = append(errs, errors.New("S3_MAX_FILE_BYTES must be between 1 MiB and 512 MiB"))
}
return errors.Join(errs...)
}