93741a5504
让预付费积分成为闭环(发放→消耗→见底拦住): - POST /admin/credits/grant:给租户充值/发放(正=grant,负=adjust 校正), 复用 store.GrantCredits(账本分录 + 物化余额,一事务)。 - 提交门控:credit_enforce 开启且租户余额≤0 → 拒绝新任务 402;默认关=软扣不拦。 开关入 billing-config(sundynix_setting KV,后台可切)。 - 修 bug:GrantCredits 是跨租户管理操作,须 store.WithoutTenant——否则 tenant 插件 会把账本分录的 tenant_id 覆盖成 admin 自己的租户(余额记目标、分录记 admin,破坏对账)。 live 验证:enforce 关→余额0可提交;开→余额0拒 402;充值后可提交、余额递减; 修复后 grant 分录落到目标租户、balance==SUM(ledger) 不变量成立。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
52 lines
1.6 KiB
Go
52 lines
1.6 KiB
Go
package store
|
|
|
|
import (
|
|
"context"
|
|
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// Setting 是平台级键值配置(全局,非租户作用域)——如 token→积分汇率 等可后台热调的计费规则。
|
|
// 表名 sundynix_setting。Key 唯一。
|
|
type Setting struct {
|
|
BaseModel
|
|
Key string `gorm:"size:64;uniqueIndex"`
|
|
Value string `gorm:"size:255"`
|
|
}
|
|
|
|
func (Setting) TableName() string { return "sundynix_setting" }
|
|
|
|
// 平台设置键。
|
|
const (
|
|
SettingTokensPerCredit = "tokens_per_credit"
|
|
SettingCreditEnforce = "credit_enforce" // "on"=余额≤0 拒绝新任务;其它/未设=软扣不拦
|
|
)
|
|
|
|
// CreditEnforceEnabled 是否开启积分硬拦截(默认关:软扣,余额可为负)。
|
|
func (p *Postgres) CreditEnforceEnabled(ctx context.Context) bool {
|
|
return p.GetSetting(ctx, SettingCreditEnforce) == "on"
|
|
}
|
|
|
|
// GetSetting 读一个平台设置;不存在返回空串(调用方回退默认)。
|
|
func (p *Postgres) GetSetting(ctx context.Context, key string) string {
|
|
if p.db == nil {
|
|
return ""
|
|
}
|
|
var s Setting
|
|
if err := p.db.WithContext(ctx).Select("value").Where("key = ?", key).First(&s).Error; err != nil {
|
|
return ""
|
|
}
|
|
return s.Value
|
|
}
|
|
|
|
// SetSetting 幂等写一个平台设置(key 唯一,重复即覆盖 value)。
|
|
func (p *Postgres) SetSetting(ctx context.Context, key, value string) error {
|
|
if p.db == nil {
|
|
return errStoreDisabled
|
|
}
|
|
return p.db.WithContext(ctx).Clauses(clause.OnConflict{
|
|
Columns: []clause.Column{{Name: "key"}},
|
|
DoUpdates: clause.AssignmentColumns([]string{"value", "updated_at"}),
|
|
}).Create(&Setting{Key: key, Value: value}).Error
|
|
}
|