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