Files
junhong_cmp_fiber/internal/infrastructure/gatewaylimit/settings.go
邹进 42e881576d
All checks were successful
构建并部署到测试环境(无 SSH) / build-and-deploy (push) Successful in 9m33s
feat: 卡网关按运营商账户限流并记录停复机调用
- 新增网关限流:按本系统运营商账户(卡 carrier_id)→ 限流组 → 操作限制每秒请求数,每次 HTTP 尝试计量,默认限流、仅后台人工点击/通道阈值/自动续费/手动刷新豁免;支持只统计与拦截两种模式,默认只统计
- Asynq 遇限流不计失败、固定短延迟重试;轮询任务与停复机业务重试遇限流短延迟重排
- 停机、复机不再使用网关内部重试,只发一次请求
- 网关返回结构化错误;新增上游错误分类规则表与测试接口
- 新增按月分区的停复机记录表:系统自动停复机只写记录,不再写统一审计与集成日志;普通轮询查询不再写集成日志
- 新增限流组、错误分类、停复机记录、按账户调用统计与枚举后台接口
- 迁移 000243~000246;同步 OpenSpec 提案(含后续 6 个 change)与技术方案、工程约束
2026-10-24 15:06:41 +09:00

137 lines
6.0 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
// Package gatewaylimit 实现按运营商账户(本系统 tb_carrier)的卡网关调用限流、错误分类与调用统计。
//
// 账户口径:限流单位是本系统的运营商账户(卡上的 carrier_id),与网关客户端自身的 appId 无关。
// 调用方已知卡所属账户时用 gateway.WithCarrier 传入;否则守卫按 ICCID 查本系统卡表解析。
package gatewaylimit
import (
"context"
stderrors "errors"
"strconv"
"sync"
"time"
"go.uber.org/zap"
"github.com/break/junhong_cmp_fiber/internal/infrastructure/systemconfig"
"github.com/break/junhong_cmp_fiber/pkg/constants"
)
// RegisterConfigDefinitions 向受控系统配置注册表注册网关限流相关配置;已注册的 Key 跳过。
func RegisterConfigDefinitions(registry *systemconfig.Registry, logger *zap.Logger) {
if registry == nil {
return
}
minOne, minZero := int64(1), int64(0)
maxQPS, maxDelay, maxAlert, maxMonths := int64(10000), int64(3600), int64(86400*7), int64(120)
definitions := []systemconfig.Definition{
{Key: constants.SystemConfigPollingRateLimitMode, Module: constants.SystemConfigModulePolling, ValueType: constants.SystemConfigTypeString, DefaultValue: constants.RateLimitModeObserve, Description: "网关限流模式:observe 只统计不拦截,enforce 拦截", EnumValues: []string{constants.RateLimitModeObserve, constants.RateLimitModeEnforce}},
{Key: constants.SystemConfigPollingGatewayDefaultQPS, Module: constants.SystemConfigModulePolling, ValueType: constants.SystemConfigTypeInt, DefaultValue: "10", Description: "未被启用限流组覆盖的卡网关操作的默认每秒请求上限", Min: &minOne, Max: &maxQPS},
{Key: constants.SystemConfigPollingRateLimitedRetryDelay, Module: constants.SystemConfigModulePolling, ValueType: constants.SystemConfigTypeInt, DefaultValue: "5", Description: "Asynq 任务遇限流后的固定重试延迟(秒)", Min: &minOne, Max: &maxDelay},
{Key: constants.SystemConfigPollingRateLimitDeferAlert, Module: constants.SystemConfigModulePolling, ValueType: constants.SystemConfigTypeInt, DefaultValue: "600", Description: "同一任务因限流累计推迟超过该时长(秒)时写告警", Min: &minOne, Max: &maxAlert},
{Key: constants.SystemConfigPollingStopResumeRecordRetention, Module: constants.SystemConfigModulePolling, ValueType: constants.SystemConfigTypeInt, DefaultValue: "6", Description: "停复机记录保留月数,到期整区删除", Min: &minOne, Max: &maxMonths},
{Key: constants.SystemConfigPollingStopResumeRecordPremake, Module: constants.SystemConfigModulePolling, ValueType: constants.SystemConfigTypeInt, DefaultValue: "3", Description: "停复机记录提前创建未来分区的月数", Min: &minZero, Max: &maxMonths},
}
for _, definition := range definitions {
if _, exists := registry.Get(definition.Key); exists {
continue
}
if err := registry.Register(definition); err != nil && logger != nil {
logger.Error("注册网关限流系统配置失败,将使用代码默认值", zap.String("config_key", definition.Key), zap.Error(err))
}
}
}
// ConfigReader 读取已注册的受控系统配置。
type ConfigReader interface {
Get(ctx context.Context, key string) (string, error)
}
// Settings 是网关限流运行参数快照。
type Settings struct {
Mode string
DefaultQPS int
RetryDelay time.Duration
DeferAlert time.Duration
RecordRetention int
RecordPremakeMonth int
}
// Enforce 判断当前是否为拦截模式。
func (s Settings) Enforce() bool {
return s.Mode == constants.RateLimitModeEnforce
}
// defaultSettings 是配置读取失败时的安全默认值:只统计、不拦截。
func defaultSettings() Settings {
return Settings{Mode: constants.RateLimitModeObserve, DefaultQPS: 10, RetryDelay: 5 * time.Second, DeferAlert: 10 * time.Minute, RecordRetention: 6, RecordPremakeMonth: 3}
}
// SettingsProvider 在进程内缓存限流参数,超过 TTL 重新读取,后台修改数秒内生效。
type SettingsProvider struct {
reader ConfigReader
logger *zap.Logger
mu sync.Mutex
cached Settings
loaded time.Time
}
// NewSettingsProvider 创建限流参数提供者;reader 为 nil 时始终返回安全默认值。
func NewSettingsProvider(reader ConfigReader, logger *zap.Logger) *SettingsProvider {
if logger == nil {
logger = zap.NewNop()
}
return &SettingsProvider{reader: reader, logger: logger, cached: defaultSettings()}
}
// Current 返回当前限流参数;读取失败时保留上一次成功值。
func (p *SettingsProvider) Current(ctx context.Context) Settings {
if p == nil {
return defaultSettings()
}
p.mu.Lock()
defer p.mu.Unlock()
if p.reader == nil || time.Since(p.loaded) < constants.GatewayRateLimitConfigTTL {
return p.cached
}
next, err := p.load(ctx)
p.loaded = time.Now()
if err != nil {
p.logger.Warn("读取网关限流配置失败,沿用上一次配置", zap.Error(err))
return p.cached
}
p.cached = next
return next
}
func (p *SettingsProvider) load(ctx context.Context) (Settings, error) {
settings := defaultSettings()
mode, err := p.reader.Get(ctx, constants.SystemConfigPollingRateLimitMode)
if err != nil {
return settings, err
}
settings.Mode = mode
ints := []struct {
key string
set func(int)
}{
{constants.SystemConfigPollingGatewayDefaultQPS, func(v int) { settings.DefaultQPS = v }},
{constants.SystemConfigPollingRateLimitedRetryDelay, func(v int) { settings.RetryDelay = time.Duration(v) * time.Second }},
{constants.SystemConfigPollingRateLimitDeferAlert, func(v int) { settings.DeferAlert = time.Duration(v) * time.Second }},
{constants.SystemConfigPollingStopResumeRecordRetention, func(v int) { settings.RecordRetention = v }},
{constants.SystemConfigPollingStopResumeRecordPremake, func(v int) { settings.RecordPremakeMonth = v }},
}
for _, item := range ints {
raw, err := p.reader.Get(ctx, item.key)
if err != nil {
return settings, err
}
value, err := strconv.Atoi(raw)
if err != nil {
return settings, stderrors.New("网关限流整数配置非法")
}
item.set(value)
}
return settings, nil
}