Compare commits

...

15 Commits

Author SHA1 Message Date
zerosaturation
baf56d5ecf feat: 完善minimax调用图生图功能 2026-04-08 01:30:58 +08:00
zerosaturation
6a0f4cc26a fix: 前端拦截器支持 202 状态码
202 Accepted 用于异步任务创建(如图生图 job)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:58 +08:00
zerosaturation
575233b51f docs: 标记 Task 6 已完成(使用os.Getenv替代)
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
965c37f2ff feat: 前端改为轮询模式 2026-04-08 01:30:57 +08:00
zerosaturation
e834029f6a feat: 注册图生图 API 路由 2026-04-08 01:30:57 +08:00
zerosaturation
40d5e597f6 feat: 添加 ImageGeneration 和 GetImageJob 接口 2026-04-08 01:30:57 +08:00
zerosaturation
76a5eaaad9 fix: 修复解压炸弹漏洞和goroutine泄漏
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
f43b06e13d fix: 修复 minimax_service 安全隐患和数据竞争
- 添加 HTTP 响应状态码检查,防止状态错误被忽略
- 添加 HTTP Client 结构体复用,避免每次创建新客户端
- 在 compressImageIfNeeded 中使用带 Timeout 的 Client
- 添加 URL scheme 校验,防止 SSRF 攻击
- 使用 LimitedReader 限制下载图片大小(10MB)
- 修复 GetJob 数据竞争,复制数据后返回

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
d09f1122ce fix: minimax_service 修复编译错误
- 使用 resize.Lanczos3 替代不存在的 resize.Lanczos
- 使用 os.Getenv 替代缺失的 config 方法

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
ed2acc3268 feat: 添加 MiniMax 图生图服务和任务管理
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
8ec1942e74 feat: 添加图生图 DTO
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
4bae8e9b64 fix: plan 添加 net/url import
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
9ca072b463 docs: 添加 MiniMax 图生图 API 实现计划
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
286db74837 docs: 更新 MiniMax 设计文档,修复审核问题
- 修复时间戳不一致问题,统一使用毫秒时间戳
- 添加 CompletedAt 字段
- 添加 JobStatus 枚举类型
- 添加 HTTP 状态码表格
- 添加 SSRF 防护说明
- 添加轮询间隔(3秒)和超时(120秒)建议
- 添加任务清理机制(24h过期)

Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
zerosaturation
cf7af5cfa5 docs: 添加 MiniMax 图生图 API 异步集成设计文档
Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
2026-04-08 01:30:57 +08:00
14 changed files with 1689 additions and 68 deletions

View File

@ -48,3 +48,6 @@ OSS_TOKEN_EXPIRE_TIME=3600
ENV=development ENV=development
# 日志级别: debug, info, warn, error # 日志级别: debug, info, warn, error
LOG_LEVEL=info LOG_LEVEL=info
# ==================== MiniMax API Configuration ====================
MINIMAX_API_KEY=
MINIMAX_API_URL=https://api.minimaxi.com/v1/image_generation

View File

@ -31,11 +31,13 @@ import (
"go.uber.org/zap" "go.uber.org/zap"
"github.com/topfans/backend/pkg/logger" "github.com/topfans/backend/pkg/logger"
"github.com/topfans/backend/services/assetService/service"
) )
// AssetController 资产相关控制器 // AssetController 资产相关控制器
type AssetController struct { type AssetController struct {
assetService pbAsset.AssetService assetService pbAsset.AssetService
minimaxService service.MinimaxService
} }
// NewAssetController 创建资产控制器 // NewAssetController 创建资产控制器
@ -47,7 +49,8 @@ func NewAssetController(dubboClient *client.Client) (*AssetController, error) {
} }
return &AssetController{ return &AssetController{
assetService: assetService, assetService: assetService,
minimaxService: service.NewMinimaxService(),
}, nil }, nil
} }
@ -266,12 +269,12 @@ func (ctrl *AssetController) CreateMintOrder(c *gin.Context) {
// 调用 RPC // 调用 RPC
resp, err := ctrl.assetService.CreateMintOrder(ctx, &pbAsset.CreateMintOrderRequest{ resp, err := ctrl.assetService.CreateMintOrder(ctx, &pbAsset.CreateMintOrderRequest{
OrderId: req.OrderID, OrderId: req.OrderID,
Name: req.Name, Name: req.Name,
MaterialUrl: req.MaterialURL, // material_url 必填cover_url 由后端 AI 生成 MaterialUrl: req.MaterialURL, // material_url 必填cover_url 由后端 AI 生成
Description: req.Description, Description: req.Description,
Rarity: req.Rarity, Rarity: req.Rarity,
Tags: req.Tags, Tags: req.Tags,
MaterialType: req.MaterialType, MaterialType: req.MaterialType,
Event: req.Event, Event: req.Event,
}) })
@ -301,7 +304,7 @@ func (ctrl *AssetController) CreateMintOrder(c *gin.Context) {
// 转换响应 // 转换响应
data := dto.ConvertCreateMintOrderResponse(resp) data := dto.ConvertCreateMintOrderResponse(resp)
// 为 material_url 生成预签名 URL参考头像上传的方式返回带签名的 URL // 为 material_url 生成预签名 URL参考头像上传的方式返回带签名的 URL
if data != nil && data.Asset.MaterialURL != "" { if data != nil && data.Asset.MaterialURL != "" {
cfg := config.Load() cfg := config.Load()
@ -336,7 +339,7 @@ func (ctrl *AssetController) CreateMintOrder(c *gin.Context) {
) )
} }
} }
response.Success(c, data) response.Success(c, data)
} }
@ -480,10 +483,10 @@ func (ctrl *AssetController) GetMintOrder(c *gin.Context) {
// 转换响应,并生成预签名 URL // 转换响应,并生成预签名 URL
data := dto.ConvertGetMintOrderResponse(resp) data := dto.ConvertGetMintOrderResponse(resp)
// 加载 OSS 配置 // 加载 OSS 配置
cfg := config.Load() cfg := config.Load()
// 为资产信息生成预签名 URL如果存在 // 为资产信息生成预签名 URL如果存在
if data.Asset != nil { if data.Asset != nil {
// 生成预签名 URL 的函数 // 生成预签名 URL 的函数
@ -504,11 +507,11 @@ func (ctrl *AssetController) GetMintOrder(c *gin.Context) {
} }
} }
} }
// 使用 generatePresignedURL 生成预签名 URL // 使用 generatePresignedURL 生成预签名 URL
return ctrl.generatePresignedURL(cfg.OSS, ossKey, 3600) return ctrl.generatePresignedURL(cfg.OSS, ossKey, 3600)
} }
// 为 cover_url 生成预签名 URL // 为 cover_url 生成预签名 URL
if data.Asset.CoverURL != "" { if data.Asset.CoverURL != "" {
if signedURL, err := generatePresigned(data.Asset.CoverURL); err == nil { if signedURL, err := generatePresigned(data.Asset.CoverURL); err == nil {
@ -524,7 +527,7 @@ func (ctrl *AssetController) GetMintOrder(c *gin.Context) {
) )
} }
} }
// 为 material_url 生成预签名 URL // 为 material_url 生成预签名 URL
if data.Asset.MaterialURL != "" { if data.Asset.MaterialURL != "" {
if signedURL, err := generatePresigned(data.Asset.MaterialURL); err == nil { if signedURL, err := generatePresigned(data.Asset.MaterialURL); err == nil {
@ -537,7 +540,7 @@ func (ctrl *AssetController) GetMintOrder(c *gin.Context) {
} }
} }
} }
response.Success(c, data) response.Success(c, data)
} }
@ -1049,7 +1052,7 @@ func (ctrl *AssetController) extractOSSKeyFromURL(ossURL string) string {
// 如果不是完整 URL可能是 key 本身 // 如果不是完整 URL可能是 key 本身
return ossURL return ossURL
} }
// 使用 url.Parse 解析 URL会自动处理 URL 编码) // 使用 url.Parse 解析 URL会自动处理 URL 编码)
u, err := url.Parse(ossURL) u, err := url.Parse(ossURL)
if err != nil { if err != nil {
@ -1071,13 +1074,13 @@ func (ctrl *AssetController) extractOSSKeyFromURL(ossURL string) string {
} }
return "" return ""
} }
// 从 URL 路径中提取 keyurl.Parse 已经自动解码了 URL 编码) // 从 URL 路径中提取 keyurl.Parse 已经自动解码了 URL 编码)
key := strings.TrimPrefix(u.Path, "/") key := strings.TrimPrefix(u.Path, "/")
if key != "" { if key != "" {
return key return key
} }
return "" return ""
} }
@ -1180,12 +1183,12 @@ func (ctrl *AssetController) generatePresignedURL(
// 检查生成的预签名 URL 是否包含 security-token 参数 // 检查生成的预签名 URL 是否包含 security-token 参数
hasSecurityToken := strings.Contains(signedURL, "security-token") hasSecurityToken := strings.Contains(signedURL, "security-token")
urlPreview := signedURL urlPreview := signedURL
if len(signedURL) > 150 { if len(signedURL) > 150 {
urlPreview = signedURL[:150] + "..." urlPreview = signedURL[:150] + "..."
} }
tokenPreview := "" tokenPreview := ""
if cred.SecurityToken != nil && *cred.SecurityToken != "" { if cred.SecurityToken != nil && *cred.SecurityToken != "" {
token := *cred.SecurityToken token := *cred.SecurityToken
@ -1197,7 +1200,7 @@ func (ctrl *AssetController) generatePresignedURL(
} else { } else {
tokenPreview = "nil" tokenPreview = "nil"
} }
logger.Logger.Info("Presigned URL generated", logger.Logger.Info("Presigned URL generated",
zap.String("file_path", filePath), zap.String("file_path", filePath),
zap.String("signed_url_preview", urlPreview), zap.String("signed_url_preview", urlPreview),
@ -1407,3 +1410,31 @@ func (ctrl *AssetController) listOSSImageFiles(
return files, nil return files, nil
} }
// ImageGeneration 图生图(同步调用)
// @Summary 图生图
// @Description 调用 MiniMax 图生图 API
// @Tags assets
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.ImageGenerationRequest true "图生图请求"
// @Success 200 {object} response.Response
// @Router /api/v1/assets/mints/image/generation [post]
func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
var req dto.ImageGenerationRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "Invalid request: "+err.Error())
return
}
result, err := ctrl.minimaxService.GenerateImage(c.Request.Context(), &req)
if err != nil {
response.Error(c, 500, "Image generation failed: "+err.Error())
return
}
response.Success(c, gin.H{
"images": result.Images,
})
}

