Files
junhong_cmp_fiber/internal/gateway/call_guard.go
邹进 2fe7c1f8ac feat: 按运营商账户调度的分发器、卡任务执行器与模式切换
- 四个轮询处理器拆为步骤函数 Step(返回结果分类),旧 Asynq Handle 只做并发控制与原有重入队映射,行为不变;carrier 模式下旧 handler 直接结束
- 网关限流增加等待模式(ctx 标记),NSQ 消费者等名额到单步超时,超时按限流处理不计失败;记录限流等待次数
- 卡任务执行器:核对 token 并抢执行权,按配置步骤顺序串行执行到期检查(单步超时、总超时、panic 恢复、流量已触发评估时跳过套餐),执行中续期并 Touch,续期失败立即停止;收尾写回到期、连续失败、退避、延迟确认与告警
- 分发器:每秒按账户抢分发锁领取(执行上限按消费能力收紧、优先占比、按限流组剩余 QPS 估算),发布到 polling.card.{账户},发布失败释放;消费旧优先提示与手动触发;心跳与处理中标记到期计数
- NSQ 生产者与按账户动态订阅的消费者管理器,优雅退出宽限期
- 失败告警异步写 tb_polling_failure_alert(同卡同类型一条未处理,成功转已恢复),账户窗口内告警数超阈值产生账户级告警
- 模式监听:切 carrier 时旧分片调度停止出队,加锁初始化新结构后开始领取并订阅;回 legacy 时停止消费、重建旧分片队列;生命周期(API 与 Worker)在 carrier 模式写账户队列
2026-10-25 18:44:23 +09:00

