From 37863bad2fa9af24ecda24eb61337b5ad31ff30c Mon Sep 17 00:00:00 2001 From: qian Date: Sat, 16 May 2026 13:29:51 +0800 Subject: [PATCH] =?UTF-8?q?feat(model):=E7=A7=9F=E6=88=B7=E6=A8=A1?= =?UTF-8?q?=E5=9E=8B=E5=AE=9A=E4=BB=B7=E5=92=8C=E6=98=BE=E7=A4=BA=E4=BC=98?= =?UTF-8?q?=E5=8C=96?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- api/admin/v1/tenant_model.go | 89 ++++--- api/tenant/v1/models.go | 39 ++- internal/dao/internal/chn_channel_keys.go | 4 +- internal/dao/internal/mdl_models.go | 4 +- internal/dao/internal/mdl_tenant_models.go | 62 ++--- internal/dao/internal/ntf_messages.go | 4 +- internal/dao/internal/sys_plugins.go | 97 ++++++++ internal/dao/internal/tnt_tenant_plugins.go | 91 +++++++ internal/dao/sys_plugins.go | 22 ++ internal/dao/tnt_tenant_plugins.go | 22 ++ internal/logic/admin/tenant_model.go | 131 ++++++---- internal/logic/billing/pricing.go | 71 ++++-- internal/logic/tenant/model.go | 196 ++++++++++++--- internal/model/do/chn_channel_keys.go | 2 +- internal/model/do/mdl_models.go | 2 +- internal/model/do/mdl_tenant_models.go | 33 +-- internal/model/do/ntf_messages.go | 2 +- internal/model/do/sys_plugins.go | 25 ++ internal/model/do/tnt_tenant_plugins.go | 22 ++ internal/model/entity/chn_channel_keys.go | 2 +- internal/model/entity/mdl_models.go | 2 +- internal/model/entity/mdl_tenant_models.go | 31 +-- internal/model/entity/ntf_messages.go | 2 +- internal/model/entity/sys_plugins.go | 23 ++ internal/model/entity/tnt_tenant_plugins.go | 20 ++ ...000002_add_tenant_model_custom_pricing.sql | 22 ++ .../src/views/tenant/TenantDetailPage.vue | 216 ++++++++++++++--- web/tenant/src/views/service/ModelsPage.vue | 226 ++++++++++++------ 28 files changed, 1148 insertions(+), 314 deletions(-) create mode 100644 internal/dao/internal/sys_plugins.go create mode 100644 internal/dao/internal/tnt_tenant_plugins.go create mode 100644 internal/dao/sys_plugins.go create mode 100644 internal/dao/tnt_tenant_plugins.go create mode 100644 internal/model/do/sys_plugins.go create mode 100644 internal/model/do/tnt_tenant_plugins.go create mode 100644 internal/model/entity/sys_plugins.go create mode 100644 internal/model/entity/tnt_tenant_plugins.go create mode 100644 migrations/000002_add_tenant_model_custom_pricing.sql diff --git a/api/admin/v1/tenant_model.go b/api/admin/v1/tenant_model.go index cb1bdca..9442ded 100644 --- a/api/admin/v1/tenant_model.go +++ b/api/admin/v1/tenant_model.go @@ -13,21 +13,34 @@ type TenantModelListRes struct { } type TenantModelItem struct { - ID int64 `json:"id"` - TenantID int64 `json:"tenant_id"` - ModelID int64 `json:"model_id"` - ModelCode string `json:"model_code"` - ModelName string `json:"model_name"` - Category string `json:"category"` - Enabled bool `json:"enabled"` - BillingMode *string `json:"billing_mode"` - PerRequestPrice *float64 `json:"per_request_price"` - DiscountRatio *float64 `json:"discount_ratio"` - MaxConcurrency *int `json:"max_concurrency"` - ChannelScope string `json:"channel_scope"` - CustomInputPrice *float64 `json:"custom_input_price"` - CustomOutputPrice *float64 `json:"custom_output_price"` - Multiplier float64 `json:"multiplier"` + ID int64 `json:"id"` + TenantID int64 `json:"tenant_id"` + ModelID int64 `json:"model_id"` + ModelCode string `json:"model_code"` + ModelName string `json:"model_name"` + Category string `json:"category"` + Enabled bool `json:"enabled"` + BillingMode *string `json:"billing_mode"` + PerRequestPrice *float64 `json:"per_request_price"` + DiscountRatio *float64 `json:"discount_ratio"` + MaxConcurrency *int `json:"max_concurrency"` + ChannelScope string `json:"channel_scope"` + CustomInputPrice *float64 `json:"custom_input_price"` + CustomOutputPrice *float64 `json:"custom_output_price"` + CustomCacheReadPrice *float64 `json:"custom_cache_read_price"` + CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price"` + CustomPricingTiers []*PricingTier `json:"custom_pricing_tiers"` + Multiplier float64 `json:"multiplier"` +} + +// PricingTier 阶梯定价项(租户自定义阶梯时使用) +type PricingTier struct { + MinTokens int64 `json:"min_tokens"` + MaxTokens *int64 `json:"max_tokens"` + InputPrice float64 `json:"input_price"` + OutputPrice float64 `json:"output_price"` + CacheReadPrice float64 `json:"cache_read_price"` + CacheCreationPrice float64 `json:"cache_creation_price"` } // TenantModelBatchAssignReq 批量分配模型给租户 @@ -38,15 +51,18 @@ type TenantModelBatchAssignReq struct { } type ModelAssignment struct { - ModelID int64 `json:"model_id" v:"required" dc:"模型ID"` - Enabled bool `json:"enabled" d:"true" dc:"是否启用"` - BillingMode *string `json:"billing_mode" dc:"覆盖计费模式"` - PerRequestPrice *float64 `json:"per_request_price" dc:"按次单价"` - DiscountRatio *float64 `json:"discount_ratio" dc:"折扣比例(0-1)"` - MaxConcurrency *int `json:"max_concurrency" dc:"单模型并发上限"` - ChannelScope *string `json:"channel_scope" dc:"渠道范围JSON数组"` - CustomInputPrice *float64 `json:"custom_input_price" dc:"自定义输入价格"` - CustomOutputPrice *float64 `json:"custom_output_price" dc:"自定义输出价格"` + ModelID int64 `json:"model_id" v:"required" dc:"模型ID"` + Enabled bool `json:"enabled" d:"true" dc:"是否启用"` + BillingMode *string `json:"billing_mode" dc:"覆盖计费模式"` + PerRequestPrice *float64 `json:"per_request_price" dc:"按次单价"` + DiscountRatio *float64 `json:"discount_ratio" dc:"折扣比例(0-1)"` + MaxConcurrency *int `json:"max_concurrency" dc:"单模型并发上限"` + ChannelScope *string `json:"channel_scope" dc:"渠道范围JSON数组"` + CustomInputPrice *float64 `json:"custom_input_price" dc:"自定义输入价格"` + CustomOutputPrice *float64 `json:"custom_output_price" dc:"自定义输出价格"` + CustomCacheReadPrice *float64 `json:"custom_cache_read_price" dc:"自定义缓存读取价格"` + CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price" dc:"自定义缓存创建价格"` + CustomPricingTiers []*PricingTier `json:"custom_pricing_tiers" dc:"自定义阶梯定价"` } type TenantModelBatchAssignRes struct { @@ -55,17 +71,20 @@ type TenantModelBatchAssignRes struct { // TenantModelUpdateReq 更新租户模型分配配置 type TenantModelUpdateReq struct { - g.Meta `path:"/tenants/{tenant_id}/models/{model_id}" method:"put" mime:"json" tags:"管理后台-租户模型" summary:"更新租户模型配置"` - TenantID int64 `json:"tenant_id" in:"path" v:"required" dc:"租户ID"` - ModelID int64 `json:"model_id" in:"path" v:"required" dc:"模型ID"` - Enabled *bool `json:"enabled" dc:"是否启用"` - BillingMode *string `json:"billing_mode" v:"in:token,per_request" dc:"计费模式"` - PerRequestPrice **float64 `json:"per_request_price" dc:"按次单价"` - DiscountRatio **float64 `json:"discount_ratio" dc:"折扣比例"` - MaxConcurrency **int `json:"max_concurrency" dc:"单模型并发上限"` - ChannelScope **string `json:"channel_scope" dc:"渠道范围JSON数组"` - CustomInputPrice **float64 `json:"custom_input_price" dc:"自定义输入价格"` - CustomOutputPrice **float64 `json:"custom_output_price" dc:"自定义输出价格"` + g.Meta `path:"/tenants/{tenant_id}/models/{model_id}" method:"put" mime:"json" tags:"管理后台-租户模型" summary:"更新租户模型配置"` + TenantID int64 `json:"tenant_id" in:"path" v:"required" dc:"租户ID"` + ModelID int64 `json:"model_id" in:"path" v:"required" dc:"模型ID"` + Enabled *bool `json:"enabled" dc:"是否启用"` + BillingMode *string `json:"billing_mode" v:"in:token,per_request,tiered" dc:"计费模式"` + PerRequestPrice **float64 `json:"per_request_price" dc:"按次单价"` + DiscountRatio **float64 `json:"discount_ratio" dc:"折扣比例"` + MaxConcurrency **int `json:"max_concurrency" dc:"单模型并发上限"` + ChannelScope **string `json:"channel_scope" dc:"渠道范围JSON数组"` + CustomInputPrice **float64 `json:"custom_input_price" dc:"自定义输入价格"` + CustomOutputPrice **float64 `json:"custom_output_price" dc:"自定义输出价格"` + CustomCacheReadPrice **float64 `json:"custom_cache_read_price" dc:"自定义缓存读取价格"` + CustomCacheCreationPrice **float64 `json:"custom_cache_creation_price" dc:"自定义缓存创建价格"` + CustomPricingTiers *[]*PricingTier `json:"custom_pricing_tiers" dc:"自定义阶梯定价"` } type TenantModelUpdateRes struct{} diff --git a/api/tenant/v1/models.go b/api/tenant/v1/models.go index 92a6ab7..2d0e175 100644 --- a/api/tenant/v1/models.go +++ b/api/tenant/v1/models.go @@ -13,19 +13,32 @@ type TenantAvailableModelsRes struct { List []TenantAvailableModelItem `json:"list"` } +// PricingTierItem 阶梯定价项 +type PricingTierItem struct { + MinTokens int64 `json:"min_tokens"` + MaxTokens *int64 `json:"max_tokens"` + InputPrice float64 `json:"input_price"` + OutputPrice float64 `json:"output_price"` +} + // TenantAvailableModelItem 租户可用模型信息 type TenantAvailableModelItem struct { - ID int64 `json:"id"` - ModelId string `json:"model_id"` - ModelName string `json:"model_name"` - Category string `json:"category"` - MaxContext int `json:"max_context_tokens"` - MaxOutput int `json:"max_output_tokens"` - Description string `json:"description"` - Tags string `json:"tags"` - Capabilities string `json:"capabilities"` - BillingMode *string `json:"billing_mode"` - PerRequestPrice *float64 `json:"per_request_price"` - DiscountRatio *float64 `json:"discount_ratio"` - MaxConcurrency *int `json:"max_concurrency"` + ID int64 `json:"id"` + ModelId string `json:"model_id"` + ModelName string `json:"model_name"` + Category string `json:"category"` + MaxContext int `json:"max_context_tokens"` + MaxOutput int `json:"max_output_tokens"` + Description string `json:"description"` + Tags string `json:"tags"` + Capabilities string `json:"capabilities"` + BillingMode *string `json:"billing_mode"` + PerRequestPrice *float64 `json:"per_request_price"` + DiscountRatio *float64 `json:"discount_ratio"` + MaxConcurrency *int `json:"max_concurrency"` + InputPrice *float64 `json:"input_price"` + OutputPrice *float64 `json:"output_price"` + CacheReadPrice *float64 `json:"cache_read_price"` + CacheCreationPrice *float64 `json:"cache_creation_price"` + PricingTiers []PricingTierItem `json:"pricing_tiers"` } diff --git a/internal/dao/internal/chn_channel_keys.go b/internal/dao/internal/chn_channel_keys.go index 5a21b93..218ce66 100644 --- a/internal/dao/internal/chn_channel_keys.go +++ b/internal/dao/internal/chn_channel_keys.go @@ -29,9 +29,9 @@ type ChnChannelKeysColumns struct { LastUsedAt string // 最后使用时间 LastError string // 最后一次错误信息 CreatedAt string // 创建时间 - UpdatedAt string // 更新时间 KeyType string // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌) TokenExpiresAt string // OAuth access_token 过期时间(仅 key_type=oauth 时有值) + UpdatedAt string // 更新时间 } // chnChannelKeysColumns holds the columns for the table chn_channel_keys. @@ -44,9 +44,9 @@ var chnChannelKeysColumns = ChnChannelKeysColumns{ LastUsedAt: "last_used_at", LastError: "last_error", CreatedAt: "created_at", - UpdatedAt: "updated_at", KeyType: "key_type", TokenExpiresAt: "token_expires_at", + UpdatedAt: "updated_at", } // NewChnChannelKeysDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/mdl_models.go b/internal/dao/internal/mdl_models.go index 6e7d88d..88fe9ec 100644 --- a/internal/dao/internal/mdl_models.go +++ b/internal/dao/internal/mdl_models.go @@ -30,12 +30,12 @@ type MdlModelsColumns struct { MaxOutputTokens string // 最大输出 token 数 Description string // 模型描述 Tags string // 标签(如 reasoning、vision、function_calling) + Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等) CreatedAt string // 创建时间 UpdatedAt string // 更新时间 DeprecatedAt string // 标记弃用的时间(NULL表示未弃用) SunsetDate string // 计划下线日期(到达后返回410 Gone,NULL表示未设置) ReplacementModel string // 推荐替代模型名(NULL表示无替代) - Capabilities string // 模型能力特性(如 vision、function_calling、reasoning 等) } // mdlModelsColumns holds the columns for the table mdl_models. @@ -49,12 +49,12 @@ var mdlModelsColumns = MdlModelsColumns{ MaxOutputTokens: "max_output_tokens", Description: "description", Tags: "tags", + Capabilities: "capabilities", CreatedAt: "created_at", UpdatedAt: "updated_at", DeprecatedAt: "deprecated_at", SunsetDate: "sunset_date", ReplacementModel: "replacement_model", - Capabilities: "capabilities", } // NewMdlModelsDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/mdl_tenant_models.go b/internal/dao/internal/mdl_tenant_models.go index 1ad2356..4f603b1 100644 --- a/internal/dao/internal/mdl_tenant_models.go +++ b/internal/dao/internal/mdl_tenant_models.go @@ -21,38 +21,44 @@ type MdlTenantModelsDao struct { // MdlTenantModelsColumns defines and stores column names for the table mdl_tenant_models. type MdlTenantModelsColumns struct { - Id string // 主键ID - TenantId string // 租户ID - ModelId string // 模型ID - Enabled string // 是否启用(禁用后该租户无法调用此模型) - CustomInputPrice string // 租户自定义输入价格(NULL 表示使用默认定价) - CustomOutputPrice string // 租户自定义输出价格(NULL 表示使用默认定价) - Multiplier string // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) - CreatedAt string // 创建时间 - UpdatedAt string // 更新时间 - BillingMode string // 覆盖模型计费方式(NULL表示跟随模型默认) - PerRequestPrice string // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效) - DiscountRatio string // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用) - MaxConcurrency string // 单模型并发上限(NULL表示不限制) - ChannelScope string // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID) + Id string // 主键ID + TenantId string // 租户ID + ModelId string // 模型ID + Enabled string // 是否启用(禁用后该租户无法调用此模型) + CustomInputPrice string // 租户自定义输入价格(NULL 表示使用默认定价) + CustomOutputPrice string // 租户自定义输出价格(NULL 表示使用默认定价) + Multiplier string // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) + CreatedAt string // 创建时间 + UpdatedAt string // 更新时间 + BillingMode string // 覆盖模型计费方式(NULL表示跟随模型默认) + PerRequestPrice string // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效) + DiscountRatio string // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用) + MaxConcurrency string // 单模型并发上限(NULL表示不限制) + ChannelScope string // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID) + CustomCacheReadPrice string // 自定义缓存读取价格($/1M token),NULL 表示使用基础定价 + CustomCacheCreationPrice string // 自定义缓存创建价格($/1M token),NULL 表示使用基础定价 + CustomPricingTiers string // 自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}] } // mdlTenantModelsColumns holds the columns for the table mdl_tenant_models. var mdlTenantModelsColumns = MdlTenantModelsColumns{ - Id: "id", - TenantId: "tenant_id", - ModelId: "model_id", - Enabled: "enabled", - CustomInputPrice: "custom_input_price", - CustomOutputPrice: "custom_output_price", - Multiplier: "multiplier", - CreatedAt: "created_at", - UpdatedAt: "updated_at", - BillingMode: "billing_mode", - PerRequestPrice: "per_request_price", - DiscountRatio: "discount_ratio", - MaxConcurrency: "max_concurrency", - ChannelScope: "channel_scope", + Id: "id", + TenantId: "tenant_id", + ModelId: "model_id", + Enabled: "enabled", + CustomInputPrice: "custom_input_price", + CustomOutputPrice: "custom_output_price", + Multiplier: "multiplier", + CreatedAt: "created_at", + UpdatedAt: "updated_at", + BillingMode: "billing_mode", + PerRequestPrice: "per_request_price", + DiscountRatio: "discount_ratio", + MaxConcurrency: "max_concurrency", + ChannelScope: "channel_scope", + CustomCacheReadPrice: "custom_cache_read_price", + CustomCacheCreationPrice: "custom_cache_creation_price", + CustomPricingTiers: "custom_pricing_tiers", } // NewMdlTenantModelsDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/ntf_messages.go b/internal/dao/internal/ntf_messages.go index c6d277b..e84cd42 100644 --- a/internal/dao/internal/ntf_messages.go +++ b/internal/dao/internal/ntf_messages.go @@ -31,8 +31,8 @@ type NtfMessagesColumns struct { IsRead string // 是否已读:0=未读, 1=已读 IsBroadcast string // 是否广播消息:0=个人消息, 1=广播消息 Metadata string // 附加元数据(JSONB,如关联资源ID、跳转链接等) - CreatedAt string // 创建时间 TargetRoles string // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) + CreatedAt string // 创建时间 } // ntfMessagesColumns holds the columns for the table ntf_messages. @@ -47,8 +47,8 @@ var ntfMessagesColumns = NtfMessagesColumns{ IsRead: "is_read", IsBroadcast: "is_broadcast", Metadata: "metadata", - CreatedAt: "created_at", TargetRoles: "target_roles", + CreatedAt: "created_at", } // NewNtfMessagesDao creates and returns a new DAO object for table data access. diff --git a/internal/dao/internal/sys_plugins.go b/internal/dao/internal/sys_plugins.go new file mode 100644 index 0000000..a3abd0c --- /dev/null +++ b/internal/dao/internal/sys_plugins.go @@ -0,0 +1,97 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// SysPluginsDao is the data access object for the table sys_plugins. +type SysPluginsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns SysPluginsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// SysPluginsColumns defines and stores column names for the table sys_plugins. +type SysPluginsColumns struct { + Id string // + Name string // 插件唯一标识,如 email-report + Label string // 显示名称 + Version string // 当前安装版本 + Status string // 状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常 + Category string // 分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展 + Config string // 插件全局配置(JSON) + ErrorMsg string // 异常信息 + CreatedAt string // + UpdatedAt string // +} + +// sysPluginsColumns holds the columns for the table sys_plugins. +var sysPluginsColumns = SysPluginsColumns{ + Id: "id", + Name: "name", + Label: "label", + Version: "version", + Status: "status", + Category: "category", + Config: "config", + ErrorMsg: "error_msg", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewSysPluginsDao creates and returns a new DAO object for table data access. +func NewSysPluginsDao(handlers ...gdb.ModelHandler) *SysPluginsDao { + return &SysPluginsDao{ + group: "default", + table: "sys_plugins", + columns: sysPluginsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *SysPluginsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *SysPluginsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *SysPluginsDao) Columns() SysPluginsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *SysPluginsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *SysPluginsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *SysPluginsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/internal/tnt_tenant_plugins.go b/internal/dao/internal/tnt_tenant_plugins.go new file mode 100644 index 0000000..9afc8cd --- /dev/null +++ b/internal/dao/internal/tnt_tenant_plugins.go @@ -0,0 +1,91 @@ +// ========================================================================== +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ========================================================================== + +package internal + +import ( + "context" + + "github.com/gogf/gf/v2/database/gdb" + "github.com/gogf/gf/v2/frame/g" +) + +// TntTenantPluginsDao is the data access object for the table tnt_tenant_plugins. +type TntTenantPluginsDao struct { + table string // table is the underlying table name of the DAO. + group string // group is the database configuration group name of the current DAO. + columns TntTenantPluginsColumns // columns contains all the column names of Table for convenient usage. + handlers []gdb.ModelHandler // handlers for customized model modification. +} + +// TntTenantPluginsColumns defines and stores column names for the table tnt_tenant_plugins. +type TntTenantPluginsColumns struct { + Id string // + TenantId string // + PluginName string // 插件标识 + Enabled string // 是否启用 + Config string // 租户级配置覆盖(JSON),优先级高于全局配置 + CreatedAt string // + UpdatedAt string // +} + +// tntTenantPluginsColumns holds the columns for the table tnt_tenant_plugins. +var tntTenantPluginsColumns = TntTenantPluginsColumns{ + Id: "id", + TenantId: "tenant_id", + PluginName: "plugin_name", + Enabled: "enabled", + Config: "config", + CreatedAt: "created_at", + UpdatedAt: "updated_at", +} + +// NewTntTenantPluginsDao creates and returns a new DAO object for table data access. +func NewTntTenantPluginsDao(handlers ...gdb.ModelHandler) *TntTenantPluginsDao { + return &TntTenantPluginsDao{ + group: "default", + table: "tnt_tenant_plugins", + columns: tntTenantPluginsColumns, + handlers: handlers, + } +} + +// DB retrieves and returns the underlying raw database management object of the current DAO. +func (dao *TntTenantPluginsDao) DB() gdb.DB { + return g.DB(dao.group) +} + +// Table returns the table name of the current DAO. +func (dao *TntTenantPluginsDao) Table() string { + return dao.table +} + +// Columns returns all column names of the current DAO. +func (dao *TntTenantPluginsDao) Columns() TntTenantPluginsColumns { + return dao.columns +} + +// Group returns the database configuration group name of the current DAO. +func (dao *TntTenantPluginsDao) Group() string { + return dao.group +} + +// Ctx creates and returns a Model for the current DAO. It automatically sets the context for the current operation. +func (dao *TntTenantPluginsDao) Ctx(ctx context.Context) *gdb.Model { + model := dao.DB().Model(dao.table) + for _, handler := range dao.handlers { + model = handler(model) + } + return model.Safe().Ctx(ctx) +} + +// Transaction wraps the transaction logic using function f. +// It rolls back the transaction and returns the error if function f returns a non-nil error. +// It commits the transaction and returns nil if function f returns nil. +// +// Note: Do not commit or roll back the transaction in function f, +// as it is automatically handled by this function. +func (dao *TntTenantPluginsDao) Transaction(ctx context.Context, f func(ctx context.Context, tx gdb.TX) error) (err error) { + return dao.Ctx(ctx).Transaction(ctx, f) +} diff --git a/internal/dao/sys_plugins.go b/internal/dao/sys_plugins.go new file mode 100644 index 0000000..2160bc1 --- /dev/null +++ b/internal/dao/sys_plugins.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "github.com/qianfree/team-api/internal/dao/internal" +) + +// sysPluginsDao is the data access object for the table sys_plugins. +// You can define custom methods on it to extend its functionality as needed. +type sysPluginsDao struct { + *internal.SysPluginsDao +} + +var ( + // SysPlugins is a globally accessible object for table sys_plugins operations. + SysPlugins = sysPluginsDao{internal.NewSysPluginsDao()} +) + +// Add your custom methods and functionality below. diff --git a/internal/dao/tnt_tenant_plugins.go b/internal/dao/tnt_tenant_plugins.go new file mode 100644 index 0000000..ed46f0b --- /dev/null +++ b/internal/dao/tnt_tenant_plugins.go @@ -0,0 +1,22 @@ +// ================================================================================= +// This file is auto-generated by the GoFrame CLI tool. You may modify it as needed. +// ================================================================================= + +package dao + +import ( + "github.com/qianfree/team-api/internal/dao/internal" +) + +// tntTenantPluginsDao is the data access object for the table tnt_tenant_plugins. +// You can define custom methods on it to extend its functionality as needed. +type tntTenantPluginsDao struct { + *internal.TntTenantPluginsDao +} + +var ( + // TntTenantPlugins is a globally accessible object for table tnt_tenant_plugins operations. + TntTenantPlugins = tntTenantPluginsDao{internal.NewTntTenantPluginsDao()} +) + +// Add your custom methods and functionality below. diff --git a/internal/logic/admin/tenant_model.go b/internal/logic/admin/tenant_model.go index 7d82170..ceee8c5 100644 --- a/internal/logic/admin/tenant_model.go +++ b/internal/logic/admin/tenant_model.go @@ -2,38 +2,42 @@ package admin import ( "context" - do "github.com/qianfree/team-api/internal/model/do" + "encoding/json" v1 "github.com/qianfree/team-api/api/admin/v1" "github.com/qianfree/team-api/internal/dao" "github.com/qianfree/team-api/internal/logic/billing" "github.com/qianfree/team-api/internal/logic/common" + do "github.com/qianfree/team-api/internal/model/do" ) // ListTenantModels 列出租户已分配的模型 func (s *sAdmin) ListTenantModels(ctx context.Context, req *v1.TenantModelListReq) (*v1.TenantModelListRes, error) { var results []struct { - ID int64 `json:"id"` - TenantID int64 `json:"tenant_id"` - ModelID int64 `json:"model_id"` - Enabled bool `json:"enabled"` - BillingMode *string `json:"billing_mode"` - PerRequestPrice *float64 `json:"per_request_price"` - DiscountRatio *float64 `json:"discount_ratio"` - MaxConcurrency *int `json:"max_concurrency"` - ChannelScope string `json:"channel_scope"` - CustomInputPrice *float64 `json:"custom_input_price"` - CustomOutputPrice *float64 `json:"custom_output_price"` - Multiplier float64 `json:"multiplier"` - ModelCode string `json:"model_code"` - ModelName string `json:"model_name"` - Category string `json:"category"` + ID int64 `json:"id"` + TenantID int64 `json:"tenant_id"` + ModelID int64 `json:"model_id"` + Enabled bool `json:"enabled"` + BillingMode *string `json:"billing_mode"` + PerRequestPrice *float64 `json:"per_request_price"` + DiscountRatio *float64 `json:"discount_ratio"` + MaxConcurrency *int `json:"max_concurrency"` + ChannelScope string `json:"channel_scope"` + CustomInputPrice *float64 `json:"custom_input_price"` + CustomOutputPrice *float64 `json:"custom_output_price"` + CustomCacheReadPrice *float64 `json:"custom_cache_read_price"` + CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price"` + CustomPricingTiers string `json:"custom_pricing_tiers"` + Multiplier float64 `json:"multiplier"` + ModelCode string `json:"model_code"` + ModelName string `json:"model_name"` + Category string `json:"category"` } err := dao.MdlTenantModels.Ctx(ctx). LeftJoin("mdl_models ON mdl_tenant_models.model_id = mdl_models.id"). Where("mdl_tenant_models.tenant_id", req.TenantID). - Fields("mdl_tenant_models.id, mdl_tenant_models.tenant_id, mdl_tenant_models.model_id, mdl_tenant_models.enabled, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, mdl_tenant_models.channel_scope, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.multiplier, mdl_models.model_id as model_code, mdl_models.model_name, mdl_models.category"). + Fields("mdl_tenant_models.id, mdl_tenant_models.tenant_id, mdl_tenant_models.model_id, mdl_tenant_models.enabled, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, mdl_tenant_models.channel_scope, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers, mdl_tenant_models.multiplier, mdl_models.model_id as model_code, mdl_models.model_name, mdl_models.category"). OrderAsc("mdl_models.category"). OrderAsc("mdl_models.model_id"). Scan(&results) @@ -43,23 +47,32 @@ func (s *sAdmin) ListTenantModels(ctx context.Context, req *v1.TenantModelListRe list := make([]v1.TenantModelItem, 0, len(results)) for _, r := range results { - list = append(list, v1.TenantModelItem{ - ID: r.ID, - TenantID: r.TenantID, - ModelID: r.ModelID, - ModelCode: r.ModelCode, - ModelName: r.ModelName, - Category: r.Category, - Enabled: r.Enabled, - BillingMode: r.BillingMode, - PerRequestPrice: r.PerRequestPrice, - DiscountRatio: r.DiscountRatio, - MaxConcurrency: r.MaxConcurrency, - ChannelScope: r.ChannelScope, - CustomInputPrice: r.CustomInputPrice, - CustomOutputPrice: r.CustomOutputPrice, - Multiplier: r.Multiplier, - }) + item := v1.TenantModelItem{ + ID: r.ID, + TenantID: r.TenantID, + ModelID: r.ModelID, + ModelCode: r.ModelCode, + ModelName: r.ModelName, + Category: r.Category, + Enabled: r.Enabled, + BillingMode: r.BillingMode, + PerRequestPrice: r.PerRequestPrice, + DiscountRatio: r.DiscountRatio, + MaxConcurrency: r.MaxConcurrency, + ChannelScope: r.ChannelScope, + CustomInputPrice: r.CustomInputPrice, + CustomOutputPrice: r.CustomOutputPrice, + CustomCacheReadPrice: r.CustomCacheReadPrice, + CustomCacheCreationPrice: r.CustomCacheCreationPrice, + Multiplier: r.Multiplier, + } + if r.CustomPricingTiers != "" && r.CustomPricingTiers != "null" && r.CustomPricingTiers != "[]" { + var tiers []*v1.PricingTier + if err := json.Unmarshal([]byte(r.CustomPricingTiers), &tiers); err == nil { + item.CustomPricingTiers = tiers + } + } + list = append(list, item) } return &v1.TenantModelListRes{List: list}, nil @@ -70,7 +83,6 @@ func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatch assigned := 0 for _, a := range req.Assignments { - // Check model exists and is active var model *struct { ID int64 `json:"id"` Status string `json:"status"` @@ -82,7 +94,6 @@ func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatch continue } - // Check if already assigned count, _ := dao.MdlTenantModels.Ctx(ctx). Where("tenant_id", req.TenantID). Where("model_id", a.ModelID). @@ -97,25 +108,33 @@ func (s *sAdmin) BatchAssignModels(ctx context.Context, req *v1.TenantModelBatch maxConc = &concurrency } + var tiersJSON string + if len(a.CustomPricingTiers) > 0 { + b, _ := json.Marshal(a.CustomPricingTiers) + tiersJSON = string(b) + } + _, err = dao.MdlTenantModels.Ctx(ctx).Insert(do.MdlTenantModels{ - TenantId: req.TenantID, - ModelId: a.ModelID, - Enabled: a.Enabled, - BillingMode: a.BillingMode, - PerRequestPrice: a.PerRequestPrice, - DiscountRatio: a.DiscountRatio, - MaxConcurrency: maxConc, - ChannelScope: a.ChannelScope, - CustomInputPrice: a.CustomInputPrice, - CustomOutputPrice: a.CustomOutputPrice, - Multiplier: 1.0, + TenantId: req.TenantID, + ModelId: a.ModelID, + Enabled: a.Enabled, + BillingMode: a.BillingMode, + PerRequestPrice: a.PerRequestPrice, + DiscountRatio: a.DiscountRatio, + MaxConcurrency: maxConc, + ChannelScope: a.ChannelScope, + CustomInputPrice: a.CustomInputPrice, + CustomOutputPrice: a.CustomOutputPrice, + CustomCacheReadPrice: a.CustomCacheReadPrice, + CustomCacheCreationPrice: a.CustomCacheCreationPrice, + CustomPricingTiers: tiersJSON, + Multiplier: 1.0, }) if err == nil { assigned++ } } - // Clear billing cache for this tenant billing.ClearTenantPriceCache(ctx, req.TenantID) return &v1.TenantModelBatchAssignRes{Assigned: assigned}, nil @@ -146,9 +165,23 @@ func (s *sAdmin) UpdateTenantModel(ctx context.Context, req *v1.TenantModelUpdat if req.CustomInputPrice != nil { data.CustomInputPrice = *req.CustomInputPrice } + if req.CustomCacheReadPrice != nil { + data.CustomCacheReadPrice = *req.CustomCacheReadPrice + } + if req.CustomCacheCreationPrice != nil { + data.CustomCacheCreationPrice = *req.CustomCacheCreationPrice + } if req.CustomOutputPrice != nil { data.CustomOutputPrice = *req.CustomOutputPrice } + if req.CustomPricingTiers != nil { + if len(*req.CustomPricingTiers) > 0 { + b, _ := json.Marshal(*req.CustomPricingTiers) + data.CustomPricingTiers = string(b) + } else { + data.CustomPricingTiers = "[]" + } + } _, err := dao.MdlTenantModels.Ctx(ctx). Where("tenant_id", req.TenantID). @@ -159,7 +192,6 @@ func (s *sAdmin) UpdateTenantModel(ctx context.Context, req *v1.TenantModelUpdat return nil, err } - // Clear billing cache billing.ClearTenantPriceCache(ctx, req.TenantID) return nil, nil @@ -180,7 +212,6 @@ func (s *sAdmin) DeleteTenantModel(ctx context.Context, req *v1.TenantModelDelet return nil, common.NewNotFoundError("模型分配") } - // Clear billing cache billing.ClearTenantPriceCache(ctx, req.TenantID) return nil, nil diff --git a/internal/logic/billing/pricing.go b/internal/logic/billing/pricing.go index b8872f9..0af4f3b 100644 --- a/internal/logic/billing/pricing.go +++ b/internal/logic/billing/pricing.go @@ -2,6 +2,7 @@ package billing import ( "context" + "encoding/json" "fmt" "math" "time" @@ -45,6 +46,9 @@ type PricingResult struct { // Cache 直接定价 CacheReadPrice float64 // 缓存读取每 1M token 价格 CacheCreationPrice float64 // 缓存创建每 1M token 价格 + + // 租户自定义阶梯定价(JSONB 解析后的原始数据,供 CalculateCost 使用) + CustomTiers []pricingTierRow } // ClearTenantPriceCache 清除租户的所有模型价格缓存 @@ -139,20 +143,23 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric // 3. 查租户独立价格(mdl_tenant_models) type tenantModelRow struct { - CustomInputPrice *float64 `json:"custom_input_price"` - CustomOutputPrice *float64 `json:"custom_output_price"` - Multiplier *float64 `json:"multiplier"` - DiscountRatio *float64 `json:"discount_ratio"` - BillingMode *string `json:"billing_mode"` - PerRequestPrice *float64 `json:"per_request_price"` - Enabled bool `json:"enabled"` + CustomInputPrice *float64 `json:"custom_input_price"` + CustomOutputPrice *float64 `json:"custom_output_price"` + CustomCacheReadPrice *float64 `json:"custom_cache_read_price"` + CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price"` + CustomPricingTiers string `json:"custom_pricing_tiers"` + Multiplier *float64 `json:"multiplier"` + DiscountRatio *float64 `json:"discount_ratio"` + BillingMode *string `json:"billing_mode"` + PerRequestPrice *float64 `json:"per_request_price"` + Enabled bool `json:"enabled"` } var tm *tenantModelRow err = dao.MdlTenantModels.Ctx(ctx). Where("tenant_id", tenantID). Where("model_id", model.ID). - Fields("custom_input_price, custom_output_price, multiplier, discount_ratio, billing_mode, per_request_price, enabled"). + Fields("custom_input_price, custom_output_price, custom_cache_read_price, custom_cache_creation_price, custom_pricing_tiers, multiplier, discount_ratio, billing_mode, per_request_price, enabled"). Scan(&tm) if err != nil { return nil, gerror.Wrapf(err, "query tenant model price") @@ -161,6 +168,7 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric tenantMultiplier := 1.0 discountRatio := 1.0 billingSource := "base" + var customTiers []pricingTierRow if tm != nil && tm.Enabled { billingSource = "tenant_custom" @@ -176,6 +184,19 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric outputPrice = *tm.CustomOutputPrice } + // 租户覆盖缓存定价 + if tm.CustomCacheReadPrice != nil && *tm.CustomCacheReadPrice > 0 { + cacheReadPrice = *tm.CustomCacheReadPrice + } + if tm.CustomCacheCreationPrice != nil && *tm.CustomCacheCreationPrice > 0 { + cacheCreationPrice = *tm.CustomCacheCreationPrice + } + + // 租户自定义阶梯定价 + if tm.CustomPricingTiers != "" && tm.CustomPricingTiers != "null" && tm.CustomPricingTiers != "[]" { + _ = json.Unmarshal([]byte(tm.CustomPricingTiers), &customTiers) + } + // discount_ratio 优先于 multiplier if tm.DiscountRatio != nil && *tm.DiscountRatio > 0 { discountRatio = *tm.DiscountRatio @@ -211,6 +232,7 @@ func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*Pric Currency: "USD", CacheReadPrice: cacheReadPrice, CacheCreationPrice: cacheCreationPrice, + CustomTiers: customTiers, } modelPriceCache.Set(ctx, cacheKey, result) @@ -245,8 +267,13 @@ func CalculateCost(ctx context.Context, tenantID int64, modelName string, inputT // Token / Tiered 计费 var inputCost, outputCost float64 if pricing.BillingMode == "tiered" { - inputCost, _ = calculateTieredCostFromPricing(ctx, modelName, inputTokens, true) - outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false) + if len(pricing.CustomTiers) > 0 { + inputCost = calculateTieredCostFromTiers(pricing.CustomTiers, inputTokens, true) + outputCost = calculateTieredCostFromTiers(pricing.CustomTiers, outputTokens, false) + } else { + inputCost, _ = calculateTieredCostFromPricing(ctx, modelName, inputTokens, true) + outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false) + } } else { // 统一单价模式 inputCost = float64(inputTokens) / 1_000_000.0 * pricing.InputPrice @@ -326,7 +353,11 @@ func CalculateCostWithUsage(ctx context.Context, tenantID int64, modelName strin // 基础输入费用 var baseInputCost float64 if pricing.BillingMode == "tiered" { - baseInputCost, _ = calculateTieredCostFromPricing(ctx, modelName, baseInputTokens, true) + if len(pricing.CustomTiers) > 0 { + baseInputCost = calculateTieredCostFromTiers(pricing.CustomTiers, baseInputTokens, true) + } else { + baseInputCost, _ = calculateTieredCostFromPricing(ctx, modelName, baseInputTokens, true) + } } else { baseInputCost = float64(baseInputTokens) / 1_000_000.0 * pricing.InputPrice } @@ -334,7 +365,11 @@ func CalculateCostWithUsage(ctx context.Context, tenantID int64, modelName strin // 输出费用 var outputCost float64 if pricing.BillingMode == "tiered" { - outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false) + if len(pricing.CustomTiers) > 0 { + outputCost = calculateTieredCostFromTiers(pricing.CustomTiers, outputTokens, false) + } else { + outputCost, _ = calculateTieredCostFromPricing(ctx, modelName, outputTokens, false) + } } else { outputCost = float64(outputTokens) / 1_000_000.0 * pricing.OutputPrice } @@ -486,6 +521,15 @@ func calculateTieredCostFromPricing(ctx context.Context, modelName string, token return 0, 1.0 } + return calculateTieredCostFromTiers(tiers, tokens, isInput), 1.0 +} + +// calculateTieredCostFromTiers 从给定的阶梯数组计算费用(租户自定义阶梯或基础阶梯共用) +func calculateTieredCostFromTiers(tiers []pricingTierRow, tokens int, isInput bool) float64 { + if tokens <= 0 || len(tiers) == 0 { + return 0 + } + var totalCost float64 remaining := int64(tokens) @@ -500,7 +544,6 @@ func calculateTieredCostFromPricing(ctx context.Context, modelName string, token } if tier.MaxTokens == nil { - // 最后一段无上限 totalCost += float64(remaining) / 1_000_000.0 * price remaining = 0 } else { @@ -517,5 +560,5 @@ func calculateTieredCostFromPricing(ctx context.Context, modelName string, token } } - return totalCost, 1.0 + return totalCost } diff --git a/internal/logic/tenant/model.go b/internal/logic/tenant/model.go index 2470b2a..0429bfd 100644 --- a/internal/logic/tenant/model.go +++ b/internal/logic/tenant/model.go @@ -2,17 +2,27 @@ package tenant import ( "context" + "encoding/json" v1 "github.com/qianfree/team-api/api/tenant/v1" "github.com/qianfree/team-api/internal/dao" ) +// pricingTierRow 阶梯定价行(与 billing.pricingTierRow 结构一致) +type pricingTierRow struct { + MinTokens int64 `json:"min_tokens"` + MaxTokens *int64 `json:"max_tokens"` + InputPrice float64 `json:"input_price"` + OutputPrice float64 `json:"output_price"` +} + // ListAvailableModels 获取租户可用的模型列表 func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailableModelsReq) (*v1.TenantAvailableModelsRes, error) { tenantID := ctxTenantID(ctx) query := dao.MdlTenantModels.Ctx(ctx). LeftJoin("mdl_models m ON mdl_tenant_models.model_id = m.id"). + LeftJoin("mdl_pricing p ON p.model_id = mdl_tenant_models.model_id AND p.min_tokens = 0"). Where("mdl_tenant_models.tenant_id", tenantID). Where("mdl_tenant_models.enabled", true). Where("m.status", "active") @@ -25,23 +35,34 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab } var results []struct { - ID int64 `json:"id"` - ModelId string `json:"model_id"` - ModelName string `json:"model_name"` - Category string `json:"category"` - MaxContextTokens int `json:"max_context_tokens"` - MaxOutputTokens int `json:"max_output_tokens"` - Description string `json:"description"` - Tags string `json:"tags"` - Capabilities string `json:"capabilities"` - BillingMode *string `json:"billing_mode"` - PerRequestPrice *float64 `json:"per_request_price"` - DiscountRatio *float64 `json:"discount_ratio"` - MaxConcurrency *int `json:"max_concurrency"` + ID int64 `json:"id"` + ModelDBID int64 `json:"model_db_id"` + ModelId string `json:"model_id"` + ModelName string `json:"model_name"` + Category string `json:"category"` + MaxContextTokens int `json:"max_context_tokens"` + MaxOutputTokens int `json:"max_output_tokens"` + Description string `json:"description"` + Tags string `json:"tags"` + Capabilities string `json:"capabilities"` + BillingMode *string `json:"billing_mode"` + PerRequestPrice *float64 `json:"per_request_price"` + DiscountRatio *float64 `json:"discount_ratio"` + MaxConcurrency *int `json:"max_concurrency"` + BaseInputPrice float64 `json:"base_input_price"` + BaseOutputPrice float64 `json:"base_output_price"` + BaseCacheReadPrice float64 `json:"base_cache_read_price"` + BaseCacheCreationPrice float64 `json:"base_cache_creation_price"` + BaseBillingMode string `json:"base_billing_mode"` + CustomInputPrice *float64 `json:"custom_input_price"` + CustomOutputPrice *float64 `json:"custom_output_price"` + CustomCacheReadPrice *float64 `json:"custom_cache_read_price"` + CustomCacheCreationPrice *float64 `json:"custom_cache_creation_price"` + CustomPricingTiers string `json:"custom_pricing_tiers"` } err := query. - Fields("mdl_tenant_models.id, m.model_id, m.model_name, m.category, m.max_context_tokens, m.max_output_tokens, m.description, m.tags, m.capabilities, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency"). + Fields("mdl_tenant_models.id, mdl_tenant_models.model_id AS model_db_id, m.model_id, m.model_name, m.category, m.max_context_tokens, m.max_output_tokens, m.description, m.tags, m.capabilities, mdl_tenant_models.billing_mode, mdl_tenant_models.per_request_price, mdl_tenant_models.discount_ratio, mdl_tenant_models.max_concurrency, p.input_price AS base_input_price, p.output_price AS base_output_price, p.cache_read_price AS base_cache_read_price, p.cache_creation_price AS base_cache_creation_price, p.billing_mode AS base_billing_mode, mdl_tenant_models.custom_input_price, mdl_tenant_models.custom_output_price, mdl_tenant_models.custom_cache_read_price, mdl_tenant_models.custom_cache_creation_price, mdl_tenant_models.custom_pricing_tiers"). OrderAsc("m.category"). OrderAsc("m.model_id"). Scan(&results) @@ -49,24 +70,141 @@ func (s *sTenant) ListAvailableModels(ctx context.Context, req *v1.TenantAvailab return nil, err } + // 收集需要查询 base 阶梯定价的 model 内部 ID(mdl_models.id) + tieredModelDBIDs := make([]int64, 0) + for _, r := range results { + effectiveBillingMode := resolveBillingMode(r.BillingMode, r.BaseBillingMode) + if effectiveBillingMode == "tiered" && r.CustomPricingTiers == "" { + tieredModelDBIDs = append(tieredModelDBIDs, r.ModelDBID) + } + } + + // 批量查询 base 阶梯定价(min_tokens > 0 的阶梯) + baseTiersMap := make(map[int64][]v1.PricingTierItem) + if len(tieredModelDBIDs) > 0 { + var baseTiers []struct { + ModelId int64 `json:"model_id"` + MinTokens int64 `json:"min_tokens"` + MaxTokens *int64 `json:"max_tokens"` + InputPrice float64 `json:"input_price"` + OutputPrice float64 `json:"output_price"` + } + err = dao.MdlPricing.Ctx(ctx). + WhereIn("model_id", tieredModelDBIDs). + Where("billing_mode", "tiered"). + Where("min_tokens > 0"). + Fields("model_id, min_tokens, max_tokens, input_price, output_price"). + OrderAsc("model_id"). + OrderAsc("min_tokens"). + Scan(&baseTiers) + if err == nil { + for _, t := range baseTiers { + baseTiersMap[t.ModelId] = append(baseTiersMap[t.ModelId], v1.PricingTierItem{ + MinTokens: t.MinTokens, + MaxTokens: t.MaxTokens, + InputPrice: t.InputPrice, + OutputPrice: t.OutputPrice, + }) + } + } + } + list := make([]v1.TenantAvailableModelItem, 0, len(results)) for _, r := range results { - list = append(list, v1.TenantAvailableModelItem{ - ID: r.ID, - ModelId: r.ModelId, - ModelName: r.ModelName, - Category: r.Category, - MaxContext: r.MaxContextTokens, - MaxOutput: r.MaxOutputTokens, - Description: r.Description, - Tags: r.Tags, - Capabilities: r.Capabilities, - BillingMode: r.BillingMode, - PerRequestPrice: r.PerRequestPrice, - DiscountRatio: r.DiscountRatio, - MaxConcurrency: r.MaxConcurrency, - }) + effectiveBillingMode := resolveBillingMode(r.BillingMode, r.BaseBillingMode) + + inputPrice := effectivePrice(r.CustomInputPrice, r.BaseInputPrice) + outputPrice := effectivePrice(r.CustomOutputPrice, r.BaseOutputPrice) + cacheReadPrice := effectivePrice(r.CustomCacheReadPrice, r.BaseCacheReadPrice) + cacheCreationPrice := effectivePrice(r.CustomCacheCreationPrice, r.BaseCacheCreationPrice) + + item := v1.TenantAvailableModelItem{ + ID: r.ID, + ModelId: r.ModelId, + ModelName: r.ModelName, + Category: r.Category, + MaxContext: r.MaxContextTokens, + MaxOutput: r.MaxOutputTokens, + Description: r.Description, + Tags: r.Tags, + Capabilities: r.Capabilities, + BillingMode: &effectiveBillingMode, + PerRequestPrice: r.PerRequestPrice, + DiscountRatio: r.DiscountRatio, + MaxConcurrency: r.MaxConcurrency, + InputPrice: inputPrice, + OutputPrice: outputPrice, + CacheReadPrice: cacheReadPrice, + CacheCreationPrice: cacheCreationPrice, + } + + // 阶梯定价处理 + if effectiveBillingMode == "tiered" { + var tiers []v1.PricingTierItem + + // 优先使用租户自定义阶梯 + if r.CustomPricingTiers != "" && r.CustomPricingTiers != "null" && r.CustomPricingTiers != "[]" { + var raw []pricingTierRow + if json.Unmarshal([]byte(r.CustomPricingTiers), &raw) == nil && len(raw) > 0 { + for _, t := range raw { + tiers = append(tiers, v1.PricingTierItem{ + MinTokens: t.MinTokens, + MaxTokens: t.MaxTokens, + InputPrice: t.InputPrice, + OutputPrice: t.OutputPrice, + }) + } + } + } + + // 使用 base 阶梯定价 + if len(tiers) == 0 { + // 第一阶梯(min_tokens=0,来自主查询的 base_input/output_price) + if r.BaseInputPrice > 0 || r.BaseOutputPrice > 0 { + tiers = append(tiers, v1.PricingTierItem{ + MinTokens: 0, + MaxTokens: nil, + InputPrice: r.BaseInputPrice, + OutputPrice: r.BaseOutputPrice, + }) + } + // 后续阶梯 + if rest, ok := baseTiersMap[r.ModelDBID]; ok { + // 设置第一阶梯的 max_tokens + if len(tiers) > 0 && len(rest) > 0 { + tiers[0].MaxTokens = &rest[0].MinTokens + } + tiers = append(tiers, rest...) + } + } + + item.PricingTiers = tiers + } + + list = append(list, item) } return &v1.TenantAvailableModelsRes{List: list}, nil } + +// resolveBillingMode 解析有效计费模式 +func resolveBillingMode(tenantMode *string, baseMode string) string { + if tenantMode != nil && *tenantMode != "" { + return *tenantMode + } + if baseMode != "" { + return baseMode + } + return "token" +} + +// effectivePrice 计算有效价格:自定义价优先,否则用基础价 +func effectivePrice(custom *float64, base float64) *float64 { + if custom != nil && *custom > 0 { + return custom + } + if base > 0 { + return &base + } + return nil +} diff --git a/internal/model/do/chn_channel_keys.go b/internal/model/do/chn_channel_keys.go index 011dd40..529e757 100644 --- a/internal/model/do/chn_channel_keys.go +++ b/internal/model/do/chn_channel_keys.go @@ -20,7 +20,7 @@ type ChnChannelKeys struct { LastUsedAt *gtime.Time // 最后使用时间 LastError any // 最后一次错误信息 CreatedAt *gtime.Time // 创建时间 - UpdatedAt *gtime.Time // 更新时间 KeyType any // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌) TokenExpiresAt *gtime.Time // OAuth access_token 过期时间(仅 key_type=oauth 时有值) + UpdatedAt *gtime.Time // 更新时间 } diff --git a/internal/model/do/mdl_models.go b/internal/model/do/mdl_models.go index 34083f2..0a54954 100644 --- a/internal/model/do/mdl_models.go +++ b/internal/model/do/mdl_models.go @@ -21,10 +21,10 @@ type MdlModels struct { MaxOutputTokens any // 最大输出 token 数 Description any // 模型描述 Tags []string // 标签(如 reasoning、vision、function_calling) + Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等) CreatedAt *gtime.Time // 创建时间 UpdatedAt *gtime.Time // 更新时间 DeprecatedAt *gtime.Time // 标记弃用的时间(NULL表示未弃用) SunsetDate *gtime.Time // 计划下线日期(到达后返回410 Gone,NULL表示未设置) ReplacementModel any // 推荐替代模型名(NULL表示无替代) - Capabilities any // 模型能力特性(如 vision、function_calling、reasoning 等) } diff --git a/internal/model/do/mdl_tenant_models.go b/internal/model/do/mdl_tenant_models.go index 52431d1..ec5d7db 100644 --- a/internal/model/do/mdl_tenant_models.go +++ b/internal/model/do/mdl_tenant_models.go @@ -11,19 +11,22 @@ import ( // MdlTenantModels is the golang structure of table mdl_tenant_models for DAO operations like Where/Data. type MdlTenantModels struct { - g.Meta `orm:"table:mdl_tenant_models, do:true"` - Id any // 主键ID - TenantId any // 租户ID - ModelId any // 模型ID - Enabled any // 是否启用(禁用后该租户无法调用此模型) - CustomInputPrice any // 租户自定义输入价格(NULL 表示使用默认定价) - CustomOutputPrice any // 租户自定义输出价格(NULL 表示使用默认定价) - Multiplier any // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) - CreatedAt *gtime.Time // 创建时间 - UpdatedAt *gtime.Time // 更新时间 - BillingMode any // 覆盖模型计费方式(NULL表示跟随模型默认) - PerRequestPrice any // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效) - DiscountRatio any // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用) - MaxConcurrency any // 单模型并发上限(NULL表示不限制) - ChannelScope any // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID) + g.Meta `orm:"table:mdl_tenant_models, do:true"` + Id any // 主键ID + TenantId any // 租户ID + ModelId any // 模型ID + Enabled any // 是否启用(禁用后该租户无法调用此模型) + CustomInputPrice any // 租户自定义输入价格(NULL 表示使用默认定价) + CustomOutputPrice any // 租户自定义输出价格(NULL 表示使用默认定价) + Multiplier any // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) + CreatedAt *gtime.Time // 创建时间 + UpdatedAt *gtime.Time // 更新时间 + BillingMode any // 覆盖模型计费方式(NULL表示跟随模型默认) + PerRequestPrice any // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效) + DiscountRatio any // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用) + MaxConcurrency any // 单模型并发上限(NULL表示不限制) + ChannelScope any // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID) + CustomCacheReadPrice any // 自定义缓存读取价格($/1M token),NULL 表示使用基础定价 + CustomCacheCreationPrice any // 自定义缓存创建价格($/1M token),NULL 表示使用基础定价 + CustomPricingTiers any // 自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}] } diff --git a/internal/model/do/ntf_messages.go b/internal/model/do/ntf_messages.go index 0ab3f96..fad9fdb 100644 --- a/internal/model/do/ntf_messages.go +++ b/internal/model/do/ntf_messages.go @@ -22,6 +22,6 @@ type NtfMessages struct { IsRead any // 是否已读:0=未读, 1=已读 IsBroadcast any // 是否广播消息:0=个人消息, 1=广播消息 Metadata any // 附加元数据(JSONB,如关联资源ID、跳转链接等) - CreatedAt *gtime.Time // 创建时间 TargetRoles any // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) + CreatedAt *gtime.Time // 创建时间 } diff --git a/internal/model/do/sys_plugins.go b/internal/model/do/sys_plugins.go new file mode 100644 index 0000000..beb754a --- /dev/null +++ b/internal/model/do/sys_plugins.go @@ -0,0 +1,25 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// SysPlugins is the golang structure of table sys_plugins for DAO operations like Where/Data. +type SysPlugins struct { + g.Meta `orm:"table:sys_plugins, do:true"` + Id any // + Name any // 插件唯一标识,如 email-report + Label any // 显示名称 + Version any // 当前安装版本 + Status any // 状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常 + Category any // 分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展 + Config any // 插件全局配置(JSON) + ErrorMsg any // 异常信息 + CreatedAt *gtime.Time // + UpdatedAt *gtime.Time // +} diff --git a/internal/model/do/tnt_tenant_plugins.go b/internal/model/do/tnt_tenant_plugins.go new file mode 100644 index 0000000..74ded44 --- /dev/null +++ b/internal/model/do/tnt_tenant_plugins.go @@ -0,0 +1,22 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package do + +import ( + "github.com/gogf/gf/v2/frame/g" + "github.com/gogf/gf/v2/os/gtime" +) + +// TntTenantPlugins is the golang structure of table tnt_tenant_plugins for DAO operations like Where/Data. +type TntTenantPlugins struct { + g.Meta `orm:"table:tnt_tenant_plugins, do:true"` + Id any // + TenantId any // + PluginName any // 插件标识 + Enabled any // 是否启用 + Config any // 租户级配置覆盖(JSON),优先级高于全局配置 + CreatedAt *gtime.Time // + UpdatedAt *gtime.Time // +} diff --git a/internal/model/entity/chn_channel_keys.go b/internal/model/entity/chn_channel_keys.go index 729b72d..a913972 100644 --- a/internal/model/entity/chn_channel_keys.go +++ b/internal/model/entity/chn_channel_keys.go @@ -18,7 +18,7 @@ type ChnChannelKeys struct { LastUsedAt *gtime.Time `json:"last_used_at" orm:"last_used_at" description:"最后使用时间"` // 最后使用时间 LastError string `json:"last_error" orm:"last_error" description:"最后一次错误信息"` // 最后一次错误信息 CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 - UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 KeyType string `json:"key_type" orm:"key_type" description:"Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌)"` // Key 类型:apikey(传统静态密钥)/ oauth(OAuth 令牌) TokenExpiresAt *gtime.Time `json:"token_expires_at" orm:"token_expires_at" description:"OAuth access_token 过期时间(仅 key_type=oauth 时有值)"` // OAuth access_token 过期时间(仅 key_type=oauth 时有值) + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 } diff --git a/internal/model/entity/mdl_models.go b/internal/model/entity/mdl_models.go index 96dfe97..5fa76c0 100644 --- a/internal/model/entity/mdl_models.go +++ b/internal/model/entity/mdl_models.go @@ -19,10 +19,10 @@ type MdlModels struct { MaxOutputTokens int `json:"max_output_tokens" orm:"max_output_tokens" description:"最大输出 token 数"` // 最大输出 token 数 Description string `json:"description" orm:"description" description:"模型描述"` // 模型描述 Tags []string `json:"tags" orm:"tags" description:"标签(如 reasoning、vision、function_calling)"` // 标签(如 reasoning、vision、function_calling) + Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等) CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 DeprecatedAt *gtime.Time `json:"deprecated_at" orm:"deprecated_at" description:"标记弃用的时间(NULL表示未弃用)"` // 标记弃用的时间(NULL表示未弃用) SunsetDate *gtime.Time `json:"sunset_date" orm:"sunset_date" description:"计划下线日期(到达后返回410 Gone,NULL表示未设置)"` // 计划下线日期(到达后返回410 Gone,NULL表示未设置) ReplacementModel string `json:"replacement_model" orm:"replacement_model" description:"推荐替代模型名(NULL表示无替代)"` // 推荐替代模型名(NULL表示无替代) - Capabilities string `json:"capabilities" orm:"capabilities" description:"模型能力特性(如 vision、function_calling、reasoning 等)"` // 模型能力特性(如 vision、function_calling、reasoning 等) } diff --git a/internal/model/entity/mdl_tenant_models.go b/internal/model/entity/mdl_tenant_models.go index 6fb3f8f..3e69ddd 100644 --- a/internal/model/entity/mdl_tenant_models.go +++ b/internal/model/entity/mdl_tenant_models.go @@ -10,18 +10,21 @@ import ( // MdlTenantModels is the golang structure for table mdl_tenant_models. type MdlTenantModels struct { - Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID - TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID - ModelId int64 `json:"model_id" orm:"model_id" description:"模型ID"` // 模型ID - Enabled bool `json:"enabled" orm:"enabled" description:"是否启用(禁用后该租户无法调用此模型)"` // 是否启用(禁用后该租户无法调用此模型) - CustomInputPrice float64 `json:"custom_input_price" orm:"custom_input_price" description:"租户自定义输入价格(NULL 表示使用默认定价)"` // 租户自定义输入价格(NULL 表示使用默认定价) - CustomOutputPrice float64 `json:"custom_output_price" orm:"custom_output_price" description:"租户自定义输出价格(NULL 表示使用默认定价)"` // 租户自定义输出价格(NULL 表示使用默认定价) - Multiplier float64 `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)"` // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) - CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 - UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 - BillingMode string `json:"billing_mode" orm:"billing_mode" description:"覆盖模型计费方式(NULL表示跟随模型默认)"` // 覆盖模型计费方式(NULL表示跟随模型默认) - PerRequestPrice float64 `json:"per_request_price" orm:"per_request_price" description:"按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)"` // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效) - DiscountRatio float64 `json:"discount_ratio" orm:"discount_ratio" description:"折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)"` // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用) - MaxConcurrency int `json:"max_concurrency" orm:"max_concurrency" description:"单模型并发上限(NULL表示不限制)"` // 单模型并发上限(NULL表示不限制) - ChannelScope string `json:"channel_scope" orm:"channel_scope" description:"渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)"` // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID) + Id int64 `json:"id" orm:"id" description:"主键ID"` // 主键ID + TenantId int64 `json:"tenant_id" orm:"tenant_id" description:"租户ID"` // 租户ID + ModelId int64 `json:"model_id" orm:"model_id" description:"模型ID"` // 模型ID + Enabled bool `json:"enabled" orm:"enabled" description:"是否启用(禁用后该租户无法调用此模型)"` // 是否启用(禁用后该租户无法调用此模型) + CustomInputPrice float64 `json:"custom_input_price" orm:"custom_input_price" description:"租户自定义输入价格(NULL 表示使用默认定价)"` // 租户自定义输入价格(NULL 表示使用默认定价) + CustomOutputPrice float64 `json:"custom_output_price" orm:"custom_output_price" description:"租户自定义输出价格(NULL 表示使用默认定价)"` // 租户自定义输出价格(NULL 表示使用默认定价) + Multiplier float64 `json:"multiplier" orm:"multiplier" description:"租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率)"` // 租户价格倍率(VIP 折扣等,最终价格 = 基础价格 × 模型倍率 × 租户倍率) + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:"更新时间"` // 更新时间 + BillingMode string `json:"billing_mode" orm:"billing_mode" description:"覆盖模型计费方式(NULL表示跟随模型默认)"` // 覆盖模型计费方式(NULL表示跟随模型默认) + PerRequestPrice float64 `json:"per_request_price" orm:"per_request_price" description:"按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效)"` // 按次计费单价(覆盖模型默认,仅 billing_mode = per_request 时有效) + DiscountRatio float64 `json:"discount_ratio" orm:"discount_ratio" description:"折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用)"` // 折扣比例(如0.8表示八折,NULL表示不打折,优先于 multiplier 使用) + MaxConcurrency int `json:"max_concurrency" orm:"max_concurrency" description:"单模型并发上限(NULL表示不限制)"` // 单模型并发上限(NULL表示不限制) + ChannelScope string `json:"channel_scope" orm:"channel_scope" description:"渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID)"` // 渠道范围覆盖(NULL跟随租户默认,[]表示全部,数组表示指定渠道ID) + CustomCacheReadPrice float64 `json:"custom_cache_read_price" orm:"custom_cache_read_price" description:"自定义缓存读取价格($/1M token),NULL 表示使用基础定价"` // 自定义缓存读取价格($/1M token),NULL 表示使用基础定价 + CustomCacheCreationPrice float64 `json:"custom_cache_creation_price" orm:"custom_cache_creation_price" description:"自定义缓存创建价格($/1M token),NULL 表示使用基础定价"` // 自定义缓存创建价格($/1M token),NULL 表示使用基础定价 + CustomPricingTiers string `json:"custom_pricing_tiers" orm:"custom_pricing_tiers" description:"自定义阶梯定价(JSONB 数组),格式: [{\"min_tokens\":0,\"max_tokens\":100000,\"input_price\":0.5,\"output_price\":1.5,\"cache_read_price\":0.1,\"cache_creation_price\":0.2}]"` // 自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}] } diff --git a/internal/model/entity/ntf_messages.go b/internal/model/entity/ntf_messages.go index e0a3751..267790e 100644 --- a/internal/model/entity/ntf_messages.go +++ b/internal/model/entity/ntf_messages.go @@ -20,6 +20,6 @@ type NtfMessages struct { IsRead int `json:"is_read" orm:"is_read" description:"是否已读:0=未读, 1=已读"` // 是否已读:0=未读, 1=已读 IsBroadcast int `json:"is_broadcast" orm:"is_broadcast" description:"是否广播消息:0=个人消息, 1=广播消息"` // 是否广播消息:0=个人消息, 1=广播消息 Metadata string `json:"metadata" orm:"metadata" description:"附加元数据(JSONB,如关联资源ID、跳转链接等)"` // 附加元数据(JSONB,如关联资源ID、跳转链接等) - CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 TargetRoles string `json:"target_roles" orm:"target_roles" description:"目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色)"` // 目标角色(NULL=全部角色,逗号分隔如 owner,admin 表示仅限这些角色) + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:"创建时间"` // 创建时间 } diff --git a/internal/model/entity/sys_plugins.go b/internal/model/entity/sys_plugins.go new file mode 100644 index 0000000..d82925b --- /dev/null +++ b/internal/model/entity/sys_plugins.go @@ -0,0 +1,23 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// SysPlugins is the golang structure for table sys_plugins. +type SysPlugins struct { + Id int64 `json:"id" orm:"id" description:""` // + Name string `json:"name" orm:"name" description:"插件唯一标识,如 email-report"` // 插件唯一标识,如 email-report + Label string `json:"label" orm:"label" description:"显示名称"` // 显示名称 + Version string `json:"version" orm:"version" description:"当前安装版本"` // 当前安装版本 + Status string `json:"status" orm:"status" description:"状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常"` // 状态:registered=已注册, installed=已安装, enabled=已启用, disabled=已禁用, error=异常 + Category string `json:"category" orm:"category" description:"分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展"` // 分类:relay=代理扩展, middleware=中间件, billing=计费, notification=通知, extension=通用扩展 + Config string `json:"config" orm:"config" description:"插件全局配置(JSON)"` // 插件全局配置(JSON) + ErrorMsg string `json:"error_msg" orm:"error_msg" description:"异常信息"` // 异常信息 + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // +} diff --git a/internal/model/entity/tnt_tenant_plugins.go b/internal/model/entity/tnt_tenant_plugins.go new file mode 100644 index 0000000..e994cec --- /dev/null +++ b/internal/model/entity/tnt_tenant_plugins.go @@ -0,0 +1,20 @@ +// ================================================================================= +// Code generated and maintained by GoFrame CLI tool. DO NOT EDIT. +// ================================================================================= + +package entity + +import ( + "github.com/gogf/gf/v2/os/gtime" +) + +// TntTenantPlugins is the golang structure for table tnt_tenant_plugins. +type TntTenantPlugins struct { + Id int64 `json:"id" orm:"id" description:""` // + TenantId int64 `json:"tenant_id" orm:"tenant_id" description:""` // + PluginName string `json:"plugin_name" orm:"plugin_name" description:"插件标识"` // 插件标识 + Enabled bool `json:"enabled" orm:"enabled" description:"是否启用"` // 是否启用 + Config string `json:"config" orm:"config" description:"租户级配置覆盖(JSON),优先级高于全局配置"` // 租户级配置覆盖(JSON),优先级高于全局配置 + CreatedAt *gtime.Time `json:"created_at" orm:"created_at" description:""` // + UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at" description:""` // +} diff --git a/migrations/000002_add_tenant_model_custom_pricing.sql b/migrations/000002_add_tenant_model_custom_pricing.sql new file mode 100644 index 0000000..85f9546 --- /dev/null +++ b/migrations/000002_add_tenant_model_custom_pricing.sql @@ -0,0 +1,22 @@ +-- +goose Up +-- 租户模型分配增加自定义缓存定价覆盖字段和阶梯定价支持 + +-- 缓存定价覆盖 +ALTER TABLE mdl_tenant_models + ADD COLUMN IF NOT EXISTS custom_cache_read_price NUMERIC(20,10), + ADD COLUMN IF NOT EXISTS custom_cache_creation_price NUMERIC(20,10); + +COMMENT ON COLUMN mdl_tenant_models.custom_cache_read_price IS '自定义缓存读取价格($/1M token),NULL 表示使用基础定价'; +COMMENT ON COLUMN mdl_tenant_models.custom_cache_creation_price IS '自定义缓存创建价格($/1M token),NULL 表示使用基础定价'; + +-- 阶梯定价 +ALTER TABLE mdl_tenant_models + ADD COLUMN IF NOT EXISTS custom_pricing_tiers JSONB; + +COMMENT ON COLUMN mdl_tenant_models.custom_pricing_tiers IS '自定义阶梯定价(JSONB 数组),格式: [{"min_tokens":0,"max_tokens":100000,"input_price":0.5,"output_price":1.5,"cache_read_price":0.1,"cache_creation_price":0.2}]'; + +-- +goose Down +ALTER TABLE mdl_tenant_models + DROP COLUMN IF EXISTS custom_cache_read_price, + DROP COLUMN IF EXISTS custom_cache_creation_price, + DROP COLUMN IF EXISTS custom_pricing_tiers; diff --git a/web/admin/src/views/tenant/TenantDetailPage.vue b/web/admin/src/views/tenant/TenantDetailPage.vue index 8901453..de9c72d 100644 --- a/web/admin/src/views/tenant/TenantDetailPage.vue +++ b/web/admin/src/views/tenant/TenantDetailPage.vue @@ -1,5 +1,5 @@