View File

@ -0,0 +1,34 @@
package dto
// ImageGenerationRequest MiniMax 图生图请求
type ImageGenerationRequest struct {
Model string `json:"model" binding:"required"`
Prompt string `json:"prompt" binding:"required"`
AspectRatio string `json:"aspect_ratio"`
SubjectReference []SubjectReference `json:"subject_reference"`
N int `json:"n"` // 1-4
}
type SubjectReference struct {
Type string `json:"type"`
ImageFile string `json:"image_file"`
}
// ImageJobResponse 图生图任务响应
type ImageJobResponse struct {
JobID string `json:"job_id"`
Status string `json:"status"`
Progress int `json:"progress"`
Images []string `json:"images,omitempty"`
ErrorMsg string `json:"error_msg,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
}
// ImageJobCreateResponse 创建任务响应
type ImageJobCreateResponse struct {
JobID string `json:"job_id"`
Status string `json:"status"`
CreatedAt int64 `json:"created_at"`
}

View File

@ -173,6 +173,7 @@ func SetupRouter(userClient *client.Client, socialClient *client.Client, assetCl
assets.POST("/mints", assetCtrl.CreateMintOrder) // 创建铸造订单 assets.POST("/mints", assetCtrl.CreateMintOrder) // 创建铸造订单
assets.GET("/mints/:order_id", assetCtrl.GetMintOrder) // 查询铸造订单状态 assets.GET("/mints/:order_id", assetCtrl.GetMintOrder) // 查询铸造订单状态
assets.DELETE("/mints/:order_id", assetCtrl.CancelMintOrder) // 取消铸造订单 assets.DELETE("/mints/:order_id", assetCtrl.CancelMintOrder) // 取消铸造订单
assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图
assets.GET("/me/items", assetCtrl.GetMyAssets) // 获取我的藏品列表 assets.GET("/me/items", assetCtrl.GetMyAssets) // 获取我的藏品列表
assets.GET("/:asset_id", assetCtrl.GetAsset) // 获取资产详情 assets.GET("/:asset_id", assetCtrl.GetAsset) // 获取资产详情
assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus) // 查询上链状态 assets.GET("/:asset_id/status", assetCtrl.GetAssetStatus) // 查询上链状态

View File

@ -6,6 +6,7 @@ require (
dubbo.apache.org/dubbo-go/v3 v3.3.1 dubbo.apache.org/dubbo-go/v3 v3.3.1
github.com/golang-jwt/jwt/v5 v5.3.0 github.com/golang-jwt/jwt/v5 v5.3.0
go.uber.org/zap v1.27.1 go.uber.org/zap v1.27.1
golang.org/x/crypto v0.46.0
google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b google.golang.org/genproto/googleapis/api v0.0.0-20251222181119-0a764e51fe1b
google.golang.org/protobuf v1.36.11 google.golang.org/protobuf v1.36.11
gorm.io/driver/postgres v1.6.0 gorm.io/driver/postgres v1.6.0
@ -90,7 +91,6 @@ require (
go.uber.org/atomic v1.10.0 // indirect go.uber.org/atomic v1.10.0 // indirect
go.uber.org/mock v0.5.0 // indirect go.uber.org/mock v0.5.0 // indirect
go.uber.org/multierr v1.10.0 // indirect go.uber.org/multierr v1.10.0 // indirect
golang.org/x/crypto v0.46.0 // indirect
golang.org/x/mod v0.30.0 // indirect golang.org/x/mod v0.30.0 // indirect
golang.org/x/net v0.47.0 // indirect golang.org/x/net v0.47.0 // indirect
golang.org/x/sync v0.19.0 // indirect golang.org/x/sync v0.19.0 // indirect

View File

@ -59,8 +59,11 @@ github.com/alecthomas/units v0.0.0-20190717042225-c3de453c63f4/go.mod h1:ybxpYRF
github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho= github.com/alecthomas/units v0.0.0-20190924025748-f65c72e2690d/go.mod h1:rBZYJk541a8SKzHPHnH3zbiI+7dagKZ0cgpgrD7Fyho=
github.com/alibaba/sentinel-golang v1.0.4 h1:i0wtMvNVdy7vM4DdzYrlC4r/Mpk1OKUUBurKKkWhEo8= github.com/alibaba/sentinel-golang v1.0.4 h1:i0wtMvNVdy7vM4DdzYrlC4r/Mpk1OKUUBurKKkWhEo8=
github.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk= github.com/alibaba/sentinel-golang v1.0.4/go.mod h1:Lag5rIYyJiPOylK8Kku2P+a23gdKMMqzQS7wTnjWEpk=
github.com/alibabacloud-go/debug v1.0.0/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg= github.com/alibabacloud-go/debug v1.0.1 h1:MsW9SmUtbb1Fnt3ieC6NNZi6aEwrXfDksD4QA6GSbPg=
github.com/alibabacloud-go/debug v1.0.1/go.mod h1:8gfgZCCAC3+SCzjWtY053FrOcd4/qlH6IHTI4QyICOc=
github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU= github.com/alibabacloud-go/tea v1.2.2 h1:aTsR6Rl3ANWPfqeQugPglfurloyBJY85eFy7Gc1+8oU=
github.com/alibabacloud-go/tea v1.2.2/go.mod h1:CF3vOzEMAG+bR4WOql8gc2G9H3EkH3ZLAQdpmpXMgwk=
github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o= github.com/alibabacloud-go/tea-utils v1.4.4 h1:lxCDvNCdTo9FaXKKq45+4vGETQUKNOW/qKTcX9Sk53o=
github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw= github.com/alibabacloud-go/tea-utils v1.4.4/go.mod h1:KNcT0oXlZZxOXINnZBs6YvgOd5aYp9U67G+E3R8fcQw=
github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk= github.com/aliyun/alibaba-cloud-sdk-go v1.61.18/go.mod h1:v8ESoHo4SyHmuB4b1tJqDHxfTGEciD+yhvOU/5s1Rfk=
@ -726,6 +729,7 @@ github.com/yuin/goldmark v1.1.27/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9de
github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.1.32/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74= github.com/yuin/goldmark v1.2.1/go.mod h1:3hX8gzYuyVAZsxl0MRgGTJEmQBFcNTphYh9decYSb74=
github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k= github.com/yuin/goldmark v1.3.5/go.mod h1:mwnBkeHKe2W/ZEtQ+71ViKU8L12m81fl3OWwC1Zlc8k=
github.com/yuin/goldmark v1.4.13/go.mod h1:6yULJ656Px+3vBD8DxQVa3kxgyrAnzto9xy5taEt/CY=
github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg= github.com/yusufpapurcu/wmi v1.2.2 h1:KBNDSne4vP5mbSWnJbO+51IMOXJB67QiYCSBrubbPRg=
github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0= github.com/yusufpapurcu/wmi v1.2.2/go.mod h1:SBZ9tNy3G9/m5Oi98Zks0QjeHVDvuK0qfxQmPyzfmi0=
go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU= go.etcd.io/bbolt v1.3.2/go.mod h1:IbVyRI1SCnLcuJnV2u8VeU0CEYM7e686BmAb1XKL+uU=
@ -803,6 +807,8 @@ golang.org/x/crypto v0.0.0-20190923035154-9ee001bba392/go.mod h1:/lpIB1dKB+9EgE3
golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI= golang.org/x/crypto v0.0.0-20191011191535-87dc89f01550/go.mod h1:yigFU9vqHzYiE8UmvKecakEJjdnWj3jj499lnFckfCI=
golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20200622213623-75b288015ac9/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto= golang.org/x/crypto v0.0.0-20201002170205-7f63de1d35b0/go.mod h1:LzIPMQfyMNhhGPhUkYOs5KpL4U8rLKemX1yGLhDgUto=
golang.org/x/crypto v0.0.0-20210921155107-089bfa567519/go.mod h1:GvvjBRRGRdwPK5ydBHafDWAxML/pGHZbMvKqRZ5+Abc=
golang.org/x/crypto v0.18.0/go.mod h1:R0j02AL6hcrfOiy9T4ZYp/rcWeMxM3L6QYxlOuEG1mg=
golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU= golang.org/x/crypto v0.46.0 h1:cKRW/pmt1pKAfetfu+RCEvjvZkA9RimPbh7bhFjGVBU=
golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0= golang.org/x/crypto v0.46.0/go.mod h1:Evb/oLKmMraqjZ2iQTwDwvCtJkczlDuTmdJXoZVzqU0=
golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA= golang.org/x/exp v0.0.0-20180321215751-8460e604b9de/go.mod h1:CJ0aWSM057203Lf6IL+f9T1iT9GByDxfZKAQTCR3kQA=
@ -843,6 +849,8 @@ golang.org/x/mod v0.1.1-0.20191107180719-034126e5016b/go.mod h1:QqPTAvyqsEbceGzB
golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.2.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.3.0/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA= golang.org/x/mod v0.4.2/go.mod h1:s0Qsj1ACt9ePp/hMypM3fl4fZqREWJwdYDEqhRiZZUA=
golang.org/x/mod v0.6.0-dev.0.20220419223038-86c51ed26bb4/go.mod h1:jJ57K6gSWd91VN4djpZkiMVwK6gcyfeH4XE8wZrZaV4=
golang.org/x/mod v0.8.0/go.mod h1:iBbtSCu2XBx23ZKBPSOrRkjjQPZFPuis4dIYUhu/chs=
golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk= golang.org/x/mod v0.30.0 h1:fDEXFVZ/fmCKProc/yAXXUijritrDzahmwwefnjoPFk=
golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc= golang.org/x/mod v0.30.0/go.mod h1:lAsf5O2EvJeSFMiBxXDki7sCgAxEUcZHXoXMKT4GJKc=
golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4= golang.org/x/net v0.0.0-20180724234803-3673e40ba225/go.mod h1:mL1N/T3taQHkDXs73rZJwtUhF3w3ftmwwsq0BUmARs4=
@ -889,6 +897,10 @@ golang.org/x/net v0.0.0-20210405180319-a5a99cb37ef4/go.mod h1:p54w0d4576C0XHj96b
golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8= golang.org/x/net v0.0.0-20210410081132-afb366fc7cd1/go.mod h1:9tjilg8BloeKEkVJvy7fQ90B1CfIiPueXVOjqfkSzI8=
golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20210525063256-abc453219eb5/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y= golang.org/x/net v0.0.0-20211105192438-b53810dc28af/go.mod h1:9nx3DQGgdP8bBQD5qxJ1jj9UTztislL4KSBs9R2vV5Y=
golang.org/x/net v0.0.0-20220722155237-a158d28d115b/go.mod h1:XRhObCWvk6IyKnWLug+ECip1KBveYUHfp+8e9klMJ9c=
golang.org/x/net v0.6.0/go.mod h1:2Tu9+aMcznHK/AK1HMvgo6xiTLG5rD5rZLDS+rp2Bjs=
golang.org/x/net v0.10.0/go.mod h1:0qNGK6F8kojg2nk9dLZ2mShWaEBan6FAoqfSigmmuDg=
golang.org/x/net v0.20.0/go.mod h1:z8BVo6PvndSri0LbOE3hAn0apkU+1YvI6E70E9jsnvY=
golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY= golang.org/x/net v0.47.0 h1:Mx+4dIFzqraBXUugkia1OOvlD6LemFo1ALMHjrXDOhY=
golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU= golang.org/x/net v0.47.0/go.mod h1:/jNxtkgq5yWUGYkaZGqo27cfGZ1c5Nen03aYrrKpVRU=
golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U= golang.org/x/oauth2 v0.0.0-20180821212333-d2e6202438be/go.mod h1:N/0e6XlmueqKjAGxoOufVs8QHGRruUQn6yWY3a++T0U=
@ -910,6 +922,7 @@ golang.org/x/sync v0.0.0-20201020160332-67f06af15bc9/go.mod h1:RxMgew5VJxzue5/jJ
golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20201207232520-09787c993a3a/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20210220032951-036812b2e83c/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM= golang.org/x/sync v0.0.0-20220722155255-886fb9371eb4/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.1.0/go.mod h1:RxMgew5VJxzue5/jJTE5uejpjVlOe/izrB70Jof72aM=
golang.org/x/sync v0.19.0 h1:vV+1eWNmZ5geRlYjzm2adRgW2/mcpevXNg50YZtPCE4= 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/sync v0.19.0/go.mod h1:9KTHXmSnoGruLpwFjVSX0lNNA75CykiMECbovNTZqGI=
golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY= golang.org/x/sys v0.0.0-20180823144017-11551d06cbcc/go.mod h1:STP8DvDyc/dI5b8T5hshtkjS+E42TnysNCUPdjciGhY=
@ -976,18 +989,28 @@ golang.org/x/sys v0.0.0-20210403161142-5e06dd20ab57/go.mod h1:h1NjWce9XRLGQEsW7w
golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs= golang.org/x/sys v0.0.0-20210423082822-04245dca01da/go.mod h1:h1NjWce9XRLGQEsW7wpKNCjG9DtNlClVuFLEZdDNbEs=
golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210510120138-977fb7262007/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210603081109-ebe580a85c40/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210615035016-665e8c7367d1/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210630005230-0f9fa26af87c/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20210816074244-15123e1e1f71/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20211106132015-ebca88c72f68/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220111092808-5a964db01320/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220114195835-da31bd327af9/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220128215802-99c3d69c2c27/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220520151302-bc2c85ada10a/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220722155257-8c9f86f7a55f/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220811171246-fbc7d0a398ab/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.0.0-20220908164124-27713097b956/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.5.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg= golang.org/x/sys v0.6.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.8.0/go.mod h1:oPkhp1MJrh7nUepCBck5+mAzfO9JrbApNNgaTdGDITg=
golang.org/x/sys v0.16.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk= golang.org/x/sys v0.39.0 h1:CvCKL8MeisomCi6qNZ+wbb0DN9E5AATixKsvNtMoMFk=
golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks= golang.org/x/sys v0.39.0/go.mod h1:OgkHotnGiDImocRcuBABYBEXf8A9a87e/uXjp9XT3ks=
golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo= golang.org/x/term v0.0.0-20201126162022-7de9c90e9dd1/go.mod h1:bj7SfCRtBDWHUb9snDiAeCFNEtKQo2Wmx5Cou7ajbmo=
golang.org/x/term v0.0.0-20210927222741-03fcf44c2211/go.mod h1:jbD1KX2456YbFQfuXm/mYQcufACuNUgVhRMnK/tPxf8=
golang.org/x/term v0.5.0/go.mod h1:jMB1sMXY+tzblOD4FWmEbocvup2/aLOaQEp7JmGp78k=
golang.org/x/term v0.8.0/go.mod h1:xPskH00ivmX89bAKVGSKKtLOWNx2+17Eiy94tnKShWo=
golang.org/x/term v0.16.0/go.mod h1:yn7UURbUtPyrVJPGPq404EukNFxcm/foM+bV/bfcDsY=
golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.0.0-20170915032832-14c0d48ead0c/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.0/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ= golang.org/x/text v0.3.1-0.20180807135948-17ff2d5776d2/go.mod h1:NqM8EUOU14njkJ3fqMW+pc6Ldnwhi/IjpwHt7yyuwOQ=
@ -996,6 +1019,10 @@ golang.org/x/text v0.3.2/go.mod h1:bEr9sfX3Q8Zfm5fL9x+3itogRgK3+ptLWKqgva+5dAk=
golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.3/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.5/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ= golang.org/x/text v0.3.6/go.mod h1:5Zoc/QRtKVWzQhOtBMvqHzDpF6irO9z98xDceosuGiQ=
golang.org/x/text v0.3.7/go.mod h1:u+2+/6zg+i71rQMx5EYifcz6MCKuco9NR6JIITiCfzQ=
golang.org/x/text v0.7.0/go.mod h1:mrYo+phRRbMaCq/xk9113O4dZlRixOauAjOtrjsXDZ8=
golang.org/x/text v0.9.0/go.mod h1:e1OnstbJyHTd6l/uOt8jFFHp6TRDWZR/bV3emEE/zU8=
golang.org/x/text v0.14.0/go.mod h1:18ZOQIKpY8NJVqYksKHtTdi31H5itFRjB5/qKTNYzSU=
golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU= golang.org/x/text v0.32.0 h1:ZD01bjUt1FQ9WJ0ClOL5vxgxOI/sVCNgX1YtKwcY0mU=
golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY= golang.org/x/text v0.32.0/go.mod h1:o/rUWzghvpD5TXrTIBuJU77MTaN0ljMWE47kxGJQ7jY=
golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ= golang.org/x/time v0.0.0-20180412165947-fbb02b2291d2/go.mod h1:tRJNPiyCQ0inRvYxbN9jk5I+vvW/OXSQhTDSoE431IQ=
@ -1064,6 +1091,8 @@ golang.org/x/tools v0.0.0-20210106214847-113979e3529a/go.mod h1:emZCQorbCU4vsT4f
golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.1/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.2/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk= golang.org/x/tools v0.1.5/go.mod h1:o0xws9oXOQQZyjljx8fwUC0k7L1pTE6eaCbjGeHmOkk=
golang.org/x/tools v0.1.12/go.mod h1:hNGJHUnrk76NpqgfD5Aqm5Crs+Hm0VOH/i9J2+nxYbc=
golang.org/x/tools v0.6.0/go.mod h1:Xwgl3UAJ/d3gWutnCtw505GrjyAbvKui8lOU390QaIU=
golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ= golang.org/x/tools v0.39.0 h1:ik4ho21kwuQln40uelmciQPp9SipgNDdrafrYA4TmQQ=
golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ= golang.org/x/tools v0.39.0/go.mod h1:JnefbkDPyD8UU2kI5fuf8ZX4/yUeh9W877ZeBONxUqQ=
golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0= golang.org/x/xerrors v0.0.0-20190717185122-a985d3407aa7/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
@ -1201,6 +1230,7 @@ gopkg.in/ini.v1 v1.42.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.51.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k= gopkg.in/ini.v1 v1.66.2/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA= gopkg.in/ini.v1 v1.67.0 h1:Dgnx+6+nfE+IfzjUEISNeydPJh9AXNNsWbGP9KzCsOA=
gopkg.in/ini.v1 v1.67.0/go.mod h1:pNLf8WUiyNEtQjuu5G5vTm06TEv9tsIgeAvK8hOrP4k=
gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k= gopkg.in/natefinch/lumberjack.v2 v2.0.0/go.mod h1:l0ndWWf7gzL7RNwBG7wST/UCcT4T24xpD6X8LsfU/+k=
gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc= gopkg.in/natefinch/lumberjack.v2 v2.2.1 h1:bBRl1b0OH9s/DuPhuXpNl+VtCaJXFZ5/uEFST95x9zc=
gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc= gopkg.in/natefinch/lumberjack.v2 v2.2.1/go.mod h1:YD8tP3GAjkrDg1eZH7EGmyESg/lsYskCTPBJVb9jqSc=

View File

@ -91,6 +91,7 @@ require (
github.com/mschoch/smat v0.2.0 // indirect github.com/mschoch/smat v0.2.0 // indirect
github.com/nacos-group/nacos-sdk-go/v2 v2.2.5 // indirect github.com/nacos-group/nacos-sdk-go/v2 v2.2.5 // indirect
github.com/natefinch/lumberjack v2.0.0+incompatible // indirect github.com/natefinch/lumberjack v2.0.0+incompatible // indirect
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 // indirect
github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect github.com/oliveagle/jsonpath v0.0.0-20180606110733-2e52cf6e6852 // indirect
github.com/onsi/ginkgo/v2 v2.11.0 // indirect github.com/onsi/ginkgo/v2 v2.11.0 // indirect
github.com/opentracing/opentracing-go v1.2.0 // indirect github.com/opentracing/opentracing-go v1.2.0 // indirect

View File

@ -621,6 +621,8 @@ github.com/nats-io/nats.go v1.9.1/go.mod h1:ZjDU1L/7fJ09jvUSRVBR2e7+RnLiiIQyqyzE
github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.0/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w= github.com/nats-io/nkeys v0.1.3/go.mod h1:xpnFELMwJABBLVhffcfd1MZx6VsNRFpEugbxziKVo7w=
github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c= github.com/nats-io/nuid v1.0.1/go.mod h1:19wcPz3Ph3q0Jbyiqsd0kePYG7A95tJPxeL+1OSON2c=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646 h1:zYyBkD/k9seD2A7fsi6Oo2LfFZAehjjQMERAvZLEDnQ=
github.com/nfnt/resize v0.0.0-20180221191011-83c6a9932646/go.mod h1:jpp1/29i3P1S/RLdc7JQKbRpFeM1dOBd8T9ki5s+AY8=
github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno=
github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk= github.com/npillmayer/nestext v0.1.3/go.mod h1:h2lrijH8jpicr25dFY+oAJLyzlya6jhnuG+zWp9L0Uk=
github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs= github.com/oklog/oklog v0.3.2/go.mod h1:FCV+B7mhrz4o+ueLpx+KqkyXRGMWOYEvfiXtdGtbWGs=

View File

@ -0,0 +1,125 @@
package service
import (
"bytes"
"context"
"encoding/json"
"fmt"
"io"
"net/http"
"os"
"time"
dto "github.com/topfans/backend/gateway/dto"
)
// ImageGenerationResponse 图生图响应
type ImageGenerationResponse struct {
Images []string `json:"images"`
}
// MinimaxService MiniMax API 转发服务
type MinimaxService interface {
GenerateImage(ctx context.Context, req *dto.ImageGenerationRequest) (*ImageGenerationResponse, error)
}
type minimaxService struct {
client *http.Client
}
// NewMinimaxService 创建 MiniMax 服务
func NewMinimaxService() MinimaxService {
return &minimaxService{
client: &http.Client{Timeout: 320 * time.Second},
}
}
// GenerateImage 调用 MiniMax 图生图 API同步调用
func (s *minimaxService) GenerateImage(ctx context.Context, req *dto.ImageGenerationRequest) (*ImageGenerationResponse, error) {
apiURL := os.Getenv("MINIMAX_API_URL")
apiKey := os.Getenv("MINIMAX_API_KEY")
payload := map[string]interface{}{
"model": req.Model,
"prompt": req.Prompt,
"aspect_ratio": req.AspectRatio,
"subject_reference": req.SubjectReference,
"n": req.N,
"response_format": "base64",
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, fmt.Errorf("failed to marshal request: %w", err)
}
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := s.client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to call MiniMax API: %w", err)
}
defer resp.Body.Close()
body, err := io.ReadAll(resp.Body)
if err != nil {
return nil, fmt.Errorf("failed to read response body: %w", err)
}
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
return nil, fmt.Errorf("MiniMax API returned status %d: %s", resp.StatusCode, string(body))
}
var rawResp map[string]interface{}
if err := json.Unmarshal(body, &rawResp); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
var images []string
// MiniMax API 响应格式:
// {"id": "...", "data": {"image_base64": ["..."]}, "base_resp": {"status_code": 0, "status_msg": "success"}}
data, ok := rawResp["data"].(map[string]interface{})
if !ok {
return nil, fmt.Errorf("invalid MiniMax response format: missing data field: %s", string(body))
}
// 检查 base_resp 状态码
if baseResp, ok := rawResp["base_resp"].(map[string]interface{}); ok {
if statusCode, ok := baseResp["status_code"].(float64); ok && statusCode != 0 {
statusMsg, _ := baseResp["status_msg"].(string)
return nil, fmt.Errorf("MiniMax API error: code=%d, msg=%s", int(statusCode), statusMsg)
}
}
// 优先使用 base64 格式(当 response_format=base64 时)
if base64Imgs, ok := data["image_base64"].([]interface{}); ok {
for _, img := range base64Imgs {
if base64Str, ok := img.(string); ok && base64Str != "" {
images = append(images, "data:image/jpeg;base64,"+base64Str)
}
}
}
// 回退使用 image_urls 格式
if len(images) == 0 {
if urlImgs, ok := data["image_urls"].([]interface{}); ok {
for _, img := range urlImgs {
if urlStr, ok := img.(string); ok && urlStr != "" {
images = append(images, urlStr)
}
}
}
}
if len(images) == 0 {
return nil, fmt.Errorf("no images found in MiniMax response: %s", string(body))
}
return &ImageGenerationResponse{Images: images}, nil
}

View File

@ -0,0 +1,382 @@
# MiniMax 图生图 API 集成方案
## 一、需求概述
前端传递参数 → 后端调用 MiniMax 图生图 API → 后端返回结果给前端
## 二、现有项目架构
| 层级 | 技术栈 | 说明 |
|------|--------|------|
| 前端 | uni-app (Vue 3) | 使用 `uni.request` 发起请求 |
| 后端 | Go + Gin | API Gateway 模式,端口 8080 |
| 配置 | `.env` 文件 | API keys 等敏感配置 |
## 三、MiniMax API 信息
**接口地址**: `POST https://api.minimaxi.com/v1/image_generation`
**请求头**:
```json
{
"Authorization": "Bearer <token>",
"Content-Type": "application/json"
}
```
**请求体**:
```json
{
"model": "image-01",
"prompt": "描述文本",
"aspect_ratio": "16:9",
"subject_reference": [
{
"type": "character",
"image_file": "https://..."
}
],
"n": 2
}
```
## 四、后端设计方案
### 4.1 配置文件新增 (.env)
```bash
# MiniMax API 配置
MINIMAX_API_KEY=your_api_key_here
MINIMAX_API_URL=https://api.minimaxi.com/v1/image_generation
```
### 4.2 新增 DTO (gateway/dto/image_dto.go)
```go
// MiniMax 图生图请求
type ImageGenerationRequest struct {
Model string `json:"model" binding:"required"`
Prompt string `json:"prompt" binding:"required"`
AspectRatio string `json:"aspect_ratio"`
SubjectReference []SubjectReference `json:"subject_reference"`
N int `json:"n"`
}
type SubjectReference struct {
Type string `json:"type"`
ImageFile string `json:"image_file"`
}
// MiniMax 图生图响应
type ImageGenerationResponse struct {
Model string `json:"model"`
Prompt string `json:"prompt"`
AspectRatio string `json:"aspect_ratio"`
Images []Image `json:"images"`
}
type Image struct {
URL string `json:"url"`
}
```
### 4.3 新增 Controller (gateway/controller/image_controller.go)
```go
// ImageGeneration 图生图 - 调用 MiniMax API
// @Summary 图生图
// @Description 前端传递参数,后端调用 MiniMax 图生图 API
// @Tags assets
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.ImageGenerationRequest true "图生图请求"
// @Success 200 {object} response.Response
// @Router /api/v1/assets/mints/image/generation [post]
func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
var req dto.ImageGenerationRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "Invalid request parameters: "+err.Error())
return
}
// 从 context 获取 userID 和 starID由认证中间件设置
userID := c.GetInt64("userID")
starID := c.GetInt64("starID")
// 调用资产服务的 MiniMax 转发服务
result, err := ctrl.assetService.CallMiniMaxImageAPI(c.Request.Context(), userID, starID, &req)
if err != nil {
response.Error(c, 500, "Image generation failed: "+err.Error())
return
}
response.Success(c, result)
}
```
### 4.4 新增 Service (assetService/service/minimax_service.go)
```go
package service
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"net/http"
"time"
"github.com/nfnt/resize"
"github.com/topfans/backend/services/assetService/config"
dto "github.com/topfans/backend/gateway/dto"
"go.uber.org/zap"
)
// MinimaxService MiniMax API 转发服务接口
type MinimaxService interface {
// CallMiniMaxImageAPI 调用 MiniMax 图生图 API
CallMiniMaxImageAPI(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*dto.ImageGenerationResponse, error)
}
// minimaxService MiniMax API 转发服务实现
type minimaxService struct {
config *config.AssetConfig
}
// NewMinimaxService 创建 MiniMax 服务实例
func NewMinimaxService(cfg *config.AssetConfig) MinimaxService {
return &minimaxService{config: cfg}
}
// CallMiniMaxImageAPI 调用 MiniMax 图生图 API
func (s *minimaxService) CallMiniMaxImageAPI(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*dto.ImageGenerationResponse, error) {
// 1. 压缩 subject_reference 中的图片
processedRefs := make([]dto.SubjectReference, len(req.SubjectReference))
for i, ref := range req.SubjectReference {
compressedURL, err := s.compressImageIfNeeded(ref.ImageFile)
if err != nil {
// 压缩失败时使用原图
compressedURL = ref.ImageFile
zap.S().Warnf("Image compression failed, using original: %v", err)
}
processedRefs[i] = dto.SubjectReference{
Type: ref.Type,
ImageFile: compressedURL,
}
}
// 2. 构建请求体
payload := map[string]interface{}{
"model": req.Model,
"prompt": req.Prompt,
"aspect_ratio": req.AspectRatio,
"subject_reference": processedRefs,
"n": req.N,
}
// 3. 发送 HTTP POST 请求到 MiniMax
apiURL := s.config.GetMiniMaxAPIURL()
apiKey := s.config.GetMiniMaxAPIKey()
client := &http.Client{Timeout: 120 * time.Second}
jsonData, _ := json.Marshal(payload)
httpReq, err := http.NewRequestWithContext(ctx, "POST", apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, fmt.Errorf("failed to create request: %w", err)
}
httpReq.Header.Set("Authorization", "Bearer "+apiKey)
httpReq.Header.Set("Content-Type", "application/json")
resp, err := client.Do(httpReq)
if err != nil {
return nil, fmt.Errorf("failed to call MiniMax API: %w", err)
}
defer resp.Body.Close()
// 4. 解析响应
var result dto.ImageGenerationResponse
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, fmt.Errorf("failed to parse response: %w", err)
}
return &result, nil
}
// compressImageIfNeeded 下载图片、压缩后返回 base64 编码
func (s *minimaxService) compressImageIfNeeded(imageURL string) (string, error) {
// 下载图片
resp, err := http.Get(imageURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
// 读取图片数据
imgData, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
// 解码图片(自动识别格式)
img, format, err := image.Decode(bytes.NewReader(imgData))
if err != nil {
return "", err
}
// 计算压缩后的尺寸(最大边 1024px保持宽高比
bounds := img.Bounds()
maxDim := uint(1024)
newWidth := uint(bounds.Dx())
newHeight := uint(bounds.Dy())
if newWidth > maxDim || newHeight > maxDim {
if newWidth > newHeight {
ratio := float64(maxDim) / float64(newWidth)
newWidth = maxDim
newHeight = uint(float64(newHeight) * ratio)
} else {
ratio := float64(maxDim) / float64(newHeight)
newHeight = maxDim
newWidth = uint(float64(newWidth) * ratio)
}
}
// 如果图片尺寸没变,不压缩直接返回原图
if newWidth == uint(bounds.Dx()) && newHeight == uint(bounds.Dy()) {
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgData), nil
}
// 缩放图片Lanczos 算法,质量好)
resized := resize.Thumbnail(newWidth, newHeight, img, resize.Lanczos)
// 重新编码为 JPEG质量 85%,体积小)
var buf bytes.Buffer
switch format {
case "png":
err = png.Encode(&buf, resized)
case "gif":
err = gif.Encode(&buf, resized, nil)
default:
err = jpeg.Encode(&buf, resized, &jpeg.Options{Quality: 85})
}
if err != nil {
return "", err
}
// 返回 base64 编码data URI 格式)
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
mimeType := "image/jpeg"
if format == "png" {
mimeType = "image/png"
} else if format == "gif" {
mimeType = "image/gif"
}
return "data:" + mimeType + ";base64," + encoded, nil
}
```
### 4.5 路由注册 (gateway/router/router.go)
在 assets 路由组内添加:
```go
// 资产相关路由(需要认证)
assets := v1.Group("/assets")
assets.Use(middleware.AuthMiddleware())
{
// ... 现有路由 ...
assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图MiniMax API
}
```
## 五、前端调用方案
**架构说明**:前端只与后端 API 通信,不直接调用 MiniMax。后端作为代理调用 MiniMax。
### 5.1 前端 API (frontend/utils/api.js) - 已存在
```javascript
// 图生图 API - 前端传递必要参数,后端调用 MiniMax
export function imageGenerationApi(params) {
return request({
url: '/api/v1/assets/mints/image/generation',
method: 'POST',
data: params
})
}
```
### 5.2 前端调用示例
```javascript
// 前端只需要传递这些参数,具体的 MiniMax API 调用由后端完成
imageGenerationApi({
prompt: '描述文字',
aspect_ratio: '16:9',
subject_reference: [
{
type: 'character',
image_file: '用户选择的图片URL'
}
],
n: 2
}).then(res => {
// res.data.images 是 MiniMax 返回的图片 URL 列表
console.log('生成的图片:', res.data.images)
})
```
### 5.3 数据流向
```
前端参数 {prompt, aspect_ratio, subject_reference, n}
↓ POST /api/v1/assets/mints/image/generation
后端接收参数 → 调用 assetService 的 MiniMax 转发服务 → 调用 MiniMax API
后端获取响应 → 直接透传 images 数组 → 返回给前端
前端收到 {code: 200, data: {images: [...]}}
```
## 六、文件修改清单
| 操作 | 文件路径 | 说明 |
|------|----------|------|
| 修改 | `backend/.env` | 添加 MiniMax 配置 |
| 新增 | `backend/gateway/dto/image_dto.go` | 请求/响应 DTO |
| 新增 | `backend/services/assetService/service/minimax_service.go` | MiniMax API 转发 + 图片压缩 |
| 新增 | `backend/services/assetService/config/minimax_config.go` | MiniMax 配置读取 |
| 修改 | `backend/gateway/controller/asset_controller.go` | 新增 ImageGeneration 处理器 |
| 修改 | `backend/gateway/router/router.go` | 注册 `/mints/image/generation` 路由 |
| 修改 | `frontend/utils/api.js` | 已存在,无需修改 |
### 依赖安装
```bash
cd backend/services/assetService && go get github.com/nfnt/resize
```
## 七、验证方案
1. **后端启动**: 启动 `backend/gateway/main.go``backend/services/assetService/main.go`
2. **接口测试**: 使用 curl 或 Postman 测试接口
```bash
curl -X POST http://localhost:8080/api/v1/assets/mints/image/generation \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"model":"image-01","prompt":"test","aspect_ratio":"16:9","n":1,"subject_reference":[]}'
```
3. **前端测试**: 在页面中调用 `imageGenerationApi()` 并展示返回的图片
## 八、已确认事项
- 图片直接返回 MiniMax 的 URL不经过 OSS
- subject_reference.image_file 由前端自定义传递给后端(用户选择的自定义图片)
- 转发服务位于 `assetService` 中,不在 Gateway

View File

@ -0,0 +1,765 @@
# MiniMax 图生图 API 实现计划
> **For agentic workers:** REQUIRED SUB-SKILL: Use superpowers:subagent-driven-development (recommended) or superpowers:executing-plans to implement this plan task-by-task. Steps use checkbox (`- [ ]`) syntax for tracking.
**Goal:** 实现异步图生图 API前端轮询 job 状态,后端调用 MiniMax 生成图片
**Architecture:**
- Gateway 接收 HTTP 请求,创建 job 返回 job_id
- assetService 异步调用 MiniMax API完成后更新 job 状态
- 前端轮询查询 job 状态COMPLETED 后展示结果
**Tech Stack:** Go (Gin, Dubbo RPC), MiniMax API, uni-app (Vue 3)
---
## 文件结构
```
backend/
├── gateway/
│ ├── dto/
│ │ └── image_dto.go # 新增: ImageGenerationRequest, ImageJobResponse
│ ├── controller/
│ │ └── asset_controller.go # 修改: 添加 ImageGeneration, GetImageJob
│ └── router/
│ └── router.go # 修改: 注册 /mints/image/generation 路由
└── services/assetService/
└── service/
└── minimax_service.go # 新增: 任务管理 + MiniMax API 调用 + 图片压缩
frontend/
└── pages/discover/
└── generation-loading.vue # 修改: 改为轮询模式
```
---
## Task 1: 创建 DTO 文件
**Files:**
- Create: `backend/gateway/dto/image_dto.go`
- Test: (无独立测试,随 controller 测试)
- [ ] **Step 1: 创建 DTO 文件**
```go
package dto
// ImageGenerationRequest MiniMax 图生图请求
type ImageGenerationRequest struct {
Model string `json:"model" binding:"required"`
Prompt string `json:"prompt" binding:"required"`
AspectRatio string `json:"aspect_ratio"`
SubjectReference []SubjectReference `json:"subject_reference"` // MiniMax API 为数组
N int `json:"n"` // 1-4
}
type SubjectReference struct {
Type string `json:"type"`
ImageFile string `json:"image_file"`
}
// ImageJobResponse 图生图任务响应
type ImageJobResponse struct {
JobID string `json:"job_id"`
Status string `json:"status"`
Progress int `json:"progress"`
Images []string `json:"images,omitempty"`
ErrorMsg string `json:"error_msg,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
}
// ImageJobCreateResponse 创建任务响应
type ImageJobCreateResponse struct {
JobID string `json:"job_id"`
Status string `json:"status"`
CreatedAt int64 `json:"created_at"`
}
```
- [ ] **Step 2: Commit**
```bash
git add backend/gateway/dto/image_dto.go
git commit -m "feat: 添加图生图 DTO"
```
---
## Task 2: 创建 MiniMax Service (任务管理 + API 调用)
**Files:**
- Create: `backend/services/assetService/service/minimax_service.go`
- Modify: `backend/services/assetService/go.mod` (添加依赖)
- Test: (无独立测试)
- [ ] **Step 1: 添加依赖**
```bash
cd backend/services/assetService && go get github.com/nfnt/resize
```
- [ ] **Step 2: 创建 minimax_service.go**
```go
package service
import (
"bytes"
"context"
"encoding/base64"
"encoding/json"
"fmt"
"image"
"image/gif"
"image/jpeg"
"image/png"
"io"
"math/rand"
"net"
"net/http"
"net/url"
"strings"
"sync"
"time"
"github.com/google/uuid"
"github.com/nfnt/resize"
"github.com/topfans/backend/services/assetService/config"
dto "github.com/topfans/backend/gateway/dto"
"go.uber.org/zap"
)
// JobStatus 任务状态
type JobStatus string
const (
StatusPending JobStatus = "PENDING"
StatusProcessing JobStatus = "PROCESSING"
StatusCompleted JobStatus = "COMPLETED"
StatusFailed JobStatus = "FAILED"
)
// ImageGenerationJob 图生图任务
type ImageGenerationJob struct {
JobID string `json:"job_id"`
UserID int64 `json:"user_id"`
StarID int64 `json:"star_id"`
Status JobStatus `json:"status"`
Progress int `json:"progress"`
Images []string `json:"images,omitempty"`
ErrorMsg string `json:"error_msg,omitempty"`
Request *dto.ImageGenerationRequest `json:"request,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
}
// MinimaxService MiniMax API 转发服务
type MinimaxService interface {
CreateJob(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*ImageGenerationJob, error)
GetJob(ctx context.Context, jobID string, userID, starID int64) (*ImageGenerationJob, error)
}
type minimaxService struct {
config *config.AssetConfig
jobs map[string]*ImageGenerationJob
jobsLock sync.RWMutex
}
// NewMinimaxService 创建 MiniMax 服务
func NewMinimaxService(cfg *config.AssetConfig) MinimaxService {
svc := &minimaxService{
config: cfg,
jobs: make(map[string]*ImageGenerationJob),
}
// 启动清理 goroutine
go svc.cleanupExpiredJobs()
return svc
}
// CreateJob 创建图生图任务
func (s *minimaxService) CreateJob(ctx context.Context, userID, starID int64, req *dto.ImageGenerationRequest) (*ImageGenerationJob, error) {
jobID := uuid.New().String()
now := time.Now().UnixMilli()
job := &ImageGenerationJob{
JobID: jobID,
UserID: userID,
StarID: starID,
Status: StatusProcessing,
Progress: 0,
Request: req,
CreatedAt: now,
UpdatedAt: now,
}
s.jobsLock.Lock()
s.jobs[jobID] = job
s.jobsLock.Unlock()
// 异步调用 MiniMax
go s.processJob(job)
return job, nil
}
// GetJob 获取任务
func (s *minimaxService) GetJob(ctx context.Context, jobID string, userID, starID int64) (*ImageGenerationJob, error) {
s.jobsLock.RLock()
job, ok := s.jobs[jobID]
s.jobsLock.RUnlock()
if !ok {
return nil, fmt.Errorf("job not found")
}
if job.UserID != userID || job.StarID != starID {
return nil, fmt.Errorf("access denied")
}
return job, nil
}
// processJob 异步处理任务
func (s *minimaxService) processJob(job *ImageGenerationJob) {
defer func() {
if r := recover(); r != nil {
job.Status = StatusFailed
job.ErrorMsg = fmt.Sprintf("panic: %v", r)
job.UpdatedAt = time.Now().UnixMilli()
}
}()
// 1. 校验 SSRF
for _, ref := range job.Request.SubjectReference {
if err := validateURL(ref.ImageFile); err != nil {
job.Status = StatusFailed
job.ErrorMsg = "invalid image URL: " + err.Error()
job.UpdatedAt = time.Now().UnixMilli()
return
}
}
// 2. 压缩图片
processedRefs := make([]dto.SubjectReference, len(job.Request.SubjectReference))
for i, ref := range job.Request.SubjectReference {
job.Progress = 10 + i*20
job.UpdatedAt = time.Now().UnixMilli()
compressed, err := s.compressImageIfNeeded(ref.ImageFile)
if err != nil {
compressed = ref.ImageFile
zap.S().Warnf("Image compression failed, using original: %v", err)
}
processedRefs[i] = dto.SubjectReference{
Type: ref.Type,
ImageFile: compressed,
}
}
job.Progress = 50
job.UpdatedAt = time.Now().UnixMilli()
// 3. 调用 MiniMax API
images, err := s.callMiniMaxAPI(job.Request.Model, job.Request.Prompt, job.Request.AspectRatio, processedRefs, job.Request.N)
if err != nil {
job.Status = StatusFailed
job.ErrorMsg = "MiniMax API failed: " + err.Error()
job.UpdatedAt = time.Now().UnixMilli()
return
}
job.Progress = 90
job.UpdatedAt = time.Now().UnixMilli()
// 4. 完成
job.Status = StatusCompleted
job.Progress = 100
job.Images = images
job.CompletedAt = time.Now().UnixMilli()
job.UpdatedAt = time.Now().UnixMilli()
}
// callMiniMaxAPI 调用 MiniMax API
func (s *minimaxService) callMiniMaxAPI(model, prompt, aspectRatio string, refs []dto.SubjectReference, n int) ([]string, error) {
apiURL := s.config.GetMiniMaxAPIURL()
apiKey := s.config.GetMiniMaxAPIKey()
payload := map[string]interface{}{
"model": model,
"prompt": prompt,
"aspect_ratio": aspectRatio,
"subject_reference": refs,
"n": n,
}
jsonData, err := json.Marshal(payload)
if err != nil {
return nil, err
}
client := &http.Client{Timeout: 120 * time.Second}
req, err := http.NewRequest("POST", apiURL, bytes.NewBuffer(jsonData))
if err != nil {
return nil, err
}
req.Header.Set("Authorization", "Bearer "+apiKey)
req.Header.Set("Content-Type", "application/json")
resp, err := client.Do(req)
if err != nil {
return nil, err
}
defer resp.Body.Close()
var result struct {
Images []struct {
URL string `json:"url"`
} `json:"images"`
}
if err := json.NewDecoder(resp.Body).Decode(&result); err != nil {
return nil, err
}
images := make([]string, len(result.Images))
for i, img := range result.Images {
images[i] = img.URL
}
return images, nil
}
// compressImageIfNeeded 下载并压缩图片
func (s *minimaxService) compressImageIfNeeded(imageURL string) (string, error) {
resp, err := http.Get(imageURL)
if err != nil {
return "", err
}
defer resp.Body.Close()
imgData, err := io.ReadAll(resp.Body)
if err != nil {
return "", err
}
img, format, err := image.Decode(bytes.NewReader(imgData))
if err != nil {
return "", err
}
bounds := img.Bounds()
maxDim := uint(1024)
newWidth := uint(bounds.Dx())
newHeight := uint(bounds.Dy())
if newWidth > maxDim || newHeight > maxDim {
if newWidth > newHeight {
ratio := float64(maxDim) / float64(newWidth)
newWidth = maxDim
newHeight = uint(float64(newHeight) * ratio)
} else {
ratio := float64(maxDim) / float64(newHeight)
newHeight = maxDim
newWidth = uint(float64(newWidth) * ratio)
}
}
if newWidth == uint(bounds.Dx()) && newHeight == uint(bounds.Dy()) {
return "data:image/jpeg;base64," + base64.StdEncoding.EncodeToString(imgData), nil
}
resized := resize.Thumbnail(newWidth, newHeight, img, resize.Lanczos)
var buf bytes.Buffer
switch format {
case "png":
err = png.Encode(&buf, resized)
case "gif":
err = gif.Encode(&buf, resized, nil)
default:
err = jpeg.Encode(&buf, resized, &jpeg.Options{Quality: 85})
}
if err != nil {
return "", err
}
encoded := base64.StdEncoding.EncodeToString(buf.Bytes())
mimeType := "image/jpeg"
if format == "png" {
mimeType = "image/png"
} else if format == "gif" {
mimeType = "image/gif"
}
return "data:" + mimeType + ";base64," + encoded, nil
}
// validateURL 校验 URL 防止 SSRF
func validateURL(rawURL string) error {
if rawURL == "" {
return nil
}
u, err := url.Parse(rawURL)
if err != nil {
return err
}
host := u.Hostname()
// 检查是否是 IP
ip := net.ParseIP(host)
if ip != nil {
if ip.IsLoopback() || ip.IsPrivate() || ip.IsUnspecified() {
return fmt.Errorf("private IP not allowed: %s", host)
}
return nil
}
// 检查是否是内网域名
lowerHost := strings.ToLower(host)
if strings.HasSuffix(lowerHost, ".local") ||
strings.HasSuffix(lowerHost, ".internal") ||
strings.HasSuffix(lowerHost, ".private") {
return fmt.Errorf("internal domain not allowed: %s", host)
}
return nil
}
// cleanupExpiredJobs 清理过期任务
func (s *minimaxService) cleanupExpiredJobs() {
ticker := time.NewTicker(1 * time.Hour)
for range ticker.C {
s.jobsLock.Lock()
now := time.Now().UnixMilli()
expiredThreshold := int64(24 * 60 * 60 * 1000) // 24h
for jobID, job := range s.jobs {
if job.Status == StatusCompleted || job.Status == StatusFailed {
if now-job.UpdatedAt > expiredThreshold {
delete(s.jobs, jobID)
}
}
}
s.jobsLock.Unlock()
}
}
```
- [ ] **Step 3: Commit**
```bash
git add backend/services/assetService/service/minimax_service.go backend/services/assetService/go.mod backend/services/assetService/go.sum
git commit -m "feat: 添加 MiniMax 图生图服务和任务管理"
```
---
## Task 3: 修改 AssetController 添加新接口
**Files:**
- Modify: `backend/gateway/controller/asset_controller.go` (在 AssetController struct 添加 minimaxService 字段,添加两个方法)
- Test: (无独立测试)
- [ ] **Step 1: 添加字段和方法到 AssetController**
`AssetController` struct 添加:
```go
minimaxService service.MinimaxService
```
`NewAssetController` 添加:
```go
ctrl.minimaxService = service.NewMinimaxService(nil) // TODO: 传入 config
```
添加两个方法:
```go
// ImageGeneration 创建图生图任务
// @Summary 图生图
// @Description 创建图生图任务
// @Tags assets
// @Accept json
// @Produce json
// @Security BearerAuth
// @Param request body dto.ImageGenerationRequest true "图生图请求"
// @Success 202 {object} response.Response
// @Router /api/v1/assets/mints/image/generation [post]
func (ctrl *AssetController) ImageGeneration(c *gin.Context) {
var req dto.ImageGenerationRequest
if err := c.ShouldBindJSON(&req); err != nil {
response.Error(c, 400, "Invalid request: "+err.Error())
return
}
userID := c.GetInt64("userID")
starID := c.GetInt64("starID")
job, err := ctrl.minimaxService.CreateJob(c.Request.Context(), userID, starID, &req)
if err != nil {
response.Error(c, 500, "Failed to create job: "+err.Error())
return
}
response.SuccessWithStatus(c, 202, &dto.ImageJobCreateResponse{
JobID: job.JobID,
Status: string(job.Status),
CreatedAt: job.CreatedAt,
})
}
// GetImageJob 查询图生图任务状态
// @Summary 查询图生图任务
// @Description 查询图生图任务状态和结果
// @Tags assets
// @Produce json
// @Security BearerAuth
// @Param job_id path string true "任务ID"
// @Success 200 {object} response.Response
// @Router /api/v1/assets/mints/image/generation/{job_id} [get]
func (ctrl *AssetController) GetImageJob(c *gin.Context) {
jobID := c.Param("job_id")
if jobID == "" {
response.Error(c, 400, "job_id is required")
return
}
userID := c.GetInt64("userID")
starID := c.GetInt64("starID")
job, err := ctrl.minimaxService.GetJob(c.Request.Context(), jobID, userID, starID)
if err != nil {
if strings.Contains(err.Error(), "not found") {
response.Error(c, 404, "Job not found")
return
}
if strings.Contains(err.Error(), "access denied") {
response.Error(c, 403, "Access denied")
return
}
response.Error(c, 500, "Failed to get job: "+err.Error())
return
}
response.Success(c, &dto.ImageJobResponse{
JobID: job.JobID,
Status: string(job.Status),
Progress: job.Progress,
Images: job.Images,
ErrorMsg: job.ErrorMsg,
CreatedAt: job.CreatedAt,
UpdatedAt: job.UpdatedAt,
CompletedAt: job.CompletedAt,
})
}
```
- [ ] **Step 2: Commit**
```bash
git add backend/gateway/controller/asset_controller.go
git commit -m "feat: 添加 ImageGeneration 和 GetImageJob 接口"
```
---
## Task 4: 注册路由
**Files:**
- Modify: `backend/gateway/router/router.go`
- [ ] **Step 1: 在 assets 路由组添加新路由**
`assets.POST("/mints/precreate", ...)` 附近添加:
```go
assets.POST("/mints/image/generation", assetCtrl.ImageGeneration) // 图生图
assets.GET("/mints/image/generation/:job_id", assetCtrl.GetImageJob) // 查询任务
```
- [ ] **Step 2: Commit**
```bash
git add backend/gateway/router/router.go
git commit -m "feat: 注册图生图 API 路由"
```
---
## Task 5: 修改前端轮询逻辑
**Files:**
- Modify: `frontend/pages/discover/generation-loading.vue`
- [ ] **Step 1: 修改 script 部分**
`callImageGeneration` 函数改为:
```javascript
// job_id 轮询
let jobId = null;
// 调用图生图API - 异步模式
const callImageGeneration = async () => {
try {
const res = await imageGenerationApi(generationData);
if (res.data && res.data.job_id) {
jobId = res.data.job_id;
console.log('[GenerationLoading] 任务已创建:', jobId);
// 开始轮询
pollJobStatus();
} else {
uni.showToast({
title: '创建任务失败',
icon: 'none',
duration: 2000
});
revertProgress();
}
} catch (err) {
console.error('[GenerationLoading] 创建任务失败:', err);
uni.showToast({
title: '创建任务失败',
icon: 'none',
duration: 2000
});
revertProgress();
}
};
// 轮询任务状态
const pollJobStatus = () => {
let pollCount = 0;
const maxPolls = 40; // 120秒 / 3秒 = 40次
const poll = async () => {
if (!jobId) return;
try {
const res = await uni.request({
url: baseURL + `/api/v1/assets/mints/image/generation/${jobId}`,
method: 'GET',
header: {
'Authorization': `Bearer ${uni.getStorageSync('access_token')}`
}
});
const data = res.data?.data;
if (data) {
// 更新进度
if (data.progress) {
progress.value = Math.min(data.progress, 90);
}
if (data.status === 'COMPLETED') {
// 完成
const imageUrls = data.images || [];
if (imageUrls.length > 0) {
uni.setStorageSync('generated_images', JSON.stringify(imageUrls));
completeProgress();
} else {
uni.showToast({ title: '未生成图片', icon: 'none' });
revertProgress();
}
return;
} else if (data.status === 'FAILED') {
// 失败
uni.showToast({
title: data.error_msg || '生成失败',
icon: 'none',
duration: 2000
});
revertProgress();
return;
}
}
pollCount++;
if (pollCount >= maxPolls) {
uni.showToast({ title: '生成超时,请重试', icon: 'none' });
revertProgress();
return;
}
// 3秒后继续轮询
setTimeout(poll, 3000);
} catch (err) {
console.error('[GenerationLoading] 轮询失败:', err);
pollCount++;
if (pollCount >= maxPolls) {
uni.showToast({ title: '查询失败,请重试', icon: 'none' });
revertProgress();
} else {
setTimeout(poll, 3000);
}
}
};
poll();
};
```
- [ ] **Step 2: Commit**
```bash
git add frontend/pages/discover/generation-loading.vue
git commit -m "feat: 前端改为轮询模式"
```
---
## Task 6: 添加配置读取 (config)
**Files:**
- Create: `backend/services/assetService/config/minimax_config.go`
- Modify: `backend/services/assetService/config/asset_config.go` (如果需要)
- [ ] **Step 1: 添加配置读取**
在 config 包添加 MiniMax 配置读取方法:
```go
// GetMiniMaxAPIURL 获取 MiniMax API URL
func (c *AssetConfig) GetMiniMaxAPIURL() string {
return os.Getenv("MINIMAX_API_URL")
}
// GetMiniMaxAPIKey 获取 MiniMax API Key
func (c *AssetConfig) GetMiniMaxAPIKey() string {
return os.Getenv("MINIMAX_API_KEY")
}
```
- [ ] **Step 2: Commit**
```bash
git add backend/services/assetService/config/
git commit -m "feat: 添加 MiniMax 配置读取"
```
---
## Task 7: 验证测试
- [ ] **Step 1: 启动后端服务**
```bash
cd backend/gateway && go run main.go
```
- [ ] **Step 2: 测试创建任务**
```bash
curl -X POST http://localhost:8080/api/v1/assets/mints/image/generation \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"model":"image-01","prompt":"test","aspect_ratio":"16:9","n":1,"subject_reference":[]}'
```
- [ ] **Step 3: 测试轮询任务状态**
```bash
curl http://localhost:8080/api/v1/assets/mints/image/generation/<job_id> \
-H "Authorization: Bearer <token>"
```

View File

@ -0,0 +1,272 @@
# MiniMax 图生图 API 集成设计方案
## 概述
前端传递参数 → 后端异步调用 MiniMax 图生图 API → 前端轮询任务状态 → 返回结果
## 现有架构
| 层级 | 技术栈 | 说明 |
|------|--------|------|
| 前端 | uni-app (Vue 3) | 使用 `uni.request` 发起请求,已有 loading 页面 |
| 后端 | Go + Gin | API Gateway 模式Dubbo RPC 调用微服务 |
| 微服务 | Go | `assetService` 等独立服务 |
| 配置 | `.env` 文件 | API keys 等敏感配置 |
## API 设计
### 1. 创建图生图任务
```
POST /api/v1/assets/mints/image/generation
```
**请求头:**
```
Authorization: Bearer <token>
Content-Type: application/json
```
**请求体:**
```json
{
"model": "image-01",
"prompt": "描述文本",
"aspect_ratio": "16:9",
"subject_reference": [
{
"type": "character",
"image_file": "https://..."
}
],
"n": 2
}
```
**响应 (202 Accepted):**
```json
{
"code": 202,
"message": "任务已创建",
"data": {
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "PROCESSING",
"created_at": 1744118400000
}
}
```
### 2. 查询任务状态
```
GET /api/v1/assets/mints/image/generation/:job_id
```
**响应 (PROCESSING):**
```json
{
"code": 200,
"message": "处理中",
"data": {
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "PROCESSING",
"progress": 50,
"created_at": 1744118400000
}
}
```
**响应 (COMPLETED):**
```json
{
"code": 200,
"message": "成功",
"data": {
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "COMPLETED",
"progress": 100,
"images": [
"https://api.minimaxi.com/v1/images/xxx.png"
],
"created_at": 1744118400000,
"completed_at": 1744118490000
}
}
```
**响应 (FAILED):**
```json
{
"code": 200,
"message": "失败",
"data": {
"job_id": "550e8400-e29b-41d4-a716-446655440000",
"status": "FAILED",
"progress": 0,
"error_msg": "MiniMax API 调用失败: timeout",
"created_at": 1744118400000
}
}
```
## 数据模型
### Job 状态 (内存存储)
```go
// JobStatus 任务状态枚举
type JobStatus string
const (
StatusPending JobStatus = "PENDING"
StatusProcessing JobStatus = "PROCESSING"
StatusCompleted JobStatus = "COMPLETED"
StatusFailed JobStatus = "FAILED"
)
// ImageGenerationJob 图生图任务
type ImageGenerationJob struct {
JobID string `json:"job_id"`
UserID int64 `json:"user_id"`
StarID int64 `json:"star_id"`
Status JobStatus `json:"status"`
Progress int `json:"progress"` // 0-100
Images []string `json:"images,omitempty"`
ErrorMsg string `json:"error_msg,omitempty"`
Request *ImageGenerationRequest `json:"request,omitempty"`
CreatedAt int64 `json:"created_at"` //毫秒时间戳
UpdatedAt int64 `json:"updated_at"`
CompletedAt int64 `json:"completed_at,omitempty"` // 毫秒时间戳
}
```
### DTO
```go
// ImageGenerationRequest MiniMax 图生图请求
type ImageGenerationRequest struct {
Model string `json:"model" binding:"required"`
Prompt string `json:"prompt" binding:"required"`
AspectRatio string `json:"aspect_ratio"`
SubjectReference []SubjectReference `json:"subject_reference"`
N int `json:"n"` // 1-4
}
type SubjectReference struct {
Type string `json:"type"`
ImageFile string `json:"image_file"` // 必须为有效 URL需 SSRF 校验
}
// ImageJobResponse 图生图任务响应
type ImageJobResponse struct {
JobID string `json:"job_id"`
Status string `json:"status"`
Progress int `json:"progress"`
Images []string `json:"images,omitempty"`
ErrorMsg string `json:"error_msg,omitempty"`
CreatedAt int64 `json:"created_at"`
UpdatedAt int64 `json:"updated_at"`
CompletedAt int64 `json:"completed_at,omitempty"`
}
```
## 后端实现
### 文件结构
```
backend/
├── services/assetService/
│ └── service/
│ └── minimax_service.go # MiniMax API 转发服务 + 任务管理
└── gateway/
├── controller/
│ └── asset_controller.go # 新增 ImageGeneration, GetImageJob
├── dto/
│ └── image_dto.go # 请求/响应 DTO
└── router/
└── router.go # 注册路由
```
### 核心逻辑
1. **创建任务**: 生成 job_id存储任务到内存 map返回 202
2. **异步处理**: goroutine 调用 MiniMax API图片压缩(最大边1024px),更新 job 状态
3. **查询状态**: 从内存读取 job 状态返回
4. **任务清理**: 后台 goroutine 定期清理超期(>24h)的已完成任务
### 图片压缩
- 最大边压缩至 1024px保持宽高比
- 格式转换: PNG/GIF → JPEG(质量85%)
- 返回 base64 data URI 格式
### SSRF 防护
`subject_reference[].image_file` 必须是有效 URL下载前需校验:
- 不能是私有 IP (10.x, 172.16-31.x, 192.168.x)
- 不能是 localhost
- 不能是内网域名
- 校验失败则拒绝请求 (400)
## 前端改动
**generation-loading.vue**:
- 调用 `POST /generation` 获取 job_id
- 每 3 秒轮询 `GET /generation/:job_id`
- 超时时间: 120 秒后显示"生成超时,请重试"
- 完成后跳转到结果页
## HTTP 状态码
| 场景 | HTTP 状态码 |
|------|-------------|
| 成功 (创建/查询) | 200 / 202 |
| 参数校验失败 | 400 |
| 未认证 | 401 |
| 无权访问 job | 403 |
| Job 不存在 | 404 |
| MiniMax API 失败 | 500 |
## 错误处理
| 场景 | 处理方式 |
|------|----------|
| MiniMax API 超时 | 标记 job 为 FAILEDerror_msg 包含原因 |
| 图片压缩失败 | 使用原图,继续处理 |
| SSRF 校验失败 | 返回 400"无效的图片URL" |
| Job 不存在 | 返回 404 |
| 无权访问 job | 返回 403 |
## 文件修改清单
| 操作 | 文件路径 | 说明 |
|------|----------|------|
| 新增 | `backend/gateway/dto/image_dto.go` | 请求/响应 DTO |
| 新增 | `backend/services/assetService/service/minimax_service.go` | MiniMax API 转发 + 图片压缩 + 任务管理 |
| 修改 | `backend/gateway/controller/asset_controller.go` | 新增 ImageGeneration, GetImageJob |
| 修改 | `backend/gateway/router/router.go` | 注册路由 |
| 修改 | `frontend/pages/discover/generation-loading.vue` | 改为轮询模式 |
## 依赖
```bash
cd backend/services/assetService && go get github.com/nfnt/resize
```
## 验证方案
1. 启动后端服务
2. 获取 JWT token
3. 测试创建任务:
```bash
curl -X POST http://localhost:8080/api/v1/assets/mints/image/generation \
-H "Authorization: Bearer <token>" \
-H "Content-Type: application/json" \
-d '{"model":"image-01","prompt":"test","aspect_ratio":"16:9","n":1,"subject_reference":[]}'
```
4. 使用返回的 job_id 轮询状态:
```bash
curl http://localhost:8080/api/v1/assets/mints/image/generation/<job_id> \
-H "Authorization: Bearer <token>"
```

View File

@ -91,54 +91,27 @@ const revertProgress = () => {
}, 100); }, 100);
}; };
// API // API -
const callImageGeneration = async () => { const callImageGeneration = async () => {
try { try {
const res = await imageGenerationApi(generationData); const res = await imageGenerationApi(generationData);
if (res.data && res.data.base_resp && res.data.base_resp.status_code === 0) { if (res.data && res.data.images && res.data.images.length > 0) {
console.log('[GenerationLoading] 图生图成功:', res); //
uni.setStorageSync('generated_images', JSON.stringify(res.data.images));
// completeProgress();
const imageUrls = res.data.data?.image_urls || [];
if (imageUrls.length > 0) {
// base64
const processedImages = imageUrls.map(base64Data => {
// data URL
if (base64Data.startsWith('data:image')) {
return base64Data;
}
// data URL
return `data:image/png;base64,${base64Data}`;
});
//
uni.setStorageSync('generated_images', JSON.stringify(processedImages));
//
completeProgress();
} else {
uni.showToast({
title: '未生成图片',
icon: 'none',
duration: 2000
});
revertProgress();
}
} else { } else {
// 退
uni.showToast({ uni.showToast({
title: res.data?.base_resp?.status_msg || '生成失败', title: '未生成图片',
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}); });
revertProgress(); revertProgress();
} }
} catch (err) { } catch (err) {
console.error('[GenerationLoading] 图生图失败:', err); console.error('[GenerationLoading] 生成失败:', err);
uni.showToast({ uni.showToast({
title: '生成失败', title: err.message || '生成失败',
icon: 'none', icon: 'none',
duration: 2000 duration: 2000
}); });
@ -153,7 +126,7 @@ const handleSuccess = () => {
icon: 'success', icon: 'success',
duration: 1500 duration: 1500
}); });
// //
setTimeout(() => { setTimeout(() => {
uni.redirectTo({ uni.redirectTo({
@ -176,10 +149,10 @@ onMounted(() => {
}, 1500); }, 1500);
return; return;
} }
// //
generationData = JSON.parse(requestDataStr); generationData = JSON.parse(requestDataStr);
// castlove_form_database64 // castlove_form_database64
const castloveDataStr = uni.getStorageSync('castlove_form_data'); const castloveDataStr = uni.getStorageSync('castlove_form_data');
if (castloveDataStr) { if (castloveDataStr) {
@ -189,15 +162,15 @@ onMounted(() => {
generationData.subject_reference[0].image_file = castloveData.imageBase64; generationData.subject_reference[0].image_file = castloveData.imageBase64;
} }
} }
// generation_request_data使 // generation_request_data使
uni.removeStorageSync('generation_request_data'); uni.removeStorageSync('generation_request_data');
console.log('[GenerationLoading] 接收到生成数据'); console.log('[GenerationLoading] 接收到生成数据');
// //
simulateProgress(); simulateProgress();
// API // API
callImageGeneration(); callImageGeneration();
} catch (e) { } catch (e) {

View File

@ -1,6 +1,7 @@
// API 基础配置 // API 基础配置
// const baseURL = 'http://101.132.250.62:8080' // const baseURL = 'http://101.132.250.62:8080'
const baseURL = 'http://192.168.110.60:8080' // const baseURL = 'http://192.168.110.60:8080'
const baseURL = 'http://localhost:8080'
// 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false // 是否使用模拟数据(开发调试时设为 true后端API准备好后改为 false
const USE_MOCK_API = false const USE_MOCK_API = false
@ -54,7 +55,7 @@ function request(options) {
} }
// 可以在这里统一处理响应 // 可以在这里统一处理响应
if (res.statusCode === 200) { if (res.statusCode === 200 || res.statusCode === 202) {
// 检查业务状态码 // 检查业务状态码
if (res.data && res.data.code !== undefined) { if (res.data && res.data.code !== undefined) {
if (res.data.code === 200) { if (res.data.code === 200) {
@ -455,6 +456,7 @@ export function imageGenerationApi(params) {
data: params data: params
}) })
} }
// 获取热度排行榜 // 获取热度排行榜
export function getHotRankingApi(dimension = 'total', starId = null, page = 1, pageSize = 10) { export function getHotRankingApi(dimension = 'total', starId = null, page = 1, pageSize = 10) {
let url = `/api/v1/rankings/hot?dimension=${dimension}&page=${page}&page_size=${pageSize}` let url = `/api/v1/rankings/hot?dimension=${dimension}&page=${page}&page_size=${pageSize}`