From fe7e4d9c88ba5c6f9a41d97add555c9d4d72b79f Mon Sep 17 00:00:00 2001 From: zhangsz Date: Tue, 1 Jul 2025 16:48:51 +0800 Subject: [PATCH] feat: support mf ticket management --- features/cdr/cdrevent.go | 72 ++++----- features/ue/mf_callback_ticket/model.go | 6 +- features/ue/mf_callback_ticket/service.go | 147 +++++++++++------- src/modules/crontask/processor/processor.go | 2 + .../psap_ticket_monitor.go | 139 +++++++++++++++-- 5 files changed, 258 insertions(+), 108 deletions(-) diff --git a/features/cdr/cdrevent.go b/features/cdr/cdrevent.go index d39893cb..a18bd6b5 100644 --- a/features/cdr/cdrevent.go +++ b/features/cdr/cdrevent.go @@ -78,11 +78,11 @@ func PostCDREventFrom(w http.ResponseWriter, r *http.Request) { } // MF网元类型特殊处理, 未接电话的回拨工单流转处理 - if neTypeLower == "mf" && cdrEvent.CDR["agentName"] == "" { + if neTypeLower == "mf" && cdrEvent.CDR["recordType"] == "MOC" && cdrEvent.CDR["agentName"] == "" { // 发送到MF网元 // 构造网元MF的API地址 - url := fmt.Sprintf("http://%s:%d/ne/config/data?neType=%s&neId=%s¶mName=agents", - neInfo.IP, neInfo.Port, neInfo.NeType, neInfo.NeId) + url := fmt.Sprintf("http://%s:%d/api/rest/systemManagement/v1/elementType/%s/objectType/config/agents", + neInfo.IP, neInfo.Port, strings.ToLower(neInfo.NeType)) // 发送HTTP请求获取座席列表 resp, err := http.Get(url) if err != nil { @@ -107,48 +107,44 @@ func PostCDREventFrom(w http.ResponseWriter, r *http.Request) { // 调用服务获取最新一个被分配工单的座席和下一个要分配的座席 mfService := ueCallBackTicket.NewCallbackTicketService() - lastAgent, err := mfService.GetLastAssignedAgent() + lastAgentName, err := mfService.GetLastAssignedAgent() if err != nil { log.Error("Failed to get last assigned agent", err) // 可以继续执行,不返回错误 } // 选择下一个要分配的座席 - selectedAgent := mfService.SelectNextAgent(agentResp.Data, lastAgent) + selectedAgent := mfService.SelectNextAgent(agentResp.Data, lastAgentName) - // 获取分配座席的邮箱 - agentEmail := "" - for _, agent := range agentResp.Data { - if agent.Name == selectedAgent { - agentEmail = agent.Email - break + if selectedAgent != nil { + // 创建回调工单 + var updatedAt *int64 = nil + ticket := ueCallBackTicket.CallbackTicket{ + CallerNumber: cdrEvent.CDR["callerParty"].(string), + CalleeNumber: cdrEvent.CDR["calledParty"].(string), + Status: ueCallBackTicket.TicketStatusNew.Enum(), + AgentName: selectedAgent.Name, + AgentEmail: selectedAgent.Email, + AgentMobile: selectedAgent.Mobile, + Comment: "", + MsdData: cdrEvent.CDR["msdData"].(string), + RmUid: cdrEvent.RmUID, + CreatedAt: time.Now().UnixMicro(), + UpdatedAt: updatedAt, + } + if err := mfService.InsertCallbackTicket(ticket); err != nil { + log.Error("Failed to insert MF callback ticket", err) + // services.ResponseInternalServerError500ProcessError(w, err) + // return + } + // 新工单分配后发送邮件通知 + if selectedAgent.Email != "" { + subject := "新工单分配通知" + body := fmt.Sprintf("您被分配了一个新的回拨工单,主叫号码:%s", ticket.CallerNumber) + go email.SendEmail(selectedAgent.Email, subject, body) // 异步发送 } } - - // 创建回调工单 - var updatedAt *int64 = nil - ticket := ueCallBackTicket.CallbackTicket{ - CallerNumber: cdrEvent.CDR["callerParty"].(string), - CalleeNumber: cdrEvent.CDR["calledParty"].(string), - Status: ueCallBackTicket.TicketStatusNew.Enum(), - AgentName: selectedAgent, - Comment: "", - MsdData: cdrEvent.CDR["msdData"].(string), - RmUid: cdrEvent.RmUID, - CreatedAt: time.Now().UnixMicro(), - UpdatedAt: updatedAt, - } - if err := mfService.InsertCallbackTicket(ticket); err != nil { - log.Error("Failed to insert MF callback ticket", err) - // services.ResponseInternalServerError500ProcessError(w, err) - // return - } - // 新工单分配后发送邮件通知 - if agentEmail != "" { - subject := "新工单分配通知" - body := fmt.Sprintf("您被分配了一个新的回拨工单,主叫号码:%s", ticket.CallerNumber) - go email.SendEmail(agentEmail, subject, body) // 异步发送 - } + log.Warn("No available agents found for callback ticket") } // MF网元类型特殊处理, 处理座席回拨的工单流转 @@ -193,12 +189,12 @@ func PostCDREventFrom(w http.ResponseWriter, r *http.Request) { } // 获取通话信息 - answerTime, _ := cdrEvent.CDR["answerTime"].(string) + seizureTime, _ := cdrEvent.CDR["seizureTime"].(string) releaseTime, _ := cdrEvent.CDR["releaseTime"].(string) cause, _ := cdrEvent.CDR["cause"].(string) // 处理回拨结果并更新工单 - if err := mfService.ProcessCallbackResult(ticket, callDuration, answerTime, releaseTime, cause); err != nil { + if err := mfService.ProcessCallbackResult(ticket, callDuration, seizureTime, releaseTime, cause); err != nil { log.Error("Failed to process callback result", err) services.ResponseInternalServerError500ProcessError(w, err) return diff --git a/features/ue/mf_callback_ticket/model.go b/features/ue/mf_callback_ticket/model.go index 0d28fee1..5376cf38 100644 --- a/features/ue/mf_callback_ticket/model.go +++ b/features/ue/mf_callback_ticket/model.go @@ -59,7 +59,7 @@ func ParseCallTag(s string) TicketStatus { return TicketStatus(i) } // 如果转换失败,则按名称匹配(忽略大小写) - switch strings.ToLower(s) { + switch strings.ToUpper(s) { case "NULL": return TicketStatusNull case "NEW": @@ -102,7 +102,9 @@ type CallbackTicket struct { CallerNumber string `json:"callerNumber" gorm:"column:caller_number"` // 主叫号码 CalleeNumber string `json:"calleeNumber" gorm:"column:callee_number"` // 被叫号码 Status string `json:"status" gorm:"column:status"` // 工单状态 - AgentName string `json:"agentName" gorm:"column:agent_name"` // 坐席名称 + AgentName string `json:"agentName" gorm:"column:agent_name"` // 座席名称 + AgentEmail string `json:"agentEmail" gorm:"column:agent_email"` // 座席邮箱 + AgentMobile string `json:"agentMobile" gorm:"column:agent_mobile"` // 座席手机号码 Comment string `json:"comment" gorm:"column:comment"` // 工单备注 MsdData string `json:"msdData" gorm:"column:msd_data"` // MSD数据 RmUid string `json:"rmUid" gorm:"column:rm_uid"` // RM用户ID diff --git a/features/ue/mf_callback_ticket/service.go b/features/ue/mf_callback_ticket/service.go index 7235336e..42e2798f 100644 --- a/features/ue/mf_callback_ticket/service.go +++ b/features/ue/mf_callback_ticket/service.go @@ -13,10 +13,20 @@ type CallbackTicketService struct { db *gorm.DB } -// 构造函数示例 +// 构造函数改为私有初始化方法 func NewCallbackTicketService() *CallbackTicketService { - db := dborm.DefaultDB() - return &CallbackTicketService{db: db} + return &CallbackTicketService{db: nil} // 先不初始化数据库连接 +} + +// 获取数据库连接的私有方法 +func (s *CallbackTicketService) getDB() *gorm.DB { + if s.db == nil { + s.db = dborm.DefaultDB() + if s.db == nil { + panic("Cannot establish database connection") + } + } + return s.db } // SelectCallbackTicket 根据条件分页查询回调工单 @@ -24,7 +34,7 @@ func (s *CallbackTicketService) SelectCallbackTicket(query CallbackTicketQuery) var tickets []CallbackTicket var total int64 - db := s.db.Table("mf_callback_ticket") + db := s.getDB().Table("mf_callback_ticket") if query.CallerNumber != "" { db = db.Where("caller_number = ?", query.CallerNumber) @@ -71,13 +81,13 @@ func (s *CallbackTicketService) SelectCallbackTicketByPage(pageNum int, pageSize var total int64 // 统计总数 - if err := s.db.Table("mf_callback_ticket").Count(&total).Error; err != nil { + if err := s.getDB().Table("mf_callback_ticket").Count(&total).Error; err != nil { return nil, 0, fmt.Errorf("failed to count callback tickets: %w", err) } // 分页查询 offset := (pageNum - 1) * pageSize - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Limit(pageSize). Offset(offset). Find(&tickets).Error; err != nil { @@ -90,7 +100,7 @@ func (s *CallbackTicketService) SelectCallbackTicketByPage(pageNum int, pageSize func (s *CallbackTicketService) InsertCallbackTicket(ticket CallbackTicket) error { // 判断主叫号码是否已存在未处理完的工单 var existingCount int64 - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("caller_number = ? AND status IN ('NEW', 'PENDING', 'IN_PROGRESS')", ticket.CallerNumber). Count(&existingCount).Error; err != nil { return fmt.Errorf("failed to check existing tickets: %w", err) @@ -100,7 +110,7 @@ func (s *CallbackTicketService) InsertCallbackTicket(ticket CallbackTicket) erro return fmt.Errorf("caller %s already has a pending ticket", ticket.CallerNumber) } // 这里可以使用ORM或其他方式将ticket插入到数据库中 - if err := s.db.Table("mf_callback_ticket").Create(&ticket).Error; err != nil { + if err := s.getDB().Table("mf_callback_ticket").Create(&ticket).Error; err != nil { return fmt.Errorf("failed to insert callback ticket: %w", err) } return nil @@ -115,7 +125,7 @@ func (s *CallbackTicketService) InsertCallbackTicket(ticket CallbackTicket) erro // mfCallbackTicketService.SelectCallbackTicketById(12345) func (s *CallbackTicketService) SelectCallbackTicketById(ticketId int64) (*CallbackTicket, error) { var ticket CallbackTicket - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("ticket_id = ?", ticketId). First(&ticket).Error; err != nil { if err == gorm.ErrRecordNotFound { @@ -133,7 +143,7 @@ func (s *CallbackTicketService) SelectCallbackTicketById(ticketId int64) (*Callb // @example // mfCallbackTicketService.UpdateCallbackTicket(ticket) func (s *CallbackTicketService) UpdateCallbackTicket(ticket CallbackTicket) error { - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("ticket_id = ?", ticket.TicketId). Updates(ticket).Error; err != nil { return fmt.Errorf("failed to update callback ticket: %w", err) @@ -148,7 +158,7 @@ func (s *CallbackTicketService) UpdateCallbackTicket(ticket CallbackTicket) erro // @example // mfCallbackTicketService.DeleteCallbackTicket(12345) func (s *CallbackTicketService) DeleteCallbackTicket(ticketId int64) error { - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("ticket_id = ?", ticketId). Delete(&CallbackTicket{}).Error; err != nil { return fmt.Errorf("failed to delete callback ticket: %w", err) @@ -162,7 +172,7 @@ func (s *CallbackTicketService) DeleteCallbackTicket(ticketId int64) error { // @return error 错误信息 func (s *CallbackTicketService) GetLastAssignedAgent() (string, error) { var lastTicket CallbackTicket - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("agent_name <> ''"). Order("created_at DESC"). First(&lastTicket).Error; err != nil { @@ -181,17 +191,15 @@ func (s *CallbackTicketService) GetLastAssignedAgent() (string, error) { // @param agents 座席列表 // @param lastAgentName 上一个座席名称 // @return string 下一个座席名称 -func (s *CallbackTicketService) SelectNextAgent(agents []AgentInfo, lastAgentName string) string { +func (s *CallbackTicketService) SelectNextAgent(agents []AgentInfo, lastAgentName string) *AgentInfo { if len(agents) == 0 { - return "" + return nil } - // 默认选第一个座席 - selectedAgent := agents[0].Name - + var firstAgent *AgentInfo = &agents[0] // 如果没有上一个座席,直接返回第一个 if lastAgentName == "" { - return selectedAgent + return firstAgent } // 找到上一个座席的下一个 @@ -199,19 +207,19 @@ func (s *CallbackTicketService) SelectNextAgent(agents []AgentInfo, lastAgentNam for i, agent := range agents { if foundLastAgent { // 找到上一个座席的下一个 - return agent.Name + return &agent } if agent.Name == lastAgentName { foundLastAgent = true // 如果是最后一个座席,则循环回第一个 if i == len(agents)-1 { - return agents[0].Name + return firstAgent } } } // 如果没找到上一个座席(可能被删除了),使用第一个座席 - return selectedAgent + return firstAgent } // FindCallbackTicketByAgentAndCaller 通过座席号码和主叫号码查找符合条件的工单 @@ -223,7 +231,7 @@ func (s *CallbackTicketService) SelectNextAgent(agents []AgentInfo, lastAgentNam func (s *CallbackTicketService) FindCallbackTicketByAgentAndCaller(agentName string, callerNumber string) (*CallbackTicket, error) { var ticket CallbackTicket - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("agent_name = ? AND caller_number = ? AND status IN ('NEW', 'PENDING', 'IN_PROGRESS', 'NO_ANSWER_1', 'NO_ANSWER_2')", agentName, callerNumber). Order("created_at DESC"). @@ -245,7 +253,7 @@ func (s *CallbackTicketService) FindCallbackTicketByAgentAndCaller(agentName str // @param releaseTime 释放时间 // @param cause 释放原因 // @return error 错误信息 -func (s *CallbackTicketService) ProcessCallbackResult(ticket *CallbackTicket, callDuration float64, answerTime string, releaseTime string, cause string) error { +func (s *CallbackTicketService) ProcessCallbackResult(ticket *CallbackTicket, callDuration float64, seizureTime string, releaseTime string, cause string) error { if ticket == nil { return fmt.Errorf("ticket cannot be nil") } @@ -258,7 +266,7 @@ func (s *CallbackTicketService) ProcessCallbackResult(ticket *CallbackTicket, ca } // 构建评论内容 - comment := fmt.Sprintf("回拨时间: %s, 释放时间: %s, 原因: %s", answerTime, releaseTime, cause) + comment := fmt.Sprintf("回拨时间: %s, 释放时间: %s, 原因: %s", seizureTime, releaseTime, cause) // 根据通话时长判断处理结果 if callDuration > 0 { @@ -288,7 +296,7 @@ func (s *CallbackTicketService) ProcessCallbackResult(ticket *CallbackTicket, ca } // 更新工单 - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("ticket_id = ?", ticket.TicketId). Updates(updatedTicket).Error; err != nil { return fmt.Errorf("failed to update callback ticket: %w", err) @@ -310,7 +318,7 @@ func (s *CallbackTicketService) FindTimeoutTickets(status string, timeoutMicros // 查询超时的工单 var tickets []CallbackTicket - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("status = ? AND created_at < ? AND (updated_at IS NULL OR updated_at < ?)", status, timeoutBeforeMicros, timeoutBeforeMicros). Find(&tickets).Error; err != nil { @@ -338,7 +346,7 @@ func (s *CallbackTicketService) UpdateTicketToTimeout(ticket *CallbackTicket, or Comment: fmt.Sprintf("%s - 工单状态为 %s 处理超时,系统自动更新为超时状态", ticket.Comment, originalStatus), UpdatedAt: &now, } - if err := s.db.Table("mf_callback_ticket"). + if err := s.getDB().Table("mf_callback_ticket"). Where("ticket_id = ?", ticket.TicketId). Updates(updatedTicket).Error; err != nil { return fmt.Errorf("更新工单 %d 状态失败: %w", ticket.TicketId, err) @@ -348,37 +356,62 @@ func (s *CallbackTicketService) UpdateTicketToTimeout(ticket *CallbackTicket, or lastAgent := ticket.AgentName newAgent := s.SelectNextAgent(agents, lastAgent) - // 查找新座席邮箱 - newAgentEmail := "" - for _, agent := range agents { - if agent.Name == newAgent { - newAgentEmail = agent.Email - break + // 3. 创建新工单 + if newAgent != nil { + newTicket := CallbackTicket{ + CallerNumber: ticket.CallerNumber, + CalleeNumber: ticket.CalleeNumber, + Status: TicketStatusNew.Enum(), + AgentName: newAgent.Name, + AgentEmail: newAgent.Email, + AgentMobile: newAgent.Mobile, + Comment: fmt.Sprintf("由超时工单 %d 自动重建", ticket.TicketId), + MsdData: ticket.MsdData, + RmUid: ticket.RmUid, + CreatedAt: now, + UpdatedAt: nil, + } + if err := s.getDB().Table("mf_callback_ticket").Create(&newTicket).Error; err != nil { + return fmt.Errorf("创建新工单失败: %w", err) + } + + // 新工单分配后发送邮件通知 + if newAgent.Email != "" { + subject := "新工单自动重建通知" + body := fmt.Sprintf("您被分配了一个自动重建的回拨工单,主叫号码:%s", newTicket.CallerNumber) + go email.SendEmail(newAgent.Email, subject, body) } } - - // 3. 创建新工单 - newTicket := CallbackTicket{ - CallerNumber: ticket.CallerNumber, - CalleeNumber: ticket.CalleeNumber, - Status: TicketStatusNew.Enum(), - AgentName: newAgent, - Comment: fmt.Sprintf("由超时工单 %d 自动重建", ticket.TicketId), - MsdData: ticket.MsdData, - RmUid: ticket.RmUid, - CreatedAt: now, - UpdatedAt: nil, - } - if err := s.db.Table("mf_callback_ticket").Create(&newTicket).Error; err != nil { - return fmt.Errorf("创建新工单失败: %w", err) - } - - // 新工单分配后发送邮件通知 - if newAgentEmail != "" { - subject := "新工单自动重建通知" - body := fmt.Sprintf("您被分配了一个自动重建的回拨工单,主叫号码:%s", newTicket.CallerNumber) - go email.SendEmail(newAgentEmail, subject, body) - } - + return nil +} + +// FindNearlyTimeoutTickets 查询即将超时的工单 +func (s *CallbackTicketService) FindNearlyTimeoutTickets(status string, timeoutMicros int64, aheadMicros int64) ([]CallbackTicket, error) { + nowMicros := time.Now().UnixMicro() + timeoutBeforeMicros := nowMicros - timeoutMicros + nearlyTimeoutBeforeMicros := nowMicros - (timeoutMicros - aheadMicros) + + var tickets []CallbackTicket + if err := s.getDB().Table("mf_callback_ticket"). + Where("status = ? AND created_at >= ? AND created_at < ? AND (updated_at IS NULL OR updated_at < ?)", + status, timeoutBeforeMicros, nearlyTimeoutBeforeMicros, nearlyTimeoutBeforeMicros). + Find(&tickets).Error; err != nil { + return nil, fmt.Errorf("查询即将超时工单失败: %w", err) + } + return tickets, nil +} + +// 新增方法:坐席开始处理工单 +func (s *CallbackTicketService) StartProcessingTicket(ticketId int64) error { + now := time.Now().UnixMicro() + if err := s.getDB().Table("mf_callback_ticket"). + Where("ticket_id = ? AND status = ?", ticketId, TicketStatusNew.Enum()). + Updates(map[string]interface{}{ + "status": TicketStatusInProgress.Enum(), + "updated_at": now, + "comment": "坐席开始处理工单", + }).Error; err != nil { + return fmt.Errorf("failed to update ticket to IN_PROGRESS: %w", err) + } return nil } diff --git a/src/modules/crontask/processor/processor.go b/src/modules/crontask/processor/processor.go index 9e41b045..39c52bb4 100644 --- a/src/modules/crontask/processor/processor.go +++ b/src/modules/crontask/processor/processor.go @@ -11,6 +11,7 @@ import ( processorMonitorSysResource "be.ems/src/modules/crontask/processor/monitor_sys_resource" processorNeConfigBackup "be.ems/src/modules/crontask/processor/ne_config_backup" processorNeDataUDM "be.ems/src/modules/crontask/processor/ne_data_udm" + "be.ems/src/modules/crontask/processor/psap_ticket_monitor" "be.ems/src/modules/crontask/processor/removeFile" ) @@ -30,4 +31,5 @@ func InitCronQueue() { cron.CreateQueue("genNeStateAlarm", genNeStateAlarm.NewProcessor) cron.CreateQueue("exportTable", exportTable.NewProcessor) cron.CreateQueue("removeFile", removeFile.NewProcessor) + cron.CreateQueue("psap_ticket_monitor", psap_ticket_monitor.NewProcessor) } diff --git a/src/modules/crontask/processor/psap_ticket_monitor/psap_ticket_monitor.go b/src/modules/crontask/processor/psap_ticket_monitor/psap_ticket_monitor.go index 04afd7f9..ed24e682 100644 --- a/src/modules/crontask/processor/psap_ticket_monitor/psap_ticket_monitor.go +++ b/src/modules/crontask/processor/psap_ticket_monitor/psap_ticket_monitor.go @@ -4,11 +4,12 @@ import ( "encoding/json" "fmt" "net/http" + "strings" ueCallBackTicket "be.ems/features/ue/mf_callback_ticket" + "be.ems/lib/email" "be.ems/lib/log" "be.ems/src/framework/cron" - "be.ems/src/framework/logger" neService "be.ems/src/modules/network_element/service" ) @@ -27,7 +28,7 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) { s.count++ // 执行次数加一 options := data.(cron.JobData) sysJob := options.SysJob - logger.Infof("执行工单监控任务 %v 任务ID %s", options.Repeat, sysJob.JobID) + log.Infof("执行工单监控任务 %v 任务ID %s", options.Repeat, sysJob.JobID) // 返回结果,用于记录执行结果 result := map[string]any{ @@ -37,10 +38,10 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) { // 处理超时的NEW状态工单 (60分钟) newTicketsUpdated, err := s.handleTimeoutTickets( ueCallBackTicket.TicketStatusNew.Enum(), - 60*1000000, // 60分钟(微秒) + 1*60*1000000, // 1分钟(微秒) ) if err != nil { - logger.Errorf("处理NEW状态超时工单失败: %v", err) + log.Errorf("处理NEW状态超时工单失败: %v", err) } result["newTicketsUpdated"] = newTicketsUpdated @@ -50,7 +51,7 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) { 60*60*1000000, // 60分钟(微秒) ) if err != nil { - logger.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err) + log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err) } result["inProgressTicketsUpdated"] = inProgressTicketsUpdated @@ -60,7 +61,7 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) { 4*60*60*1000000, // 4小时(微秒) ) if err != nil { - logger.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err) + log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err) } result["noAnswer1TicketsUpdated"] = noAnswer1TicketsUpdated @@ -70,15 +71,69 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) { 8*60*60*1000000, // 8小时(微秒) ) if err != nil { - logger.Errorf("处理NO_ANSWER_2状态超时工单失败: %v", err) + log.Errorf("处理NO_ANSWER_2状态超时工单失败: %v", err) } result["noAnswer2TicketsUpdated"] = noAnswer2TicketsUpdated // 汇总结果 - totalUpdated := newTicketsUpdated + inProgressTicketsUpdated + noAnswer1TicketsUpdated + noAnswer2TicketsUpdated + totalUpdated := newTicketsUpdated + inProgressTicketsUpdated + + noAnswer1TicketsUpdated + noAnswer2TicketsUpdated result["totalUpdated"] = totalUpdated - logger.Infof("工单监控任务完成,共处理 %d 个超时工单", totalUpdated) + log.Infof("工单监控任务完成,共处理 %d 个超时工单", totalUpdated) + + // 处理超时的NEW状态工单 (60分钟) + newTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( + ueCallBackTicket.TicketStatusNew.Enum(), + 60*60*1000000, // 60分钟(微秒) + 10*60*1000000, // 提前10分钟提醒(微秒) + ) + if err != nil { + log.Errorf("处理NEW状态超时工单失败: %v", err) + } + result["newTicketsNearlyTimeout"] = newTicketsNearlyTimeout + + // 处理超时的IN_PROGRESS状态工单 (60分钟) + inProgressTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( + ueCallBackTicket.TicketStatusInProgress.Enum(), + 60*60*1000000, // 60分钟(微秒) + 10*60*1000000, // 提前10分钟提醒(微秒) + ) + if err != nil { + log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err) + } + result["inProgressTicketsNearlyTimeout"] = inProgressTicketsNearlyTimeout + + // 处理超时的NO_ANSWER_1状态工单 (4小时) + noAnswer1TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( + ueCallBackTicket.TicketStatusNoAnswer1.Enum(), + 4*60*60*1000000, // 4小时(微秒) + 10*60*1000000, // 提前10分钟提醒(微秒) + ) + if err != nil { + log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err) + } + result["noAnswer1TicketsNearlyTimeout"] = noAnswer1TicketsNearlyTimeout + + // 处理超时的NO_ANSWER_2状态工单 (8小时) + noAnswer2TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( + ueCallBackTicket.TicketStatusNoAnswer2.Enum(), + 8*60*60*1000000, // 8小时(微秒) + 10*60*1000000, // 提前10分钟提醒(微秒) + ) + if err != nil { + log.Errorf("处理NO_ANSWER_2状态即将超时工单失败: %v", err) + } + result["noAnswer2TicketsNearlyTimeout"] = noAnswer2TicketsNearlyTimeout + + // 汇总结果 + totalTicketsNearlyTimeout := newTicketsNearlyTimeout + inProgressTicketsNearlyTimeout + + noAnswer1TicketsNearlyTimeout + noAnswer2TicketsNearlyTimeout + + result["totalTicketsNearlyTimeout"] = totalTicketsNearlyTimeout + + log.Infof("工单监控任务完成,共处理 %d 个即将超时工单", totalUpdated) + return result, nil } @@ -100,8 +155,8 @@ func (s *PsapTicketMonitor) handleTimeoutTickets(status string, timeoutMicros in // 获取网元信息 neInfo := neService.NewNeInfo.SelectNeInfoByRmuid(ticket.RmUid) // 构造网元MF的API地址 - url := fmt.Sprintf("http://%s:%d/ne/config/data?neType=%s&neId=%s¶mName=agents", - neInfo.IP, neInfo.Port, neInfo.NeType, neInfo.NeId) + url := fmt.Sprintf("http://%s:%d/api/rest/systemManagement/v1/elementType/%s/objectType/config/agents", + neInfo.IP, neInfo.Port, strings.ToLower(neInfo.NeType)) // 发送HTTP请求获取座席列表 resp, err := http.Get(url) if err != nil { @@ -132,3 +187,65 @@ func (s *PsapTicketMonitor) handleTimeoutTickets(status string, timeoutMicros in return updatedCount, nil } + +// handleTimeoutTickets 处理指定状态的超时工单 +func (s *PsapTicketMonitor) handleNearlyTimeoutTickets(status string, timeoutMicros int64, aheadMicros int64) (int, error) { + // 查询超时的工单 + tickets, err := s.callbackTicketService.FindNearlyTimeoutTickets(status, timeoutMicros, aheadMicros) + if err != nil { + return 0, err + } + + if len(tickets) == 0 { + return 0, nil // 没有即将超时工单 + } + + // 更新超时工单状态 + var updatedCount int + for _, ticket := range tickets { + if ticket.AgentEmail != "" { + subject := "工单即将超时提醒" + body := fmt.Sprintf("您负责的回拨工单(主叫号码:%s)即将超时,请及时处理。", ticket.CallerNumber) + go email.SendEmail(ticket.AgentEmail, subject, body) + } + } + + return updatedCount, nil +} + +func (s *PsapTicketMonitor) GetAgentEmail(ticket ueCallBackTicket.CallbackTicket) string { + // 获取网元信息 + neInfo := neService.NewNeInfo.SelectNeInfoByRmuid(ticket.RmUid) + // 构造网元MF的API地址 + url := fmt.Sprintf("http://%s:%d/api/rest/systemManagement/v1/elementType/%s/objectType/config/agents", + neInfo.IP, neInfo.Port, strings.ToLower(neInfo.NeType)) + // 发送HTTP请求获取座席列表 + resp, err := http.Get(url) + if err != nil { + log.Error("Failed to get MF agents", err) + return "" + } + defer resp.Body.Close() + + // 解析座席列表响应 + var agentResp struct { + Code int `json:"code"` + Data []ueCallBackTicket.AgentInfo `json:"data"` + Msg string `json:"msg"` + } + + if err := json.NewDecoder(resp.Body).Decode(&agentResp); err != nil { + log.Error("Failed to decode MF agents response", err) + return "" + } + + // 查找当前工单分配的座席邮箱 + agentEmail := "" + for _, agent := range agentResp.Data { + if agent.Name == ticket.AgentName { + agentEmail = agent.Email + break + } + } + return agentEmail +}