From 758276ecfcefa2f4c019dfafde564b8ab2e66e2a Mon Sep 17 00:00:00 2001 From: zhangsz Date: Wed, 2 Jul 2025 17:22:35 +0800 Subject: [PATCH] feat: ticket enhancemnet --- config/etc/default/psap.yaml | 25 +++ config/etc/default/restconf.yaml | 7 +- features/cdr/cdrevent.go | 31 ++- features/ue/mf_callback_ticket/model.go | 24 +-- features/ue/mf_callback_ticket/service.go | 137 ++++++++++++- lib/config/config.go | 20 ++ lib/config/psap.go | 184 ++++++++++++++++++ lib/email/email.go | 82 +++++++- restagent/etc/psap.yaml | 38 ++-- restagent/etc/restconf.yaml | 5 + .../psap_ticket_monitor.go | 132 +++++++------ 11 files changed, 577 insertions(+), 108 deletions(-) create mode 100644 config/etc/default/psap.yaml create mode 100644 lib/config/psap.go diff --git a/config/etc/default/psap.yaml b/config/etc/default/psap.yaml new file mode 100644 index 00000000..75a51caf --- /dev/null +++ b/config/etc/default/psap.yaml @@ -0,0 +1,25 @@ +ticket: + notifcation: + smtp: + enabled: true + host: mail.smtp.com + port: 25 + username: smtpext@smtp.com + # 注意:密码中如果包含特殊字符(如@、#、$等), + # 需要使用双引号括起来,避免解析错误 + # 例如:password: "1000smtp@omc!" + password: 123456 + tlsSkipVerify: true + from: omc@psap + to: # 可以是多个收件人 + - admin@psap.com + - user1@psap.com + timeout: # 超时设置 + # 这些时间单位是分钟 + # 注意:这些时间是相对于工单创建时间的 + # 例如:new: 60分钟,inProgress: 60分钟 + new: 1 + inProgress: 60 + noAnswer1: 240 + noAnswer2: 480 + nearlyTimeout: 20 diff --git a/config/etc/default/restconf.yaml b/config/etc/default/restconf.yaml index 75173d72..33944f9d 100644 --- a/config/etc/default/restconf.yaml +++ b/config/etc/default/restconf.yaml @@ -186,4 +186,9 @@ params: testConfig: enabled: false - file: /usr/local/omc/etc/testconfig.yaml \ No newline at end of file + file: /usr/local/omc/etc/testconfig.yaml + +# PSAP RESTCONF配置 +psapConfig: + enabled: true + file: ./etc/psap.yaml \ No newline at end of file diff --git a/features/cdr/cdrevent.go b/features/cdr/cdrevent.go index a18bd6b5..78e8cca5 100644 --- a/features/cdr/cdrevent.go +++ b/features/cdr/cdrevent.go @@ -132,16 +132,39 @@ func PostCDREventFrom(w http.ResponseWriter, r *http.Request) { CreatedAt: time.Now().UnixMicro(), UpdatedAt: updatedAt, } - if err := mfService.InsertCallbackTicket(ticket); err != nil { + 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) // 异步发送 + // 发送邮件通知 + emailConfig := config.GetSMTPConfig() + if emailConfig != nil && emailConfig.Enabled { + // 创建配置副本,避免修改全局配置 + emailCopy := *emailConfig // 浅拷贝结构体 + + // 合并配置中的To地址和当前工单的座席邮箱 + var recipients []string + + // 添加配置中的原始收件人(如管理员、监控人员等) + if len(emailConfig.To) > 0 { + recipients = append(recipients, emailConfig.To...) + } + + // 添加当前工单的座席邮箱 + recipients = append(recipients, ticket.AgentEmail) + + // 去重处理(避免重复邮箱) + emailCopy.To = email.RemoveDuplicateEmails(recipients) + + // 设置邮件主题和内容 + emailCopy.Subject = "新工单分配通知" + emailCopy.Body = fmt.Sprintf("您被分配了一个新的回拨工单(编号:%d, 主叫号码:%s), 请及时处理.", + ticket.TicketId, ticket.CallerNumber) + go email.SendEmailWithGomail(emailCopy) // 异步发送 + } } } log.Warn("No available agents found for callback ticket") diff --git a/features/ue/mf_callback_ticket/model.go b/features/ue/mf_callback_ticket/model.go index 5376cf38..3c2e3778 100644 --- a/features/ue/mf_callback_ticket/model.go +++ b/features/ue/mf_callback_ticket/model.go @@ -98,18 +98,18 @@ type CallbackTicketQuery struct { // @Description VoLTE用户信息 type CallbackTicket struct { - TicketId int64 `json:"ticketId" gorm:"column:ticket_id"` // 工单ID - 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"` // 座席名称 - 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 - CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` // 创建时间 - UpdatedAt *int64 `json:"updatedAt" gorm:"column:updated_at;autoUpdateTime:false"` // 更新时间 + TicketId int64 `json:"ticketId" gorm:"column:ticket_id;primaryKey;autoIncrement"` // 工单ID + 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"` // 座席名称 + 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 + CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` // 创建时间 + UpdatedAt *int64 `json:"updatedAt" gorm:"column:updated_at;autoUpdateTime:false"` // 更新时间 } type AgentInfo struct { diff --git a/features/ue/mf_callback_ticket/service.go b/features/ue/mf_callback_ticket/service.go index 42e2798f..e2e3712c 100644 --- a/features/ue/mf_callback_ticket/service.go +++ b/features/ue/mf_callback_ticket/service.go @@ -4,8 +4,10 @@ import ( "fmt" "time" + "be.ems/lib/config" "be.ems/lib/dborm" "be.ems/lib/email" + "be.ems/lib/log" "gorm.io/gorm" ) @@ -97,7 +99,7 @@ func (s *CallbackTicketService) SelectCallbackTicketByPage(pageNum int, pageSize return tickets, int(total), nil } -func (s *CallbackTicketService) InsertCallbackTicket(ticket CallbackTicket) error { +func (s *CallbackTicketService) InsertCallbackTicket(ticket *CallbackTicket) error { // 判断主叫号码是否已存在未处理完的工单 var existingCount int64 if err := s.getDB().Table("mf_callback_ticket"). @@ -110,7 +112,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.getDB().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 @@ -174,7 +176,7 @@ func (s *CallbackTicketService) GetLastAssignedAgent() (string, error) { var lastTicket CallbackTicket if err := s.getDB().Table("mf_callback_ticket"). Where("agent_name <> ''"). - Order("created_at DESC"). + Order("created_at DESC, ticket_id DESC"). First(&lastTicket).Error; err != nil { if err == gorm.ErrRecordNotFound { // 没有记录属于正常情况,返回空字符串 @@ -377,14 +379,137 @@ func (s *CallbackTicketService) UpdateTicketToTimeout(ticket *CallbackTicket, or // 新工单分配后发送邮件通知 if newAgent.Email != "" { - subject := "新工单自动重建通知" - body := fmt.Sprintf("您被分配了一个自动重建的回拨工单,主叫号码:%s", newTicket.CallerNumber) - go email.SendEmail(newAgent.Email, subject, body) + // 发送邮件通知 + // 获取SMTP配置 + emailConfig := config.GetSMTPConfig() + if emailConfig != nil && emailConfig.Enabled { + // 创建配置副本,避免修改全局配置 + emailCopy := *emailConfig // 浅拷贝结构体 + + // 合并配置中的To地址和当前工单的座席邮箱 + var recipients []string + + // 添加配置中的原始收件人(如管理员、监控人员等) + if len(emailConfig.To) > 0 { + recipients = append(recipients, emailConfig.To...) + } + + // 添加当前工单的座席邮箱 + recipients = append(recipients, ticket.AgentEmail) + + // 去重处理(避免重复邮箱) + emailCopy.To = email.RemoveDuplicateEmails(recipients) + + // 设置邮件主题和内容 + emailCopy.Subject = "新工单自动重建通知" + emailCopy.Body = fmt.Sprintf("您被分配了一个自动重建的回拨工单,ID: %d, 主叫号码:%s, 请及时处理。", + newTicket.TicketId, newTicket.CallerNumber) + go email.SendEmailWithGomail(emailCopy) + } } } return nil } +// BatchUpdateTimeoutTickets 批量处理超时工单 +func (s *CallbackTicketService) BatchUpdateTimeoutTickets(tickets []CallbackTicket, originalStatus string, agents []AgentInfo) error { + if len(tickets) == 0 { + return nil + } + + // 获取当前最后分配的座席 + lastAssignedAgent, err := s.GetLastAssignedAgent() + if err != nil { + log.Errorf("获取最后分配座席失败: %v", err) + lastAssignedAgent = "" + } + + now := time.Now().UnixMicro() + var successCount int + for i, ticket := range tickets { + // 1. 更新原工单为超时 + updatedTicket := CallbackTicket{ + TicketId: ticket.TicketId, + Status: TicketStatusTimeout.Enum(), + Comment: fmt.Sprintf("%s - 工单状态为 %s 处理超时,系统自动更新为超时状态", ticket.Comment, originalStatus), + UpdatedAt: &now, + } + if err := s.getDB().Table("mf_callback_ticket"). + Where("ticket_id = ?", ticket.TicketId). + Updates(updatedTicket).Error; err != nil { + log.Errorf("更新工单 %d 状态失败: %v", ticket.TicketId, err) + continue + } + + // 2. 选择下一个座席(使用批处理中维护的lastAssignedAgent) + newAgent := s.SelectNextAgent(agents, lastAssignedAgent) + if newAgent == nil { + log.Errorf("没有可用的座席分配给工单 %d", ticket.TicketId) + continue + } + + // 3. 创建新工单 + 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 + int64(i), // 确保每个工单的创建时间不同, + UpdatedAt: nil, + } + if err := s.getDB().Table("mf_callback_ticket").Create(&newTicket).Error; err != nil { + log.Errorf("创建新工单失败: %v", err) + continue + } + + // 4. 更新最后分配的座席 + lastAssignedAgent = newAgent.Name + + // 5. 发送邮件通知 + if newAgent.Email != "" { + // 发送邮件通知 + // 获取SMTP配置 + emailConfig := config.GetSMTPConfig() + if emailConfig != nil && emailConfig.Enabled { + // 创建配置副本,避免修改全局配置 + emailCopy := *emailConfig // 浅拷贝结构体 + + // 合并配置中的To地址和当前工单的座席邮箱 + var recipients []string + + // 添加配置中的原始收件人(如管理员、监控人员等) + if len(emailConfig.To) > 0 { + recipients = append(recipients, emailConfig.To...) + } + + // 添加当前工单的座席邮箱 + recipients = append(recipients, ticket.AgentEmail) + + // 去重处理(避免重复邮箱) + emailCopy.To = email.RemoveDuplicateEmails(recipients) + + // 设置邮件主题和内容 + emailCopy.Subject = "新工单自动重建通知" + emailCopy.Body = fmt.Sprintf("您被分配了一个自动重建的回拨工单,ID: %d, 主叫号码:%s, 请及时处理。", + newTicket.TicketId, newTicket.CallerNumber) + go email.SendEmailWithGomail(emailCopy) + } + } + + successCount++ + log.Infof("工单 %d 已重建为新工单 %d,分配给座席 %s (第%d个处理)", + ticket.TicketId, newTicket.TicketId, newAgent.Name, i+1) + } + + log.Infof("批量处理完成,成功处理 %d/%d 个超时工单", successCount, len(tickets)) + return nil +} + // FindNearlyTimeoutTickets 查询即将超时的工单 func (s *CallbackTicketService) FindNearlyTimeoutTickets(status string, timeoutMicros int64, aheadMicros int64) ([]CallbackTicket, error) { nowMicros := time.Now().UnixMicro() diff --git a/lib/config/config.go b/lib/config/config.go index defbef2c..66358ba6 100644 --- a/lib/config/config.go +++ b/lib/config/config.go @@ -126,6 +126,11 @@ type YamlConfig struct { Enabled bool `yaml:"enabled"` File string `yaml:"file"` } `yaml:"testConfig"` + + PsapConfig struct { + Enabled bool `yaml:"enabled"` + File string `yaml:"file"` + } `yaml:"psapConfig"` } type RestParam struct { @@ -253,6 +258,16 @@ func NewYamlConfig() YamlConfig { } } +// InitPsapConfig 初始化PSAP配置 +func InitPsapConfig() error { + if !yamlConfig.PsapConfig.Enabled { + return nil + } + + _, err := ReadPsapConfig(yamlConfig.PsapConfig.File) + return err +} + func ReadConfig(configFile string) { YamlConfigInfo.FilePath = configFile @@ -270,6 +285,11 @@ func ReadConfig(configFile string) { } yamlConfig = YamlConfigInfo.ConfigLines + // 初始化PSAP配置 + if err := InitPsapConfig(); err != nil { + fmt.Printf("Failed to load PSAP config: %v\n", err) + } + ReadOriginalConfig(configFile) } diff --git a/lib/config/psap.go b/lib/config/psap.go new file mode 100644 index 00000000..837b00a2 --- /dev/null +++ b/lib/config/psap.go @@ -0,0 +1,184 @@ +package config + +import ( + "fmt" + "os" + + "be.ems/lib/email" + "gopkg.in/yaml.v3" +) + +// PsapConfig PSAP配置结构体 +type PsapConfig struct { + Ticket struct { + Notification struct { + SMTP email.EmailConfig `yaml:"smtp"` + } `yaml:"notifcation"` // 注意:配置文件中是 "notifcation",保持一致 + Timeout struct { + // 时间单位为分钟 + New int `yaml:"new"` // NEW状态超时时间(分钟) + InProgress int `yaml:"inProgress"` // IN_PROGRESS状态超时时间(分钟) + NoAnswer1 int `yaml:"noAnswer1"` // NO_ANSWER_1状态超时时间(分钟) + NoAnswer2 int `yaml:"noAnswer2"` // NO_ANSWER_2状态超时时间(分钟) + NearlyTimeout int `yaml:"nearlyTimeout"` // 提前提醒时间(分钟) + } `yaml:"timeout"` + } `yaml:"ticket"` +} + +var psapConfig *PsapConfig + +// ReadPsapConfig 读取PSAP配置文件 +func ReadPsapConfig(configFile string) (*PsapConfig, error) { + yamlFile, err := os.ReadFile(configFile) + if err != nil { + return nil, fmt.Errorf("read psap config file error: %w", err) + } + + var config PsapConfig + err = yaml.Unmarshal(yamlFile, &config) + if err != nil { + return nil, fmt.Errorf("unmarshal psap config error: %w", err) + } + + psapConfig = &config + return &config, nil +} + +// GetPsapConfig 获取PSAP配置 +func GetPsapConfig() *PsapConfig { + if psapConfig == nil { + // 如果配置未加载,尝试从默认位置加载 + config, err := ReadPsapConfig("./etc/psap.yaml") + if err != nil { + fmt.Printf("Failed to load PSAP config: %v\n", err) + return nil + } + psapConfig = config + } + return psapConfig +} + +// GetSMTPConfig 获取SMTP配置 +func GetSMTPConfig() *email.EmailConfig { + config := GetPsapConfig() + if config == nil { + return nil + } + return &config.Ticket.Notification.SMTP +} + +// GetTimeoutConfig 获取超时配置 +func GetTimeoutConfig() *struct { + New int `yaml:"new"` + InProgress int `yaml:"inProgress"` + NoAnswer1 int `yaml:"noAnswer1"` + NoAnswer2 int `yaml:"noAnswer2"` + NearlyTimeout int `yaml:"nearlyTimeout"` +} { + config := GetPsapConfig() + if config == nil { + return nil + } + return &config.Ticket.Timeout +} + +// 以下是具体的配置获取方法 + +// GetNewTicketTimeoutMicros 获取NEW状态超时时间(微秒) +func GetNewTicketTimeoutMicros() int64 { + config := GetTimeoutConfig() + if config == nil { + return 60 * 60 * 1000000 // 默认60分钟 + } + return int64(config.New) * 60 * 1000000 // 分钟转微秒 +} + +// GetInProgressTicketTimeoutMicros 获取IN_PROGRESS状态超时时间(微秒) +func GetInProgressTicketTimeoutMicros() int64 { + config := GetTimeoutConfig() + if config == nil { + return 60 * 60 * 1000000 // 默认60分钟 + } + return int64(config.InProgress) * 60 * 1000000 +} + +// GetNoAnswer1TicketTimeoutMicros 获取NO_ANSWER_1状态超时时间(微秒) +func GetNoAnswer1TicketTimeoutMicros() int64 { + config := GetTimeoutConfig() + if config == nil { + return 4 * 60 * 60 * 1000000 // 默认4小时 + } + return int64(config.NoAnswer1) * 60 * 1000000 +} + +// GetNoAnswer2TicketTimeoutMicros 获取NO_ANSWER_2状态超时时间(微秒) +func GetNoAnswer2TicketTimeoutMicros() int64 { + config := GetTimeoutConfig() + if config == nil { + return 8 * 60 * 60 * 1000000 // 默认8小时 + } + return int64(config.NoAnswer2) * 60 * 1000000 +} + +// GetNearlyTimeoutMicros 获取提前提醒时间(微秒) +func GetNearlyTimeoutMicros() int64 { + config := GetTimeoutConfig() + if config == nil { + return 20 * 60 * 1000000 // 默认20分钟 + } + return int64(config.NearlyTimeout) * 60 * 1000000 +} + +// IsSMTPEnabled 检查SMTP是否启用 +func IsSMTPEnabled() bool { + config := GetSMTPConfig() + if config == nil { + return false + } + return config.Enabled +} + +// GetSMTPHost 获取SMTP主机 +func GetSMTPHost() string { + config := GetSMTPConfig() + if config == nil { + return "" + } + return config.Host +} + +// GetSMTPPort 获取SMTP端口 +func GetSMTPPort() int { + config := GetSMTPConfig() + if config == nil { + return 25 + } + return config.Port +} + +// GetSMTPUser 获取SMTP用户名 +func GetSMTPUsername() string { + config := GetSMTPConfig() + if config == nil { + return "" + } + return config.Username +} + +// GetSMTPPassword 获取SMTP密码 +func GetSMTPPassword() string { + config := GetSMTPConfig() + if config == nil { + return "" + } + return config.Password +} + +// GetSMTPFrom 获取SMTP发件人 +func GetSMTPFrom() string { + config := GetSMTPConfig() + if config == nil { + return "" + } + return config.From +} diff --git a/lib/email/email.go b/lib/email/email.go index a5fbaa53..78a23d9b 100644 --- a/lib/email/email.go +++ b/lib/email/email.go @@ -1,19 +1,83 @@ package email -import "net/smtp" +import ( + "crypto/tls" + "fmt" + "net/smtp" + "strconv" + "strings" + + "gopkg.in/gomail.v2" +) + +type EmailConfig struct { + Enabled bool `yaml:"enabled"` // 是否启用邮件发送 + Host string `yaml:"host"` + Port int `yaml:"port"` + Username string `yaml:"username"` + Password string `yaml:"password"` + TLSSkipVerify bool `yaml:"tlsSkipVerify"` + From string `yaml:"from"` + To []string `yaml:"to"` + Subject string `yaml:"subject"` + Body string `yaml:"body"` +} // 简单邮件发送函数 -func SendEmail(to, subject, body string) error { - from := "your@email.com" - password := "your_password" - smtpHost := "smtp.yourserver.com" - smtpPort := "587" +// 该函数使用标准库的 smtp 包发送邮件 +// 注意:此函数不支持 TLS 加密,建议使用 gomail 包发送邮件以支持 TLS 和其他高级功能 +// gomail 包的使用示例见 SendEmailWithGomail 函数 +func SendEmail(email EmailConfig) error { + username := email.Username + from := email.From + password := email.Password + to := strings.Join(email.To, ",") // 将多个收件人用逗号连接 + subject := email.Subject + body := email.Body + smtpHost := email.Host + smtpPort := email.Port msg := "From: " + from + "\n" + "To: " + to + "\n" + "Subject: " + subject + "\n\n" + body - - auth := smtp.PlainAuth("", from, password, smtpHost) - return smtp.SendMail(smtpHost+":"+smtpPort, auth, from, []string{to}, []byte(msg)) + auth := smtp.PlainAuth(from, username, password, smtpHost) + return smtp.SendMail(smtpHost+":"+strconv.Itoa(smtpPort), auth, from, []string{to}, []byte(msg)) +} + +func SendEmailWithGomail(email EmailConfig) error { + m := gomail.NewMessage() + m.SetHeader("From", email.From) + m.SetHeader("To", email.To...) + m.SetHeader("Subject", email.Subject) + m.SetBody("text/plain", email.Body) + + d := gomail.NewDialer(email.Host, email.Port, email.Username, email.Password) + + // 配置 TLS + d.TLSConfig = &tls.Config{ + InsecureSkipVerify: email.TLSSkipVerify, + } + + // gomail 会自动处理 STARTTLS + if err := d.DialAndSend(m); err != nil { + fmt.Printf("Failed to DialAndSend:%v", err) + return err + } + return nil +} + +// RemoveDuplicateEmails 去除重复的邮箱地址 +func RemoveDuplicateEmails(emails []string) []string { + emailMap := make(map[string]struct{}) + var uniqueEmails []string + + for _, email := range emails { + if _, exists := emailMap[email]; !exists { + emailMap[email] = struct{}{} + uniqueEmails = append(uniqueEmails, email) + } + } + + return uniqueEmails } diff --git a/restagent/etc/psap.yaml b/restagent/etc/psap.yaml index 2da3b7e6..87a61a54 100644 --- a/restagent/etc/psap.yaml +++ b/restagent/etc/psap.yaml @@ -1,25 +1,25 @@ ticket: notifcation: - enabled: true - type: [smtp, sms] smtp: - host: mail.smtp.com + enabled: true + host: mail.agrandtech.com port: 25 - user: smtpext@smtp.com - password: "1000smtp@omc!" + username: smtpext@agrandtech.com + # 注意:密码中如果包含特殊字符(如@、#、$等), + # 需要使用双引号括起来,避免解析错误 + # 例如:password: "1000smtp@omc!" + password: Smtp123@agt tlsSkipVerify: true from: restagent@localhost - to: support@localhost - subject: "Ticket Notification" - body: "A new ticket has been created with ID: {{.ID}} and Subject: {{.Subject}}" - sms: - enabled: false - provider: twilio - account: "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" - token: "your_auth_token" - from: "+1234567890" - to: "+0987654321" - timeout: - create: 60 - update: 60 - close: 60 \ No newline at end of file + to: + - simonzhangsz@outlook.com # 可以是多个收件人 + - shuzone@126.com + timeout: # 超时设置 + # 这些时间单位是分钟 + # 注意:这些时间是相对于工单创建时间的 + # 例如:new: 60分钟,inProgress: 60分钟 + new: 1 + inProgress: 60 + noAnswer1: 240 + noAnswer2: 480 + nearlyTimeout: 20 diff --git a/restagent/etc/restconf.yaml b/restagent/etc/restconf.yaml index 7687c003..d8afb8db 100644 --- a/restagent/etc/restconf.yaml +++ b/restagent/etc/restconf.yaml @@ -203,3 +203,8 @@ staticFile: upload: prefix: "/upload" dir: "./upload" + +# PSAP RESTCONF配置 +psapConfig: + enabled: true + file: ./etc/psap.yaml \ No newline at end of file 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 ed24e682..410c0b06 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 @@ -7,6 +7,7 @@ import ( "strings" ueCallBackTicket "be.ems/features/ue/mf_callback_ticket" + "be.ems/lib/config" "be.ems/lib/email" "be.ems/lib/log" "be.ems/src/framework/cron" @@ -35,40 +36,40 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) { "count": s.count, } - // 处理超时的NEW状态工单 (60分钟) + // 处理超时的NEW状态工单 newTicketsUpdated, err := s.handleTimeoutTickets( ueCallBackTicket.TicketStatusNew.Enum(), - 1*60*1000000, // 1分钟(微秒) + config.GetNewTicketTimeoutMicros(), ) if err != nil { log.Errorf("处理NEW状态超时工单失败: %v", err) } result["newTicketsUpdated"] = newTicketsUpdated - // 处理超时的IN_PROGRESS状态工单 (60分钟) + // 处理超时的IN_PROGRESS状态工单 inProgressTicketsUpdated, err := s.handleTimeoutTickets( ueCallBackTicket.TicketStatusInProgress.Enum(), - 60*60*1000000, // 60分钟(微秒) + config.GetInProgressTicketTimeoutMicros(), ) if err != nil { log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err) } result["inProgressTicketsUpdated"] = inProgressTicketsUpdated - // 处理超时的NO_ANSWER_1状态工单 (4小时) + // 处理超时的NO_ANSWER_1状态工单 noAnswer1TicketsUpdated, err := s.handleTimeoutTickets( ueCallBackTicket.TicketStatusNoAnswer1.Enum(), - 4*60*60*1000000, // 4小时(微秒) + config.GetNoAnswer1TicketTimeoutMicros(), ) if err != nil { log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err) } result["noAnswer1TicketsUpdated"] = noAnswer1TicketsUpdated - // 处理超时的NO_ANSWER_2状态工单 (8小时) + // 处理超时的NO_ANSWER_2状态工单 noAnswer2TicketsUpdated, err := s.handleTimeoutTickets( ueCallBackTicket.TicketStatusNoAnswer2.Enum(), - 8*60*60*1000000, // 8小时(微秒) + config.GetNoAnswer2TicketTimeoutMicros(), ) if err != nil { log.Errorf("处理NO_ANSWER_2状态超时工单失败: %v", err) @@ -82,44 +83,44 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) { log.Infof("工单监控任务完成,共处理 %d 个超时工单", totalUpdated) - // 处理超时的NEW状态工单 (60分钟) + // 处理超时的NEW状态工单 newTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( ueCallBackTicket.TicketStatusNew.Enum(), - 60*60*1000000, // 60分钟(微秒) - 10*60*1000000, // 提前10分钟提醒(微秒) + config.GetNewTicketTimeoutMicros(), + config.GetNearlyTimeoutMicros(), ) if err != nil { log.Errorf("处理NEW状态超时工单失败: %v", err) } result["newTicketsNearlyTimeout"] = newTicketsNearlyTimeout - // 处理超时的IN_PROGRESS状态工单 (60分钟) + // 处理超时的IN_PROGRESS状态工单 inProgressTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( ueCallBackTicket.TicketStatusInProgress.Enum(), - 60*60*1000000, // 60分钟(微秒) - 10*60*1000000, // 提前10分钟提醒(微秒) + config.GetInProgressTicketTimeoutMicros(), + config.GetNearlyTimeoutMicros(), ) if err != nil { log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err) } result["inProgressTicketsNearlyTimeout"] = inProgressTicketsNearlyTimeout - // 处理超时的NO_ANSWER_1状态工单 (4小时) + // 处理超时的NO_ANSWER_1状态工单 noAnswer1TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( ueCallBackTicket.TicketStatusNoAnswer1.Enum(), - 4*60*60*1000000, // 4小时(微秒) - 10*60*1000000, // 提前10分钟提醒(微秒) + config.GetNoAnswer1TicketTimeoutMicros(), + config.GetNearlyTimeoutMicros(), ) if err != nil { log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err) } result["noAnswer1TicketsNearlyTimeout"] = noAnswer1TicketsNearlyTimeout - // 处理超时的NO_ANSWER_2状态工单 (8小时) + // 处理超时的NO_ANSWER_2状态工单 noAnswer2TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( ueCallBackTicket.TicketStatusNoAnswer2.Enum(), - 8*60*60*1000000, // 8小时(微秒) - 10*60*1000000, // 提前10分钟提醒(微秒) + config.GetNoAnswer2TicketTimeoutMicros(), + config.GetNearlyTimeoutMicros(), ) if err != nil { log.Errorf("处理NO_ANSWER_2状态即将超时工单失败: %v", err) @@ -149,43 +150,37 @@ func (s *PsapTicketMonitor) handleTimeoutTickets(status string, timeoutMicros in return 0, nil // 没有超时工单 } - // 更新超时工单状态 - var updatedCount int - for _, ticket := range tickets { - // 获取网元信息 - 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) - continue - } - defer resp.Body.Close() + // 获取网元信息 + neInfo := neService.NewNeInfo.SelectNeInfoByRmuid(tickets[0].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 0, err + } + 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) - continue - } - - if err := s.callbackTicketService.UpdateTicketToTimeout(&ticket, status, agentResp.Data); err != nil { - log.Errorf("更新工单 %d 状态失败: %v", ticket.TicketId, err) - continue - } - updatedCount++ - log.Infof("工单 %d 已更新为超时状态 (原状态: %s)", ticket.TicketId, status) + // 解析座席列表响应 + var agentResp struct { + Code int `json:"code"` + Data []ueCallBackTicket.AgentInfo `json:"data"` + Msg string `json:"msg"` } - return updatedCount, nil + if err := json.NewDecoder(resp.Body).Decode(&agentResp); err != nil { + log.Error("Failed to decode MF agents response", err) + return 0, err + } + + if err := s.callbackTicketService.BatchUpdateTimeoutTickets(tickets, status, agentResp.Data); err != nil { + log.Errorf("Faild to batch update tickets: %v", err) + return 0, err + } + + return len(tickets), nil } // handleTimeoutTickets 处理指定状态的超时工单 @@ -204,10 +199,33 @@ func (s *PsapTicketMonitor) handleNearlyTimeoutTickets(status string, timeoutMic var updatedCount int for _, ticket := range tickets { if ticket.AgentEmail != "" { - subject := "工单即将超时提醒" - body := fmt.Sprintf("您负责的回拨工单(主叫号码:%s)即将超时,请及时处理。", ticket.CallerNumber) - go email.SendEmail(ticket.AgentEmail, subject, body) + emailConfig := config.GetSMTPConfig() + if emailConfig != nil && emailConfig.Enabled { + // 创建配置副本,避免修改全局配置 + emailCopy := *emailConfig // 浅拷贝结构体 + + // 合并配置中的To地址和当前工单的座席邮箱 + var recipients []string + + // 添加配置中的原始收件人(如管理员、监控人员等) + if len(emailConfig.To) > 0 { + recipients = append(recipients, emailConfig.To...) + } + + // 添加当前工单的座席邮箱 + recipients = append(recipients, ticket.AgentEmail) + + // 去重处理(避免重复邮箱) + emailCopy.To = email.RemoveDuplicateEmails(recipients) + + // 设置邮件主题和内容 + emailCopy.Subject = "工单即将超时提醒" + emailCopy.Body = fmt.Sprintf("您负责的回拨工单(编号:%d, 主叫号码:%s)即将在(%d分钟)超时,请及时处理。", + ticket.TicketId, ticket.CallerNumber, aheadMicros/1000/1000/60) + go email.SendEmailWithGomail(emailCopy) + } } + updatedCount++ } return updatedCount, nil