chore: import clean Agent Desk snapshot (3/7)
This commit is contained in:
690
internal/services/ticket_service.go
Normal file
690
internal/services/ticket_service.go
Normal file
@@ -0,0 +1,690 @@
|
||||
package services
|
||||
|
||||
import (
|
||||
"context"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"agent-desk/internal/events"
|
||||
"agent-desk/internal/models"
|
||||
"agent-desk/internal/pkg/dto"
|
||||
"agent-desk/internal/pkg/dto/request"
|
||||
"agent-desk/internal/pkg/enums"
|
||||
"agent-desk/internal/pkg/errorsx"
|
||||
"agent-desk/internal/pkg/eventbus"
|
||||
"agent-desk/internal/pkg/i18nx"
|
||||
"agent-desk/internal/pkg/utils"
|
||||
"agent-desk/internal/repositories"
|
||||
|
||||
"agent-desk/internal/pkg/httpx/params"
|
||||
|
||||
"github.com/mlogclub/simple/sqls"
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
var TicketService = newTicketService()
|
||||
|
||||
func newTicketService() *ticketService {
|
||||
return &ticketService{}
|
||||
}
|
||||
|
||||
type TicketDetailAggregate struct {
|
||||
Ticket *models.Ticket
|
||||
Tags []models.Tag
|
||||
Customer *models.Customer
|
||||
Progresses []models.TicketProgress
|
||||
Users map[int64]*models.User
|
||||
}
|
||||
|
||||
type TicketSummaryAggregate struct {
|
||||
All int64
|
||||
Pending int64
|
||||
InProgress int64
|
||||
Done int64
|
||||
Unassigned int64
|
||||
Mine int64
|
||||
Stale int64
|
||||
}
|
||||
|
||||
type TicketListAggregate struct {
|
||||
List []models.Ticket
|
||||
Paging *sqls.Paging
|
||||
TagsByTicketID map[int64][]models.Tag
|
||||
Users map[int64]*models.User
|
||||
Customers map[int64]*models.Customer
|
||||
}
|
||||
|
||||
type ticketService struct {
|
||||
}
|
||||
|
||||
func normalizeTicketStaleHours(staleHours int) int {
|
||||
switch staleHours {
|
||||
case 24, 48, 168:
|
||||
return staleHours
|
||||
default:
|
||||
return 24
|
||||
}
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
|
||||
func (s *ticketService) Take(where ...any) *models.Ticket {
|
||||
return repositories.TicketRepository.Take(sqls.DB(), where...)
|
||||
}
|
||||
|
||||
func (s *ticketService) Find(cnd *sqls.Cnd) []models.Ticket {
|
||||
return repositories.TicketRepository.Find(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *ticketService) FindOne(cnd *sqls.Cnd) *models.Ticket {
|
||||
return repositories.TicketRepository.FindOne(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *ticketService) FindPageByParams(params *params.QueryParams) (list []models.Ticket, paging *sqls.Paging) {
|
||||
return repositories.TicketRepository.FindPageByParams(sqls.DB(), params)
|
||||
}
|
||||
|
||||
func (s *ticketService) FindPageByCnd(cnd *sqls.Cnd) (list []models.Ticket, paging *sqls.Paging) {
|
||||
return repositories.TicketRepository.FindPageByCnd(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *ticketService) FindPageAggregateByCnd(cnd *sqls.Cnd, _ int64) (*TicketListAggregate, error) {
|
||||
list, paging := repositories.TicketRepository.FindPageByCnd(sqls.DB(), cnd)
|
||||
return s.buildTicketListAggregate(sqls.DB(), list, paging), nil
|
||||
}
|
||||
|
||||
func (s *ticketService) ApplyStaleFilter(cnd *sqls.Cnd, staleHours int) *sqls.Cnd {
|
||||
if cnd == nil {
|
||||
cnd = sqls.NewCnd()
|
||||
}
|
||||
staleHour := normalizeTicketStaleHours(staleHours)
|
||||
return cnd.
|
||||
NotEq("status", enums.TicketStatusDone).
|
||||
Where("updated_at < ?", time.Now().Add(-time.Duration(staleHour)*time.Hour))
|
||||
}
|
||||
|
||||
func (s *ticketService) Count(cnd *sqls.Cnd) int64 {
|
||||
return repositories.TicketRepository.Count(sqls.DB(), cnd)
|
||||
}
|
||||
|
||||
func (s *ticketService) Create(t *models.Ticket) error {
|
||||
return repositories.TicketRepository.Create(sqls.DB(), t)
|
||||
}
|
||||
|
||||
func (s *ticketService) Update(t *models.Ticket) error {
|
||||
return repositories.TicketRepository.Update(sqls.DB(), t)
|
||||
}
|
||||
|
||||
func (s *ticketService) Updates(id int64, columns map[string]any) error {
|
||||
return repositories.TicketRepository.Updates(sqls.DB(), id, columns)
|
||||
}
|
||||
|
||||
func (s *ticketService) UpdateColumn(id int64, name string, value any) error {
|
||||
return repositories.TicketRepository.UpdateColumn(sqls.DB(), id, name, value)
|
||||
}
|
||||
|
||||
func (s *ticketService) Delete(id int64) {
|
||||
repositories.TicketRepository.Delete(sqls.DB(), id)
|
||||
}
|
||||
|
||||
func (s *ticketService) GetTags(ticketID int64) []models.Tag {
|
||||
if ticketID <= 0 {
|
||||
return nil
|
||||
}
|
||||
relations := TicketTagService.Find(sqls.NewCnd().Eq("ticket_id", ticketID).Asc("id"))
|
||||
if len(relations) == 0 {
|
||||
return nil
|
||||
}
|
||||
tagIDs := make([]int64, 0, len(relations))
|
||||
for i := range relations {
|
||||
tagIDs = append(tagIDs, relations[i].TagID)
|
||||
}
|
||||
tags := repositories.TagRepository.Find(sqls.DB(), sqls.NewCnd().In("id", tagIDs))
|
||||
if len(tags) <= 1 {
|
||||
return tags
|
||||
}
|
||||
tagMap := make(map[int64]models.Tag, len(tags))
|
||||
for i := range tags {
|
||||
tagMap[tags[i].ID] = tags[i]
|
||||
}
|
||||
ordered := make([]models.Tag, 0, len(relations))
|
||||
for _, tagID := range tagIDs {
|
||||
if tag, ok := tagMap[tagID]; ok {
|
||||
ordered = append(ordered, tag)
|
||||
}
|
||||
}
|
||||
return ordered
|
||||
}
|
||||
|
||||
func (s *ticketService) CreateTicket(req request.CreateTicketRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) {
|
||||
if operator == nil {
|
||||
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
title := strings.TrimSpace(req.Title)
|
||||
description := strings.TrimSpace(req.Description)
|
||||
if title == "" {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0181")
|
||||
}
|
||||
if description == "" {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0179")
|
||||
}
|
||||
source := enums.TicketSource(strings.TrimSpace(req.Source))
|
||||
if source == "" {
|
||||
source = enums.TicketSourceManual
|
||||
}
|
||||
if !enums.IsValidTicketSource(string(source)) {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0180")
|
||||
}
|
||||
if err := s.validateTicketRefs(req.CustomerID, req.ConversationID, req.CurrentAssigneeID); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
tagIDs, err := TicketTagService.ValidateTagIDs(req.TagIDs)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
ticket := &models.Ticket{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Source: source,
|
||||
Channel: strings.TrimSpace(req.Channel),
|
||||
CustomerID: req.CustomerID,
|
||||
ConversationID: req.ConversationID,
|
||||
Status: enums.TicketStatusPending,
|
||||
CurrentAssigneeID: req.CurrentAssigneeID,
|
||||
AuditFields: utils.BuildAuditFields(operator),
|
||||
}
|
||||
|
||||
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: "Created ticket",
|
||||
AuthorID: operator.UserID,
|
||||
CreatedAt: time.Now(),
|
||||
})
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
eventbus.PublishAsync(context.Background(), events.TicketCreatedEvent{
|
||||
TicketID: ticket.ID,
|
||||
OperatorID: operator.UserID,
|
||||
})
|
||||
return s.Get(ticket.ID), nil
|
||||
}
|
||||
|
||||
func (s *ticketService) CreateFromConversation(req request.CreateTicketFromConversationRequest, operator *dto.AuthPrincipal) (*models.Ticket, error) {
|
||||
if operator == nil {
|
||||
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
conversation := ConversationService.Get(req.ConversationID)
|
||||
if conversation == nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0116")
|
||||
}
|
||||
title := strings.TrimSpace(req.Title)
|
||||
if title == "" {
|
||||
title = strings.TrimSpace(ConversationService.BuildConversationSummary(conversation))
|
||||
}
|
||||
if title == "" {
|
||||
title = i18nx.Get("ticket.defaultConversationTitle")
|
||||
}
|
||||
description := strings.TrimSpace(req.Description)
|
||||
if description == "" {
|
||||
description = strings.TrimSpace(conversation.LastMessageSummary)
|
||||
}
|
||||
if description == "" {
|
||||
description = title
|
||||
}
|
||||
return s.CreateTicket(request.CreateTicketRequest{
|
||||
Title: title,
|
||||
Description: description,
|
||||
Source: string(enums.TicketSourceConversation),
|
||||
Channel: s.resolveConversationChannel(conversation),
|
||||
CustomerID: conversation.CustomerID,
|
||||
ConversationID: conversation.ID,
|
||||
TagIDs: req.TagIDs,
|
||||
CurrentAssigneeID: req.CurrentAssigneeID,
|
||||
}, operator)
|
||||
}
|
||||
|
||||
func (s *ticketService) UpdateTicket(req request.UpdateTicketRequest, operator *dto.AuthPrincipal) error {
|
||||
if operator == nil {
|
||||
return errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
title := strings.TrimSpace(req.Title)
|
||||
description := strings.TrimSpace(req.Description)
|
||||
if title == "" {
|
||||
return errorsx.InvalidParamI18n("error.e0181")
|
||||
}
|
||||
if description == "" {
|
||||
return errorsx.InvalidParamI18n("error.e0179")
|
||||
}
|
||||
ticket := s.Get(req.TicketID)
|
||||
if ticket == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0178")
|
||||
}
|
||||
if err := s.validateAssignee(req.CurrentAssigneeID); err != nil {
|
||||
return err
|
||||
}
|
||||
tagIDs, err := TicketTagService.ValidateTagIDs(req.TagIDs)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
return sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
if err := repositories.TicketRepository.Updates(ctx.Tx, ticket.ID, map[string]any{
|
||||
"title": title,
|
||||
"description": description,
|
||||
"current_assignee_id": req.CurrentAssigneeID,
|
||||
"updated_at": now,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
return TicketTagService.ReplaceTicketTags(ctx.Tx, ticket.ID, tagIDs, operator)
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ticketService) LinkCustomer(ticketID int64, customerID int64, operator *dto.AuthPrincipal) error {
|
||||
if operator == nil {
|
||||
return errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
ticket := s.Get(ticketID)
|
||||
if ticket == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0178")
|
||||
}
|
||||
if customerID <= 0 || CustomerService.Get(customerID) == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0155")
|
||||
}
|
||||
if ticket.ConversationID > 0 {
|
||||
conversation := ConversationService.Get(ticket.ConversationID)
|
||||
if conversation == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0116")
|
||||
}
|
||||
if conversation.CustomerID > 0 && conversation.CustomerID != customerID {
|
||||
return errorsx.InvalidParamI18n("error.e0118")
|
||||
}
|
||||
}
|
||||
now := time.Now()
|
||||
return repositories.TicketRepository.Updates(sqls.DB(), ticket.ID, map[string]any{
|
||||
"customer_id": customerID,
|
||||
"updated_at": now,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ticketService) AssignTicket(req request.AssignTicketRequest, operator *dto.AuthPrincipal) error {
|
||||
if operator == nil {
|
||||
return errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
var assignedEvent *events.TicketAssignedEvent
|
||||
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
event, err := s.assignTicketTx(ctx.Tx, req, operator)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
assignedEvent = event
|
||||
return nil
|
||||
}); err != nil {
|
||||
return err
|
||||
}
|
||||
if assignedEvent != nil {
|
||||
eventbus.PublishAsync(context.Background(), *assignedEvent)
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ticketService) ChangeStatus(req request.ChangeTicketStatusRequest, operator *dto.AuthPrincipal) error {
|
||||
if operator == nil {
|
||||
return errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
status := strings.TrimSpace(req.Status)
|
||||
if !enums.IsValidTicketStatus(status) {
|
||||
return errorsx.InvalidParamI18n("error.e0182")
|
||||
}
|
||||
ticket := s.Get(req.TicketID)
|
||||
if ticket == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0178")
|
||||
}
|
||||
now := time.Now()
|
||||
var handledAt *time.Time
|
||||
if enums.TicketStatus(status) == enums.TicketStatusDone {
|
||||
handledAt = &now
|
||||
}
|
||||
return repositories.TicketRepository.Updates(sqls.DB(), ticket.ID, map[string]any{
|
||||
"status": enums.TicketStatus(status),
|
||||
"handled_at": handledAt,
|
||||
"updated_at": now,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
})
|
||||
}
|
||||
|
||||
func (s *ticketService) AddProgress(req request.CreateTicketProgressRequest, operator *dto.AuthPrincipal) (*models.TicketProgress, error) {
|
||||
if operator == nil {
|
||||
return nil, errorsx.UnauthorizedI18n("error.auth.expired")
|
||||
}
|
||||
content := strings.TrimSpace(req.Content)
|
||||
if content == "" {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0148")
|
||||
}
|
||||
ticket := s.Get(req.TicketID)
|
||||
if ticket == nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0178")
|
||||
}
|
||||
now := time.Now()
|
||||
progress := &models.TicketProgress{
|
||||
TicketID: ticket.ID,
|
||||
Content: content,
|
||||
AuthorID: operator.UserID,
|
||||
CreatedAt: now,
|
||||
}
|
||||
if err := sqls.WithTransaction(func(ctx *sqls.TxContext) error {
|
||||
if err := repositories.TicketProgressRepository.Create(ctx.Tx, progress); err != nil {
|
||||
return err
|
||||
}
|
||||
return repositories.TicketRepository.Updates(ctx.Tx, ticket.ID, map[string]any{
|
||||
"updated_at": now,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
})
|
||||
}); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
return progress, nil
|
||||
}
|
||||
|
||||
func (s *ticketService) GetDetail(id int64) (*TicketDetailAggregate, error) {
|
||||
ticket := s.Get(id)
|
||||
if ticket == nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0178")
|
||||
}
|
||||
aggregate := &TicketDetailAggregate{
|
||||
Ticket: ticket,
|
||||
Tags: s.GetTags(id),
|
||||
Progresses: repositories.TicketProgressRepository.Find(sqls.DB(), sqls.NewCnd().Eq("ticket_id", id).Asc("id")),
|
||||
Users: make(map[int64]*models.User),
|
||||
}
|
||||
if ticket.CustomerID > 0 {
|
||||
aggregate.Customer = CustomerService.Get(ticket.CustomerID)
|
||||
}
|
||||
userIDs := make([]int64, 0)
|
||||
seen := make(map[int64]struct{})
|
||||
addUserID := func(userID int64) {
|
||||
if userID <= 0 {
|
||||
return
|
||||
}
|
||||
if _, ok := seen[userID]; ok {
|
||||
return
|
||||
}
|
||||
seen[userID] = struct{}{}
|
||||
userIDs = append(userIDs, userID)
|
||||
}
|
||||
addUserID(ticket.CurrentAssigneeID)
|
||||
for i := range aggregate.Progresses {
|
||||
addUserID(aggregate.Progresses[i].AuthorID)
|
||||
}
|
||||
if len(userIDs) > 0 {
|
||||
users := repositories.UserRepository.FindByIds(sqls.DB(), userIDs)
|
||||
for i := range users {
|
||||
item := users[i]
|
||||
aggregate.Users[item.ID] = &item
|
||||
}
|
||||
}
|
||||
return aggregate, nil
|
||||
}
|
||||
|
||||
func (s *ticketService) GetSummary(operator *dto.AuthPrincipal, staleHours ...int) *TicketSummaryAggregate {
|
||||
staleHour := 0
|
||||
if len(staleHours) > 0 {
|
||||
staleHour = staleHours[0]
|
||||
}
|
||||
summary := &TicketSummaryAggregate{
|
||||
All: s.Count(sqls.NewCnd()),
|
||||
Pending: s.Count(sqls.NewCnd().Eq("status", enums.TicketStatusPending)),
|
||||
InProgress: s.Count(sqls.NewCnd().Eq("status", enums.TicketStatusInProgress)),
|
||||
Done: s.Count(sqls.NewCnd().Eq("status", enums.TicketStatusDone)),
|
||||
Unassigned: s.Count(sqls.NewCnd().Eq("current_assignee_id", 0)),
|
||||
Stale: s.Count(s.ApplyStaleFilter(sqls.NewCnd(), staleHour)),
|
||||
}
|
||||
if operator != nil {
|
||||
summary.Mine = s.Count(sqls.NewCnd().Eq("current_assignee_id", operator.UserID))
|
||||
}
|
||||
return summary
|
||||
}
|
||||
|
||||
func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequest, operator *dto.AuthPrincipal) (*events.TicketAssignedEvent, error) {
|
||||
ticket := repositories.TicketRepository.Get(tx, req.TicketID)
|
||||
if ticket == nil {
|
||||
return nil, errorsx.InvalidParamI18n("error.e0178")
|
||||
}
|
||||
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.InvalidParamI18n("error.e0334")
|
||||
}
|
||||
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,
|
||||
"updated_at": now,
|
||||
"update_user_id": operator.UserID,
|
||||
"update_user_name": operator.Username,
|
||||
}); 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,
|
||||
ToUserID: req.ToUserID,
|
||||
OperatorID: operator.UserID,
|
||||
Reason: strings.TrimSpace(req.Reason),
|
||||
}, nil
|
||||
}
|
||||
|
||||
func (s *ticketService) buildTicketListAggregate(db *gorm.DB, list []models.Ticket, paging *sqls.Paging) *TicketListAggregate {
|
||||
aggregate := &TicketListAggregate{
|
||||
List: list,
|
||||
Paging: paging,
|
||||
TagsByTicketID: make(map[int64][]models.Tag),
|
||||
Users: make(map[int64]*models.User),
|
||||
Customers: make(map[int64]*models.Customer),
|
||||
}
|
||||
if len(list) == 0 {
|
||||
return aggregate
|
||||
}
|
||||
ticketIDs := make([]int64, 0, len(list))
|
||||
customerIDs := make([]int64, 0)
|
||||
userIDs := make([]int64, 0)
|
||||
ticketSeen := make(map[int64]struct{})
|
||||
customerSeen := make(map[int64]struct{})
|
||||
userSeen := make(map[int64]struct{})
|
||||
for i := range list {
|
||||
item := &list[i]
|
||||
if _, ok := ticketSeen[item.ID]; !ok {
|
||||
ticketSeen[item.ID] = struct{}{}
|
||||
ticketIDs = append(ticketIDs, item.ID)
|
||||
}
|
||||
if item.CustomerID > 0 {
|
||||
if _, ok := customerSeen[item.CustomerID]; !ok {
|
||||
customerSeen[item.CustomerID] = struct{}{}
|
||||
customerIDs = append(customerIDs, item.CustomerID)
|
||||
}
|
||||
}
|
||||
if item.CurrentAssigneeID > 0 {
|
||||
if _, ok := userSeen[item.CurrentAssigneeID]; !ok {
|
||||
userSeen[item.CurrentAssigneeID] = struct{}{}
|
||||
userIDs = append(userIDs, item.CurrentAssigneeID)
|
||||
}
|
||||
}
|
||||
}
|
||||
s.enrichTicketTags(db, aggregate, ticketIDs)
|
||||
if len(userIDs) > 0 {
|
||||
users := repositories.UserRepository.FindByIds(db, userIDs)
|
||||
for i := range users {
|
||||
item := users[i]
|
||||
aggregate.Users[item.ID] = &item
|
||||
}
|
||||
}
|
||||
if len(customerIDs) > 0 {
|
||||
customers := repositories.CustomerRepository.Find(db, sqls.NewCnd().In("id", customerIDs))
|
||||
for i := range customers {
|
||||
item := customers[i]
|
||||
aggregate.Customers[item.ID] = &item
|
||||
}
|
||||
}
|
||||
return aggregate
|
||||
}
|
||||
|
||||
func (s *ticketService) enrichTicketTags(db *gorm.DB, aggregate *TicketListAggregate, ticketIDs []int64) {
|
||||
if len(ticketIDs) == 0 {
|
||||
return
|
||||
}
|
||||
ticketTags := repositories.TicketTagRepository.Find(db, sqls.NewCnd().In("ticket_id", ticketIDs).Asc("id"))
|
||||
if len(ticketTags) == 0 {
|
||||
return
|
||||
}
|
||||
tagIDs := make([]int64, 0)
|
||||
tagSeen := make(map[int64]struct{})
|
||||
ticketTagMap := make(map[int64][]int64, len(ticketIDs))
|
||||
for i := range ticketTags {
|
||||
relation := ticketTags[i]
|
||||
ticketTagMap[relation.TicketID] = append(ticketTagMap[relation.TicketID], relation.TagID)
|
||||
if _, ok := tagSeen[relation.TagID]; !ok {
|
||||
tagSeen[relation.TagID] = struct{}{}
|
||||
tagIDs = append(tagIDs, relation.TagID)
|
||||
}
|
||||
}
|
||||
tags := repositories.TagRepository.Find(db, sqls.NewCnd().In("id", tagIDs))
|
||||
tagMap := make(map[int64]models.Tag, len(tags))
|
||||
for i := range tags {
|
||||
tagMap[tags[i].ID] = tags[i]
|
||||
}
|
||||
for ticketID, orderedTagIDs := range ticketTagMap {
|
||||
orderedTags := make([]models.Tag, 0, len(orderedTagIDs))
|
||||
for _, tagID := range orderedTagIDs {
|
||||
if tag, ok := tagMap[tagID]; ok {
|
||||
orderedTags = append(orderedTags, tag)
|
||||
}
|
||||
}
|
||||
aggregate.TagsByTicketID[ticketID] = orderedTags
|
||||
}
|
||||
}
|
||||
|
||||
func (s *ticketService) validateTicketRefs(customerID, conversationID, assigneeID int64) error {
|
||||
if customerID > 0 && CustomerService.Get(customerID) == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0155")
|
||||
}
|
||||
if conversationID > 0 {
|
||||
conversation := ConversationService.Get(conversationID)
|
||||
if conversation == nil {
|
||||
return errorsx.InvalidParamI18n("error.e0116")
|
||||
}
|
||||
if customerID > 0 && conversation.CustomerID != customerID {
|
||||
return errorsx.InvalidParamI18n("error.e0118")
|
||||
}
|
||||
}
|
||||
return s.validateAssignee(assigneeID)
|
||||
}
|
||||
|
||||
func (s *ticketService) validateAssignee(userID int64) error {
|
||||
if userID <= 0 {
|
||||
return nil
|
||||
}
|
||||
return s.validateRequiredAssignee(userID)
|
||||
}
|
||||
|
||||
func (s *ticketService) validateRequiredAssignee(userID int64) error {
|
||||
if userID <= 0 {
|
||||
return errorsx.InvalidParamI18n("error.e0334")
|
||||
}
|
||||
user := UserService.Get(userID)
|
||||
if user == nil || user.Status != enums.StatusOk {
|
||||
return errorsx.InvalidParamI18n("error.e0334")
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (s *ticketService) resolveConversationChannel(conversation *models.Conversation) string {
|
||||
if conversation == nil || conversation.ChannelID <= 0 {
|
||||
return ""
|
||||
}
|
||||
if channel := ChannelService.Get(conversation.ChannelID); channel != nil {
|
||||
return channel.ChannelType
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
func normalizeInt64IDs(ids []int64) []int64 {
|
||||
if len(ids) == 0 {
|
||||
return nil
|
||||
}
|
||||
seen := make(map[int64]struct{}, len(ids))
|
||||
result := make([]int64, 0, len(ids))
|
||||
for _, id := range ids {
|
||||
if id <= 0 {
|
||||
continue
|
||||
}
|
||||
if _, ok := seen[id]; ok {
|
||||
continue
|
||||
}
|
||||
seen[id] = struct{}{}
|
||||
result = append(result, id)
|
||||
}
|
||||
return result
|
||||
}
|
||||
Reference in New Issue
Block a user