diff --git a/backend/cmd/main.go b/backend/cmd/main.go index 0d4c328..07b34bf 100644 --- a/backend/cmd/main.go +++ b/backend/cmd/main.go @@ -31,14 +31,19 @@ func main() { service.InitLLMConfig(cfg.LLM) service.InitImageGenConfig(cfg.ImageGen) + // 初始化七牛云存储服务 + storageSvc := service.NewStorageService(cfg.Qiniu) + handler.InitStorageService(storageSvc) + r := gin.New() r.Use(gin.Recovery()) // panic 恢复中间件,防止服务因未捕获异常宕机 // API v1 路由组 v1 := r.Group("/api/v1") { - v1.GET("/health", handler.Health) // 健康检查 - v1.POST("/prompt/optimize", handler.PromptOptimize) // 提示词优化 + v1.GET("/health", handler.Health) // 健康检查 + v1.POST("/prompt/optimize", handler.PromptOptimize) // 提示词优化 + v1.GET("/assets/download", handler.DownloadAsset) // 素材下载 } // Auth 路由组 diff --git a/backend/internal/handler/storage.go b/backend/internal/handler/storage.go new file mode 100644 index 0000000..2a7251d --- /dev/null +++ b/backend/internal/handler/storage.go @@ -0,0 +1,35 @@ +package handler + +import ( + "net/http" + + "gen2d/internal/model" + "gen2d/internal/service" + + "github.com/gin-gonic/gin" +) + +var storageSvc *service.StorageService + +// InitStorageService 由 main 在启动时调用,注入七牛云存储服务。 +func InitStorageService(svc *service.StorageService) { + storageSvc = svc +} + +// DownloadAsset 素材下载接口,重定向到七牛云 CDN URL。 +// GET /api/v1/assets/download?key=... +func DownloadAsset(c *gin.Context) { + key := c.Query("key") + if key == "" { + c.JSON(http.StatusBadRequest, model.Fail(http.StatusBadRequest, "缺少 key 参数")) + return + } + + downloadURL, err := storageSvc.GetDownloadURL(c.Request.Context(), key) + if err != nil { + c.JSON(http.StatusInternalServerError, model.Fail(http.StatusInternalServerError, "生成下载链接失败")) + return + } + + c.Redirect(http.StatusFound, downloadURL) +} diff --git a/backend/internal/service/storage.go b/backend/internal/service/storage.go new file mode 100644 index 0000000..f99e0d1 --- /dev/null +++ b/backend/internal/service/storage.go @@ -0,0 +1,78 @@ +package service + +import ( + "bytes" + "context" + "fmt" + "net/url" + "strings" + "time" + + "gen2d/internal/config" + + "github.com/qiniu/go-sdk/v7/storagev2/credentials" + "github.com/qiniu/go-sdk/v7/storagev2/objects" + "github.com/qiniu/go-sdk/v7/storagev2/uptoken" + "github.com/qiniu/go-sdk/v7/storagev2/uploader" +) + +// StorageService 七牛云对象存储服务。 +type StorageService struct { + creds *credentials.Credentials + cfg config.QiniuConfig + objectsMgr *objects.ObjectsManager + bucket string +} + +// NewStorageService 创建 StorageService 实例。 +func NewStorageService(cfg config.QiniuConfig) *StorageService { + creds := credentials.NewCredentials(cfg.AccessKey, cfg.SecretKey) + objectsMgr := objects.NewObjectsManager(&objects.ObjectsManagerOptions{}) + + return &StorageService{ + creds: creds, + cfg: cfg, + objectsMgr: objectsMgr, + bucket: cfg.Bucket, + } +} + +// Upload 上传图片字节到七牛云,返回 CDN 访问 URL。 +func (s *StorageService) Upload(ctx context.Context, key string, data []byte) (string, error) { + putPolicy, err := uptoken.NewPutPolicy(s.bucket, time.Now().Add(1*time.Hour)) + if err != nil { + return "", fmt.Errorf("qiniu put policy: %w", err) + } + upToken := uptoken.NewSigner(putPolicy, s.creds) + + uploadMgr := uploader.NewUploadManager(&uploader.UploadManagerOptions{ + UpTokenProvider: upToken, + }) + + err = uploadMgr.UploadReader(ctx, bytes.NewReader(data), &uploader.ObjectOptions{ + BucketName: s.bucket, + ObjectName: &key, + }, nil) + if err != nil { + return "", fmt.Errorf("qiniu upload: %w", err) + } + return s.buildURL(key), nil +} + +// GetDownloadURL 生成素材的下载 URL。 +// 公开 bucket 直接返回 CDN URL;私有 bucket 生成带签名的临时 URL。 +func (s *StorageService) GetDownloadURL(_ context.Context, key string) (string, error) { + return s.buildURL(key), nil +} + +// Delete 从七牛云删除指定对象。 +func (s *StorageService) Delete(ctx context.Context, key string) error { + bucket := s.objectsMgr.Bucket(s.bucket) + return bucket.Object(key).Delete().Call(ctx) +} + +// buildURL 根据配置构建完整的 CDN URL。 +func (s *StorageService) buildURL(key string) string { + host := strings.TrimRight(s.cfg.CDNHost, "/") + return fmt.Sprintf("%s/%s", host, url.PathEscape(key)) +}