159 lines
5.7 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 gateway
import (
"context"
stderrors "errors"
"time"
"unicode/utf8"
"github.com/break/junhong_cmp_fiber/pkg/constants"
"github.com/break/junhong_cmp_fiber/pkg/errors"
)
// GatewayError 是网关调用失败的结构化错误。
// 它包裹原有的 *errors.AppError:Error() 文本与错误码保持不变,调用方仍可 errors.As 取 AppError;
// 需要类别、上游业务码、上游信息与耗时的调用方用 errors.As 取 *GatewayError。
type GatewayError struct {
Category string // 错误类别,见 constants.GatewayError*
ProviderCode int // 网关业务码(仅上游业务错误时非 0)
ProviderMessage string // 网关返回信息(已截断,仅上游业务错误时非空)
HTTPStatus int // HTTP 状态码(仅 HTTP 异常时非 0)
Duration time.Duration // 本次 HTTP 尝试耗时
app *errors.AppError
}
// Error 返回与原 AppError 一致的文本。
func (e *GatewayError) Error() string {
if e == nil || e.app == nil {
return "Gateway 调用失败"
}
return e.app.Error()
}
// Unwrap 返回原 AppError,保持错误码判断兼容。
func (e *GatewayError) Unwrap() error {
if e == nil || e.app == nil {
return nil
}
return e.app
}
func newGatewayError(category string, app *errors.AppError) *GatewayError {
return &GatewayError{Category: category, app: app}
}
// AsGatewayError 从错误链中取出结构化网关错误。
func AsGatewayError(err error) (*GatewayError, bool) {
var gwErr *GatewayError
if stderrors.As(err, &gwErr) && gwErr != nil {
return gwErr, true
}
return nil, false
}
// IsRateLimited 判断错误是否为本系统运营商账户限流拒绝(未发出请求)。
func IsRateLimited(err error) bool {
gwErr, ok := AsGatewayError(err)
return ok && gwErr.Category == constants.GatewayErrorRateLimited
}
// NewRateLimitedError 创建限流拒绝错误;由限流守卫在拿不到名额时返回。
func NewRateLimitedError() error {
return newGatewayError(constants.GatewayErrorRateLimited,
errors.New(errors.CodeTooManyRequests, "运营商账户网关调用已达限流上限,请稍后重试"))
}
// truncateProviderMessage 按字符截断上游信息,避免超长内容落库或展示。
func truncateProviderMessage(msg string) string {
if utf8.RuneCountInString(msg) <= constants.GatewayProviderMessageMaxLen {
return msg
}
runes := []rune(msg)
return string(runes[:constants.GatewayProviderMessageMaxLen])
}
// CallInfo 描述一次卡网关 HTTP 尝试,供调用守卫限流与统计。
type CallInfo struct {
Operation string // 卡网关操作,见 constants.GatewayOp*
CardNo string // 卡号(ICCID)
CarrierID uint // 调用方已知的运营商账户 ID(本系统 tb_carrier.id),0 表示由守卫按卡号解析
ExemptSource string // 豁免限流来源,空表示受限流约束
Attempt int // 本次方法调用内的尝试序号,从 1 开始
}
// CallGuard 是卡网关调用守卫:每次 HTTP 尝试前取限流名额,尝试结束后记录统计。
// BeforeAttempt 返回错误时本次方法调用立即结束,不再发起后续尝试。
type CallGuard interface {
BeforeAttempt(ctx context.Context, call CallInfo) error
AfterAttempt(ctx context.Context, call CallInfo, callErr error, duration time.Duration)
}
type cardCallKey struct{}
type carrierKey struct{}
type exemptKey struct{}
type rateLimitWaitKey struct{}
type cardCall struct {
operation string
cardNo string
}
// WithCarrier 声明本次调用目标卡所属的运营商账户(本系统 tb_carrier.id),避免守卫按卡号再查一次。
func WithCarrier(ctx context.Context, carrierID uint) context.Context {
if carrierID == 0 {
return ctx
}
return context.WithValue(ctx, carrierKey{}, carrierID)
}
// WithRateLimitExempt 声明本次调用豁免运营商账户限流,只允许后台人工点击、通道阈值、自动续费、手动刷新使用。
func WithRateLimitExempt(ctx context.Context, source string) context.Context {
if source == "" {
return ctx
}
return context.WithValue(ctx, exemptKey{}, source)
}
// WithRateLimitWait 声明本次调用在拿不到限流名额时等待,总等待不超过 maxWait;超时返回限流错误。
// 只用于不占 Asynq 协程的执行方(按运营商账户调度的 NSQ 消费者);未声明时拿不到名额立即返回。
func WithRateLimitWait(ctx context.Context, maxWait time.Duration) context.Context {
if maxWait <= 0 {
return ctx
}
return context.WithValue(ctx, rateLimitWaitKey{}, maxWait)
}
// RateLimitWait 返回 ctx 中声明的限流最长等待;未声明返回 0。
func RateLimitWait(ctx context.Context) time.Duration {
wait, _ := ctx.Value(rateLimitWaitKey{}).(time.Duration)
return wait
}
// RateLimitExemptSource 返回 ctx 中声明的豁免来源,未声明返回空串。
func RateLimitExemptSource(ctx context.Context) string {
source, _ := ctx.Value(exemptKey{}).(string)
return source
}
func withCardCall(ctx context.Context, operation, cardNo string) context.Context {
return context.WithValue(ctx, cardCallKey{}, cardCall{operation: operation, cardNo: cardNo})
}
// cardCallInfo 从 ctx 取出本次卡网关调用信息;非卡操作返回 false。
func cardCallInfo(ctx context.Context) (CallInfo, bool) {
call, ok := ctx.Value(cardCallKey{}).(cardCall)
if !ok {
return CallInfo{}, false
}
carrierID, _ := ctx.Value(carrierKey{}).(uint)
return CallInfo{
Operation: call.operation, CardNo: call.cardNo, CarrierID: carrierID,
ExemptSource: RateLimitExemptSource(ctx),
}, true
}
// SetCallGuard 设置卡网关调用守卫;为 nil 时不限流、不统计。
func (c *Client) SetCallGuard(guard CallGuard) {
c.guard = guard
}