// Package quota 用户级软配额:三段式(预扣 → 结算 → 退款,shenshi-gateway.md §7)。 // // P0/P1 只做预算熔断(token 口径,HJ-2004 配额不足);1.0 官方云计费属 P2。 // 换算与钳位集中在本包单一函数,饱和截断防溢出变负。 package quota import ( "errors" "fmt" "hunjing/shenshi/internal/store" ) // ErrInsufficient 配额不足(HJ-2004)。 var ErrInsufficient = errors.New("HJ-2004 配额不足") // Limiter 用户配额裁决器;budget=0 表示不限制(本地开发档)。 type Limiter struct { store store.Store budget int64 } // New 构造裁决器。 func New(st store.Store, budgetTokens int64) *Limiter { return &Limiter{store: st, budget: budgetTokens} } // clamp 集中单一换算函数:预扣额 = min(请求上限, 钳位上限),且饱和截断不为负。 // 请求侧任何计费乘数(max_tokens 等)在进入本函数前已由 IR 校验限幅。 func clamp(requested int64) int64 { const reserveCeiling = 1 << 20 // 1M token 单次预扣上限(校验期设上限,防溢出) if requested <= 0 { return 0 } if requested > reserveCeiling { return reserveCeiling } return requested } // Reserve 预扣(按上限):配额不足时拒绝并返回 ErrInsufficient。 func (l *Limiter) Reserve(userID string, requested int64) (reserved int64, err error) { reserved = clamp(requested) if l.budget <= 0 || reserved <= 0 { // 不限制档仍记账(可见性),但不裁决 if reserved > 0 { return reserved, l.store.Reserve(userID, reserved) } return 0, nil } b, err := l.store.Balance(userID) if err != nil { return 0, err } if b.Reserved+b.Used+reserved > l.budget { return 0, fmt.Errorf("%w:已用 %d + 预扣 %d + 本次 %d > 预算 %d", ErrInsufficient, b.Used, b.Reserved, reserved, l.budget) } if err := l.store.Reserve(userID, reserved); err != nil { return 0, err } return reserved, nil } // Settle 结算:实际用量回填,差额(预扣-实际)即时释放。幂等性由 usage_logs // 主键(同一尝试 ID 只落一条)保证:结算与落库同批执行,重放不会二次扣减。 func (l *Limiter) Settle(userID string, reserved, actual int64) error { return l.store.Settle(userID, reserved, clamp(actual)) } // Refund 失败退款:全额释放预扣(幂等——释放至多为预扣额,GREATEST 饱和截断)。 func (l *Limiter) Refund(userID string, reserved int64) error { if reserved <= 0 { return nil } return l.store.Refund(userID, reserved) } // Budget 预算与余额总览(/me/usage 附带)。 func (l *Limiter) Budget(userID string) (budget, used, reserved int64, err error) { b, err := l.store.Balance(userID) if err != nil { return 0, 0, 0, err } return l.budget, b.Used, b.Reserved, nil }