From d0937c2c35a3a8a134886367d4ed8ec5a72ce43b Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 25 May 2026 12:56:58 +0800 Subject: [PATCH 1/3] =?UTF-8?q?feat:=20=E6=B7=BB=E5=8A=A0=E4=B8=83?= =?UTF-8?q?=E7=89=9B=E4=BA=91=20SDK=20=E4=BE=9D=E8=B5=96=E4=B8=8E=20QiniuC?= =?UTF-8?q?onfig=20=E9=85=8D=E7=BD=AE?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - go.mod 新增 github.com/qiniu/go-sdk/v7 v7.26.12 - config.go 新增 QiniuConfig 结构体,绑定 GEN2D_QINIU_* 环境变量 - .env.example 补充七牛云环境变量模板 --- backend/.env.example | 7 +++++++ backend/go.mod | 7 +++++++ backend/go.sum | 16 ++++++++++++++++ backend/internal/config/config.go | 22 ++++++++++++++++++++++ 4 files changed, 52 insertions(+) diff --git a/backend/.env.example b/backend/.env.example index 97bf0b8..3bdd87e 100644 --- a/backend/.env.example +++ b/backend/.env.example @@ -28,3 +28,10 @@ GEN2D_IMAGE_HEIGHT=1024 GEN2D_IMAGE_NUM_IMAGES=1 GEN2D_IMAGE_STEPS=30 GEN2D_IMAGE_CFG_SCALE=7.0 + +# 七牛云对象存储 +GEN2D_QINIU_ACCESS_KEY=your-access-key +GEN2D_QINIU_SECRET_KEY=your-secret-key +GEN2D_QINIU_BUCKET=your-bucket-name +GEN2D_QINIU_CDN_HOST=https://cdn.example.com +GEN2D_QINIU_USE_HTTPS=true diff --git a/backend/go.mod b/backend/go.mod index 8b21941..0a2d76c 100644 --- a/backend/go.mod +++ b/backend/go.mod @@ -7,6 +7,7 @@ require ( github.com/gin-gonic/gin v1.12.0 github.com/golang-jwt/jwt/v5 v5.3.1 github.com/joho/godotenv v1.5.1 + github.com/qiniu/go-sdk/v7 v7.26.12 github.com/spf13/viper v1.21.0 golang.org/x/crypto v0.48.0 gorm.io/driver/sqlite v1.6.0 @@ -14,6 +15,8 @@ require ( ) require ( + github.com/BurntSushi/toml v1.3.2 // indirect + github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 // indirect github.com/bahlo/generic-list-go v0.2.0 // indirect github.com/buger/jsonparser v1.1.1 // indirect github.com/bytedance/gopkg v0.1.3 // indirect @@ -24,6 +27,7 @@ require ( github.com/eino-contrib/jsonschema v1.0.3 // indirect github.com/fsnotify/fsnotify v1.9.0 // indirect github.com/gabriel-vasile/mimetype v1.4.12 // indirect + github.com/gammazero/toposort v0.1.1 // indirect github.com/gin-contrib/sse v1.1.0 // indirect github.com/go-playground/locales v0.14.1 // indirect github.com/go-playground/universal-translator v0.18.1 // indirect @@ -31,6 +35,7 @@ require ( github.com/go-viper/mapstructure/v2 v2.4.0 // indirect github.com/goccy/go-json v0.10.5 // indirect github.com/goccy/go-yaml v1.19.2 // indirect + github.com/gofrs/flock v0.8.1 // indirect github.com/google/uuid v1.6.0 // indirect github.com/goph/emperror v0.17.2 // indirect github.com/jinzhu/inflection v1.0.0 // indirect @@ -65,8 +70,10 @@ require ( golang.org/x/arch v0.22.0 // indirect golang.org/x/exp v0.0.0-20230713183714-613f0c0eb8a1 // indirect golang.org/x/net v0.51.0 // indirect + golang.org/x/sync v0.19.0 // indirect golang.org/x/sys v0.41.0 // indirect golang.org/x/text v0.34.0 // indirect google.golang.org/protobuf v1.36.10 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect + modernc.org/fileutil v1.0.0 // indirect ) diff --git a/backend/go.sum b/backend/go.sum index e6265f5..9ea4cb0 100644 --- a/backend/go.sum +++ b/backend/go.sum @@ -1,4 +1,8 @@ +github.com/BurntSushi/toml v1.3.2 h1:o7IhLm0Msx3BaB+n3Ag7L8EVlByGnpq14C4YWiu/gL8= +github.com/BurntSushi/toml v1.3.2/go.mod h1:CxXYINrC8qIiEnFrOxCa7Jy5BFHlXnUU2pbicEuybxQ= github.com/airbrake/gobrake v3.6.1+incompatible/go.mod h1:wM4gu3Cn0W0K7GUuVWnlXZU11AGBXMILnrdOU8Kn00o= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82 h1:7dONQ3WNZ1zy960TmkxJPuwoolZwL7xKtpcM04MBnt4= +github.com/alex-ant/gomath v0.0.0-20160516115720-89013a210a82/go.mod h1:nLnM0KdK1CmygvjpDUO6m1TjSsiQtL61juhNsvV/JVI= github.com/bahlo/generic-list-go v0.2.0 h1:5sz/EEAK+ls5wF+NeqDpk5+iNdMDXrh3z3nPnH1Wvgk= github.com/bahlo/generic-list-go v0.2.0/go.mod h1:2KvAjgMlE5NNynlg/5iLrrCCZ2+5xWbdbCW3pNTGyYg= github.com/bitly/go-simplejson v0.5.0/go.mod h1:cXHtHw4XUPsvGaxgjIAn8PhEWG9NfngEKAMDJEczWVA= @@ -32,6 +36,8 @@ github.com/fsnotify/fsnotify v1.9.0 h1:2Ml+OJNzbYCTzsxtv8vKSFD9PbJjmhYF14k/jKC7S github.com/fsnotify/fsnotify v1.9.0/go.mod h1:8jBTzvmWwFyi3Pb8djgCCO5IBqzKJ/Jwo8TRcHyHii0= github.com/gabriel-vasile/mimetype v1.4.12 h1:e9hWvmLYvtp846tLHam2o++qitpguFiYCKbn0w9jyqw= github.com/gabriel-vasile/mimetype v1.4.12/go.mod h1:d+9Oxyo1wTzWdyVUPMmXFvp4F9tea18J8ufA774AB3s= +github.com/gammazero/toposort v0.1.1 h1:OivGxsWxF3U3+U80VoLJ+f50HcPU1MIqE1JlKzoJ2Eg= +github.com/gammazero/toposort v0.1.1/go.mod h1:H2cozTnNpMw0hg2VHAYsAxmkHXBYroNangj2NTBQDvw= github.com/getsentry/raven-go v0.2.0/go.mod h1:KungGk8q33+aIAZUIVWZDr2OfAEBsO49PX4NzFV5kcQ= github.com/gin-contrib/sse v1.1.0 h1:n0w2GMuUpWDVp7qSpvze6fAu9iRxJY4Hmj6AmBOU05w= github.com/gin-contrib/sse v1.1.0/go.mod h1:hxRZ5gVpWMT7Z0B0gSNYqqsSCNIJMjzvm6fqCz9vjwM= @@ -53,6 +59,8 @@ github.com/goccy/go-json v0.10.5 h1:Fq85nIqj+gXn/S5ahsiTlK3TmC85qgirsdTP/+DeaC4= github.com/goccy/go-json v0.10.5/go.mod h1:oq7eo15ShAhp70Anwd5lgX2pLfOS3QCiwU/PULtXL6M= github.com/goccy/go-yaml v1.19.2 h1:PmFC1S6h8ljIz6gMRBopkjP1TVT7xuwrButHID66PoM= github.com/goccy/go-yaml v1.19.2/go.mod h1:XBurs7gK8ATbW4ZPGKgcbrY1Br56PdM69F7LkFRi1kA= +github.com/gofrs/flock v0.8.1 h1:+gYjHKf32LDeiEEFhQaotPbLuUXjY5ZqxKgXy7n59aw= +github.com/gofrs/flock v0.8.1/go.mod h1:F1TvTiK9OcQqauNUHlbJvyl9Qa1QvF/gOUDKA14jxHU= github.com/gofrs/uuid v3.2.0+incompatible/go.mod h1:b2aQJv3Z4Fp6yNu3cdSllBxTCLRxnplIgP/c0N/04lM= github.com/golang-jwt/jwt/v5 v5.3.1 h1:kYf81DTWFe7t+1VvL7eS+jKFVWaUnK9cB1qbwn63YCY= github.com/golang-jwt/jwt/v5 v5.3.1/go.mod h1:fxCRLWMO43lRc8nhHWY6LGqRcf+1gQWArsqaEUEa5bE= @@ -66,6 +74,8 @@ github.com/goph/emperror v0.17.2 h1:yLapQcmEsO0ipe9p5TaN22djm3OFV/TfM/fcYP0/J18= github.com/goph/emperror v0.17.2/go.mod h1:+ZbQ+fUNO/6FNiUo0ujtMjhgad9Xa6fQL9KhH4LNHic= github.com/gopherjs/gopherjs v1.17.2 h1:fQnZVsXk8uxXIStYb0N4bGk7jeyTalG/wsZjQ25dO0g= github.com/gopherjs/gopherjs v1.17.2/go.mod h1:pRRIvn/QzFLrKfvEz3qUuEhtE/zLCWfreZ6J5gM2i+k= +github.com/gorilla/mux v1.8.1 h1:TuBL49tXwgrFYWhqrNgrUNEY92u81SPhu7sTdzQEiWY= +github.com/gorilla/mux v1.8.1/go.mod h1:AKf9I4AEqPTmMytcMc0KkNouC66V3BtZ4qD5fmWSiMQ= github.com/hpcloud/tail v1.0.0/go.mod h1:ab1qPbhIpdTxEkNHXyeSf5vhxWSCs/tWer42PpOxQnU= github.com/jinzhu/inflection v1.0.0 h1:K317FqzuhWc8YvSVlFMCCUb36O/S9MCKRDI7QkRKD/E= github.com/jinzhu/inflection v1.0.0/go.mod h1:h+uFLlag+Qp1Va5pdKtLDYj+kHp5pxUVkryuEj+Srlc= @@ -118,6 +128,8 @@ github.com/pkg/errors v0.9.1 h1:FEBLx1zS214owpjy7qsBeixbURkuhQAwrK5UwLGTwt4= github.com/pkg/errors v0.9.1/go.mod h1:bwawxfHBFNV+L2hUp1rHADufV3IMtnDRdf1r5NINEl0= github.com/pmezard/go-difflib v1.0.0 h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM= github.com/pmezard/go-difflib v1.0.0/go.mod h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4= +github.com/qiniu/go-sdk/v7 v7.26.12 h1:AnWiKjBY62XpULoB/MySKdNmlUo9S9pQB7/0s7QueCo= +github.com/qiniu/go-sdk/v7 v7.26.12/go.mod h1:ri7fGwbio0pRDFr8EK5TUpx0DbnpIMJ2bMSDxGWfCbk= github.com/quic-go/qpack v0.6.0 h1:g7W+BMYynC1LbYLSqRt8PBg5Tgwxn214ZZR34VIOjz8= github.com/quic-go/qpack v0.6.0/go.mod h1:lUpLKChi8njB4ty2bFLX2x4gzDqXwUpaO1DP9qMDZII= github.com/quic-go/quic-go v0.59.0 h1:OLJkp1Mlm/aS7dpKgTc6cnpynnD2Xg7C1pwL6vy/SAw= @@ -189,6 +201,8 @@ golang.org/x/net v0.0.0-20180906233101-161cd47e91fd/go.mod h1:mL1N/T3taQHkDXs73r golang.org/x/net v0.51.0 h1:94R/GTO7mt3/4wIKpcR5gkGmRLOuE/2hNGeWq/GBIFo= golang.org/x/net v0.51.0/go.mod h1:aamm+2QF5ogm02fjy5Bb7CQ0WMt1/WVM7FtyaTLlA9Y= golang.org/x/sync v0.0.0-20180314180146-1d60e4601c6f/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= +golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= +golang.org/x/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI= golang.org/x/sys v0.0.0-20180905080454-ebe1bf3edb33/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180909124046-d0be0721c37e/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20220715151400-c0bba94af5f8/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= @@ -215,3 +229,5 @@ gorm.io/driver/sqlite v1.6.0 h1:WHRRrIiulaPiPFmDcod6prc4l2VGVWHz80KspNsxSfQ= gorm.io/driver/sqlite v1.6.0/go.mod h1:AO9V1qIQddBESngQUKWL9yoH93HIeA1X6V633rBwyT8= gorm.io/gorm v1.31.1 h1:7CA8FTFz/gRfgqgpeKIBcervUn3xSyPUmr6B2WXJ7kg= gorm.io/gorm v1.31.1/go.mod h1:XyQVbO2k6YkOis7C2437jSit3SsDK72s7n7rsSHd+Gs= +modernc.org/fileutil v1.0.0 h1:Z1AFLZwl6BO8A5NldQg/xTSjGLetp+1Ubvl4alfGx8w= +modernc.org/fileutil v1.0.0/go.mod h1:JHsWpkrk/CnVV1H/eGlFf85BEpfkrp56ro8nojIq9Q8= diff --git a/backend/internal/config/config.go b/backend/internal/config/config.go index 62359f3..38d90b2 100644 --- a/backend/internal/config/config.go +++ b/backend/internal/config/config.go @@ -14,6 +14,7 @@ type Config struct { JWT JWTConfig `mapstructure:"jwt"` LLM LLMConfig `mapstructure:"llm"` ImageGen ImageGenConfig `mapstructure:"image_gen"` + Qiniu QiniuConfig `mapstructure:"qiniu"` } // ServerConfig HTTP 服务配置。 @@ -55,6 +56,15 @@ type ImageGenConfig struct { CFGScale float64 `mapstructure:"cfg_scale"` } +// QiniuConfig 七牛云对象存储配置。 +type QiniuConfig struct { + AccessKey string `mapstructure:"access_key"` + SecretKey string `mapstructure:"secret_key"` + Bucket string `mapstructure:"bucket"` + CDNHost string `mapstructure:"cdn_host"` + UseHTTPS bool `mapstructure:"use_https"` +} + // Load 从 YAML 配置文件和环境变量加载配置。 // 优先级:环境变量 > YAML 文件 > 默认值。 func Load() *Config { @@ -111,6 +121,12 @@ func setDefaults(v *viper.Viper) { v.SetDefault("image_gen.num_images", 1) v.SetDefault("image_gen.steps", 30) v.SetDefault("image_gen.cfg_scale", 7.0) + + v.SetDefault("qiniu.access_key", "") + v.SetDefault("qiniu.secret_key", "") + v.SetDefault("qiniu.bucket", "") + v.SetDefault("qiniu.cdn_host", "") + v.SetDefault("qiniu.use_https", true) } func bindEnvVars(v *viper.Viper) { @@ -135,4 +151,10 @@ func bindEnvVars(v *viper.Viper) { v.BindEnv("image_gen.num_images", "GEN2D_IMAGE_NUM_IMAGES") v.BindEnv("image_gen.steps", "GEN2D_IMAGE_STEPS") v.BindEnv("image_gen.cfg_scale", "GEN2D_IMAGE_CFG_SCALE") + + v.BindEnv("qiniu.access_key", "GEN2D_QINIU_ACCESS_KEY") + v.BindEnv("qiniu.secret_key", "GEN2D_QINIU_SECRET_KEY") + v.BindEnv("qiniu.bucket", "GEN2D_QINIU_BUCKET") + v.BindEnv("qiniu.cdn_host", "GEN2D_QINIU_CDN_HOST") + v.BindEnv("qiniu.use_https", "GEN2D_QINIU_USE_HTTPS") } From 30a46ff74b7e24e293ccd6509cc76ea317ea8371 Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 25 May 2026 12:57:13 +0800 Subject: [PATCH 2/3] =?UTF-8?q?feat:=20=E4=B8=83=E7=89=9B=E4=BA=91?= =?UTF-8?q?=E5=AD=98=E5=82=A8=E6=9C=8D=E5=8A=A1=E4=B8=8E=E7=B4=A0=E6=9D=90?= =?UTF-8?q?=E4=B8=8B=E8=BD=BD=E6=8E=A5=E5=8F=A3?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - service/storage.go: StorageService 提供 Upload/GetDownloadURL/Delete - handler/storage.go: GET /api/v1/assets/download 重定向到 CDN URL - main.go: 注入 StorageService 并注册下载路由 --- backend/cmd/main.go | 9 +++- backend/internal/handler/storage.go | 35 +++++++++++++ backend/internal/service/storage.go | 78 +++++++++++++++++++++++++++++ 3 files changed, 120 insertions(+), 2 deletions(-) create mode 100644 backend/internal/handler/storage.go create mode 100644 backend/internal/service/storage.go 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)) +} From f61508a24964b51ade22b86bc4bf5b08a4d7e165 Mon Sep 17 00:00:00 2001 From: wonder Date: Mon, 25 May 2026 12:57:41 +0800 Subject: [PATCH 3/3] =?UTF-8?q?docs:=20=E6=9B=B4=E6=96=B0=E6=9E=B6?= =?UTF-8?q?=E6=9E=84=E6=96=87=E6=A1=A3=EF=BC=8C=E6=96=87=E4=BB=B6=E5=AD=98?= =?UTF-8?q?=E5=82=A8=E5=88=87=E6=8D=A2=E4=B8=BA=E4=B8=83=E7=89=9B=E4=BA=91?= =?UTF-8?q?=E5=AF=B9=E8=B1=A1=E5=AD=98=E5=82=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - _index.md: 技术栈加入七牛云对象存储 - api.md: 新增素材下载接口,asset URL 改为 CDN URL - backend.md: 分层结构新增 storage.go,FormatAdapter 职责增加上传 - database.md: 文件存储改为七牛云,配置表新增 GEN2D_QINIU_* 环境变量 --- docs/_index.md | 4 ++-- docs/api.md | 18 ++++++++++++++++-- docs/backend.md | 8 +++++--- docs/database.md | 47 ++++++++++++++++++++--------------------------- 4 files changed, 43 insertions(+), 34 deletions(-) diff --git a/docs/_index.md b/docs/_index.md index 2526eb6..b1a5453 100644 --- a/docs/_index.md +++ b/docs/_index.md @@ -4,13 +4,13 @@ gen2d — AI 驱动的 2D 游戏素材生成工具。通过文本提示词生成 ## 技术栈 -Go + Gin + Eino / Vite + React + TypeScript + zustand / SQLite / 可替换 AI 推理模型 +Go + Gin + Eino / Vite + React + TypeScript + zustand / SQLite / 七牛云对象存储 / 可替换 AI 推理模型 ## 文档索引 - [后端工程](backend.md) — 分层结构、管线设计(PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter)、风格模型 - [前端工程](frontend.md) — 组件树、状态管理、路由、WebSocket 通信 - [异步任务](async-tasks.md) — 任务队列、状态机、并发控制、失败重试 -- [数据存储](database.md) — SQLite 选型、表结构、本地文件存储 +- [数据存储](database.md) — SQLite 选型、表结构、七牛云对象存储 - [API 设计](api.md) — 接口列表、请求/响应示例、实现状态 - [预设风格键](style-keys.md) — 美术风格、色调、线条等风格键分类与可选值 diff --git a/docs/api.md b/docs/api.md index 74bf155..c06ffa7 100644 --- a/docs/api.md +++ b/docs/api.md @@ -300,7 +300,7 @@ POST /api/v1/prompt/optimize ### DELETE /api/v1/projects/:projectId -级联删除工程下的所有任务、素材及本地文件。 +级联删除工程下的所有任务、素材及七牛云对象。 响应: @@ -372,6 +372,7 @@ POST /api/v1/prompt/optimize | [ ] | GET | `/api/v1/tasks/:taskId` | 查询任务状态与进度 | | [ ] | GET | `/api/v1/tasks/:taskId/assets` | 获取生成结果(素材列表 + 元数据) | | [ ] | WS | `/api/v1/tasks/:taskId/ws` | WebSocket 实时进度推送 | +| [x] | GET | `/api/v1/assets/download` | 下载素材(重定向到 CDN) | ### POST /api/v1/generate @@ -483,7 +484,7 @@ POST /api/v1/prompt/optimize "assets": [ { "id": "asset_001", - "url": "users/user_a1B2c3D4/projects/proj_abc123/tasks/task_xyz789/output/spritesheet.png", + "url": "https://cdn.example.com/users/user_a1B2c3D4/projects/proj_abc123/tasks/task_xyz789/output/spritesheet.png", "format": "png", "width": 256, "height": 64, @@ -499,6 +500,19 @@ POST /api/v1/prompt/optimize } ``` +### GET /api/v1/assets/download + +需认证。返回 302 重定向到七牛云 CDN URL。 + +| 参数 | 类型 | 说明 | +|------|------|------| +| `key` | string | 对象存储 Key,查询参数 | + +响应: +- HTTP 302,`Location` 头指向 CDN URL +- 400:缺少 key 参数 +- 500:生成下载链接失败 + ### WebSocket 消息格式 连接路径:`ws://host/api/v1/tasks/:taskId/ws`(通过 httpOnly Cookie 自动携带认证信息) diff --git a/docs/backend.md b/docs/backend.md index 9a50263..947357e 100644 --- a/docs/backend.md +++ b/docs/backend.md @@ -16,7 +16,8 @@ backend/internal/ │ ├── login.go # [已有] 用户登录(handler 骨架 + 参数校验) │ ├── project.go # 工程 CRUD + 任务列表 │ ├── generate.go # 生成任务提交 / 查询 / WebSocket -│ └── project_style.go # 工程风格 CRUD +│ ├── project_style.go # 工程风格 CRUD +│ └── storage.go # [已有] 素材下载(重定向到七牛云 CDN) ├── service/ # 业务逻辑层 │ ├── pipeline.go # [已有] Eino compose.Graph 编排:PromptOptimizer → AssetGenerator → QualitySupervisor → FormatAdapter │ ├── nodes.go # [已有] 管线四个节点的实现(每个节点单一职责) @@ -24,7 +25,8 @@ backend/internal/ │ ├── inference.go # [已有] AI 推理 API 调用封装(当前为 mock 实现) │ ├── pipeline_test.go # [已有] 管线测试(happy path / 重试 / 降级 / 风格合并) │ ├── auth.go # 用户认证:注册、登录、JWT 签发与校验 -│ └── project_style.go # 工程风格管理 & 风格合并逻辑 +│ ├── project_style.go # 工程风格管理 & 风格合并逻辑 +│ └── storage.go # [已有] 七牛云对象存储:上传、下载 URL 生成、删除 ├── model/ # 数据模型 / DTO │ ├── response.go # [已有] 统一响应 │ ├── user.go # [已有] 用户模型 @@ -106,7 +108,7 @@ finalStyle = merge(projectStyle.KVPairs, taskStyle.KVPairs) - **输入**:通过质检的图片 + 素材类型 - **输出**:游戏引擎可用的素材文件 + 元数据 -- **职责**:格式转换、spritesheet 打包、元数据生成。 +- **职责**:格式转换、spritesheet 打包、元数据生成、上传素材到七牛云对象存储。 - **示例**:输入 4 张 64×64 的角色行走帧 PNG → 输出一张 256×64 的 spritesheet + JSON 元数据(帧尺寸、帧数、锚点偏移),可直接拖入 Unity 2D Animation 使用。 --- diff --git a/docs/database.md b/docs/database.md index 1658709..c5b7589 100644 --- a/docs/database.md +++ b/docs/database.md @@ -5,7 +5,7 @@ | 用途 | 方案 | 说明 | |------|------|------| | 持久化存储 | SQLite 3 | 工程、任务、素材元数据、风格配置全部落库,单文件部署 | -| 文件存储 | 本地文件系统 | 生成的图片与 spritesheet 文件保存到本地目录,通过 Gin 静态文件服务访问 | +| 文件存储 | 七牛云对象存储 | 生成的图片与 spritesheet 文件上传到七牛云 Bucket,通过 CDN URL 访问 | | 认证 | JWT + Cookie | 简单注册登录,httpOnly Cookie 鉴权 | | 缓存 / 去重 | 应用层内存 | 请求去重通过应用层 `sync.Map` 实现短期去重窗口,无需外部依赖 | @@ -133,44 +133,33 @@ task 1 ── N asset ## 文件存储 -生成的素材文件保存到本地文件系统,通过 Gin 静态文件服务访问。 +生成的素材文件上传到七牛云对象存储,通过 CDN URL 访问。 -### 目录结构 +### 存储结构 + +对象 Key 命名规则(与原有本地路径保持一致): ``` -{dataDir}/ -└── users/ - └── {userId}/ - └── projects/ - └── {projectId}/ - └── tasks/ - └── {taskId}/ - ├── raw/ # AssetGenerator 输出的原始图片 - │ ├── 0.png - │ ├── 1.png - │ └── ... - ├── output/ # FormatAdapter 输出的最终素材 - │ ├── spritesheet.png - │ └── metadata.json - └── pipeline.log # 管线执行日志 +users/{userId}/projects/{projectId}/tasks/{taskId}/output/{filename} ``` -`dataDir` 通过环境变量 `GEN2D_DATA_DIR` 配置,默认 `./data`。 +示例: +``` +users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png +``` ### 访问方式 -Gin 静态文件服务,路由 `/files/*` 映射到 `{dataDir}/` 目录: +素材上传后返回 CDN URL,前端直接通过该 URL 访问图片。 -```go -router.Static("/files", dataDir) -``` +下载接口 `GET /api/v1/assets/download?key=...` 返回 302 重定向到 CDN URL。 ### asset 表的 url 字段 -存储相对路径,前端通过 `/files/{url}` 拼接访问: +存储七牛云 CDN 完整 URL,如: +`https://cdn.example.com/users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png` -- 如 `users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png` -- 前端拼接为 `/files/users/user_a1B2c3/projects/proj_abc/tasks/task_xyz/output/spritesheet.png` +前端直接使用该 URL 加载图片,无需拼接路径。 --- @@ -194,9 +183,13 @@ router.Static("/files", dataDir) |----------|--------|------| | `GEN2D_SERVER_PORT` | `8080` | HTTP 服务监听端口 | | `GEN2D_DB_PATH` | `./data/gen2d.db` | SQLite 数据库文件路径 | -| `GEN2D_DATA_DIR` | `./data` | 本地文件存储根目录 | | `GEN2D_JWT_SECRET` | — | JWT 签名密钥(必填) | | `GEN2D_JWT_EXPIRE` | `7200` | Token 过期时间(秒) | +| `GEN2D_QINIU_ACCESS_KEY` | — | 七牛云 AccessKey | +| `GEN2D_QINIU_SECRET_KEY` | — | 七牛云 SecretKey | +| `GEN2D_QINIU_BUCKET` | — | 七牛云 Bucket 名称 | +| `GEN2D_QINIU_CDN_HOST` | — | CDN 域名(如 `https://cdn.example.com`) | +| `GEN2D_QINIU_USE_HTTPS` | `true` | 是否使用 HTTPS | 在 `config.go` 中扩展字段即可,无需额外依赖。