diff --git a/internal/builders/ticket_builder.go b/internal/builders/ticket_builder.go index a6fbe48..cbc0f66 100644 --- a/internal/builders/ticket_builder.go +++ b/internal/builders/ticket_builder.go @@ -47,24 +47,16 @@ func BuildTicketWithContext(item *models.Ticket, ctx *TicketBuildContext) *respo } if ctx != nil && ctx.TagsByTicketID != nil { ret.Tags = BuildTagResponses(ctx.TagsByTicketID[item.ID]) - } else { - ret.Tags = BuildTagResponses(services.TicketService.GetTags(item.ID)) } if item.CurrentAssigneeID > 0 { if ctx != nil && ctx.Users != nil { ret.CurrentAssigneeName = buildTicketUserDisplayName(ctx.Users[item.CurrentAssigneeID]) } - if ret.CurrentAssigneeName == "" { - ret.CurrentAssigneeName = buildTicketUserDisplayName(services.UserService.Get(item.CurrentAssigneeID)) - } } if item.CustomerID > 0 { if ctx != nil && ctx.Customers != nil { ret.Customer = BuildCustomer(ctx.Customers[item.CustomerID]) } - if ret.Customer == nil { - ret.Customer = BuildCustomer(services.CustomerService.Get(item.CustomerID)) - } } return ret } @@ -105,9 +97,6 @@ func BuildTicketProgressWithContext(item *models.TicketProgress, ctx *TicketDeta if ctx != nil && ctx.Users != nil { ret.AuthorName = buildTicketUserDisplayName(ctx.Users[item.AuthorID]) } - if ret.AuthorName == "" { - ret.AuthorName = buildTicketUserDisplayName(services.UserService.Get(item.AuthorID)) - } } return ret } diff --git a/internal/builders/ticket_builder_test.go b/internal/builders/ticket_builder_test.go index 46ab069..204b3a1 100644 --- a/internal/builders/ticket_builder_test.go +++ b/internal/builders/ticket_builder_test.go @@ -58,6 +58,39 @@ func TestBuildLightweightTicket(t *testing.T) { } } +func TestBuildTicketWithoutContextLeavesOptionalLookupsEmpty(t *testing.T) { + now := time.Date(2026, 5, 2, 12, 30, 0, 0, time.Local) + ticket := &models.Ticket{ + ID: 12, + TicketNo: "TK202605020001", + Title: "登录失败", + Description: "客户反馈无法登录", + CustomerID: 3, + CurrentAssigneeID: 5, + Status: enums.TicketStatusPending, + AuditFields: models.AuditFields{ + CreateUserID: 1, + CreateUserName: "admin", + CreatedAt: now, + UpdatedAt: now, + }, + } + + out := BuildTicket(ticket) + if out == nil { + t.Fatalf("expected ticket response") + } + if out.Tags != nil { + t.Fatalf("expected tags to stay empty without context, got %+v", out.Tags) + } + if out.Customer != nil { + t.Fatalf("expected customer to stay empty without context, got %+v", out.Customer) + } + if out.CurrentAssigneeName != "" { + t.Fatalf("expected assignee name to stay empty without context, got %q", out.CurrentAssigneeName) + } +} + func TestBuildTicketProgress(t *testing.T) { now := time.Date(2026, 5, 2, 12, 30, 0, 0, time.Local) progress := &models.TicketProgress{ diff --git a/internal/controllers/dashboard/ticket_controller.go b/internal/controllers/dashboard/ticket_controller.go index b50e375..5630b34 100644 --- a/internal/controllers/dashboard/ticket_controller.go +++ b/internal/controllers/dashboard/ticket_controller.go @@ -208,7 +208,7 @@ func (c *TicketController) PostChange_status() *web.JsonResult { } func (c *TicketController) PostAdd_progress() *web.JsonResult { - operator, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionTicketUpdate) + operator, err := services.AuthService.RequirePermission(c.Ctx, constants.PermissionTicketProgress) if err != nil { return web.JsonError(err) } diff --git a/internal/pkg/constants/auth.go b/internal/pkg/constants/auth.go index ec5c8fd..453d67f 100644 --- a/internal/pkg/constants/auth.go +++ b/internal/pkg/constants/auth.go @@ -68,22 +68,12 @@ var ( PermissionConversationLinkCustomer = Permission{Name: "关联会话客户", Code: "conversation.linkCustomer", Type: "api", GroupName: "conversation", Method: "POST", APIPath: "/api/dashboard/conversation/link_customer", SortNo: 495} // 工单相关权限 - PermissionTicketView = Permission{Name: "查看工单", Code: "ticket.view", Type: "api", GroupName: "ticket", Method: "ANY", APIPath: "/api/dashboard/ticket/list", SortNo: 500} - PermissionTicketCreate = Permission{Name: "创建工单", Code: "ticket.create", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/create", SortNo: 510} - PermissionTicketUpdate = Permission{Name: "更新工单", Code: "ticket.update", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/update", SortNo: 520} - PermissionTicketAssign = Permission{Name: "指派工单", Code: "ticket.assign", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/assign", SortNo: 530} - PermissionTicketChangeStatus = Permission{Name: "变更工单状态", Code: "ticket.changeStatus", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/change_status", SortNo: 540} - PermissionTicketReply = Permission{Name: "回复工单", Code: "ticket.reply", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/reply", SortNo: 550} - PermissionTicketClose = Permission{Name: "关闭工单", Code: "ticket.close", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/close", SortNo: 560} - PermissionTicketReopen = Permission{Name: "重开工单", Code: "ticket.reopen", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/reopen", SortNo: 570} - PermissionTicketResolutionCodeView = Permission{Name: "查看工单解决码", Code: "ticketResolutionCode.view", Type: "api", GroupName: "ticketResolutionCode", Method: "ANY", APIPath: "/api/dashboard/ticket-resolution-code/list", SortNo: 580} - PermissionTicketResolutionCodeCreate = Permission{Name: "创建工单解决码", Code: "ticketResolutionCode.create", Type: "api", GroupName: "ticketResolutionCode", Method: "POST", APIPath: "/api/dashboard/ticket-resolution-code/create", SortNo: 590} - PermissionTicketResolutionCodeUpdate = Permission{Name: "更新工单解决码", Code: "ticketResolutionCode.update", Type: "api", GroupName: "ticketResolutionCode", Method: "POST", APIPath: "/api/dashboard/ticket-resolution-code/update", SortNo: 600} - PermissionTicketResolutionCodeDelete = Permission{Name: "删除工单解决码", Code: "ticketResolutionCode.delete", Type: "api", GroupName: "ticketResolutionCode", Method: "POST", APIPath: "/api/dashboard/ticket-resolution-code/delete", SortNo: 610} - PermissionTicketPriorityConfigView = Permission{Name: "查看工单优先级", Code: "ticketPriorityConfig.view", Type: "api", GroupName: "ticketPriorityConfig", Method: "ANY", APIPath: "/api/dashboard/ticket-priority-config/list", SortNo: 620} - PermissionTicketPriorityConfigCreate = Permission{Name: "创建工单优先级", Code: "ticketPriorityConfig.create", Type: "api", GroupName: "ticketPriorityConfig", Method: "POST", APIPath: "/api/dashboard/ticket-priority-config/create", SortNo: 630} - PermissionTicketPriorityConfigUpdate = Permission{Name: "更新工单优先级", Code: "ticketPriorityConfig.update", Type: "api", GroupName: "ticketPriorityConfig", Method: "POST", APIPath: "/api/dashboard/ticket-priority-config/update", SortNo: 640} - PermissionTicketPriorityConfigDelete = Permission{Name: "删除工单优先级", Code: "ticketPriorityConfig.delete", Type: "api", GroupName: "ticketPriorityConfig", Method: "POST", APIPath: "/api/dashboard/ticket-priority-config/delete", SortNo: 650} + PermissionTicketView = Permission{Name: "查看工单", Code: "ticket.view", Type: "api", GroupName: "ticket", Method: "ANY", APIPath: "/api/dashboard/ticket/list", SortNo: 500} + PermissionTicketCreate = Permission{Name: "创建工单", Code: "ticket.create", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/create", SortNo: 510} + PermissionTicketUpdate = Permission{Name: "更新工单", Code: "ticket.update", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/update", SortNo: 520} + PermissionTicketAssign = Permission{Name: "指派工单", Code: "ticket.assign", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/assign", SortNo: 530} + PermissionTicketChangeStatus = Permission{Name: "变更工单状态", Code: "ticket.changeStatus", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/change_status", SortNo: 540} + PermissionTicketProgress = Permission{Name: "更新工单进展", Code: "ticket.progress", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/add_progress", SortNo: 550} // 通知相关权限 PermissionNotificationView = Permission{Name: "查看通知", Code: "notification.view", Type: "api", GroupName: "notification", Method: "ANY", APIPath: "/api/dashboard/notification/list", SortNo: 680} @@ -214,17 +204,7 @@ var Permissions = []Permission{ PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, - PermissionTicketReply, - PermissionTicketClose, - PermissionTicketReopen, - PermissionTicketResolutionCodeView, - PermissionTicketResolutionCodeCreate, - PermissionTicketResolutionCodeUpdate, - PermissionTicketResolutionCodeDelete, - PermissionTicketPriorityConfigView, - PermissionTicketPriorityConfigCreate, - PermissionTicketPriorityConfigUpdate, - PermissionTicketPriorityConfigDelete, + PermissionTicketProgress, PermissionNotificationView, PermissionNotificationUpdate, PermissionQuickReplyView, @@ -324,9 +304,7 @@ var RolePermissions = map[string][]Permission{ PermissionPermissionView, PermissionPermissionSync, PermissionSessionView, PermissionSessionRevoke, PermissionConversationView, PermissionConversationAssign, PermissionConversationTransfer, PermissionConversationClose, PermissionConversationSend, PermissionConversationTag, PermissionConversationHandover, PermissionConversationRecycle, PermissionConversationLinkCustomer, - PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketReply, PermissionTicketClose, PermissionTicketReopen, - PermissionTicketResolutionCodeView, PermissionTicketResolutionCodeCreate, PermissionTicketResolutionCodeUpdate, PermissionTicketResolutionCodeDelete, - PermissionTicketPriorityConfigView, PermissionTicketPriorityConfigCreate, PermissionTicketPriorityConfigUpdate, PermissionTicketPriorityConfigDelete, + PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketProgress, PermissionNotificationView, PermissionNotificationUpdate, PermissionQuickReplyView, PermissionQuickReplyCreate, PermissionQuickReplyUpdate, PermissionQuickReplyDelete, PermissionTagView, PermissionTagCreate, PermissionTagUpdate, PermissionTagDelete, @@ -347,9 +325,7 @@ var RolePermissions = map[string][]Permission{ PermissionPermissionView, PermissionSessionView, PermissionConversationView, PermissionConversationClose, PermissionConversationSend, PermissionConversationTag, PermissionConversationHandover, PermissionConversationRecycle, PermissionConversationLinkCustomer, - PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketReply, PermissionTicketClose, PermissionTicketReopen, - PermissionTicketResolutionCodeView, PermissionTicketResolutionCodeCreate, PermissionTicketResolutionCodeUpdate, PermissionTicketResolutionCodeDelete, - PermissionTicketPriorityConfigView, PermissionTicketPriorityConfigCreate, PermissionTicketPriorityConfigUpdate, PermissionTicketPriorityConfigDelete, + PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketProgress, PermissionNotificationView, PermissionNotificationUpdate, PermissionQuickReplyView, PermissionQuickReplyCreate, PermissionQuickReplyUpdate, PermissionQuickReplyDelete, PermissionTagView, PermissionTagCreate, PermissionTagUpdate, PermissionTagDelete, @@ -369,8 +345,7 @@ var RolePermissions = map[string][]Permission{ PermissionRoleView, PermissionPermissionView, PermissionConversationView, - PermissionTicketView, PermissionTicketCreate, PermissionTicketReply, - PermissionTicketResolutionCodeView, PermissionTicketPriorityConfigView, + PermissionTicketView, PermissionTicketCreate, PermissionTicketProgress, PermissionNotificationView, PermissionNotificationUpdate, PermissionQuickReplyView, PermissionTagView, diff --git a/internal/services/ticket_service.go b/internal/services/ticket_service.go index 1b46c7a..386eb1e 100644 --- a/internal/services/ticket_service.go +++ b/internal/services/ticket_service.go @@ -462,7 +462,7 @@ func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequ if ticket == nil { return nil, errorsx.InvalidParam("工单不存在") } - if err := s.validateAssignee(req.ToUserID); err != nil { + if err := s.validateRequiredAssignee(req.ToUserID); err != nil { return nil, err } now := time.Now() @@ -586,6 +586,13 @@ 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.InvalidParam("负责人不存在") + } user := UserService.Get(userID) if user == nil || user.Status == enums.StatusDeleted { return errorsx.InvalidParam("负责人不存在") diff --git a/internal/services/ticket_service_test.go b/internal/services/ticket_service_test.go index fdd81ae..1b5a8a1 100644 --- a/internal/services/ticket_service_test.go +++ b/internal/services/ticket_service_test.go @@ -190,6 +190,24 @@ func TestTicketServiceAddProgressStoresContentAndAuthor(t *testing.T) { } } +func TestTicketServiceAssignTicketRequiresTargetUser(t *testing.T) { + setupTicketTestDB(t) + operator := createTestOperator(t, "assign-operator") + ticket, err := services.TicketService.CreateTicket(createTestTicketRequest("assign-ticket"), operator) + if err != nil { + t.Fatalf("CreateTicket() error = %v", err) + } + + err = services.TicketService.AssignTicket(request.AssignTicketRequest{ + TicketID: ticket.ID, + ToUserID: 0, + Reason: "invalid assignment", + }, operator) + if err == nil { + t.Fatalf("expected AssignTicket() to reject empty target user") + } +} + func TestTicketServiceSummaryCountsStaleTickets(t *testing.T) { setupTicketTestDB(t) operator := createTestOperator(t, "summary-operator")