From 0dde11dbdff61e68def109b9a3014f1c09d74a7b Mon Sep 17 00:00:00 2001 From: qhd <1766646056@qq.com> Date: Tue, 1 Sep 2026 11:21:34 +0800 Subject: [PATCH] =?UTF-8?q?fix:=20=E9=94=99=E8=AF=AF=E8=AE=B0=E5=BF=86DAO?= =?UTF-8?q?=E5=85=A8=E5=B1=80=E5=8C=96(NoTenantId)+GetByKey=20miss?= =?UTF-8?q?=E5=A5=91=E7=BA=A6=E4=BF=AE=E5=A4=8D;=20=E5=88=86=E6=9E=90promp?= =?UTF-8?q?t=E8=A1=A5=E5=8F=AF=E9=87=8D=E8=AF=95=E7=99=BD=E5=90=8D?= =?UTF-8?q?=E5=8D=95?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Co-Authored-By: Claude Code --- dao/error_memory_dao.go | 13 +++++++++--- dao/error_memory_dao_test.go | 41 ++++++++++++++++++++++++++++++++++++ service/error_analysis.go | 6 ++++-- 3 files changed, 55 insertions(+), 5 deletions(-) create mode 100644 dao/error_memory_dao_test.go diff --git a/dao/error_memory_dao.go b/dao/error_memory_dao.go index 9b825c2..c7a7b1f 100644 --- a/dao/error_memory_dao.go +++ b/dao/error_memory_dao.go @@ -13,13 +13,19 @@ var ErrorMemory = &errorMemoryDao{} type errorMemoryDao struct{} // GetByKey 按记忆键查询(未命中返回 (nil, nil)) +// 错误记忆为全局表:NoTenantId 绕过租户过滤,跨租户共享;r.IsEmpty() 兜底 miss 契约, +// 避免对空记录 r.Struct(&res) 上浮 sql.ErrNoRows 导致调用方 fail-closed。 func (d *errorMemoryDao) GetByKey(ctx context.Context, key string) (res *entity.ErrorMemory, err error) { r, err := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory). + NoTenantId(ctx). Where(entity.ErrorMemoryCol.MemoryKey, key). One() if err != nil { return } + if r.IsEmpty() { + return nil, nil + } err = r.Struct(&res) return } @@ -27,7 +33,8 @@ func (d *errorMemoryDao) GetByKey(ctx context.Context, key string) (res *entity. // Upsert 存在则更新 retryable/reason/analyzed_by,不存在则插入 func (d *errorMemoryDao) Upsert(ctx context.Context, m *entity.ErrorMemory) (err error) { model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory) - n, err := model.Where(entity.ErrorMemoryCol.MemoryKey, m.MemoryKey).Count() + // Count 同样全局化:跨租户已存在的记忆键需命中更新分支,而非重复插入 + n, err := model.NoTenantId(ctx).Where(entity.ErrorMemoryCol.MemoryKey, m.MemoryKey).Count() if err != nil { return } @@ -43,9 +50,9 @@ func (d *errorMemoryDao) Upsert(ctx context.Context, m *entity.ErrorMemory) (err return } -// List 分页查询(按 id 倒序) +// List 分页查询(按 id 倒序);全局表,管理端列表展示所有租户记忆 func (d *errorMemoryDao) List(ctx context.Context, page, pageSize int) (list []entity.ErrorMemory, total int64, err error) { - model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory) + model := gfdb.DB(ctx, public.DbNameModelGateway).Model(ctx, public.TableNameErrorMemory).NoTenantId(ctx) n, err := model.Count() if err != nil { return diff --git a/dao/error_memory_dao_test.go b/dao/error_memory_dao_test.go new file mode 100644 index 0000000..c1c7c6c --- /dev/null +++ b/dao/error_memory_dao_test.go @@ -0,0 +1,41 @@ +package dao + +import ( + "context" + "fmt" + "testing" + "time" + + "model-gateway/consts/public" + + _ "gitea.redpowerfuture.com/red-future/common/consul" + "gitea.redpowerfuture.com/red-future/common/db/gfdb" + _ "github.com/gogf/gf/contrib/drivers/pgsql/v2" + "github.com/gogf/gf/v2/net/gtrace" +) + +// TestErrorMemoryDaoGetByKeyMiss 回归:错误记忆未命中必须返回 (nil, nil), +// 而非把空记录丢给 r.Struct(&res) 上浮 sql.ErrNoRows —— 否则 service.shouldRetryWithMemory +// 会把一切查询错误当 fail-closed,导致 分析→落库→重试 整条链路变死代码。 +// 依赖真实 PG;不可达时跳过(不在无 PG 环境硬失败)。 +func TestErrorMemoryDaoGetByKeyMiss(t *testing.T) { + ctx, span := gtrace.NewSpan(context.Background(), "TestErrorMemoryDaoGetByKeyMiss") + defer span.End() + + // 探测连接:不可用则跳过 + if _, err := gfdb.DB(ctx, public.DbNameModelGateway). + Model(ctx, public.TableNameErrorMemory). + NoTenantId(ctx). + Count(); err != nil { + t.Skipf("DB不可用,跳过: %v", err) + } + + key := fmt.Sprintf("test-miss-%d", time.Now().UnixNano()) + row, err := ErrorMemory.GetByKey(ctx, key) + if err != nil { + t.Fatalf("GetByKey 未命中不应报错: %v", err) + } + if row != nil { + t.Fatalf("GetByKey 未命中应返回 nil, got %+v", row) + } +} diff --git a/service/error_analysis.go b/service/error_analysis.go index 8a6e6bc..8d311e2 100644 --- a/service/error_analysis.go +++ b/service/error_analysis.go @@ -45,11 +45,13 @@ const analysisSystemPrompt = `你是 AI 模型网关的错误分析器。上游 ## 值得重试(retryable: true) - 限流:429、rate limit、请求过密、并发超限 -- 服务端瞬时故障:5xx、服务过载、上游临时不可用、连接抖动 +- 服务端瞬时故障:5xx、InternalServiceError、服务过载、上游临时不可用 +- 超时/取消/连接:Timeout、RequestCanceled、Error while connecting、连接抖动 +- 媒体源暂不可用(视频/音频生成类上游常见):Invalid video_url、Invalid audio track、Error while downloading、download failed —— 通常是源尚未就绪或下载瞬断,重试可成功,不要误判为永久参数错误 - 资源暂时不足:quota 暂时受限 ## 不值得重试(retryable: false) -- 请求/参数错误:400、invalid_argument、格式错误 +- 请求/参数错误:400、invalid_argument、格式错误(注意 Invalid video_url / Invalid audio track 属上类的媒体源错误,不归此类) - 鉴权失败:401、403、invalid_api_key、签名错误 - 模型不存在:404、model_not_found - 余额不足:insufficient_quota