diff --git a/internal/services/ticket_service.go b/internal/services/ticket_service.go index ae846b4..6170556 100644 --- a/internal/services/ticket_service.go +++ b/internal/services/ticket_service.go @@ -64,6 +64,32 @@ func normalizeTicketStaleHours(staleHours int) int { } } +func buildTicketAssignmentProgressContent(fromUser *models.User, toUser *models.User, reason string) string { + fromName := ticketAssignmentUserDisplayName(fromUser) + if fromName == "" { + fromName = "未分配" + } + toName := ticketAssignmentUserDisplayName(toUser) + if toName == "" && toUser != nil { + toName = toUser.Username + } + content := "指派处理人:" + fromName + " -> " + toName + if trimmedReason := strings.TrimSpace(reason); trimmedReason != "" { + content += ",原因:" + trimmedReason + } + return content +} + +func ticketAssignmentUserDisplayName(user *models.User) string { + if user == nil { + return "" + } + if strings.TrimSpace(user.Nickname) != "" { + return strings.TrimSpace(user.Nickname) + } + return strings.TrimSpace(user.Username) +} + func (s *ticketService) Get(id int64) *models.Ticket { return repositories.TicketRepository.Get(sqls.DB(), id) } @@ -475,6 +501,14 @@ func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequ if err := s.validateRequiredAssignee(req.ToUserID); err != nil { return nil, err } + toUser := repositories.UserRepository.Get(tx, req.ToUserID) + if toUser == nil || toUser.Status != enums.StatusOk { + return nil, errorsx.InvalidParam("负责人不存在") + } + var fromUser *models.User + if ticket.CurrentAssigneeID > 0 { + fromUser = repositories.UserRepository.Get(tx, ticket.CurrentAssigneeID) + } now := time.Now() if err := repositories.TicketRepository.Updates(tx, ticket.ID, map[string]any{ "current_assignee_id": req.ToUserID, @@ -484,6 +518,14 @@ func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequ }); err != nil { return nil, err } + if err := repositories.TicketProgressRepository.Create(tx, &models.TicketProgress{ + TicketID: ticket.ID, + Content: buildTicketAssignmentProgressContent(fromUser, toUser, req.Reason), + AuthorID: operator.UserID, + CreatedAt: now, + }); err != nil { + return nil, err + } return &events.TicketAssignedEvent{ TicketID: ticket.ID, FromUserID: ticket.CurrentAssigneeID, diff --git a/internal/services/ticket_service_test.go b/internal/services/ticket_service_test.go index efe3abb..b2991a0 100644 --- a/internal/services/ticket_service_test.go +++ b/internal/services/ticket_service_test.go @@ -265,6 +265,47 @@ func TestTicketServiceAssignTicketRejectsDisabledUser(t *testing.T) { } } +func TestTicketServiceAssignTicketCreatesProgressEntry(t *testing.T) { + setupTicketTestDB(t) + operator := createTestOperator(t, "assign-progress-operator") + firstAssignee := createTestOperator(t, "assign-progress-first") + nextAssignee := createTestOperator(t, "assign-progress-next") + ticket, err := services.TicketService.CreateTicket(request.CreateTicketRequest{ + Title: "assign progress ticket", + Description: "assign progress description", + CurrentAssigneeID: firstAssignee.UserID, + }, operator) + if err != nil { + t.Fatalf("CreateTicket() error = %v", err) + } + + if err := services.TicketService.AssignTicket(request.AssignTicketRequest{ + TicketID: ticket.ID, + ToUserID: nextAssignee.UserID, + Reason: "需要二线继续跟进", + }, operator); err != nil { + t.Fatalf("AssignTicket() error = %v", err) + } + + progresses := services.TicketProgressService.Find(sqls.NewCnd().Eq("ticket_id", ticket.ID).Asc("id")) + if len(progresses) != 2 { + t.Fatalf("expected create progress and assignment progress, got %d: %+v", len(progresses), progresses) + } + assignmentProgress := progresses[1] + if assignmentProgress.AuthorID != operator.UserID { + t.Fatalf("expected assignment progress author %d, got %d", operator.UserID, assignmentProgress.AuthorID) + } + if !strings.Contains(assignmentProgress.Content, "指派处理人") { + t.Fatalf("expected assignment progress content to mention assignment, got %q", assignmentProgress.Content) + } + if !strings.Contains(assignmentProgress.Content, firstAssignee.Username) || !strings.Contains(assignmentProgress.Content, nextAssignee.Username) { + t.Fatalf("expected assignment progress to include assignee names, got %q", assignmentProgress.Content) + } + if !strings.Contains(assignmentProgress.Content, "需要二线继续跟进") { + t.Fatalf("expected assignment reason in progress content, got %q", assignmentProgress.Content) + } +} + func TestTicketServiceCreateTicketRejectsMismatchedCustomerConversation(t *testing.T) { setupTicketTestDB(t) operator := createTestOperator(t, "mismatch-operator")