Files
sub2api/backend/internal/service/gateway_profit_control.go
T
李建琦 6d655c9903
Release / update-version (push) Has been cancelled
Release / build-frontend (push) Has been cancelled
Release / release (push) Has been cancelled
Release / sync-version-file (push) Has been cancelled
CI / shell (push) Canceled after 0s
CI / test (push) Canceled after 0s
CI / frontend (push) Canceled after 0s
CI / golangci-lint (push) Canceled after 0s
Security Scan / backend-security (push) Canceled after 0s
Security Scan / frontend-security (push) Canceled after 0s
Sub2API v1.0 - AI API 网关(二开初始版本,基于上游 Wei-Shaw/sub2api)
2026-08-21 18:30:13 +08:00

113 lines
4.6 KiB
Go

package service
import (
"context"
"log/slog"
"github.com/Wei-Shaw/sub2api/internal/pkg/ctxkey"
)
// withGatewayProfitControlGate installs the gate only for explicitly marked
// token requests. This keeps media, metadata, and models-list paths outside
// the profit-control surface by construction.
func (s *GatewayService) withGatewayProfitControlGate(ctx context.Context, groupID *int64) context.Context {
if _, ok := gatewayTokenRequestPricingAtFromContext(ctx); !ok || groupID == nil || *groupID <= 0 {
return ctx
}
if existing, ok := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate); ok && existing != nil && existing.groupID == *groupID {
return ctx
}
group, err := s.resolveProfitControlGroup(ctx, *groupID)
if err != nil {
slog.Warn("profit_control_group_load_failed", "group_id", *groupID, "error", err)
return s.clearForeignProfitControlGate(ctx, groupID)
}
if group == nil || !group.ProfitControlEnabled || !profitControlPlatformSupported(group.Platform) {
return s.clearForeignProfitControlGate(ctx, groupID)
}
pricingAt, _ := gatewayTokenRequestPricingAtFromContext(ctx)
billingGroup := gatewayTokenRequestBillingGroupFromContext(ctx)
if billingGroup == nil {
if ctxGroup, ok := ctx.Value(ctxkey.Group).(*Group); ok && IsGroupContextValid(ctxGroup) {
billingGroup = ctxGroup
} else {
billingGroup = group
}
}
downstream := billingGroup.RateMultiplier
if userID, _ := ctx.Value(ctxkey.UserID).(int64); userID > 0 {
downstream = s.ResolveUserGroupRateMultiplier(ctx, userID, billingGroup.ID, billingGroup.RateMultiplier)
}
downstream *= billingGroup.PeakMultiplierAt(pricingAt)
threshold := clampProfitControlThreshold(downstream * (1 - group.ProfitMinMargin - group.ProfitSafetyBuffer))
gate := &openAIProfitControlGate{
groupID: group.ID,
platform: group.Platform,
threshold: threshold,
pricingAt: pricingAt,
}
openAIProfitControlObserverInstance.recordInstall(gate.groupID, gate.platform, gate.threshold)
return context.WithValue(ctx, openAIProfitControlGateCtxKey{}, gate)
}
func (s *GatewayService) clearForeignProfitControlGate(ctx context.Context, groupID *int64) context.Context {
existing, ok := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate)
if !ok || existing == nil || groupID == nil || existing.groupID == *groupID {
return ctx
}
return context.WithValue(ctx, openAIProfitControlGateCtxKey{}, (*openAIProfitControlGate)(nil))
}
func (s *GatewayService) resolveProfitControlGroup(ctx context.Context, groupID int64) (*Group, error) {
if group, ok := ctx.Value(ctxkey.Group).(*Group); ok && IsGroupContextValid(group) && group.ID == groupID {
return group, nil
}
if s.schedulerSnapshot != nil {
// Lite 读取:门只用平台/倍率/利润/高峰字段,不需要账号计数聚合。
return s.schedulerSnapshot.GetGroupByIDLite(ctx, groupID)
}
return s.resolveGroupByID(ctx, groupID)
}
// GatewayProfitControlVetoLatest performs the terminal post-slot check against
// the latest scheduler snapshot. Snapshot read failures are deliberately
// fail-open to preserve availability, but are observable.
func (s *GatewayService) GatewayProfitControlVetoLatest(ctx context.Context, selected *Account) (*Account, bool, string) {
return profitControlVetoLatest(ctx, selected, s.schedulerSnapshot)
}
func profitControlVetoLatest(ctx context.Context, selected *Account, snapshot *SchedulerSnapshotService) (*Account, bool, string) {
gate, _ := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate)
if gate == nil || selected == nil {
return selected, false, ""
}
latest := selected
if snapshot != nil {
refreshed, err := snapshot.GetAccount(ctx, selected.ID)
if err != nil || refreshed == nil {
slog.Warn("profit_control_account_refresh_failed", "group_id", gate.groupID, "platform", gate.platform, "account_id", selected.ID, "error", err)
openAIProfitControlObserverInstance.recordRefreshFailure(gate.groupID, gate.platform, gate.threshold)
} else if !refreshed.UpdatedAt.Before(selected.UpdatedAt) {
// 选号路径可能已做过 DB recheck,selected 比缓存快照更新鲜;只有
// 快照不落后时才替换,避免终检把新鲜账号换回较旧的缓存对象。
latest = refreshed
}
}
vetoed, reason := openAIProfitControlVetoReason(ctx, latest)
return latest, vetoed, reason
}
func (s *GatewayService) isGatewayAccountProfitEligible(ctx context.Context, account *Account) bool {
vetoed, _ := openAIProfitControlVetoReason(ctx, account)
return !vetoed
}
func gatewayProfitControlGateActive(ctx context.Context) bool {
gate, _ := ctx.Value(openAIProfitControlGateCtxKey{}).(*openAIProfitControlGate)
return gate != nil
}