feat(gateway): SaaS P2 计量·增量2 —— 积分账本 + 物化余额 + 用量 rollup
- credit_ledger(append-only):grant/usage/adjust 分录,扣量/充值的事实源。 - tenant.credit_balance_micro:物化余额(= 账本之和),用量扣、充值增。 - usage_rollup:租户/天 upsert 累加(配额/账单读一行,不扫明细)。 - 消费 usage_event 一个事务内:落明细 → applyUsageCredit(账本负分录+扣余额) → upsertRollup(累加)。幂等锚在 usage_event.task_id 唯一:RowsAffected==0(重投) 则跳过账本/余额/rollup,绝不重复计费。软扣:余额可为负(不拦,硬闸留 P4)。 - GrantCredits 充值/发放 API(记 grant 分录+增余额;增量3 admin 接)。 live 验证:ledger 负分录(ref=task_id) / 余额递减 / rollup 两任务累加(count=2,tok/credits求和) / **balance==seed+SUM(ledger) 不变量成立**(物化余额 == 账本真值)。 增量3(GET /admin/usage 趋势+余额)待做。 Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
This commit is contained in:
@@ -4,7 +4,9 @@ import (
|
||||
"context"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"gorm.io/gorm"
|
||||
"gorm.io/gorm/clause"
|
||||
|
||||
"github.com/sundynix/sundynix-shared/contract"
|
||||
@@ -77,11 +79,26 @@ func (p *Postgres) SaveUsageEvent(ctx context.Context, ev *contract.UsageEvent)
|
||||
CreditsMicro: creditsMicro, CostMicros: costMicros, Currency: currency,
|
||||
Exceeded: ev.Exceeded, TS: ev.TS,
|
||||
}
|
||||
// 幂等:同一 task_id 已有明细则不重复插入(防 NATS 重投重复计费)。
|
||||
return p.db.WithContext(ctx).Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "task_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(row).Error
|
||||
day := time.UnixMilli(ev.TS).Format("20060102")
|
||||
|
||||
// 一个事务内:落明细 → 扣积分(账本+余额) → 累加 rollup。
|
||||
// 幂等锚点:usage_event 的 task_id 唯一;插入若被冲突吞掉(RowsAffected==0)→重投,跳过后续,绝不重复计费。
|
||||
return p.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
res := tx.Clauses(clause.OnConflict{
|
||||
Columns: []clause.Column{{Name: "task_id"}},
|
||||
DoNothing: true,
|
||||
}).Create(row)
|
||||
if res.Error != nil {
|
||||
return res.Error
|
||||
}
|
||||
if res.RowsAffected == 0 {
|
||||
return nil // 重投:明细已存在,账本/余额/rollup 均不再动
|
||||
}
|
||||
if err := applyUsageCredit(tx, ev.TenantID, ev.TaskID, creditsMicro); err != nil {
|
||||
return err
|
||||
}
|
||||
return upsertRollup(tx, ev.TenantID, day, int64(ev.TotalTok), creditsMicro, costMicros, currency)
|
||||
})
|
||||
}
|
||||
|
||||
// pricingForModelName 按模型名查计价(join model 表,pricing 以 model_id 关联)。查不到返回 nil。
|
||||
|
||||
Reference in New Issue
Block a user