feat(storage): 完善对象存储配置与过期策略

补齐 OSS/S3 的 Endpoint、Region、Bucket、CDN、对象前缀和签名有效期配置,并为生成结果与请求素材自动维护分级生命周期规则。普通上传继续保持永久,私有资源按配置生成限时签名 URL,管理端连接测试覆盖生命周期、上传、读取和删除。\n\n新增可重复的真实 OSS 验收脚本,凭据仅从本地环境读取,接口响应继续保持脱敏。\n\n验证:Go 全量测试、迁移安全检查、pnpm lint、pnpm test、pnpm build、本地阿里云 OSS 真实上传下载删除验收。
This commit is contained in:
2026-08-04 12:44:24 +08:00
parent fe56aa46b9
commit 6a64d3936a
16 changed files with 1274 additions and 47 deletions
@@ -417,6 +417,8 @@ func (s *Server) ensureRequestAsset(ctx context.Context, decoded decodedRequestA
expiry := now.Add(time.Duration(s.localTempAssetTTLHours()) * time.Hour)
expiresAt = &expiry
localPath = requestAssetLocalPath(s.cfg.LocalUploadedStorageDir, stringFromRequestAny(upload["fileName"]))
} else {
expiresAt = requestAssetUploadExpiresAt(upload)
}
asset, err := s.store.UpsertRequestAsset(ctx, store.RequestAssetInput{
SHA256: sha,
@@ -576,7 +578,7 @@ func requestAssetStillUsable(asset store.RequestAsset, now time.Time) bool {
if asset.ExpiredAt != nil {
return false
}
if asset.ExpiresAt != nil && !asset.ExpiresAt.After(now) {
if asset.ExpiresAt != nil && !asset.ExpiresAt.After(now.Add(30*time.Second)) {
return false
}
if strings.EqualFold(strings.TrimSpace(asset.StorageProvider), "local_static") {
@@ -592,6 +594,25 @@ func requestAssetStillUsable(asset store.RequestAsset, now time.Time) bool {
return strings.TrimSpace(asset.URL) != ""
}
func requestAssetUploadExpiresAt(upload map[string]any) *time.Time {
var earliest *time.Time
for _, key := range []string{"urlExpiresAt", "objectExpiresAt"} {
value := strings.TrimSpace(stringFromRequestAny(upload[key]))
if value == "" {
continue
}
parsed, err := time.Parse(time.RFC3339, value)
if err != nil {
continue
}
if earliest == nil || parsed.Before(*earliest) {
candidate := parsed
earliest = &candidate
}
}
return earliest
}
func requestAssetStorageProvider(upload map[string]any) string {
if channel, ok := upload["storageChannel"].(map[string]any); ok {
if provider := stringFromRequestAny(channel["provider"]); provider != "" {