fix(ticket): close backend core review gaps

This commit is contained in:
mlogclub
2026-05-02 18:57:40 +08:00
parent fce0b5709d
commit a06ce7074e
6 changed files with 70 additions and 48 deletions
-11
View File
@@ -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
}
+33
View File
@@ -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{
@@ -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)
}
+10 -35
View File
@@ -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,
+8 -1
View File
@@ -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("负责人不存在")
+18
View File
@@ -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")