feat(channels): 渠道管理 - 可视化排序与别名并修复注册及外观清理 (#444)

This commit is contained in:
Ray
2026-09-08 20:56:42 +08:00
committed by GitHub
parent 381edf37e3
commit 8360b2c933
41 changed files with 1485 additions and 148 deletions

View File

@@ -21,6 +21,7 @@ import (
)
func RegisterAuthRoutes(r *gin.RouterGroup, svc *service.Service) {
registerChannelOrderRoutes(r, svc)
r.GET("/auth/settings", func(c *gin.Context) {
settings, err := svc.PublicAuthSettings()
if err != nil {
@@ -61,6 +62,9 @@ func RegisterAuthRoutes(r *gin.RouterGroup, svc *service.Service) {
if !available || !enforceRateLimit(c, "email-code:"+c.ClientIP(), policy.Request.EmailCodePerHour, time.Hour) {
return
}
if !enforceRateLimit(c, "registration-email-account:"+passwordResetRateLimitSubject(req.Email), 10, time.Hour) {
return
}
if err := svc.SendRegistrationEmailCode(req.Email); err != nil {
failService(c, err)
return

View File

@@ -0,0 +1,43 @@
package handler
import (
"github.com/gin-gonic/gin"
"infinite-canvas/backend/internal/service"
"net/http"
)
func registerChannelOrderRoutes(r *gin.RouterGroup, svc *service.Service) {
for _, path := range []string{"/admin/channels/order", "/admin/channels/:id/models/order"} {
r.GET(path, func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
items, err := svc.AdminChannelOrder(user, c.Param("id"))
if err != nil {
failService(c, err)
return
}
ok(c, gin.H{"items": items})
})
r.PUT(path, func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
c.Request.Body = http.MaxBytesReader(c.Writer, c.Request.Body, 1<<20)
var req service.ChannelOrderRequest
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
if err := svc.SaveAdminChannelOrder(user, c.Param("id"), req); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"saved": true})
})
}
}

View File

@@ -293,6 +293,23 @@ func RegisterFinanceRoutes(r *gin.RouterGroup, svc *service.Service) {
r.PATCH("/admin/channels/:id/models/:modelId", func(c *gin.Context) {
saveChannelModel(c, svc, c.Param("modelId"))
})
r.PATCH("/admin/channels/:id/models/:modelId/sort", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {
failService(c, err)
return
}
var req service.ChannelModelSortRequest
if err := c.ShouldBindJSON(&req); err != nil {
fail(c, http.StatusBadRequest, err)
return
}
if err := svc.UpdateAdminChannelModelSort(user, c.Param("id"), c.Param("modelId"), req); err != nil {
failService(c, err)
return
}
ok(c, gin.H{"updated": true})
})
r.DELETE("/admin/channels/:id/models/:modelId", func(c *gin.Context) {
user, err := currentUser(c, svc)
if err != nil {

View File

@@ -4,6 +4,7 @@ import (
"errors"
"log"
"net/http"
"strconv"
"strings"
"infinite-canvas/backend/internal/service"
@@ -27,6 +28,12 @@ func fail(c *gin.Context, status int, err error) {
}
func failService(c *gin.Context, err error) {
var cooldown *service.EmailCodeCooldownError
if errors.As(err, &cooldown) {
c.Header("Retry-After", strconv.Itoa(cooldown.Seconds))
fail(c, http.StatusTooManyRequests, cooldown)
return
}
var appErr *service.AppError
if errors.As(err, &appErr) && validErrorStatus(appErr.Status) {
code := appErr.Code

View File

@@ -18,6 +18,15 @@ type failureEnvelope struct {
Msg string `json:"msg"`
}
func TestFailServiceRegistrationCooldown(t *testing.T) {
recorder, context := responseTestContext()
failService(context, &service.EmailCodeCooldownError{Seconds: 47})
response := decodeFailureEnvelope(t, recorder)
if recorder.Code != http.StatusTooManyRequests || response.Code != 429 || recorder.Header().Get("Retry-After") != "47" || !strings.Contains(response.Msg, "47") {
t.Fatalf("cooldown response: status=%d header=%s body=%#v", recorder.Code, recorder.Header().Get("Retry-After"), response)
}
}
func TestFailServiceProjectsAppError(t *testing.T) {
recorder, context := responseTestContext()
err := service.NewAppError(http.StatusTooManyRequests, "请求过于频繁,请稍后重试")

View File

@@ -152,8 +152,10 @@ func enforceRateLimit(c *gin.Context, key string, limit int, window time.Duratio
if allowed {
return true
}
c.Header("Retry-After", "60")
fail(c, http.StatusTooManyRequests, errors.New("请求过于频繁,请稍后再试"))
wait := runtimeService.RequestRetryAfter(c.Request.Context(), key, window)
seconds := max(1, int((wait+time.Second-1)/time.Second))
c.Header("Retry-After", strconv.Itoa(seconds))
fail(c, http.StatusTooManyRequests, fmt.Errorf("请求次数已达上限,请在 %d 秒后重试", seconds))
return false
}