feat: record ticket assignment progress
This commit is contained in:
@@ -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 {
|
func (s *ticketService) Get(id int64) *models.Ticket {
|
||||||
return repositories.TicketRepository.Get(sqls.DB(), id)
|
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 {
|
if err := s.validateRequiredAssignee(req.ToUserID); err != nil {
|
||||||
return nil, err
|
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()
|
now := time.Now()
|
||||||
if err := repositories.TicketRepository.Updates(tx, ticket.ID, map[string]any{
|
if err := repositories.TicketRepository.Updates(tx, ticket.ID, map[string]any{
|
||||||
"current_assignee_id": req.ToUserID,
|
"current_assignee_id": req.ToUserID,
|
||||||
@@ -484,6 +518,14 @@ func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequ
|
|||||||
}); err != nil {
|
}); err != nil {
|
||||||
return nil, err
|
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{
|
return &events.TicketAssignedEvent{
|
||||||
TicketID: ticket.ID,
|
TicketID: ticket.ID,
|
||||||
FromUserID: ticket.CurrentAssigneeID,
|
FromUserID: ticket.CurrentAssigneeID,
|
||||||
|
|||||||
@@ -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) {
|
func TestTicketServiceCreateTicketRejectsMismatchedCustomerConversation(t *testing.T) {
|
||||||
setupTicketTestDB(t)
|
setupTicketTestDB(t)
|
||||||
operator := createTestOperator(t, "mismatch-operator")
|
operator := createTestOperator(t, "mismatch-operator")
|
||||||
|
|||||||
Reference in New Issue
Block a user