From 47ffb4e2a964d8a5fa5abbcee62d47815fc88479 Mon Sep 17 00:00:00 2001 From: qian Date: Mon, 3 Aug 2026 21:44:47 +0800 Subject: [PATCH] =?UTF-8?q?fix(model):=E4=BF=AE=E5=A4=8D=E6=A8=A1=E5=9E=8B?= =?UTF-8?q?=E8=AE=BE=E7=BD=AE=E4=BB=B7=E6=A0=BC=E5=90=8E=E4=B8=8D=E8=83=BD?= =?UTF-8?q?=E7=AB=8B=E5=8D=B3=E8=B0=83=E7=94=A8?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- internal/logic/admin/model.go | 31 +++++++++++++++++++++ internal/logic/admin/model_import_export.go | 3 +- internal/logic/billing/pricing.go | 8 ++++++ 3 files changed, 41 insertions(+), 1 deletion(-) diff --git a/internal/logic/admin/model.go b/internal/logic/admin/model.go index 995dc28..ee5edfd 100644 --- a/internal/logic/admin/model.go +++ b/internal/logic/admin/model.go @@ -385,6 +385,16 @@ func (s *sAdmin) DeleteModel(ctx context.Context, req *v1.ModelDeleteReq) (*v1.M } dao.MdlGroupModels.Ctx(ctx).Where("model_id", req.ID).Fields("group_id").Scan(&affectedGroups) + // 查出模型编码,供事务提交后按模型清除所有租户的价格缓存 + var model struct { + ModelId string `json:"model_id"` + } + if err := dao.MdlModels.Ctx(ctx).Where("id", req.ID).Fields("model_id").Scan(&model); err != nil { + if err = common.IgnoreScanNoRows(err); err != nil { + return nil, err + } + } + // 四表删除(定价 / 租户分配 / 分组关联 / 模型本体)放入同一事务, // 避免中途失败留下「模型已删但定价/分配记录残留」的孤儿数据。 err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { @@ -411,6 +421,11 @@ func (s *sAdmin) DeleteModel(ctx context.Context, req *v1.ModelDeleteReq) (*v1.M invalidateTenantsInGroup(ctx, ag.GroupId) } + // 清除该模型在所有租户下的价格缓存,避免删除后旧价格残留最多 600s + if model.ModelId != "" { + billing.ClearModelPriceCache(ctx, model.ModelId) + } + return nil, nil } @@ -464,6 +479,16 @@ func (s *sAdmin) GetModelPricing(ctx context.Context, req *v1.PricingGetReq) (*v // SetModelPricing 设置模型定价(全量替换) func (s *sAdmin) SetModelPricing(ctx context.Context, req *v1.PricingSetReq) (*v1.PricingSetRes, error) { + // 先查模型编码,供事务提交后按模型清除所有租户的价格缓存 + var model struct { + ModelId string `json:"model_id"` + } + if err := dao.MdlModels.Ctx(ctx).Where("id", req.ModelID).Fields("model_id").Scan(&model); err != nil { + if err = common.IgnoreScanNoRows(err); err != nil { + return nil, err + } + } + // 全量替换:先删旧定价再插新定价,放入同一事务。否则若删除后插入中途失败, // 该模型会残留「无定价」状态,导致计费失败或回退到默认价。 err := g.DB().Transaction(ctx, func(ctx context.Context, tx gdb.TX) error { @@ -500,6 +525,12 @@ func (s *sAdmin) SetModelPricing(ctx context.Context, req *v1.PricingSetReq) (*v return nil, err } + // 事务提交后再清价格缓存:模型基础定价变更影响所有租户,按模型编码全量失效, + // 否则各租户缓存的旧价格(如新建时的零价)最多残留 600s,出现「已设价仍报未配置定价」。 + if model.ModelId != "" { + billing.ClearModelPriceCache(ctx, model.ModelId) + } + return nil, nil } diff --git a/internal/logic/admin/model_import_export.go b/internal/logic/admin/model_import_export.go index ff0c9d8..e8106cf 100644 --- a/internal/logic/admin/model_import_export.go +++ b/internal/logic/admin/model_import_export.go @@ -105,7 +105,7 @@ func (s *sAdmin) ExportModelsJson(ctx context.Context, req *v1.ModelExportJsonRe MinTokens: p.MinTokens, MaxTokens: p.MaxTokens, InputPrice: p.InputPrice, - OutputPrice: p.OutputPrice, + OutputPrice: p.OutputPrice, PerRequestPrice: p.PerRequestPrice, CacheReadPrice: p.CacheReadPrice, CacheCreationPrice: p.CacheCreationPrice, @@ -299,6 +299,7 @@ func (s *sAdmin) ImportModels(ctx context.Context, req *v1.ModelImportReq) (*v1. } relay.NewDataProvider().InvalidateModelCache(item.ModelId) + billing.ClearModelPriceCache(ctx, item.ModelId) res.Imported++ } else { // 新建模型 diff --git a/internal/logic/billing/pricing.go b/internal/logic/billing/pricing.go index 5a26fd1..8050dd4 100644 --- a/internal/logic/billing/pricing.go +++ b/internal/logic/billing/pricing.go @@ -75,6 +75,14 @@ func ClearTenantPriceCache(ctx context.Context, tenantID int64) { } } +// ClearModelPriceCache 清除指定模型在所有租户下的价格缓存。 +// 管理后台修改模型基础定价(SetModelPricing / ImportModels 更新 / DeleteModel)后必须调用, +// 否则各租户缓存中的旧价格最多残留 600s,出现「已设价但调用仍报未配置定价/按旧价计费」。 +// 缓存键格式为 {tenantID}:{modelName},此处按模型名匹配所有租户的条目。 +func ClearModelPriceCache(ctx context.Context, modelName string) { + modelPriceCache.DeleteByPattern(ctx, fmt.Sprintf("*:%s", modelName)) +} + // GetModelPrice 获取模型价格 // 优先级:租户独立价 > 套餐价 > 模型基础价 > 硬编码默认 func GetModelPrice(ctx context.Context, tenantID int64, modelName string) (*PricingResult, error) {