feat: ticket enhancemnet
This commit is contained in:
25
config/etc/default/psap.yaml
Normal file
25
config/etc/default/psap.yaml
Normal 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
|
||||||
@@ -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
|
||||||
@@ -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")
|
||||||
|
|||||||
@@ -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 {
|
||||||
|
|||||||
@@ -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()
|
||||||
|
|||||||
@@ -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
184
lib/config/psap.go
Normal 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
|
||||||
|
}
|
||||||
@@ -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
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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
|
|
||||||
|
|||||||
@@ -203,3 +203,8 @@ staticFile:
|
|||||||
upload:
|
upload:
|
||||||
prefix: "/upload"
|
prefix: "/upload"
|
||||||
dir: "./upload"
|
dir: "./upload"
|
||||||
|
|
||||||
|
# PSAP RESTCONF配置
|
||||||
|
psapConfig:
|
||||||
|
enabled: true
|
||||||
|
file: ./etc/psap.yaml
|
||||||
@@ -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
|
||||||
|
|||||||
Reference in New Issue
Block a user