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
|
||||
@@ -187,3 +187,8 @@ params:
|
||||
testConfig:
|
||||
enabled: false
|
||||
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(),
|
||||
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")
|
||||
|
||||
@@ -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 {
|
||||
|
||||
@@ -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()
|
||||
|
||||
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
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
|
||||
|
||||
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
|
||||
}
|
||||
|
||||
@@ -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
|
||||
to:
|
||||
- simonzhangsz@outlook.com # 可以是多个收件人
|
||||
- shuzone@126.com
|
||||
timeout: # 超时设置
|
||||
# 这些时间单位是分钟
|
||||
# 注意:这些时间是相对于工单创建时间的
|
||||
# 例如:new: 60分钟,inProgress: 60分钟
|
||||
new: 1
|
||||
inProgress: 60
|
||||
noAnswer1: 240
|
||||
noAnswer2: 480
|
||||
nearlyTimeout: 20
|
||||
|
||||
@@ -203,3 +203,8 @@ staticFile:
|
||||
upload:
|
||||
prefix: "/upload"
|
||||
dir: "./upload"
|
||||
|
||||
# PSAP RESTCONF配置
|
||||
psapConfig:
|
||||
enabled: true
|
||||
file: ./etc/psap.yaml
|
||||
@@ -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
|
||||
|
||||
Reference in New Issue
Block a user