Files

327 lines
9.5 KiB
Go

package admin
import (
"context"
"github.com/gogf/gf/v2/errors/gerror"
"github.com/gogf/gf/v2/os/gtime"
v1 "github.com/qianfree/team-api/api/admin/v1"
"github.com/qianfree/team-api/internal/dao"
"github.com/qianfree/team-api/internal/logic/common"
do "github.com/qianfree/team-api/internal/model/do"
"github.com/qianfree/team-api/internal/model/entity"
)
// ListAllTickets 获取全部工单列表(管理后台)
func (s *sAdmin) ListAllTickets(ctx context.Context, req *v1.TicketListReq) (*v1.TicketListRes, error) {
page, pageSize := common.NormalizePagination(req.Page, req.PageSize)
query := dao.SptTickets.Ctx(ctx)
if req.Status != "" {
query = query.Where("spt_tickets.status", req.Status)
}
if req.Category != "" {
query = query.Where("spt_tickets.category", req.Category)
}
if req.TenantID > 0 {
query = query.Where("spt_tickets.tenant_id", req.TenantID)
}
if req.AssignedAdminID > 0 {
query = query.Where("spt_tickets.assigned_admin_id", req.AssignedAdminID)
}
var total int
type ticketRow struct {
Id int64 `json:"id" orm:"id"`
TenantId int64 `json:"tenant_id" orm:"tenant_id"`
UserId int64 `json:"user_id" orm:"user_id"`
Category string `json:"category" orm:"category"`
Title string `json:"title" orm:"title"`
Description string `json:"description" orm:"description"`
Urgency string `json:"urgency" orm:"urgency"`
Status string `json:"status" orm:"status"`
AssignedAdminId int64 `json:"assigned_admin_id" orm:"assigned_admin_id"`
CreatedAt *gtime.Time `json:"created_at" orm:"created_at"`
UpdatedAt *gtime.Time `json:"updated_at" orm:"updated_at"`
TenantName string `json:"tenant_name" orm:"tenant_name"`
UserDisplayName string `json:"user_display_name" orm:"user_display_name"`
AssignedAdminName string `json:"assigned_admin_name" orm:"assigned_admin_name"`
}
rows := make([]ticketRow, 0)
err := query.
LeftJoin("tnt_tenants tt", "spt_tickets.tenant_id = tt.id").
LeftJoin("tnt_users tu", "spt_tickets.user_id = tu.id").
LeftJoin("sys_admin_users sa", "spt_tickets.assigned_admin_id = sa.id").
Fields("spt_tickets.*, COALESCE(tt.name, '') as tenant_name, COALESCE(tu.display_name, '') as user_display_name, COALESCE(sa.display_name, '') as assigned_admin_name").
OrderDesc("spt_tickets.created_at").
Page(page, pageSize).
ScanAndCount(&rows, &total, false)
if err != nil {
return nil, err
}
items := make([]*v1.TicketItem, 0, len(rows))
for _, r := range rows {
items = append(items, &v1.TicketItem{
Id: r.Id,
TenantId: r.TenantId,
UserId: r.UserId,
Category: r.Category,
Title: r.Title,
Description: r.Description,
Urgency: r.Urgency,
Status: r.Status,
AssignedAdminId: r.AssignedAdminId,
TenantName: r.TenantName,
UserDisplayName: r.UserDisplayName,
AssignedAdminName: r.AssignedAdminName,
CreatedAt: r.CreatedAt,
UpdatedAt: r.UpdatedAt,
})
}
return &v1.TicketListRes{
List: items,
Total: total,
Page: page,
PageSize: pageSize,
}, nil
}
// GetTicketAdmin 获取工单详情(管理后台,含回复)
func (s *sAdmin) GetTicketAdmin(ctx context.Context, req *v1.TicketGetReq) (*v1.TicketGetRes, error) {
type ticketDetail struct {
entity.SptTickets
TenantName string `json:"tenant_name"`
UserDisplayName string `json:"user_display_name"`
AssignedAdminName string `json:"assigned_admin_name"`
}
var ticket *ticketDetail
err := dao.SptTickets.Ctx(ctx).
LeftJoin("tnt_tenants tt", "spt_tickets.tenant_id = tt.id").
LeftJoin("tnt_users tu", "spt_tickets.user_id = tu.id").
LeftJoin("sys_admin_users sa", "spt_tickets.assigned_admin_id = sa.id").
Fields("spt_tickets.*, COALESCE(tt.name, '') as tenant_name, COALESCE(tu.display_name, '') as user_display_name, COALESCE(sa.display_name, '') as assigned_admin_name").
Where("spt_tickets.id", req.Id).
Scan(&ticket)
if err != nil {
return nil, err
}
if ticket == nil {
return nil, common.NewNotFoundError("工单")
}
type replyRow struct {
entity.SptReplies
UserName string `json:"user_name" orm:"user_name"`
}
var replyRows []*replyRow
err = dao.SptReplies.Ctx(ctx).
LeftJoin("sys_admin_users sa", "spt_replies.user_type = 'admin' AND spt_replies.user_id = sa.id").
LeftJoin("tnt_users tu", "spt_replies.user_type = 'tenant' AND spt_replies.user_id = tu.id").
Fields("spt_replies.*, CASE WHEN spt_replies.user_type = 'admin' THEN COALESCE(sa.display_name, '') ELSE COALESCE(tu.display_name, '') END as user_name").
Where("spt_replies.ticket_id", req.Id).
OrderAsc("spt_replies.created_at").
Scan(&replyRows)
if err != nil {
return nil, err
}
replies := make([]*v1.TicketReplyItem, 0, len(replyRows))
for _, r := range replyRows {
replies = append(replies, &v1.TicketReplyItem{
Id: r.Id,
TicketId: r.TicketId,
UserId: r.UserId,
UserType: r.UserType,
UserName: r.UserName,
Content: r.Content,
CreatedAt: r.CreatedAt,
})
}
var attachRows []*entity.SptAttachments
err = dao.SptAttachments.Ctx(ctx).
Where("ticket_id", req.Id).
OrderAsc("created_at").
Scan(&attachRows)
if err != nil {
return nil, err
}
attachments := make([]*v1.TicketAttachmentItem, 0, len(attachRows))
for _, a := range attachRows {
attachments = append(attachments, &v1.TicketAttachmentItem{
Id: a.Id,
TicketId: a.TicketId,
ReplyId: a.ReplyId,
FileName: a.FileName,
FileUrl: a.FileUrl,
FileSize: a.FileSize,
ContentType: a.ContentType,
CreatedAt: a.CreatedAt,
})
}
return &v1.TicketGetRes{
Id: ticket.Id,
TenantId: ticket.TenantId,
UserId: ticket.UserId,
Category: ticket.Category,
Title: ticket.Title,
Description: ticket.Description,
Urgency: ticket.Urgency,
Status: ticket.Status,
AssignedAdminId: ticket.AssignedAdminId,
TenantName: ticket.TenantName,
UserDisplayName: ticket.UserDisplayName,
AssignedAdminName: ticket.AssignedAdminName,
CreatedAt: ticket.CreatedAt,
UpdatedAt: ticket.UpdatedAt,
Replies: replies,
Attachments: attachments,
}, nil
}
// AssignTicket 分配工单给管理员
func (s *sAdmin) AssignTicket(ctx context.Context, req *v1.TicketAssignReq) (*v1.TicketAssignRes, error) {
var ticket *struct {
Status string `json:"status"`
}
err := dao.SptTickets.Ctx(ctx).
Where("id", req.Id).
Fields("status").
Scan(&ticket)
if err != nil {
return nil, err
}
if ticket == nil {
return nil, common.NewNotFoundError("工单")
}
newStatus := ticket.Status
if ticket.Status == "pending" {
newStatus = "processing"
}
_, err = dao.SptTickets.Ctx(ctx).
Where("id", req.Id).
Data(do.SptTickets{
AssignedAdminId: req.AdminID,
Status: newStatus,
}).Update()
if err != nil {
return nil, err
}
return nil, nil
}
// ReplyToTicketAdmin 管理员回复工单
func (s *sAdmin) ReplyToTicketAdmin(ctx context.Context, req *v1.TicketReplyReq) (*v1.TicketReplyRes, error) {
if req.Content == "" {
return nil, common.NewBadRequestError("回复内容不能为空")
}
adminID := common.GetCtxUserID(ctx)
var ticket *struct {
Status string `json:"status"`
AssignedAdminID *int64 `json:"assigned_admin_id"`
}
err := dao.SptTickets.Ctx(ctx).
Where("id", req.Id).
Fields("status, assigned_admin_id").
Scan(&ticket)
if err != nil {
return nil, err
}
if ticket == nil {
return nil, common.NewNotFoundError("工单")
}
if ticket.Status == "closed" {
return nil, common.NewBadRequestError("工单已关闭,无法回复")
}
_, err = dao.SptReplies.Ctx(ctx).Insert(do.SptReplies{
TicketId: req.Id,
UserId: adminID,
UserType: "admin",
Content: req.Content,
})
if err != nil {
return nil, err
}
data := do.SptTickets{
Status: "replied",
}
if ticket.AssignedAdminID == nil {
data.AssignedAdminId = adminID
}
_, err = dao.SptTickets.Ctx(ctx).
Where("id", req.Id).
Data(data).Update()
return nil, err
}
// UpdateTicketStatus 更新工单状态
func (s *sAdmin) UpdateTicketStatus(ctx context.Context, req *v1.TicketStatusUpdateReq) (*v1.TicketStatusUpdateRes, error) {
validStatuses := map[string]bool{
"pending": true, "processing": true, "replied": true,
"closed": true, "reopened": true,
}
if !validStatuses[req.Status] {
return nil, common.NewBadRequestError("无效的工单状态")
}
var ticket *struct {
Status string `json:"status"`
}
err := dao.SptTickets.Ctx(ctx).
Where("id", req.Id).
Fields("status").
Scan(&ticket)
if err != nil {
return nil, err
}
if ticket == nil {
return nil, common.NewNotFoundError("工单")
}
validTransitions := map[string][]string{
"pending": {"processing", "closed"},
"processing": {"replied", "closed"},
"replied": {"processing", "closed"},
"closed": {"reopened"},
"reopened": {"processing", "closed"},
}
allowed, ok := validTransitions[ticket.Status]
if !ok {
return nil, common.NewBadRequestError("当前状态不允许变更")
}
valid := false
for _, st := range allowed {
if st == req.Status {
valid = true
break
}
}
if !valid {
return nil, gerror.Newf("不允许从 %s 变更为 %s", ticket.Status, req.Status)
}
_, err = dao.SptTickets.Ctx(ctx).
Where("id", req.Id).
Data(do.SptTickets{
Status: req.Status,
}).Update()
if err != nil {
return nil, err
}
return nil, nil
}