Files
image2api/backend/internal/http/router/router.go
T
chiyiandClaude Opus 4.8 5cf6206ee9 feat: 并发分组系统 + 品牌/Logo 上传 + 兑换码开关 + 文档分辨率表
并发分组(新功能):
- 新表 concurrency_groups(名称/上限/默认),用户加 concurrency_group_id
- 启动自动建「默认并发」组(上限10、默认),老用户回填、新注册自动绑定
- 并发计数改用 Redis(自愈 sorted-set + TTL + fail-open):
  · 用户并发(画图台 + API key 合计)受其分组上限限制,0=不限制 → 超返回 429
  · 账号级并发也从内存 gate 换成同一套 Redis(6 处调用点)
  · 移除旧的「已有正在生成的任务」单任务锁
- 后台「并发分组」新菜单:增删改、设默认、用户数;默认组不可删(删别的组成员转默认)
- 用户管理:并发列 + 新建/编辑可选分组
- 个人设置页:账户信息卡(用户名/邮箱/角色/余额/并发);/me 暴露 concurrency_group/limit

品牌 / Logo(上传到 RustFS):
- Logo 改成拖拽/点击上传,点保存才上传;替换自动删旧;branding/ 设为公开且被清理任务 pin 住(永不删)
- 有自定义就用:前台左侧 nav + 后台侧栏 + favicon(浏览器标签);没有则默认 V 图标
- 前台页头还原成文字;首页 Hero 子标题用 site.subtitle(默认那句宣传语,设置页预填)
- 邮件验证码标题用站点名;新增 POST/DELETE /settings/logo + POST /settings/asset(首页底图上传)

兑换码开关:
- 系统设置→积分 新增「开启兑换码」(默认开);关闭后后端拒绝兑换、前台隐藏兑换入口(/site 暴露 cdk_redeem_enabled)

文档 / 分辨率:
- 去掉 quality 参数:size(宽x高)同时决定比例 + 分辨率档(长边映射 1K/2K/4K)
- 文档加「分辨率对照表」(14 个比例 × 1K/2K/4K → size 该传的值);guessRatio 与自定义模型 RATIO_OPTS 对齐到 14 个

其它:
- 删模型时同步清掉各上游账号「支持模型」里的该 id
- 首页设置/兑换码弹窗去固定高度滚动条;展示位弹窗浅色主题适配

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-30 13:52:31 +08:00

178 lines
8.2 KiB
Go

