fix(ticket): close backend core review gaps
This commit is contained in:
@@ -47,24 +47,16 @@ func BuildTicketWithContext(item *models.Ticket, ctx *TicketBuildContext) *respo
|
|||||||
}
|
}
|
||||||
if ctx != nil && ctx.TagsByTicketID != nil {
|
if ctx != nil && ctx.TagsByTicketID != nil {
|
||||||
ret.Tags = BuildTagResponses(ctx.TagsByTicketID[item.ID])
|
ret.Tags = BuildTagResponses(ctx.TagsByTicketID[item.ID])
|
||||||
} else {
|
|
||||||
ret.Tags = BuildTagResponses(services.TicketService.GetTags(item.ID))
|
|
||||||
}
|
}
|
||||||
if item.CurrentAssigneeID > 0 {
|
if item.CurrentAssigneeID > 0 {
|
||||||
if ctx != nil && ctx.Users != nil {
|
if ctx != nil && ctx.Users != nil {
|
||||||
ret.CurrentAssigneeName = buildTicketUserDisplayName(ctx.Users[item.CurrentAssigneeID])
|
ret.CurrentAssigneeName = buildTicketUserDisplayName(ctx.Users[item.CurrentAssigneeID])
|
||||||
}
|
}
|
||||||
if ret.CurrentAssigneeName == "" {
|
|
||||||
ret.CurrentAssigneeName = buildTicketUserDisplayName(services.UserService.Get(item.CurrentAssigneeID))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
if item.CustomerID > 0 {
|
if item.CustomerID > 0 {
|
||||||
if ctx != nil && ctx.Customers != nil {
|
if ctx != nil && ctx.Customers != nil {
|
||||||
ret.Customer = BuildCustomer(ctx.Customers[item.CustomerID])
|
ret.Customer = BuildCustomer(ctx.Customers[item.CustomerID])
|
||||||
}
|
}
|
||||||
if ret.Customer == nil {
|
|
||||||
ret.Customer = BuildCustomer(services.CustomerService.Get(item.CustomerID))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
@@ -105,9 +97,6 @@ func BuildTicketProgressWithContext(item *models.TicketProgress, ctx *TicketDeta
|
|||||||
if ctx != nil && ctx.Users != nil {
|
if ctx != nil && ctx.Users != nil {
|
||||||
ret.AuthorName = buildTicketUserDisplayName(ctx.Users[item.AuthorID])
|
ret.AuthorName = buildTicketUserDisplayName(ctx.Users[item.AuthorID])
|
||||||
}
|
}
|
||||||
if ret.AuthorName == "" {
|
|
||||||
ret.AuthorName = buildTicketUserDisplayName(services.UserService.Get(item.AuthorID))
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
return ret
|
return ret
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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) {
|
func TestBuildTicketProgress(t *testing.T) {
|
||||||
now := time.Date(2026, 5, 2, 12, 30, 0, 0, time.Local)
|
now := time.Date(2026, 5, 2, 12, 30, 0, 0, time.Local)
|
||||||
progress := &models.TicketProgress{
|
progress := &models.TicketProgress{
|
||||||
|
|||||||
@@ -208,7 +208,7 @@ func (c *TicketController) PostChange_status() *web.JsonResult {
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (c *TicketController) PostAdd_progress() *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 {
|
if err != nil {
|
||||||
return web.JsonError(err)
|
return web.JsonError(err)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -73,17 +73,7 @@ var (
|
|||||||
PermissionTicketUpdate = Permission{Name: "更新工单", Code: "ticket.update", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/update", SortNo: 520}
|
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}
|
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}
|
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}
|
PermissionTicketProgress = Permission{Name: "更新工单进展", Code: "ticket.progress", Type: "api", GroupName: "ticket", Method: "POST", APIPath: "/api/dashboard/ticket/add_progress", 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}
|
|
||||||
|
|
||||||
// 通知相关权限
|
// 通知相关权限
|
||||||
PermissionNotificationView = Permission{Name: "查看通知", Code: "notification.view", Type: "api", GroupName: "notification", Method: "ANY", APIPath: "/api/dashboard/notification/list", SortNo: 680}
|
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,
|
PermissionTicketUpdate,
|
||||||
PermissionTicketAssign,
|
PermissionTicketAssign,
|
||||||
PermissionTicketChangeStatus,
|
PermissionTicketChangeStatus,
|
||||||
PermissionTicketReply,
|
PermissionTicketProgress,
|
||||||
PermissionTicketClose,
|
|
||||||
PermissionTicketReopen,
|
|
||||||
PermissionTicketResolutionCodeView,
|
|
||||||
PermissionTicketResolutionCodeCreate,
|
|
||||||
PermissionTicketResolutionCodeUpdate,
|
|
||||||
PermissionTicketResolutionCodeDelete,
|
|
||||||
PermissionTicketPriorityConfigView,
|
|
||||||
PermissionTicketPriorityConfigCreate,
|
|
||||||
PermissionTicketPriorityConfigUpdate,
|
|
||||||
PermissionTicketPriorityConfigDelete,
|
|
||||||
PermissionNotificationView,
|
PermissionNotificationView,
|
||||||
PermissionNotificationUpdate,
|
PermissionNotificationUpdate,
|
||||||
PermissionQuickReplyView,
|
PermissionQuickReplyView,
|
||||||
@@ -324,9 +304,7 @@ var RolePermissions = map[string][]Permission{
|
|||||||
PermissionPermissionView, PermissionPermissionSync,
|
PermissionPermissionView, PermissionPermissionSync,
|
||||||
PermissionSessionView, PermissionSessionRevoke,
|
PermissionSessionView, PermissionSessionRevoke,
|
||||||
PermissionConversationView, PermissionConversationAssign, PermissionConversationTransfer, PermissionConversationClose, PermissionConversationSend, PermissionConversationTag, PermissionConversationHandover, PermissionConversationRecycle, PermissionConversationLinkCustomer,
|
PermissionConversationView, PermissionConversationAssign, PermissionConversationTransfer, PermissionConversationClose, PermissionConversationSend, PermissionConversationTag, PermissionConversationHandover, PermissionConversationRecycle, PermissionConversationLinkCustomer,
|
||||||
PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketReply, PermissionTicketClose, PermissionTicketReopen,
|
PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketProgress,
|
||||||
PermissionTicketResolutionCodeView, PermissionTicketResolutionCodeCreate, PermissionTicketResolutionCodeUpdate, PermissionTicketResolutionCodeDelete,
|
|
||||||
PermissionTicketPriorityConfigView, PermissionTicketPriorityConfigCreate, PermissionTicketPriorityConfigUpdate, PermissionTicketPriorityConfigDelete,
|
|
||||||
PermissionNotificationView, PermissionNotificationUpdate,
|
PermissionNotificationView, PermissionNotificationUpdate,
|
||||||
PermissionQuickReplyView, PermissionQuickReplyCreate, PermissionQuickReplyUpdate, PermissionQuickReplyDelete,
|
PermissionQuickReplyView, PermissionQuickReplyCreate, PermissionQuickReplyUpdate, PermissionQuickReplyDelete,
|
||||||
PermissionTagView, PermissionTagCreate, PermissionTagUpdate, PermissionTagDelete,
|
PermissionTagView, PermissionTagCreate, PermissionTagUpdate, PermissionTagDelete,
|
||||||
@@ -347,9 +325,7 @@ var RolePermissions = map[string][]Permission{
|
|||||||
PermissionPermissionView,
|
PermissionPermissionView,
|
||||||
PermissionSessionView,
|
PermissionSessionView,
|
||||||
PermissionConversationView, PermissionConversationClose, PermissionConversationSend, PermissionConversationTag, PermissionConversationHandover, PermissionConversationRecycle, PermissionConversationLinkCustomer,
|
PermissionConversationView, PermissionConversationClose, PermissionConversationSend, PermissionConversationTag, PermissionConversationHandover, PermissionConversationRecycle, PermissionConversationLinkCustomer,
|
||||||
PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketReply, PermissionTicketClose, PermissionTicketReopen,
|
PermissionTicketView, PermissionTicketCreate, PermissionTicketUpdate, PermissionTicketAssign, PermissionTicketChangeStatus, PermissionTicketProgress,
|
||||||
PermissionTicketResolutionCodeView, PermissionTicketResolutionCodeCreate, PermissionTicketResolutionCodeUpdate, PermissionTicketResolutionCodeDelete,
|
|
||||||
PermissionTicketPriorityConfigView, PermissionTicketPriorityConfigCreate, PermissionTicketPriorityConfigUpdate, PermissionTicketPriorityConfigDelete,
|
|
||||||
PermissionNotificationView, PermissionNotificationUpdate,
|
PermissionNotificationView, PermissionNotificationUpdate,
|
||||||
PermissionQuickReplyView, PermissionQuickReplyCreate, PermissionQuickReplyUpdate, PermissionQuickReplyDelete,
|
PermissionQuickReplyView, PermissionQuickReplyCreate, PermissionQuickReplyUpdate, PermissionQuickReplyDelete,
|
||||||
PermissionTagView, PermissionTagCreate, PermissionTagUpdate, PermissionTagDelete,
|
PermissionTagView, PermissionTagCreate, PermissionTagUpdate, PermissionTagDelete,
|
||||||
@@ -369,8 +345,7 @@ var RolePermissions = map[string][]Permission{
|
|||||||
PermissionRoleView,
|
PermissionRoleView,
|
||||||
PermissionPermissionView,
|
PermissionPermissionView,
|
||||||
PermissionConversationView,
|
PermissionConversationView,
|
||||||
PermissionTicketView, PermissionTicketCreate, PermissionTicketReply,
|
PermissionTicketView, PermissionTicketCreate, PermissionTicketProgress,
|
||||||
PermissionTicketResolutionCodeView, PermissionTicketPriorityConfigView,
|
|
||||||
PermissionNotificationView, PermissionNotificationUpdate,
|
PermissionNotificationView, PermissionNotificationUpdate,
|
||||||
PermissionQuickReplyView,
|
PermissionQuickReplyView,
|
||||||
PermissionTagView,
|
PermissionTagView,
|
||||||
|
|||||||
@@ -462,7 +462,7 @@ func (s *ticketService) assignTicketTx(tx *gorm.DB, req request.AssignTicketRequ
|
|||||||
if ticket == nil {
|
if ticket == nil {
|
||||||
return nil, errorsx.InvalidParam("工单不存在")
|
return nil, errorsx.InvalidParam("工单不存在")
|
||||||
}
|
}
|
||||||
if err := s.validateAssignee(req.ToUserID); err != nil {
|
if err := s.validateRequiredAssignee(req.ToUserID); err != nil {
|
||||||
return nil, err
|
return nil, err
|
||||||
}
|
}
|
||||||
now := time.Now()
|
now := time.Now()
|
||||||
@@ -586,6 +586,13 @@ func (s *ticketService) validateAssignee(userID int64) error {
|
|||||||
if userID <= 0 {
|
if userID <= 0 {
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
return s.validateRequiredAssignee(userID)
|
||||||
|
}
|
||||||
|
|
||||||
|
func (s *ticketService) validateRequiredAssignee(userID int64) error {
|
||||||
|
if userID <= 0 {
|
||||||
|
return errorsx.InvalidParam("负责人不存在")
|
||||||
|
}
|
||||||
user := UserService.Get(userID)
|
user := UserService.Get(userID)
|
||||||
if user == nil || user.Status == enums.StatusDeleted {
|
if user == nil || user.Status == enums.StatusDeleted {
|
||||||
return errorsx.InvalidParam("负责人不存在")
|
return errorsx.InvalidParam("负责人不存在")
|
||||||
|
|||||||
@@ -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) {
|
func TestTicketServiceSummaryCountsStaleTickets(t *testing.T) {
|
||||||
setupTicketTestDB(t)
|
setupTicketTestDB(t)
|
||||||
operator := createTestOperator(t, "summary-operator")
|
operator := createTestOperator(t, "summary-operator")
|
||||||
|
|||||||
Reference in New Issue
Block a user