M3 收尾: 限流/配额 + 前端骨架屏 + 单端口托管前端
- ratelimit(内存计数): 密钥级每日请求数/Token 配额、用户级每秒速率 (OT_RATELIMIT_USER_RPS), 超限返回 429 - 网关 Auth 前置配额/限流检查, finishUsage 累计密钥 token 用量 - 前端 Skeleton 组件 + Dashboard/管理总览加载态 - Go 服务托管 web/dist 静态资源(SPA 回退), 单端口即可访问前后端 Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -12,6 +12,7 @@ import (
|
||||
"github.com/openteam/server/internal/channel"
|
||||
"github.com/openteam/server/internal/pkg/apikey"
|
||||
"github.com/openteam/server/internal/pkg/crypto"
|
||||
"github.com/openteam/server/internal/pkg/ratelimit"
|
||||
"github.com/openteam/server/internal/proxy/convert"
|
||||
"github.com/openteam/server/internal/store"
|
||||
"github.com/openteam/server/internal/usage"
|
||||
@@ -25,20 +26,24 @@ const (
|
||||
)
|
||||
|
||||
type Gateway struct {
|
||||
db *gorm.DB
|
||||
ch *channel.Service
|
||||
rec *usage.Recorder
|
||||
enc *crypto.Encryptor
|
||||
hc *http.Client
|
||||
db *gorm.DB
|
||||
ch *channel.Service
|
||||
rec *usage.Recorder
|
||||
enc *crypto.Encryptor
|
||||
lim *ratelimit.Limiter
|
||||
userRPS int
|
||||
hc *http.Client
|
||||
}
|
||||
|
||||
func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder) *Gateway {
|
||||
func NewGateway(db *gorm.DB, enc *crypto.Encryptor, rec *usage.Recorder, lim *ratelimit.Limiter, userRPS int) *Gateway {
|
||||
return &Gateway{
|
||||
db: db,
|
||||
ch: channel.NewService(db, enc),
|
||||
rec: rec,
|
||||
enc: enc,
|
||||
hc: &http.Client{Timeout: 120 * time.Second},
|
||||
db: db,
|
||||
ch: channel.NewService(db, enc),
|
||||
rec: rec,
|
||||
enc: enc,
|
||||
lim: lim,
|
||||
userRPS: userRPS,
|
||||
hc: &http.Client{Timeout: 120 * time.Second},
|
||||
}
|
||||
}
|
||||
|
||||
@@ -81,6 +86,25 @@ func (g *Gateway) Auth(c *gin.Context) {
|
||||
return
|
||||
}
|
||||
|
||||
// 限流与配额(内存计数)
|
||||
if g.lim != nil {
|
||||
if k.QuotaRequestsPerDay != nil && !g.lim.AllowRequestDaily(k.ID, *k.QuotaRequestsPerDay) {
|
||||
apiError(c, http.StatusTooManyRequests, "rate_limit_exceeded", "Daily request quota exceeded for this API key")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if k.QuotaTokensPerDay != nil && g.lim.TokensUsed(k.ID) >= *k.QuotaTokensPerDay {
|
||||
apiError(c, http.StatusTooManyRequests, "rate_limit_exceeded", "Daily token quota exceeded for this API key")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
if !g.lim.AllowUserRate(u.ID, g.userRPS) {
|
||||
apiError(c, http.StatusTooManyRequests, "rate_limit_exceeded", "Too many requests. Please slow down.")
|
||||
c.Abort()
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
c.Set(CtxUserID, u.ID)
|
||||
c.Set(CtxKeyID, k.ID)
|
||||
c.Set(CtxTrace, newTraceID())
|
||||
|
||||
@@ -433,6 +433,11 @@ func (g *Gateway) finishUsage(c *gin.Context, ch *store.Channel, start time.Time
|
||||
chID = ch.ID
|
||||
}
|
||||
|
||||
// 密钥今日 token 用量累计(配额检查用)
|
||||
if g.lim != nil && kidVal > 0 {
|
||||
g.lim.AddTokens(kidVal, in+out)
|
||||
}
|
||||
|
||||
g.rec.Record(&store.UsageLog{
|
||||
RequestID: fmt.Sprintf("trace-%s", traceStr),
|
||||
TraceID: traceStr,
|
||||
|
||||
Reference in New Issue
Block a user