From f2a7eb3295c20b7a80170fb4f7cf69e8266e5a92 Mon Sep 17 00:00:00 2001 From: mlogclub Date: Sun, 3 May 2026 12:44:44 +0800 Subject: [PATCH] refactor(ticket): simplify ticket number generation and remove SQLite lock --- .../runtime/executor/knowledge_guard_test.go | 130 ------------------ internal/services/ticket_no_service.go | 16 +-- internal/services/ticket_service.go | 47 +++---- internal/services/ticket_service_test.go | 12 +- 4 files changed, 27 insertions(+), 178 deletions(-) delete mode 100644 internal/ai/runtime/executor/knowledge_guard_test.go diff --git a/internal/ai/runtime/executor/knowledge_guard_test.go b/internal/ai/runtime/executor/knowledge_guard_test.go deleted file mode 100644 index 0717755..0000000 --- a/internal/ai/runtime/executor/knowledge_guard_test.go +++ /dev/null @@ -1,130 +0,0 @@ -package executor - -import ( - "strings" - "testing" - - "cs-agent/internal/ai/rag" - "cs-agent/internal/ai/runtime/internal/impl/retrievers" - "cs-agent/internal/models" - "cs-agent/internal/pkg/enums" -) - -func TestBuildKnowledgeGuardDecisionFallsBackWhenKnowledgeMisses(t *testing.T) { - agent := newKnowledgeGuardAgentFixture() - agent.FallbackMode = enums.AIAgentFallbackModeSuggestRetry - decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{ - KnowledgeBaseIDs: []int64{1}, - }) - - if decision.FallbackReply != "当前知识库里没有找到足够明确的信息,你可以换个更具体的问法再试一次。" { - t.Fatalf("unexpected fallback reply: %q", decision.FallbackReply) - } - if len(decision.Instructions) != 0 { - t.Fatalf("expected no instructions on miss, got %d", len(decision.Instructions)) - } -} - -func TestBuildKnowledgeGuardDecisionUsesAgentFallbackMessage(t *testing.T) { - agent := newKnowledgeGuardAgentFixture() - agent.FallbackMessage = "请联系人工客服" - decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{ - KnowledgeBaseIDs: []int64{1}, - }) - - if decision.FallbackReply != "请联系人工客服" { - t.Fatalf("expected agent fallback message, got %q", decision.FallbackReply) - } -} - -func TestBuildKnowledgeGuardDecisionInjectsStrictInstructionOnHit(t *testing.T) { - agent := newKnowledgeGuardAgentFixture() - decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{ - KnowledgeBaseIDs: []int64{1}, - Hits: []rag.RetrieveResult{ - {KnowledgeBaseID: 1, Score: 0.88}, - }, - ContextText: "知识库上下文", - AnswerMode: enums.KnowledgeAnswerModeStrict, - }) - - if decision.FallbackReply != "" { - t.Fatalf("expected no fallback reply on hit, got %q", decision.FallbackReply) - } - if len(decision.Instructions) != 1 { - t.Fatalf("expected one instruction, got %d", len(decision.Instructions)) - } - content := decision.Instructions[0].Content - if !strings.Contains(content, "只能依据后续提供的知识片段回答") { - t.Fatalf("unexpected strict instruction: %q", content) - } - if !strings.Contains(content, "当前知识库暂无明确信息。") { - t.Fatalf("expected fallback text in instruction, got %q", content) - } -} - -func TestBuildKnowledgeGuardDecisionFallsBackWhenHitHasNoContext(t *testing.T) { - agent := newKnowledgeGuardAgentFixture() - agent.FallbackMessage = "我暂时没有找到足够准确的信息。" - decision := buildKnowledgeGuardDecision(agent, &retrievers.KnowledgeRetrieveResult{ - KnowledgeBaseIDs: []int64{1}, - Hits: []rag.RetrieveResult{ - {KnowledgeBaseID: 1, Score: 0.88}, - }, - AnswerMode: enums.KnowledgeAnswerModeStrict, - }) - - if decision.FallbackReply != "我暂时没有找到足够准确的信息。" { - t.Fatalf("expected fallback on empty context, got %q", decision.FallbackReply) - } - if len(decision.Instructions) != 0 { - t.Fatalf("expected no instructions on empty context, got %d", len(decision.Instructions)) - } -} - -func TestBuildKnowledgeUnavailableDecisionFallsBackWhenAgentHasKnowledge(t *testing.T) { - agent := newKnowledgeGuardAgentFixture() - agent.FallbackMessage = "知识库暂时不可用。" - decision := buildKnowledgeUnavailableDecision(agent, []int64{1}) - - if decision.FallbackReply != "知识库暂时不可用。" { - t.Fatalf("expected fallback when knowledge unavailable, got %q", decision.FallbackReply) - } -} - -func TestBuildKnowledgeUnavailableDecisionSkipsWhenAgentHasNoKnowledge(t *testing.T) { - decision := buildKnowledgeUnavailableDecision(newKnowledgeGuardAgentFixture(), nil) - - if decision.FallbackReply != "" { - t.Fatalf("expected no fallback without knowledge bases, got %q", decision.FallbackReply) - } -} - -func TestResolveKnowledgeHumanSupportFallbackUsesAgentMessage(t *testing.T) { - agent := newKnowledgeGuardAgentFixture() - agent.FallbackMessage = "我暂时没有找到足够准确的信息。" - - got := resolveKnowledgeHumanSupportFallback(agent) - - want := "我暂时没有找到足够准确的信息。 建议你联系人工客服进一步确认。" - if got != want { - t.Fatalf("unexpected fallback: %q", got) - } -} - -func TestResolveKnowledgeHumanSupportFallbackUsesDefault(t *testing.T) { - agent := newKnowledgeGuardAgentFixture() - - got := resolveKnowledgeHumanSupportFallback(agent) - - want := "当前知识库暂无明确信息。 建议你联系人工客服进一步确认。" - if got != want { - t.Fatalf("unexpected fallback: %q", got) - } -} - -func newKnowledgeGuardAgentFixture() models.AIAgent { - return models.AIAgent{ - FallbackMode: enums.AIAgentFallbackModeNoAnswer, - } -} diff --git a/internal/services/ticket_no_service.go b/internal/services/ticket_no_service.go index 7979886..8fc64ce 100644 --- a/internal/services/ticket_no_service.go +++ b/internal/services/ticket_no_service.go @@ -8,6 +8,7 @@ import ( "sync" "time" + "github.com/mlogclub/simple/sqls" "gorm.io/gorm" ) @@ -21,16 +22,11 @@ type ticketNoSequenceService struct { ticketNoSQLiteMu sync.Mutex } -func (s *ticketNoSequenceService) Next(tx *gorm.DB, now time.Time) (string, error) { - if tx == nil { - return "", fmt.Errorf("ticket number transaction is required") - } - if tx.Dialector.Name() == "sqlite" { - s.ticketNoSQLiteMu.Lock() - defer s.ticketNoSQLiteMu.Unlock() - return s.nextWithRetry(tx, now) - } - return s.nextWithRetry(tx, now) +func (s *ticketNoSequenceService) Next(now time.Time) (string, error) { + s.ticketNoSQLiteMu.Lock() + defer s.ticketNoSQLiteMu.Unlock() + + return s.nextWithRetry(sqls.DB(), now) } func (s *ticketNoSequenceService) nextWithRetry(tx *gorm.DB, now time.Time) (string, error) { diff --git a/internal/services/ticket_service.go b/internal/services/ticket_service.go index 16e1ba2..ae846b4 100644 --- a/internal/services/ticket_service.go +++ b/internal/services/ticket_service.go @@ -183,7 +183,6 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator * return nil, err } - now := time.Now() ticket := &models.Ticket{ Title: title, Description: description, @@ -195,27 +194,25 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator * CurrentAssigneeID: req.CurrentAssigneeID, AuditFields: utils.BuildAuditFields(operator), } - ticket.UpdatedAt = now - if err := withSQLiteTicketCreateLock(sqls.DB(), func() error { - return sqls.WithTransaction(func(ctx *sqls.TxContext) error { - ticketNo, err := TicketNoSequenceService.nextWithRetry(ctx.Tx, now) - if err != nil { - return err - } - ticket.TicketNo = ticketNo - if err := repositories.TicketRepository.Create(ctx.Tx, ticket); err != nil { - return err - } - if err := TicketTagService.ReplaceTicketTags(ctx.Tx, ticket.ID, tagIDs, operator); err != nil { - return err - } - return repositories.TicketProgressRepository.Create(ctx.Tx, &models.TicketProgress{ - TicketID: ticket.ID, - Content: "创建工单", - AuthorID: operator.UserID, - CreatedAt: now, - }) + ticketNo, err := TicketNoSequenceService.Next(ticket.CreatedAt) + if err != nil { + return nil, err + } + + if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { + ticket.TicketNo = ticketNo + if err := repositories.TicketRepository.Create(ctx.Tx, ticket); err != nil { + return err + } + if err := TicketTagService.ReplaceTicketTags(ctx.Tx, ticket.ID, tagIDs, operator); err != nil { + return err + } + return repositories.TicketProgressRepository.Create(ctx.Tx, &models.TicketProgress{ + TicketID: ticket.ID, + Content: "创建工单", + AuthorID: operator.UserID, + CreatedAt: time.Now(), }) }); err != nil { return nil, err @@ -228,14 +225,6 @@ func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator * return s.Get(ticket.ID), nil } -func withSQLiteTicketCreateLock(db *gorm.DB, fn func() error) error { - if db != nil && db.Dialector.Name() == "sqlite" { - TicketNoSequenceService.ticketNoSQLiteMu.Lock() - defer TicketNoSequenceService.ticketNoSQLiteMu.Unlock() - } - return fn() -} - func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConversationRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) { if operator == nil { return nil, errorsx.Unauthorized("未登录或登录已过期") diff --git a/internal/services/ticket_service_test.go b/internal/services/ticket_service_test.go index b79eccf..efe3abb 100644 --- a/internal/services/ticket_service_test.go +++ b/internal/services/ticket_service_test.go @@ -424,21 +424,15 @@ func TestTicketServiceTicketNoNextConcurrent(t *testing.T) { errs := make(chan error, count) var wg sync.WaitGroup - for i := 0; i < count; i++ { + for range count { wg.Add(1) go func() { defer wg.Done() - err := sqls.WithTransaction(func(ctx *sqls.TxContext) error { - ticketNo, err := services.TicketNoSequenceService.Next(ctx.Tx, time.Now()) - if err != nil { - return err - } - results <- ticketNo - return nil - }) + ticketNo, err := services.TicketNoSequenceService.Next(time.Now()) if err != nil { errs <- err } + results <- ticketNo }() }