feat(models): 模型目录与计费 - 按展示名分组并隔离管理员积分成本

This commit is contained in:
ddCat committed 2026-09-19 09:54:27 +08:00
1 parent 5ad05efb58
commit 237f2e2a6e
49 files changed
+802 -276

No files matched your search

@@ -8,11 +8,8 @@ import (
"gorm.io/gorm"
)
func (r *Repository) UpdateSystemChannelPresentation(channelID string, alias *string, sortOrder *int, now time.Time) error {
func (r *Repository) UpdateSystemChannelPresentation(channelID string, sortOrder *int, now time.Time) error {
updates := map[string]any{"updated_at": now}
if alias != nil {
updates["public_alias"] = *alias
}
if sortOrder != nil {
updates["sort_order"] = *sortOrder
}
@@ -278,7 +278,7 @@ func (r *Repository) RouteAttempts(taskID string, routeRun int) ([]model.RouteAt
// SwitchTaskLogicalRoute 同时更新任务执行目标和账单。跟随供应价格时会原子调整预留积分,
// 保证明确未创建上游任务后的故障切线不会沿用上一条线路的价格快照。
func (r *Repository) SwitchTaskLogicalRoute(taskID string, expectedRouteID string, routeID string, inputJSON string, billingOrderID string, channelID string, channelModelID string, replacement *model.BillingOrder) error {
func (r *Repository) SwitchTaskLogicalRoute(taskID string, expectedRouteID string, routeID string, inputJSON string, billingOrderID string, channelID string, channelModelID string, replacement *model.BillingOrder, cost model.BillingCostSnapshot) error {
return r.db.Transaction(func(tx *gorm.DB) error {
updated := tx.Model(&model.Task{}).
Where("id = ? AND status = ? AND route_id = ?", taskID, model.TaskStatusRunning, expectedRouteID).
@@ -298,6 +298,14 @@ func (r *Repository) SwitchTaskLogicalRoute(taskID string, expectedRouteID strin
}
now := time.Now()
updates := map[string]any{"channel_id": channelID, "channel_model_id": channelModelID, "updated_at": now}
updates["cost_configured"] = cost.CostPricing.Configured
updates["cost_unit_price_microcredits"] = cost.CostPricing.UnitPriceMicrocredits
updates["cost_input_token_price_microcredits"] = cost.CostPricing.InputTokenPriceMicrocredits
updates["cost_output_token_price_microcredits"] = cost.CostPricing.OutputTokenPriceMicrocredits
updates["cost_cached_token_price_microcredits"] = cost.CostPricing.CachedTokenPriceMicrocredits
updates["cost_billing_mode"] = cost.CostBillingMode
updates["cost_quantity"] = cost.CostQuantity
updates["cost_video_formula_tokens"] = cost.CostVideoFormulaTokens
if replacement != nil {
if replacement.UserID != order.UserID || replacement.TaskID != taskID || replacement.AmountMicrocredits <= 0 {
return ErrBillingStateConflict
@@ -11,6 +11,47 @@ import (
"gorm.io/gorm"
)
func TestSwitchTaskLogicalRouteUpdatesCostWithoutChangingUnifiedSales(t *testing.T) {
db, err := gorm.Open(sqlite.Open(":memory:"), &gorm.Config{})
if err != nil {
t.Fatal(err)
}
if err := db.AutoMigrate(&model.Task{}, &model.BillingOrder{}); err != nil {
t.Fatal(err)
}
task := model.Task{ID: "task", UserID: "user", Status: model.TaskStatusRunning, RouteID: "old-route"}
order := model.BillingOrder{ID: "order", TaskID: task.ID, UserID: task.UserID, Status: model.BillingStatusRunning, ChannelID: "old-channel", AmountMicrocredits: 900_000, ReservedAmountMicrocredits: 900_000, UnitPriceMicrocredits: 900_000,
BillingCostSnapshot: model.BillingCostSnapshot{CostPricing: model.CreditCostPricing{Configured: true, UnitPriceMicrocredits: 500_000}, CostBillingMode: "fixed_request", CostQuantity: 1}}
if err := db.Create(&task).Error; err != nil {
t.Fatal(err)
}
if err := db.Create(&order).Error; err != nil {
t.Fatal(err)
}
repo := New(db)
cost := model.BillingCostSnapshot{CostPricing: model.CreditCostPricing{Configured: true, UnitPriceMicrocredits: 123_456}, CostBillingMode: "per_second", CostQuantity: 15}
if err := repo.SwitchTaskLogicalRoute(task.ID, "old-route", "new-route", "{}", order.ID, "new-channel", "new-model", nil, cost); err != nil {
t.Fatal(err)
}
var saved model.BillingOrder
if err := db.First(&saved, "id = ?", order.ID).Error; err != nil {
t.Fatal(err)
}
if saved.BillingCostSnapshot != cost || saved.ChannelID != "new-channel" || saved.AmountMicrocredits != 900_000 || saved.ReservedAmountMicrocredits != 900_000 || saved.UnitPriceMicrocredits != 900_000 {
t.Fatalf("wrong route snapshot: %#v", saved)
}
// A route without configured costs must clear the previous route's snapshot.
if err := repo.SwitchTaskLogicalRoute(task.ID, "new-route", "unknown-cost-route", "{}", order.ID, "third-channel", "third-model", nil, model.BillingCostSnapshot{}); err != nil {
t.Fatal(err)
}
if err := db.First(&saved, "id = ?", order.ID).Error; err != nil {
t.Fatal(err)
}
if saved.BillingCostSnapshot != (model.BillingCostSnapshot{}) || saved.AmountMicrocredits != 900_000 {
t.Fatalf("stale cost after route switch: %#v", saved)
}
}
func TestSaveLogicalModelBundleAllocatesMonotonicRevisionSequence(t *testing.T) {
db, err := gorm.Open(sqlite.Open("file:logical-model-revision-sequence?mode=memory&cache=shared"), &gorm.Config{})
if err != nil {
+1 -1
View File
@@ -708,7 +708,7 @@ func (r *Repository) AdminSystemChannels(keyword string, status string, limit in
query := r.db.Model(&model.ModelChannel{}).Where("scope = ?", model.ChannelScopeSystem)
if value := strings.TrimSpace(keyword); value != "" {
pattern := "%" + strings.ToLower(value) + "%"
query = query.Where("lower(name) LIKE ? OR lower(public_alias) LIKE ? OR lower(base_url) LIKE ?", pattern, pattern, pattern)
query = query.Where("lower(name) LIKE ? OR lower(base_url) LIKE ?", pattern, pattern)
}
if status == "enabled" {
query = query.Where("enabled = ?", true)