Publish Docker image (Multi-arch) / Build & push (amd64) (push) Failing after 1m40s
Release (Linux, macOS, Windows) / Linux Release (push) Failing after 42m16s
Publish Docker image (Multi-arch) / Build & push (arm64) (push) Canceled after 0s
Release (Linux, macOS, Windows) / macOS Release (push) Canceled after 0s
Release (Linux, macOS, Windows) / Windows Release (push) Canceled after 0s
Publish Docker image (Multi-arch) / Create multi-arch manifests (push) Canceled after 0s
Sync production source including routing, billing, Astra detection and public uptime. Update custom build instructions and license packaging.
589 lines
21 KiB
Go
589 lines
21 KiB
Go
package model
|
|
|
|
import (
|
|
"crypto/sha256"
|
|
"errors"
|
|
"fmt"
|
|
"maps"
|
|
"math"
|
|
"slices"
|
|
"sort"
|
|
"strings"
|
|
"sync"
|
|
|
|
"github.com/QuantumNous/new-api/common"
|
|
"github.com/QuantumNous/new-api/pkg/billingexpr"
|
|
"github.com/QuantumNous/new-api/pkg/jsplugin"
|
|
"github.com/QuantumNous/new-api/setting/billing_setting"
|
|
"github.com/QuantumNous/new-api/setting/operation_setting"
|
|
"github.com/QuantumNous/new-api/setting/ratio_setting"
|
|
"gorm.io/gorm"
|
|
"gorm.io/gorm/clause"
|
|
)
|
|
|
|
// PricingValues is one model's configuration, keyed by the existing option
|
|
// names. A missing key inherits the engine's default; an explicit zero is free.
|
|
type PricingValues map[string]any
|
|
|
|
type ModelPricingChange struct {
|
|
ModelName string `json:"model_name"`
|
|
ExpectedVersion string `json:"expected_version"`
|
|
Pricing PricingValues `json:"pricing"`
|
|
Reset bool `json:"reset,omitempty"`
|
|
}
|
|
|
|
type ModelPricingEntry struct {
|
|
ModelPricingDescription
|
|
PluginVariants []ModelPricingPluginVariant `json:"plugin_variants,omitempty"`
|
|
ModelName string `json:"model_name"`
|
|
Version string `json:"version"`
|
|
Configured PricingValues `json:"configured"`
|
|
UsageSchema map[string]jsplugin.UsageFieldSchema `json:"usage_schema,omitempty"`
|
|
}
|
|
|
|
type ModelPricingPluginVariant struct {
|
|
PluginKey string `json:"plugin_key"`
|
|
PluginName string `json:"plugin_name"`
|
|
Icon string `json:"icon,omitempty"`
|
|
UsageSchema map[string]jsplugin.UsageFieldSchema `json:"usage_schema"`
|
|
UsageExamples []jsplugin.UsageExample `json:"usage_examples,omitempty"`
|
|
Configured string `json:"configured"`
|
|
Effective string `json:"effective"`
|
|
Compatible bool `json:"compatible"`
|
|
Stale bool `json:"stale,omitempty"`
|
|
}
|
|
|
|
type ModelPricingSnapshot struct {
|
|
Entries []ModelPricingEntry `json:"entries"`
|
|
Options map[string]string `json:"options"`
|
|
EmptyVersion string `json:"empty_version"`
|
|
}
|
|
|
|
var ErrModelPricingConflict = errors.New("model pricing changed; reload before saving")
|
|
|
|
// Lock order is stable across instances. Creating missing option rows inside
|
|
// the transaction also serializes the first write to an unconfigured database.
|
|
var modelPricingOptionKeys = []string{
|
|
"AudioCompletionRatio", "AudioRatio", "CacheRatio", "CompletionRatio",
|
|
"CreateCacheRatio", "ImageRatio", "ModelPrice", "ModelRatio",
|
|
"billing_setting.billing_expr", "billing_setting.billing_mode", billing_setting.PluginBillingExprOption,
|
|
}
|
|
|
|
var modelPricingMutationMu sync.Mutex
|
|
|
|
func IsModelPricingOption(key string) bool {
|
|
return slices.Contains(modelPricingOptionKeys, key)
|
|
}
|
|
|
|
func ModelPricingVersion(values PricingValues) string {
|
|
encoded, _ := common.Marshal(values)
|
|
return fmt.Sprintf("%x", sha256.Sum256(encoded))
|
|
}
|
|
|
|
func defaultPricingMaps() map[string]map[string]any {
|
|
result := make(map[string]map[string]any, len(modelPricingOptionKeys))
|
|
for _, key := range modelPricingOptionKeys {
|
|
result[key] = make(map[string]any)
|
|
}
|
|
for key, values := range ratio_setting.GetDefaultPricingMaps() {
|
|
for name, value := range values {
|
|
result[key][name] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
func readModelPricingMaps(db *gorm.DB) (map[string]map[string]any, map[string]bool, []string, error) {
|
|
var rows []Option
|
|
if err := db.Where(commonKeyCol+" IN ?", modelPricingOptionKeys).Find(&rows).Error; err != nil {
|
|
return nil, nil, nil, err
|
|
}
|
|
values := defaultPricingMaps()
|
|
existing := make(map[string]bool)
|
|
counts := make(map[string]int)
|
|
for _, row := range rows {
|
|
var entries map[string]any
|
|
if err := common.UnmarshalJsonStr(row.Value, &entries); err != nil {
|
|
return nil, nil, nil, fmt.Errorf("%s: %w", row.Key, err)
|
|
}
|
|
if entries == nil {
|
|
return nil, nil, nil, fmt.Errorf("%s must be a JSON object", row.Key)
|
|
}
|
|
values[row.Key] = entries
|
|
existing[row.Key] = true
|
|
counts[row.Key]++
|
|
}
|
|
var duplicated []string
|
|
for _, key := range modelPricingOptionKeys {
|
|
if counts[key] > 1 {
|
|
duplicated = append(duplicated, key)
|
|
}
|
|
}
|
|
return values, existing, duplicated, nil
|
|
}
|
|
|
|
func modelPricingValues(values map[string]map[string]any, name string) PricingValues {
|
|
result := make(PricingValues)
|
|
for _, key := range modelPricingOptionKeys {
|
|
if key == billing_setting.PluginBillingExprOption {
|
|
variants := make(map[string]any)
|
|
for variant, expression := range values[key] {
|
|
if plugin, model, ok := billing_setting.SplitPluginBillingExprKey(variant); ok && model == name {
|
|
variants[plugin] = expression
|
|
}
|
|
}
|
|
if len(variants) > 0 {
|
|
result[key] = variants
|
|
}
|
|
continue
|
|
}
|
|
if value, exists := values[key][name]; exists {
|
|
result[key] = value
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// replaceModelPricing writes one complete model draft into the option maps.
|
|
// Plugin expressions are grouped in the draft and flattened only in storage.
|
|
func replaceModelPricing(values map[string]map[string]any, name string, draft PricingValues) {
|
|
for _, key := range modelPricingOptionKeys {
|
|
if key == billing_setting.PluginBillingExprOption {
|
|
for variant := range values[key] {
|
|
if _, model, ok := billing_setting.SplitPluginBillingExprKey(variant); ok && model == name {
|
|
delete(values[key], variant)
|
|
}
|
|
}
|
|
variants, _ := draft[key].(map[string]any)
|
|
for plugin, expr := range variants {
|
|
values[key][billing_setting.PluginBillingExprKey(plugin, name)] = expr
|
|
}
|
|
continue
|
|
}
|
|
delete(values[key], name)
|
|
if value, exists := draft[key]; exists {
|
|
values[key][name] = value
|
|
}
|
|
}
|
|
}
|
|
|
|
func effectiveModelPricing(values map[string]map[string]any, name string) PricingValues {
|
|
result := modelPricingValues(values, name)
|
|
// Legacy wildcard aliases are resolved by the same normalization as relay.
|
|
alias := ratio_setting.FormatMatchingModelName(name)
|
|
for _, key := range []string{"ModelPrice", "ModelRatio", "CompletionRatio", "AudioRatio", "AudioCompletionRatio"} {
|
|
delete(result, key)
|
|
if value, exists := values[key][alias]; exists {
|
|
result[key] = value
|
|
}
|
|
}
|
|
mode, _ := result["billing_setting.billing_mode"].(string)
|
|
if mode == "" {
|
|
_, hasPrice := result["ModelPrice"]
|
|
_, hasRatio := result["ModelRatio"]
|
|
if _, builtin := billing_setting.GetBuiltinBillingExpr(name); builtin && !hasPrice && !hasRatio {
|
|
mode = "tiered_expr"
|
|
}
|
|
}
|
|
if mode == "tiered_expr" {
|
|
result["billing_setting.billing_mode"] = mode
|
|
if _, exists := result["billing_setting.billing_expr"]; !exists {
|
|
if expression, ok := billing_setting.GetBuiltinBillingExpr(name); ok {
|
|
result["billing_setting.billing_expr"] = expression
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
if _, exists := result["ModelPrice"]; exists {
|
|
return result
|
|
}
|
|
if _, exists := result["ModelRatio"]; !exists && operation_setting.SelfUseModeEnabled {
|
|
result["ModelRatio"] = float64(37.5)
|
|
}
|
|
// Completion ratios include engine-enforced model defaults. Expose their
|
|
// effective value without persisting them into the editable configuration.
|
|
var configuredCompletion *float64
|
|
if ratio, exists := result["CompletionRatio"].(float64); exists {
|
|
configuredCompletion = &ratio
|
|
}
|
|
result["CompletionRatio"] = ratio_setting.ResolveCompletionRatio(name, configuredCompletion).Ratio
|
|
for key, fallback := range map[string]float64{
|
|
"CacheRatio": ratio_setting.DefaultCacheRatio,
|
|
"CreateCacheRatio": ratio_setting.DefaultCreateCacheRatio,
|
|
"ImageRatio": ratio_setting.DefaultImageRatio,
|
|
} {
|
|
if _, exists := result[key]; !exists {
|
|
result[key] = fallback
|
|
}
|
|
}
|
|
return result
|
|
}
|
|
|
|
// PreviewModelPricing resolves a complete editable draft using the same defaults
|
|
// as the saved-price display and conversion. It has no write side effects.
|
|
func PreviewModelPricing(name string, draft PricingValues) (PricingValues, error) {
|
|
if draft == nil {
|
|
return nil, errors.New("pricing draft is required")
|
|
}
|
|
values, _, _, err := readModelPricingMaps(DB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if err := validateModelPricing(name, draft, modelPricingValues(values, name)); err != nil {
|
|
return nil, err
|
|
}
|
|
replaceModelPricing(values, name, draft)
|
|
return effectiveModelPricing(values, name), nil
|
|
}
|
|
|
|
func GetModelPricingSnapshot(names []string) (*ModelPricingSnapshot, error) {
|
|
values, _, _, err := readModelPricingMaps(DB)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
if len(names) == 0 {
|
|
nameSet := make(map[string]bool)
|
|
for key, entries := range values {
|
|
for name := range entries {
|
|
if key == billing_setting.PluginBillingExprOption {
|
|
_, model, ok := billing_setting.SplitPluginBillingExprKey(name)
|
|
if !ok {
|
|
continue
|
|
}
|
|
name = model
|
|
}
|
|
nameSet[name] = true
|
|
}
|
|
}
|
|
for name := range billing_setting.GetBuiltinBillingExprCopy() {
|
|
nameSet[name] = true
|
|
}
|
|
for name := range nameSet {
|
|
names = append(names, name)
|
|
}
|
|
}
|
|
sort.Strings(names)
|
|
result := &ModelPricingSnapshot{Entries: make([]ModelPricingEntry, 0, len(names)), Options: make(map[string]string), EmptyVersion: ModelPricingVersion(PricingValues{})}
|
|
generation := jsplugin.DefaultRegistry.Generation()
|
|
for _, name := range names {
|
|
configured := modelPricingValues(values, name)
|
|
entry := ModelPricingEntry{ModelName: name, Version: ModelPricingVersion(configured), Configured: configured,
|
|
ModelPricingDescription: ModelPricingDescription{Effective: effectiveModelPricing(values, name)}}
|
|
entry.CacheWriteMode = ResolveCacheWriteMode(name, configured)
|
|
entry.BillingDetails = ResolveLegacyBillingDetails(name, entry.Effective, configured)
|
|
if plugin, ok := generation.GetByModel(name); ok {
|
|
entry.UsageSchema, _ = plugin.Meta.UsageForModel(name)
|
|
} else if target, ok := ResolveTaskModelAlias(generation, name); ok {
|
|
if plugin, ok := generation.Get(target.PluginKey); ok {
|
|
entry.UsageSchema, _ = plugin.Meta.UsageForModel(target.Declared)
|
|
}
|
|
}
|
|
plugins := generation.PluginsByModel(name)
|
|
configuredVariants, _ := configured[billing_setting.PluginBillingExprOption].(map[string]any)
|
|
if len(plugins) >= 2 || len(configuredVariants) > 0 {
|
|
keys := make(map[string]bool, len(plugins)+len(configuredVariants))
|
|
for _, plugin := range plugins {
|
|
keys[plugin.Meta.Key] = true
|
|
}
|
|
for key := range configuredVariants {
|
|
keys[key] = true
|
|
}
|
|
for _, key := range slices.Sorted(maps.Keys(keys)) {
|
|
configuredValue, overridden := configuredVariants[key]
|
|
configuredExpr, _ := configuredValue.(string)
|
|
plugin, exists := generation.Get(key)
|
|
if !exists || !slices.Contains(plugin.Meta.Models, name) {
|
|
variant := ModelPricingPluginVariant{
|
|
PluginKey: key, PluginName: key, Configured: configuredExpr,
|
|
UsageSchema: map[string]jsplugin.UsageFieldSchema{}, Stale: true,
|
|
}
|
|
if exists {
|
|
variant.PluginName, variant.Icon = plugin.Meta.Name, plugin.Meta.Icon
|
|
}
|
|
entry.PluginVariants = append(entry.PluginVariants, variant)
|
|
continue
|
|
}
|
|
schema, examples := plugin.Meta.UsageForModel(name)
|
|
if schema == nil {
|
|
schema = map[string]jsplugin.UsageFieldSchema{}
|
|
}
|
|
expression := configuredExpr
|
|
if !overridden && entry.Effective["billing_setting.billing_mode"] == billing_setting.BillingModeTieredExpr {
|
|
expression, _ = entry.Effective["billing_setting.billing_expr"].(string)
|
|
}
|
|
entry.PluginVariants = append(entry.PluginVariants, ModelPricingPluginVariant{
|
|
PluginKey: plugin.Meta.Key, PluginName: plugin.Meta.Name, Icon: plugin.Meta.Icon,
|
|
UsageSchema: schema, UsageExamples: examples, Configured: configuredExpr, Effective: expression,
|
|
Compatible: billing_setting.TaskExprCompatible(expression, schema),
|
|
})
|
|
}
|
|
}
|
|
result.Entries = append(result.Entries, entry)
|
|
}
|
|
// Preserve the existing settings editor's full-map interface. Built-in
|
|
// expressions are display defaults only; per-model writes do not persist them.
|
|
for name, expression := range billing_setting.GetBuiltinBillingExprCopy() {
|
|
effective := effectiveModelPricing(values, name)
|
|
if effective["billing_setting.billing_mode"] != "tiered_expr" {
|
|
continue
|
|
}
|
|
if _, ok := values["billing_setting.billing_mode"][name]; !ok {
|
|
values["billing_setting.billing_mode"][name] = "tiered_expr"
|
|
}
|
|
if _, ok := values["billing_setting.billing_expr"][name]; !ok {
|
|
values["billing_setting.billing_expr"][name] = expression
|
|
}
|
|
}
|
|
for key, entries := range values {
|
|
encoded, err := common.Marshal(entries)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
result.Options[key] = string(encoded)
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func ValidateModelPricing(name string, values PricingValues) error {
|
|
variants := make(map[string]any)
|
|
for key, expression := range billing_setting.GetPluginBillingExprCopy() {
|
|
if plugin, model, ok := billing_setting.SplitPluginBillingExprKey(key); ok && model == name {
|
|
variants[plugin] = expression
|
|
}
|
|
}
|
|
previous := PricingValues{billing_setting.PluginBillingExprOption: variants}
|
|
if expression, ok := billing_setting.GetBillingExpr(name); ok {
|
|
previous["billing_setting.billing_expr"] = expression
|
|
}
|
|
return validateModelPricing(name, values, previous)
|
|
}
|
|
|
|
// Writes pass the locked database snapshot here, so allowing an unchanged stale
|
|
// override cannot bypass validation through an out-of-date process-local cache.
|
|
func validateModelPricing(name string, values, previous PricingValues) error {
|
|
if strings.TrimSpace(name) == "" {
|
|
return errors.New("model name is required")
|
|
}
|
|
generation := jsplugin.DefaultRegistry.Generation()
|
|
previousVariants, _ := previous[billing_setting.PluginBillingExprOption].(map[string]any)
|
|
variants := map[string]any{}
|
|
if value, exists := values[billing_setting.PluginBillingExprOption]; exists {
|
|
var ok bool
|
|
variants, ok = value.(map[string]any)
|
|
if !ok || variants == nil {
|
|
return errors.New("plugin billing expressions must be a plugin-to-expression object")
|
|
}
|
|
for key, value := range variants {
|
|
expression, ok := value.(string)
|
|
if !ok || strings.TrimSpace(expression) == "" {
|
|
return fmt.Errorf("model %s: plugin %s: billing expression is required", name, key)
|
|
}
|
|
plugin, exists := generation.Get(key)
|
|
if !exists || !slices.Contains(plugin.Meta.Models, name) {
|
|
if previousVariants[key] == expression {
|
|
continue
|
|
}
|
|
return fmt.Errorf("model %s: plugin %s does not declare this model", name, key)
|
|
}
|
|
schema, _ := plugin.Meta.UsageForModel(name)
|
|
if err := billing_setting.SmokeTestTaskExpr(expression, schema); err != nil {
|
|
return fmt.Errorf("model %s: plugin %s: %w", name, key, err)
|
|
}
|
|
}
|
|
}
|
|
for key, value := range values {
|
|
if key == billing_setting.PluginBillingExprOption {
|
|
continue
|
|
}
|
|
if !IsModelPricingOption(key) {
|
|
return fmt.Errorf("unsupported pricing field: %s", key)
|
|
}
|
|
if key == "billing_setting.billing_mode" {
|
|
if value != "ratio" && value != "tiered_expr" {
|
|
return errors.New("invalid billing mode")
|
|
}
|
|
continue
|
|
}
|
|
if key == "billing_setting.billing_expr" {
|
|
expression, ok := value.(string)
|
|
if !ok || strings.TrimSpace(expression) == "" {
|
|
return errors.New("billing expression is required")
|
|
}
|
|
// Even a model expression currently shadowed by every provider must
|
|
// compile; only its schema-specific smoke tests can be skipped.
|
|
if _, err := billingexpr.CompileFromCache(expression); err != nil {
|
|
return fmt.Errorf("model %s: %w", name, err)
|
|
}
|
|
var err error
|
|
if plugins := generation.PluginsByModel(name); len(plugins) > 0 {
|
|
for _, plugin := range plugins {
|
|
if _, overridden := variants[plugin.Meta.Key]; overridden {
|
|
continue
|
|
}
|
|
schema, _ := plugin.Meta.UsageForModel(name)
|
|
if err = billing_setting.SmokeTestTaskExpr(expression, schema); err != nil {
|
|
return fmt.Errorf("model %s: plugin %s: %w", name, plugin.Meta.Key, err)
|
|
}
|
|
}
|
|
} else if target, resolved := ResolveTaskModelAlias(generation, name); resolved {
|
|
if plugin, ok := generation.Get(target.PluginKey); ok {
|
|
schema, _ := plugin.Meta.UsageForModel(target.Declared)
|
|
err = billing_setting.SmokeTestTaskExpr(expression, schema)
|
|
} else {
|
|
err = billing_setting.SmokeTestExpr(expression)
|
|
}
|
|
} else if previous[key] != expression || len(billingexpr.UsedUsageKeys(expression)) == 0 {
|
|
err = billing_setting.SmokeTestExpr(expression)
|
|
}
|
|
// With no remaining plugin, an unchanged stored usage expression has
|
|
// no schema to test. Preserve it so removing stale overrides or saving
|
|
// other model prices does not become impossible.
|
|
if err != nil {
|
|
return fmt.Errorf("model %s: %w", name, err)
|
|
}
|
|
continue
|
|
}
|
|
number, ok := value.(float64)
|
|
if !ok || math.IsNaN(number) || math.IsInf(number, 0) || number < 0 {
|
|
return fmt.Errorf("%s must be a finite, non-negative number", key)
|
|
}
|
|
}
|
|
if values["billing_setting.billing_mode"] == "tiered_expr" {
|
|
if _, exists := values["billing_setting.billing_expr"]; !exists {
|
|
if _, builtin := billing_setting.GetBuiltinBillingExpr(name); !builtin {
|
|
return errors.New("billing expression is required")
|
|
}
|
|
}
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func UpdateModelPricing(changes []ModelPricingChange) error {
|
|
if len(changes) == 0 {
|
|
return errors.New("select model pricing changes before saving")
|
|
}
|
|
seen := make(map[string]bool)
|
|
for _, change := range changes {
|
|
if seen[change.ModelName] {
|
|
return errors.New("duplicate model pricing change")
|
|
}
|
|
seen[change.ModelName] = true
|
|
if change.ExpectedVersion == "" {
|
|
return ErrModelPricingConflict
|
|
}
|
|
}
|
|
return mutateModelPricingOptions(func(_ *gorm.DB, values map[string]map[string]any) error {
|
|
defaults := defaultPricingMaps()
|
|
for _, change := range changes {
|
|
previous := modelPricingValues(values, change.ModelName)
|
|
if ModelPricingVersion(previous) != change.ExpectedVersion {
|
|
return fmt.Errorf("%w: %s", ErrModelPricingConflict, change.ModelName)
|
|
}
|
|
pricing := change.Pricing
|
|
if change.Reset {
|
|
pricing = modelPricingValues(defaults, change.ModelName)
|
|
}
|
|
if err := validateModelPricing(change.ModelName, pricing, previous); err != nil {
|
|
return err
|
|
}
|
|
replaceModelPricing(values, change.ModelName, pricing)
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
// UpdateModelPricingOptions keeps legacy single-option callers on the same
|
|
// locking, validation and transaction path as the model-level API.
|
|
func UpdateModelPricingOptions(updates map[string]string) error {
|
|
return mutateModelPricingOptions(func(_ *gorm.DB, values map[string]map[string]any) error {
|
|
previous := maps.Clone(values)
|
|
names := make(map[string]bool)
|
|
for key, raw := range updates {
|
|
if !IsModelPricingOption(key) {
|
|
return fmt.Errorf("unsupported pricing field: %s", key)
|
|
}
|
|
var entries map[string]any
|
|
if err := common.UnmarshalJsonStr(raw, &entries); err != nil {
|
|
return err
|
|
}
|
|
if entries == nil {
|
|
return fmt.Errorf("%s must be a JSON object", key)
|
|
}
|
|
for _, entriesForKey := range []map[string]any{values[key], entries} {
|
|
for name := range entriesForKey {
|
|
if key == billing_setting.PluginBillingExprOption {
|
|
_, model, ok := billing_setting.SplitPluginBillingExprKey(name)
|
|
if !ok {
|
|
return fmt.Errorf("invalid plugin billing expression key: %s", name)
|
|
}
|
|
name = model
|
|
}
|
|
names[name] = true
|
|
}
|
|
}
|
|
values[key] = entries
|
|
}
|
|
for name := range names {
|
|
if err := validateModelPricing(name, modelPricingValues(values, name), modelPricingValues(previous, name)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
return nil
|
|
})
|
|
}
|
|
|
|
func mutateModelPricingOptions(mutate func(*gorm.DB, map[string]map[string]any) error) error {
|
|
modelPricingMutationMu.Lock()
|
|
defer modelPricingMutationMu.Unlock()
|
|
var committed map[string]map[string]any
|
|
err := DB.Transaction(func(tx *gorm.DB) error {
|
|
values, existing, duplicated, err := readModelPricingMaps(lockForUpdate(tx))
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if len(duplicated) > 0 {
|
|
common.SysError("options table has duplicate pricing keys [" + strings.Join(duplicated, ", ") + "]; the table is missing a primary key")
|
|
}
|
|
defaults := defaultPricingMaps()
|
|
for _, key := range modelPricingOptionKeys {
|
|
if existing[key] {
|
|
continue
|
|
}
|
|
encoded, err := common.Marshal(defaults[key])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
row := Option{Key: key, Value: string(encoded)}
|
|
if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&row).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
if err := mutate(tx, values); err != nil {
|
|
return err
|
|
}
|
|
for _, key := range modelPricingOptionKeys {
|
|
encoded, err := common.Marshal(values[key])
|
|
if err != nil {
|
|
return err
|
|
}
|
|
if err := tx.Model(&Option{}).Where(commonKeyCol+" = ?", key).Update("value", string(encoded)).Error; err != nil {
|
|
return err
|
|
}
|
|
}
|
|
committed = values
|
|
return nil
|
|
})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
for _, key := range modelPricingOptionKeys {
|
|
encoded, _ := common.Marshal(committed[key])
|
|
if err := updateOptionMap(key, string(encoded)); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
RefreshPricing()
|
|
ratio_setting.InvalidateExposedDataCache()
|
|
return nil
|
|
}
|