Files
be.ems/features/ue/mf_callback_ticket/service.go
2025-07-08 14:45:46 +08:00

544 lines
18 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
package mf_callback_ticket
import (
"fmt"
"strings"
"time"
"be.ems/lib/config"
"be.ems/lib/dborm"
"be.ems/lib/email"
"be.ems/lib/log"
"gorm.io/gorm"
)
type CallbackTicketService struct {
db *gorm.DB
}
// 构造函数改为私有初始化方法
func NewCallbackTicketService() *CallbackTicketService {
return &CallbackTicketService{db: nil} // 先不初始化数据库连接
}
// 获取数据库连接的私有方法
func (s *CallbackTicketService) getDB() *gorm.DB {
if s.db == nil {
s.db = dborm.DefaultDB()
if s.db == nil {
panic("Cannot establish database connection")
}
}
return s.db
}
// SelectCallbackTicket 根据条件分页查询回调工单
func (s *CallbackTicketService) SelectCallbackTicket(query CallbackTicketQuery) ([]CallbackTicket, int, error) {
var tickets []CallbackTicket
var total int64
db := s.getDB().Table("mf_callback_ticket")
if query.CallerNumber != "" {
db = db.Where("caller_number = ?", query.CallerNumber)
}
if query.AgentName != "" {
db = db.Where("agent_name = ?", query.AgentName)
}
if query.Status != "" {
db = db.Where("status = ?", query.Status)
}
if query.StartTime != "" && query.EndTime != "" {
db = db.Where("created_at BETWEEN ? AND ?", query.StartTime, query.EndTime)
} else if query.StartTime != "" {
db = db.Where("created_at >= ?", query.StartTime)
} else if query.EndTime != "" {
db = db.Where("created_at <= ?", query.EndTime)
}
// 统计总数
if err := db.Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count callback tickets: %w", err)
}
// 分页查询
offset := (query.PageNum - 1) * query.PageSize
if err := db.Limit(query.PageSize).Offset(offset).Order("created_at desc").Find(&tickets).Error; err != nil {
return nil, 0, fmt.Errorf("failed to select callback tickets: %w", err)
}
return tickets, int(total), nil
}
// SelectCallbackTicketByPage 分页查询回调工单
// @Description 分页查询回调工单
// @param page 页码
// @param pageSize 每页大小
// @return []MfCallbackTicket 回调工单列表
// @return int 总记录数
// @return error 错误信息
// @example
// mfCallbackTicketService.SelectCallbackTicketByPage(1, 10)
func (s *CallbackTicketService) SelectCallbackTicketByPage(pageNum int, pageSize int) ([]CallbackTicket, int, error) {
var tickets []CallbackTicket
var total int64
// 统计总数
if err := s.getDB().Table("mf_callback_ticket").Count(&total).Error; err != nil {
return nil, 0, fmt.Errorf("failed to count callback tickets: %w", err)
}
// 分页查询
offset := (pageNum - 1) * pageSize
if err := s.getDB().Table("mf_callback_ticket").
Limit(pageSize).
Offset(offset).
Find(&tickets).Error; err != nil {
return nil, 0, fmt.Errorf("failed to select callback tickets: %w", err)
}
return tickets, int(total), nil
}
func (s *CallbackTicketService) InsertCallbackTicket(ticket *CallbackTicket) error {
// 判断主叫号码是否已存在未处理完的工单
var existingCount int64
if err := s.getDB().Table("mf_callback_ticket").
Where("caller_number = ? AND status IN ('NEW', 'PENDING', 'IN_PROGRESS')", ticket.CallerNumber).
Count(&existingCount).Error; err != nil {
return fmt.Errorf("failed to check existing tickets: %w", err)
}
// 如果存在未处理完的工单,返回错误
if existingCount > 0 {
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 {
return fmt.Errorf("failed to insert callback ticket: %w", err)
}
return nil
}
// SelectCallbackTicketById 根据工单ID查询回调工单
// @Description 根据工单ID查询回调工单
// @param ticketId 工单ID
// @return *CallbackTicket 回调工单对象
// @return error 错误信息
// @example
// mfCallbackTicketService.SelectCallbackTicketById(12345)
func (s *CallbackTicketService) SelectCallbackTicketById(ticketId int64) (*CallbackTicket, error) {
var ticket CallbackTicket
if err := s.getDB().Table("mf_callback_ticket").
Where("ticket_id = ?", ticketId).
First(&ticket).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, fmt.Errorf("callback ticket with ID %d not found", ticketId)
}
return nil, fmt.Errorf("failed to select callback ticket: %w", err)
}
return &ticket, nil
}
// UpdateCallbackTicket 更新回调工单
// @Description 更新回调工单
// @param ticket 回调工单对象
// @return error 错误信息
// @example
// mfCallbackTicketService.UpdateCallbackTicket(ticket)
func (s *CallbackTicketService) UpdateCallbackTicket(ticket CallbackTicket) error {
if err := s.getDB().Table("mf_callback_ticket").
Where("ticket_id = ?", ticket.TicketId).
Updates(ticket).Error; err != nil {
return fmt.Errorf("failed to update callback ticket: %w", err)
}
return nil
}
// DeleteCallbackTicket 删除回调工单
// @Description 删除回调工单
// @param ticketId 工单ID
// @return error 错误信息
// @example
// mfCallbackTicketService.DeleteCallbackTicket(12345)
func (s *CallbackTicketService) DeleteCallbackTicket(ticketId int64) error {
if err := s.getDB().Table("mf_callback_ticket").
Where("ticket_id = ?", ticketId).
Delete(&CallbackTicket{}).Error; err != nil {
return fmt.Errorf("failed to delete callback ticket: %w", err)
}
return nil
}
// GetLastAssignedAgent 获取最近一个分配的座席名称
// @Description 获取最近分配工单的座席名称
// @return string 座席名称
// @return error 错误信息
func (s *CallbackTicketService) GetLastAssignedAgent() (string, error) {
var lastTicket CallbackTicket
if err := s.getDB().Table("mf_callback_ticket").
Where("agent_name <> ''").
Order("created_at DESC, ticket_id DESC").
First(&lastTicket).Error; err != nil {
if err == gorm.ErrRecordNotFound {
// 没有记录属于正常情况,返回空字符串
return "", nil
}
return "", fmt.Errorf("failed to query last ticket: %w", err)
}
return lastTicket.AgentName, nil
}
// SelectNextAgent 根据上一个座席和座席列表选择下一个座席
// @Description 选择下一个要分配的座席
// @param agents 座席列表
// @param lastAgentName 上一个座席名称
// @return string 下一个座席名称
func (s *CallbackTicketService) SelectNextAgent(agents []AgentInfo, lastAgentName string) *AgentInfo {
if len(agents) == 0 {
return nil
}
var firstAgent *AgentInfo = &agents[0]
// 如果没有上一个座席,直接返回第一个
if lastAgentName == "" {
return firstAgent
}
// 找到上一个座席的下一个
foundLastAgent := false
for i, agent := range agents {
if foundLastAgent {
// 找到上一个座席的下一个
return &agent
}
if agent.Name == lastAgentName {
foundLastAgent = true
// 如果是最后一个座席,则循环回第一个
if i == len(agents)-1 {
return firstAgent
}
}
}
// 如果没找到上一个座席(可能被删除了),使用第一个座席
return firstAgent
}
// FindCallbackTicketByAgentAndCaller 通过座席号码和主叫号码查找符合条件的工单
// @Description 通过座席号码和主叫号码查找符合条件的工单
// @param agentName 座席号码
// @param callerNumber 主叫号码
// @return *CallbackTicket 回调工单对象
// @return error 错误信息
func (s *CallbackTicketService) FindCallbackTicketByAgentAndCaller(agentName string, callerNumber string) (*CallbackTicket, error) {
var ticket CallbackTicket
if err := s.getDB().Table("mf_callback_ticket").
Where("agent_name = ? AND caller_number = ? AND status IN ('NEW', 'PENDING', 'IN_PROGRESS', 'NO_ANSWER_1', 'NO_ANSWER_2')",
agentName, callerNumber).
Order("created_at DESC").
First(&ticket).Error; err != nil {
if err == gorm.ErrRecordNotFound {
return nil, nil // 没有找到记录返回nil, nil
}
return nil, fmt.Errorf("failed to find callback ticket: %w", err)
}
return &ticket, nil
}
// ProcessCallbackResult 处理回拨结果并更新工单
// @Description 处理回拨结果并更新工单状态
// @param ticket 要更新的工单
// @param callDuration 通话时长
// @param answerTime 应答时间
// @param releaseTime 释放时间
// @param cause 释放原因
// @return error 错误信息
func (s *CallbackTicketService) ProcessCallbackResult(ticket *CallbackTicket, callDuration float64, seizureTime string, releaseTime string, cause string) error {
if ticket == nil {
return fmt.Errorf("ticket cannot be nil")
}
// 构建更新信息
now := time.Now().UnixMicro()
updatedTicket := CallbackTicket{
TicketId: ticket.TicketId,
UpdatedAt: &now,
}
// 构建评论内容
comment := fmt.Sprintf("回拨时间: %s, 释放时间: %s, 原因: %s", seizureTime, releaseTime, cause)
// 根据通话时长判断处理结果
if callDuration > 0 {
// 通话已接通,标记为已处理完毕
updatedTicket.Status = TicketStatusClosed.Enum()
updatedTicket.Comment = fmt.Sprintf("%s, 通话时长: %.1f秒, 状态: 已接通并完成", comment, callDuration)
} else {
// 通话未接通,根据当前状态修改为相应状态
switch ticket.Status {
case TicketStatusNew.Enum(), TicketStatusInProgress.Enum():
// 第一次未接通
updatedTicket.Status = TicketStatusNoAnswer1.Enum()
updatedTicket.Comment = fmt.Sprintf("%s, 通话时长: %.1f秒, 状态: 第一次未接通", comment, callDuration)
case TicketStatusNoAnswer1.Enum():
// 第二次未接通
updatedTicket.Status = TicketStatusNoAnswer2.Enum()
updatedTicket.Comment = fmt.Sprintf("%s, 通话时长: %.1f秒, 状态: 第二次未接通", comment, callDuration)
case TicketStatusNoAnswer2.Enum():
// 第三次未接通,挂起
updatedTicket.Status = TicketStatusPending.Enum()
updatedTicket.Comment = fmt.Sprintf("%s, 通话时长: %.1f秒, 状态: 多次未接通,工单挂起", comment, callDuration)
default:
// 其他状态保持不变
updatedTicket.Status = ticket.Status
updatedTicket.Comment = fmt.Sprintf("%s, 通话时长: %.1f秒, 状态: 未知状态", comment, callDuration)
}
}
// 更新工单
if err := s.getDB().Table("mf_callback_ticket").
Where("ticket_id = ?", ticket.TicketId).
Updates(updatedTicket).Error; err != nil {
return fmt.Errorf("failed to update callback ticket: %w", err)
}
return nil
}
// FindTimeoutTickets 查询指定状态的超时工单
// @Description 查询指定状态且超过指定时间未处理的工单
// @param status 工单状态
// @param timeoutMicros 超时时间(微秒)
// @return []CallbackTicket 超时工单列表
// @return error 错误信息
func (s *CallbackTicketService) FindTimeoutTickets(status string, timeoutMicros int64) ([]CallbackTicket, error) {
// 计算超时时间点
nowMicros := time.Now().UnixMicro()
timeoutBeforeMicros := nowMicros - timeoutMicros
// 查询超时的工单
var tickets []CallbackTicket
if err := s.getDB().Table("mf_callback_ticket").
Where("status = ? AND created_at < ? AND (updated_at IS NULL OR updated_at < ?)",
status, timeoutBeforeMicros, timeoutBeforeMicros).
Find(&tickets).Error; err != nil {
return nil, fmt.Errorf("查询超时工单失败: %w", err)
}
return tickets, nil
}
// UpdateTicketToTimeout 将工单状态更新为超时
// @Description 将指定工单更新为超时状态,并添加备注信息
// @param ticket 要更新的工单
// @param originalStatus 原工单状态
// @return error 错误信息
func (s *CallbackTicketService) UpdateTicketToTimeout(ticket *CallbackTicket, originalStatus string, agents []AgentInfo) error {
if ticket == nil {
return fmt.Errorf("ticket cannot be nil")
}
now := time.Now().UnixMicro()
// 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 {
return fmt.Errorf("更新工单 %d 状态失败: %w", ticket.TicketId, err)
}
// 2. 选择新座席
lastAgent := ticket.AgentName
newAgent := s.SelectNextAgent(agents, lastAgent)
// 3. 创建新工单
if newAgent != nil {
newTicket := CallbackTicket{
CallerNumber: ticket.CallerNumber,
CalleeNumber: ticket.CalleeNumber,
Status: TicketStatusNew.Enum(),
AgentName: newAgent.Name,
AgentEmail: newAgent.Email,
AgentMobile: newAgent.Mobile,
Comment: fmt.Sprintf("由超时工单 %d 自动重建", ticket.TicketId),
MsdData: ticket.MsdData,
RmUid: ticket.RmUid,
CreatedAt: now,
UpdatedAt: nil,
}
if err := s.getDB().Table("mf_callback_ticket").Create(&newTicket).Error; err != nil {
return fmt.Errorf("创建新工单失败: %w", err)
}
// 新工单分配后发送邮件通知
if newAgent.Email != "" {
// 发送邮件通知
// 获取SMTP配置
emailConfig := config.GetSMTPConfig()
if emailConfig != nil && emailConfig.Enabled {
// 创建配置副本,避免修改全局配置
emailCopy := *emailConfig // 浅拷贝结构体
// 合并配置中的To地址和当前工单的座席邮箱
var recipients []string
// 添加配置中的原始收件人(如管理员、监控人员等)
if len(emailConfig.To) > 0 {
recipients = append(recipients, strings.Split(emailConfig.To, ",")...)
}
// 添加当前工单的座席邮箱
recipients = append(recipients, ticket.AgentEmail)
// 去重处理(避免重复邮箱)
emailCopy.To = strings.Join(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, strings.Split(emailConfig.To, ",")...)
}
// 添加当前工单的座席邮箱
recipients = append(recipients, ticket.AgentEmail)
// 去重处理(避免重复邮箱)
emailCopy.To = strings.Join(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()
timeoutBeforeMicros := nowMicros - timeoutMicros
nearlyTimeoutBeforeMicros := nowMicros - (timeoutMicros - aheadMicros)
var tickets []CallbackTicket
if err := s.getDB().Table("mf_callback_ticket").
Where("status = ? AND created_at >= ? AND created_at < ? AND (updated_at IS NULL OR updated_at < ?)",
status, timeoutBeforeMicros, nearlyTimeoutBeforeMicros, nearlyTimeoutBeforeMicros).
Find(&tickets).Error; err != nil {
return nil, fmt.Errorf("查询即将超时工单失败: %w", err)
}
return tickets, nil
}
// 新增方法:坐席开始处理工单
func (s *CallbackTicketService) StartProcessingTicket(ticketId int64) error {
now := time.Now().UnixMicro()
if err := s.getDB().Table("mf_callback_ticket").
Where("ticket_id = ? AND status = ?", ticketId, TicketStatusNew.Enum()).
Updates(map[string]interface{}{
"status": TicketStatusInProgress.Enum(),
"updated_at": now,
"comment": "坐席开始处理工单",
}).Error; err != nil {
return fmt.Errorf("failed to update ticket to IN_PROGRESS: %w", err)
}
return nil
}