package accounts import ( "errors" "sync" "github.com/wangjia/pay/config" ) // ErrNoAccount 表示该 channel/region 下没有可选账户(候选为空,或被策略全部排除)。 var ErrNoAccount = errors.New("accounts: no candidate account for channel/region") // 路由策略名(配置 routing. 的取值;缺省/未知一律当 round_robin)。 const ( StrategyRoundRobin = "round_robin" StrategyWeighted = "weighted" StrategyLimitAware = "limit_aware" ) // PickHint 选账户的上下文提示(确定性 + retry 换号)。 type PickHint struct { OutTradeNo string // weighted 的确定性锚点;crypto 地址池亲和 AmountMinor int64 // limit_aware 判定 used+amount<=DailyLimit ExcludeAccounts []string // retry:排除已试过的账户,换一个重试 } // Picker 面向 gateway 的路由入口:按渠道配置的策略从 enabled 账户里选一个。 type Picker interface { Pick(channel, region string, hint PickHint) (config.AccountConfig, error) } // Strategy 是纯函数式选择器:对「已过滤的候选切片」选一个账户。 // key = channel+"|"+region,round_robin 用它分桶进程计数器,其余策略忽略。 type Strategy interface { Pick(key string, candidates []config.AccountConfig, hint PickHint) (config.AccountConfig, error) } // RoundRobin 进程内轮询:按 key 分桶各自递增计数器,mod 候选数。 // 重启归零、不落库、不跨实例共享(见计划 D2);只为均摊,不作强配额。 type RoundRobin struct { mu sync.Mutex counters map[string]uint64 } func NewRoundRobin() *RoundRobin { return &RoundRobin{counters: map[string]uint64{}} } func (r *RoundRobin) Pick(key string, candidates []config.AccountConfig, _ PickHint) (config.AccountConfig, error) { if len(candidates) == 0 { return config.AccountConfig{}, ErrNoAccount } r.mu.Lock() n := r.counters[key] r.counters[key] = n + 1 r.mu.Unlock() return candidates[int(n%uint64(len(candidates)))], nil }