package router
import (
"backend/internal/config"
"backend/internal/http/handler"
"backend/internal/http/middleware"
"backend/internal/service"
"github.com/gin-contrib/cors"
"github.com/gin-gonic/gin"
)
type Handlers struct {
Health *handler.HealthHandler
Images *handler.ImageHandler
V1 *handler.V1Handler
Site *handler.SiteHandler
Showcase *handler.ShowcaseHandler
Auth *handler.AuthHandler
SiteSettings *handler.SiteSettingsHandler
AppSettings *handler.AppSettingsHandler
AdminRead *handler.AdminReadHandler
AdminWrite *handler.AdminWriteHandler
CDK *handler.CDKHandler
UserTools *handler.UserToolsHandler
UserGen *handler.UserGenerationHandler
ProviderAdmin *handler.ProviderAdminHandler
ConcGroups *handler.ConcurrencyGroupHandler
}
func New(cfg *config.Config, auth *service.AuthService, handlers Handlers) *gin.Engine {
if cfg.AppEnv != "development" {
gin.SetMode(gin.ReleaseMode)
}
engine := gin.New()
engine.Use(gin.Recovery())
engine.Use(middleware.RequestID())
engine.Use(cors.New(cors.Config{
AllowOrigins: cfg.CORSOrigins,
AllowMethods: []string{"GET", "POST", "PUT", "PATCH", "DELETE", "OPTIONS"},
AllowHeaders: []string{"Authorization", "Content-Type", "X-Request-Id"},
AllowCredentials: true,
}))
engine.GET("/health", handlers.Health.Handle)
engine.GET("/images/:user/:name", handlers.Images.Serve)
engine.GET("/v1/models", handlers.V1.Models)
engine.POST("/v1/images/generations", handlers.V1.ImageGenerations)
engine.POST("/v1/images/edits", handlers.V1.ImageEdits)
// OpenAI Sora-style async video: create job → poll → stream content.
engine.POST("/v1/videos", handlers.V1.CreateVideo)
engine.GET("/v1/videos/:id", handlers.V1.GetVideo)
engine.GET("/v1/videos/:id/content", handlers.V1.GetVideoContent)
publicAdmin := engine.Group("/admin/api")
{
publicAdmin.GET("/site", handlers.Site.Public)
publicAdmin.GET("/showcase", handlers.Showcase.List)
publicAdmin.GET("/managed-models", handlers.AdminRead.Models)
publicAdmin.GET("/stats", handlers.AdminRead.Stats)
publicAdmin.GET("/video-presets", handlers.UserGen.VideoPresets)
publicAdmin.GET("/catalog", handlers.UserGen.Catalog)
publicAdmin.GET("/models", handlers.UserGen.Models)
}
authGroup := engine.Group("/admin/api/auth")
{
authGroup.GET("/config", handlers.Auth.Config)
authGroup.POST("/send-code", handlers.Auth.SendCode)
authGroup.POST("/register", handlers.Auth.Register)
authGroup.POST("/login", handlers.Auth.Login)
authGroup.POST("/logout", handlers.Auth.Logout)
authGroup.POST("/reset-password", handlers.Auth.ResetPassword)
}
userAuthed := engine.Group("/admin/api")
userAuthed.Use(middleware.RequireSession(auth))
{
userAuthed.GET("/logs", handlers.UserGen.Logs)
userAuthed.POST("/generate", handlers.UserGen.Generate)
userAuthed.POST("/test", handlers.UserGen.Test)
userAuthed.GET("/jobs/mine", handlers.UserGen.MyJobs)
userAuthed.GET("/my-images", handlers.UserGen.MyImages)
}
authed := engine.Group("/admin/api")
authed.Use(middleware.RequireAdminSession(auth))
{
authed.GET("/dashboard", handlers.AdminRead.Dashboard)
authed.GET("/users", handlers.AdminRead.Users)
authed.GET("/invites", handlers.AdminRead.Invites)
authed.POST("/users", handlers.AdminWrite.CreateUser)
authed.POST("/users/delete-bulk", handlers.AdminWrite.DeleteUsersBulk)
authed.PATCH("/users/:user_id", handlers.AdminWrite.UpdateUser)
authed.DELETE("/users/:user_id", handlers.AdminWrite.DeleteUser)
authed.POST("/users/:user_id/credits", handlers.AdminWrite.AdjustUserCredits)
authed.POST("/users/:user_id/api-keys", handlers.AdminWrite.CreateUserAPIKey)
authed.DELETE("/users/:user_id/api-keys/:key_id", handlers.AdminWrite.DeleteUserAPIKey)
authed.GET("/concurrency-groups", handlers.ConcGroups.List)
authed.POST("/concurrency-groups", handlers.ConcGroups.Create)
authed.PATCH("/concurrency-groups/:id", handlers.ConcGroups.Update)
authed.POST("/concurrency-groups/:id/default", handlers.ConcGroups.SetDefault)
authed.DELETE("/concurrency-groups/:id", handlers.ConcGroups.Delete)
authed.GET("/cdks", handlers.CDK.List)
authed.POST("/cdks", handlers.CDK.Create)
authed.POST("/cdks/delete-bulk", handlers.CDK.DeleteBulk)
authed.DELETE("/cdks/:code", handlers.CDK.Delete)
authed.GET("/tokens", handlers.ProviderAdmin.TokensList)
authed.POST("/tokens", handlers.ProviderAdmin.TokensCreate)
authed.POST("/tokens/import-chatgpt-token", handlers.ProviderAdmin.ImportChatGPTToken)
authed.POST("/tokens/import-adobe-cookie", handlers.ProviderAdmin.ImportAdobeCookie)
authed.POST("/tokens/import-runway-token", handlers.ProviderAdmin.ImportRunwayToken)
authed.POST("/tokens/import-leonardo-cookie", handlers.ProviderAdmin.ImportLeonardoCookie)
authed.POST("/tokens/import-krea-cookie", handlers.ProviderAdmin.ImportKreaCookie)
authed.POST("/tokens/import-imagine-token", handlers.ProviderAdmin.ImportImagineToken)
authed.POST("/tokens/import-grok-token", handlers.ProviderAdmin.ImportGrokToken)
authed.POST("/tokens/import-custom-account", handlers.ProviderAdmin.ImportCustomAccount)
authed.POST("/tokens/delete-bulk", handlers.ProviderAdmin.TokenDeleteBulk)
authed.PATCH("/tokens/:pool/:id", handlers.ProviderAdmin.TokenUpdate)
authed.DELETE("/tokens/:pool/:id", handlers.ProviderAdmin.TokenDelete)
authed.GET("/accounts", handlers.ProviderAdmin.AccountsList)
authed.GET("/accounts/:pool/:id/quota", handlers.ProviderAdmin.AccountQuota)
authed.GET("/accounts/:pool/:id/email", handlers.ProviderAdmin.AccountEmail)
authed.GET("/providers", handlers.AdminRead.Providers)
authed.GET("/images", handlers.AdminRead.Images)
authed.GET("/refresh/profiles", handlers.ProviderAdmin.RefreshProfiles)
authed.POST("/refresh/profiles/:profile_id/refresh-now", handlers.ProviderAdmin.RefreshNow)
authed.PATCH("/refresh/profiles/:profile_id", handlers.ProviderAdmin.RefreshUpdate)
authed.DELETE("/refresh/profiles/:profile_id", handlers.ProviderAdmin.RefreshDelete)
authed.POST("/managed-models", handlers.AdminWrite.CreateModel)
authed.PATCH("/managed-models/:model_id", handlers.AdminWrite.UpdateModel)
authed.DELETE("/managed-models/:model_id", handlers.AdminWrite.DeleteModel)
authed.DELETE("/logs", handlers.AdminWrite.ClearLogs)
authed.DELETE("/logs/pending", handlers.AdminWrite.ClearPendingLogs)
authed.POST("/showcase", handlers.AdminWrite.CreateShowcase)
authed.PATCH("/showcase/:entry_id", handlers.AdminWrite.UpdateShowcase)
authed.DELETE("/showcase/:entry_id", handlers.AdminWrite.DeleteShowcase)
settings := authed.Group("/settings")
{
settings.GET("/site", handlers.SiteSettings.Get)
settings.PUT("/site", handlers.SiteSettings.Put)
settings.POST("/logo", handlers.AppSettings.LogoUpload)
settings.DELETE("/logo", handlers.AppSettings.LogoDelete)
settings.POST("/asset", handlers.AppSettings.AssetUpload)
settings.GET("/registration", handlers.AppSettings.RegistrationGet)
settings.PUT("/registration", handlers.AppSettings.RegistrationPut)
settings.GET("/smtp", handlers.AppSettings.SMTPGet)
settings.PUT("/smtp", handlers.AppSettings.SMTPPut)
settings.POST("/smtp/test", handlers.AppSettings.SMTPTest)
settings.GET("/proxy", handlers.AppSettings.ProxyGet)
settings.PUT("/proxy", handlers.AppSettings.ProxyPut)
settings.POST("/proxy/test", handlers.AppSettings.ProxyTest)
settings.GET("/credits", handlers.AppSettings.CreditsGet)
settings.PUT("/credits", handlers.AppSettings.CreditsPut)
settings.GET("/logs", handlers.AppSettings.LogsGet)
settings.PUT("/logs", handlers.AppSettings.LogsPut)
settings.GET("/media", handlers.AppSettings.MediaGet)
settings.PUT("/media", handlers.AppSettings.MediaPut)
}
}
authGroup.Use(middleware.RequireSession(auth))
{
authGroup.GET("/me", handlers.Auth.Me)
authGroup.GET("/invites", handlers.Auth.Invites)
authGroup.POST("/checkin", handlers.Auth.Checkin)
authGroup.POST("/change-password", handlers.Auth.ChangePassword)
authGroup.GET("/api-key", handlers.UserTools.APIKeyGet)
authGroup.POST("/api-key", handlers.UserTools.APIKeyMint)
authGroup.DELETE("/api-key", handlers.UserTools.APIKeyDelete)
authGroup.POST("/redeem-cdk", handlers.UserTools.RedeemCDK)
}
return engine
}