feat: ticket enhancemnet

This commit is contained in:
zhangsz
2025-07-02 17:22:35 +08:00
parent 24ed4e874a
commit 758276ecfc
11 changed files with 577 additions and 108 deletions

View File

@@ -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

View File

@@ -186,4 +186,9 @@ params:
testConfig: testConfig:
enabled: false enabled: false
file: /usr/local/omc/etc/testconfig.yaml file: /usr/local/omc/etc/testconfig.yaml
# PSAP RESTCONF配置
psapConfig:
enabled: true
file: ./etc/psap.yaml

View File

@@ -132,16 +132,39 @@ func PostCDREventFrom(w http.ResponseWriter, r *http.Request) {
CreatedAt: time.Now().UnixMicro(), CreatedAt: time.Now().UnixMicro(),
UpdatedAt: updatedAt, 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) log.Error("Failed to insert MF callback ticket", err)
// services.ResponseInternalServerError500ProcessError(w, err) // services.ResponseInternalServerError500ProcessError(w, err)
// return // return
} }
// 新工单分配后发送邮件通知 // 新工单分配后发送邮件通知
if selectedAgent.Email != "" { if selectedAgent.Email != "" {
subject := "新工单分配通知" // 发送邮件通知
body := fmt.Sprintf("您被分配了一个新的回拨工单,主叫号码:%s", ticket.CallerNumber) emailConfig := config.GetSMTPConfig()
go email.SendEmail(selectedAgent.Email, subject, body) // 异步发送 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") log.Warn("No available agents found for callback ticket")

View File

@@ -98,18 +98,18 @@ type CallbackTicketQuery struct {
// @Description VoLTE用户信息 // @Description VoLTE用户信息
type CallbackTicket struct { type CallbackTicket struct {
TicketId int64 `json:"ticketId" gorm:"column:ticket_id"` // 工单ID TicketId int64 `json:"ticketId" gorm:"column:ticket_id;primaryKey;autoIncrement"` // 工单ID
CallerNumber string `json:"callerNumber" gorm:"column:caller_number"` // 主叫号码 CallerNumber string `json:"callerNumber" gorm:"column:caller_number"` // 主叫号码
CalleeNumber string `json:"calleeNumber" gorm:"column:callee_number"` // 被叫号码 CalleeNumber string `json:"calleeNumber" gorm:"column:callee_number"` // 被叫号码
Status string `json:"status" gorm:"column:status"` // 工单状态 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"` // 座席邮箱 AgentEmail string `json:"agentEmail" gorm:"column:agent_email"` // 座席邮箱
AgentMobile string `json:"agentMobile" gorm:"column:agent_mobile"` // 座席手机号码 AgentMobile string `json:"agentMobile" gorm:"column:agent_mobile"` // 座席手机号码
Comment string `json:"comment" gorm:"column:comment"` // 工单备注 Comment string `json:"comment" gorm:"column:comment"` // 工单备注
MsdData string `json:"msdData" gorm:"column:msd_data"` // MSD数据 MsdData string `json:"msdData" gorm:"column:msd_data"` // MSD数据
RmUid string `json:"rmUid" gorm:"column:rm_uid"` // RM用户ID RmUid string `json:"rmUid" gorm:"column:rm_uid"` // RM用户ID
CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` // 创建时间 CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` // 创建时间
UpdatedAt *int64 `json:"updatedAt" gorm:"column:updated_at;autoUpdateTime:false"` // 更新时间 UpdatedAt *int64 `json:"updatedAt" gorm:"column:updated_at;autoUpdateTime:false"` // 更新时间
} }
type AgentInfo struct { type AgentInfo struct {

View File

@@ -4,8 +4,10 @@ import (
"fmt" "fmt"
"time" "time"
"be.ems/lib/config"
"be.ems/lib/dborm" "be.ems/lib/dborm"
"be.ems/lib/email" "be.ems/lib/email"
"be.ems/lib/log"
"gorm.io/gorm" "gorm.io/gorm"
) )
@@ -97,7 +99,7 @@ func (s *CallbackTicketService) SelectCallbackTicketByPage(pageNum int, pageSize
return tickets, int(total), nil return tickets, int(total), nil
} }
func (s *CallbackTicketService) InsertCallbackTicket(ticket CallbackTicket) error { func (s *CallbackTicketService) InsertCallbackTicket(ticket *CallbackTicket) error {
// 判断主叫号码是否已存在未处理完的工单 // 判断主叫号码是否已存在未处理完的工单
var existingCount int64 var existingCount int64
if err := s.getDB().Table("mf_callback_ticket"). 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) return fmt.Errorf("caller %s already has a pending ticket", ticket.CallerNumber)
} }
// 这里可以使用ORM或其他方式将ticket插入到数据库中 // 这里可以使用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 fmt.Errorf("failed to insert callback ticket: %w", err)
} }
return nil return nil
@@ -174,7 +176,7 @@ func (s *CallbackTicketService) GetLastAssignedAgent() (string, error) {
var lastTicket CallbackTicket var lastTicket CallbackTicket
if err := s.getDB().Table("mf_callback_ticket"). if err := s.getDB().Table("mf_callback_ticket").
Where("agent_name <> ''"). Where("agent_name <> ''").
Order("created_at DESC"). Order("created_at DESC, ticket_id DESC").
First(&lastTicket).Error; err != nil { First(&lastTicket).Error; err != nil {
if err == gorm.ErrRecordNotFound { if err == gorm.ErrRecordNotFound {
// 没有记录属于正常情况,返回空字符串 // 没有记录属于正常情况,返回空字符串
@@ -377,14 +379,137 @@ func (s *CallbackTicketService) UpdateTicketToTimeout(ticket *CallbackTicket, or
// 新工单分配后发送邮件通知 // 新工单分配后发送邮件通知
if newAgent.Email != "" { if newAgent.Email != "" {
subject := "新工单自动重建通知" // 发送邮件通知
body := fmt.Sprintf("您被分配了一个自动重建的回拨工单,主叫号码:%s", newTicket.CallerNumber) // 获取SMTP配置
go email.SendEmail(newAgent.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("您被分配了一个自动重建的回拨工单ID: %d, 主叫号码:%s, 请及时处理。",
newTicket.TicketId, newTicket.CallerNumber)
go email.SendEmailWithGomail(emailCopy)
}
} }
} }
return nil 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 查询即将超时的工单 // FindNearlyTimeoutTickets 查询即将超时的工单
func (s *CallbackTicketService) FindNearlyTimeoutTickets(status string, timeoutMicros int64, aheadMicros int64) ([]CallbackTicket, error) { func (s *CallbackTicketService) FindNearlyTimeoutTickets(status string, timeoutMicros int64, aheadMicros int64) ([]CallbackTicket, error) {
nowMicros := time.Now().UnixMicro() nowMicros := time.Now().UnixMicro()

View File

@@ -126,6 +126,11 @@ type YamlConfig struct {
Enabled bool `yaml:"enabled"` Enabled bool `yaml:"enabled"`
File string `yaml:"file"` File string `yaml:"file"`
} `yaml:"testConfig"` } `yaml:"testConfig"`
PsapConfig struct {
Enabled bool `yaml:"enabled"`
File string `yaml:"file"`
} `yaml:"psapConfig"`
} }
type RestParam struct { 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) { func ReadConfig(configFile string) {
YamlConfigInfo.FilePath = configFile YamlConfigInfo.FilePath = configFile
@@ -270,6 +285,11 @@ func ReadConfig(configFile string) {
} }
yamlConfig = YamlConfigInfo.ConfigLines yamlConfig = YamlConfigInfo.ConfigLines
// 初始化PSAP配置
if err := InitPsapConfig(); err != nil {
fmt.Printf("Failed to load PSAP config: %v\n", err)
}
ReadOriginalConfig(configFile) ReadOriginalConfig(configFile)
} }

184
lib/config/psap.go Normal file
View File

@@ -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
}

View File

@@ -1,19 +1,83 @@
package email 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 { // 该函数使用标准库的 smtp 包发送邮件
from := "your@email.com" // 注意:此函数不支持 TLS 加密,建议使用 gomail 包发送邮件以支持 TLS 和其他高级功能
password := "your_password" // gomail 包的使用示例见 SendEmailWithGomail 函数
smtpHost := "smtp.yourserver.com" func SendEmail(email EmailConfig) error {
smtpPort := "587" 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" + msg := "From: " + from + "\n" +
"To: " + to + "\n" + "To: " + to + "\n" +
"Subject: " + subject + "\n\n" + "Subject: " + subject + "\n\n" +
body body
auth := smtp.PlainAuth(from, username, password, smtpHost)
auth := smtp.PlainAuth("", from, password, smtpHost) return smtp.SendMail(smtpHost+":"+strconv.Itoa(smtpPort), auth, from, []string{to}, []byte(msg))
return smtp.SendMail(smtpHost+":"+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
} }

View File

@@ -1,25 +1,25 @@
ticket: ticket:
notifcation: notifcation:
enabled: true
type: [smtp, sms]
smtp: smtp:
host: mail.smtp.com enabled: true
host: mail.agrandtech.com
port: 25 port: 25
user: smtpext@smtp.com username: smtpext@agrandtech.com
password: "1000smtp@omc!" # 注意:密码中如果包含特殊字符(如@、#、$等),
# 需要使用双引号括起来,避免解析错误
# 例如password: "1000smtp@omc!"
password: Smtp123@agt
tlsSkipVerify: true tlsSkipVerify: true
from: restagent@localhost from: restagent@localhost
to: support@localhost to:
subject: "Ticket Notification" - simonzhangsz@outlook.com # 可以是多个收件人
body: "A new ticket has been created with ID: {{.ID}} and Subject: {{.Subject}}" - shuzone@126.com
sms: timeout: # 超时设置
enabled: false # 这些时间单位是分钟
provider: twilio # 注意:这些时间是相对于工单创建时间的
account: "ACXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXXX" # 例如new: 60分钟inProgress: 60分钟
token: "your_auth_token" new: 1
from: "+1234567890" inProgress: 60
to: "+0987654321" noAnswer1: 240
timeout: noAnswer2: 480
create: 60 nearlyTimeout: 20
update: 60
close: 60

View File

@@ -203,3 +203,8 @@ staticFile:
upload: upload:
prefix: "/upload" prefix: "/upload"
dir: "./upload" dir: "./upload"
# PSAP RESTCONF配置
psapConfig:
enabled: true
file: ./etc/psap.yaml

View File

@@ -7,6 +7,7 @@ import (
"strings" "strings"
ueCallBackTicket "be.ems/features/ue/mf_callback_ticket" ueCallBackTicket "be.ems/features/ue/mf_callback_ticket"
"be.ems/lib/config"
"be.ems/lib/email" "be.ems/lib/email"
"be.ems/lib/log" "be.ems/lib/log"
"be.ems/src/framework/cron" "be.ems/src/framework/cron"
@@ -35,40 +36,40 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) {
"count": s.count, "count": s.count,
} }
// 处理超时的NEW状态工单 (60分钟) // 处理超时的NEW状态工单
newTicketsUpdated, err := s.handleTimeoutTickets( newTicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusNew.Enum(), ueCallBackTicket.TicketStatusNew.Enum(),
1*60*1000000, // 1分钟(微秒) config.GetNewTicketTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理NEW状态超时工单失败: %v", err) log.Errorf("处理NEW状态超时工单失败: %v", err)
} }
result["newTicketsUpdated"] = newTicketsUpdated result["newTicketsUpdated"] = newTicketsUpdated
// 处理超时的IN_PROGRESS状态工单 (60分钟) // 处理超时的IN_PROGRESS状态工单
inProgressTicketsUpdated, err := s.handleTimeoutTickets( inProgressTicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusInProgress.Enum(), ueCallBackTicket.TicketStatusInProgress.Enum(),
60*60*1000000, // 60分钟(微秒) config.GetInProgressTicketTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err) log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err)
} }
result["inProgressTicketsUpdated"] = inProgressTicketsUpdated result["inProgressTicketsUpdated"] = inProgressTicketsUpdated
// 处理超时的NO_ANSWER_1状态工单 (4小时) // 处理超时的NO_ANSWER_1状态工单
noAnswer1TicketsUpdated, err := s.handleTimeoutTickets( noAnswer1TicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusNoAnswer1.Enum(), ueCallBackTicket.TicketStatusNoAnswer1.Enum(),
4*60*60*1000000, // 4小时(微秒) config.GetNoAnswer1TicketTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err) log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err)
} }
result["noAnswer1TicketsUpdated"] = noAnswer1TicketsUpdated result["noAnswer1TicketsUpdated"] = noAnswer1TicketsUpdated
// 处理超时的NO_ANSWER_2状态工单 (8小时) // 处理超时的NO_ANSWER_2状态工单
noAnswer2TicketsUpdated, err := s.handleTimeoutTickets( noAnswer2TicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusNoAnswer2.Enum(), ueCallBackTicket.TicketStatusNoAnswer2.Enum(),
8*60*60*1000000, // 8小时(微秒) config.GetNoAnswer2TicketTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理NO_ANSWER_2状态超时工单失败: %v", err) log.Errorf("处理NO_ANSWER_2状态超时工单失败: %v", err)
@@ -82,44 +83,44 @@ func (s *PsapTicketMonitor) Execute(data any) (any, error) {
log.Infof("工单监控任务完成,共处理 %d 个超时工单", totalUpdated) log.Infof("工单监控任务完成,共处理 %d 个超时工单", totalUpdated)
// 处理超时的NEW状态工单 (60分钟) // 处理超时的NEW状态工单
newTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( newTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets(
ueCallBackTicket.TicketStatusNew.Enum(), ueCallBackTicket.TicketStatusNew.Enum(),
60*60*1000000, // 60分钟(微秒) config.GetNewTicketTimeoutMicros(),
10*60*1000000, // 提前10分钟提醒(微秒) config.GetNearlyTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理NEW状态超时工单失败: %v", err) log.Errorf("处理NEW状态超时工单失败: %v", err)
} }
result["newTicketsNearlyTimeout"] = newTicketsNearlyTimeout result["newTicketsNearlyTimeout"] = newTicketsNearlyTimeout
// 处理超时的IN_PROGRESS状态工单 (60分钟) // 处理超时的IN_PROGRESS状态工单
inProgressTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( inProgressTicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets(
ueCallBackTicket.TicketStatusInProgress.Enum(), ueCallBackTicket.TicketStatusInProgress.Enum(),
60*60*1000000, // 60分钟(微秒) config.GetInProgressTicketTimeoutMicros(),
10*60*1000000, // 提前10分钟提醒(微秒) config.GetNearlyTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err) log.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err)
} }
result["inProgressTicketsNearlyTimeout"] = inProgressTicketsNearlyTimeout result["inProgressTicketsNearlyTimeout"] = inProgressTicketsNearlyTimeout
// 处理超时的NO_ANSWER_1状态工单 (4小时) // 处理超时的NO_ANSWER_1状态工单
noAnswer1TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( noAnswer1TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets(
ueCallBackTicket.TicketStatusNoAnswer1.Enum(), ueCallBackTicket.TicketStatusNoAnswer1.Enum(),
4*60*60*1000000, // 4小时(微秒) config.GetNoAnswer1TicketTimeoutMicros(),
10*60*1000000, // 提前10分钟提醒(微秒) config.GetNearlyTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err) log.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err)
} }
result["noAnswer1TicketsNearlyTimeout"] = noAnswer1TicketsNearlyTimeout result["noAnswer1TicketsNearlyTimeout"] = noAnswer1TicketsNearlyTimeout
// 处理超时的NO_ANSWER_2状态工单 (8小时) // 处理超时的NO_ANSWER_2状态工单
noAnswer2TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets( noAnswer2TicketsNearlyTimeout, err := s.handleNearlyTimeoutTickets(
ueCallBackTicket.TicketStatusNoAnswer2.Enum(), ueCallBackTicket.TicketStatusNoAnswer2.Enum(),
8*60*60*1000000, // 8小时(微秒) config.GetNoAnswer2TicketTimeoutMicros(),
10*60*1000000, // 提前10分钟提醒(微秒) config.GetNearlyTimeoutMicros(),
) )
if err != nil { if err != nil {
log.Errorf("处理NO_ANSWER_2状态即将超时工单失败: %v", err) log.Errorf("处理NO_ANSWER_2状态即将超时工单失败: %v", err)
@@ -149,43 +150,37 @@ func (s *PsapTicketMonitor) handleTimeoutTickets(status string, timeoutMicros in
return 0, nil // 没有超时工单 return 0, nil // 没有超时工单
} }
// 更新超时工单状态 // 获取网元信息
var updatedCount int neInfo := neService.NewNeInfo.SelectNeInfoByRmuid(tickets[0].RmUid)
for _, ticket := range tickets { // 构造网元MF的API地址
// 获取网元信息 url := fmt.Sprintf("http://%s:%d/api/rest/systemManagement/v1/elementType/%s/objectType/config/agents",
neInfo := neService.NewNeInfo.SelectNeInfoByRmuid(ticket.RmUid) neInfo.IP, neInfo.Port, strings.ToLower(neInfo.NeType))
// 构造网元MF的API地址 // 发送HTTP请求获取座席列表
url := fmt.Sprintf("http://%s:%d/api/rest/systemManagement/v1/elementType/%s/objectType/config/agents", resp, err := http.Get(url)
neInfo.IP, neInfo.Port, strings.ToLower(neInfo.NeType)) if err != nil {
// 发送HTTP请求获取座席列表 log.Error("Failed to get MF agents", err)
resp, err := http.Get(url) return 0, err
if err != nil { }
log.Error("Failed to get MF agents", err) defer resp.Body.Close()
continue
}
defer resp.Body.Close()
// 解析座席列表响应 // 解析座席列表响应
var agentResp struct { var agentResp struct {
Code int `json:"code"` Code int `json:"code"`
Data []ueCallBackTicket.AgentInfo `json:"data"` Data []ueCallBackTicket.AgentInfo `json:"data"`
Msg string `json:"msg"` 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)
} }
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 处理指定状态的超时工单 // handleTimeoutTickets 处理指定状态的超时工单
@@ -204,10 +199,33 @@ func (s *PsapTicketMonitor) handleNearlyTimeoutTickets(status string, timeoutMic
var updatedCount int var updatedCount int
for _, ticket := range tickets { for _, ticket := range tickets {
if ticket.AgentEmail != "" { if ticket.AgentEmail != "" {
subject := "工单即将超时提醒" emailConfig := config.GetSMTPConfig()
body := fmt.Sprintf("您负责的回拨工单(主叫号码:%s即将超时请及时处理。", ticket.CallerNumber) if emailConfig != nil && emailConfig.Enabled {
go email.SendEmail(ticket.AgentEmail, subject, body) // 创建配置副本,避免修改全局配置
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 return updatedCount, nil