feat: callback ticket features

This commit is contained in:
zhangsz
2025-06-11 17:24:41 +08:00
parent 647367394d
commit 39e91bbbe0
8 changed files with 890 additions and 1 deletions

View File

@@ -0,0 +1,37 @@
/*
Navicat Premium Data Transfer
Source Server : omc@192.168.2.223-psap
Source Server Type : MariaDB
Source Server Version : 100622 (10.6.22-MariaDB-0ubuntu0.22.04.1)
Source Host : 192.168.2.223:33066
Source Schema : omc_db
Target Server Type : MariaDB
Target Server Version : 100622 (10.6.22-MariaDB-0ubuntu0.22.04.1)
File Encoding : 65001
Date: 04/06/2025 10:50:08
*/
SET NAMES utf8mb4;
SET FOREIGN_KEY_CHECKS = 0;
-- ----------------------------
-- Table structure for mf_callback_ticket
-- ----------------------------
DROP TABLE IF EXISTS `mf_callback_ticket`;
CREATE TABLE `mf_callback_ticket` (
`ticket_id` bigint(20) NOT NULL AUTO_INCREMENT COMMENT 'Ticket ID',
`caller_number` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT 'caller number',
`callee_number` varchar(64) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT NULL COMMENT 'callee number',
`status` enum('NEW','PENDING','IN_PROGRESS','FAILED','COMPLETED','TIMEOUT','RECREATED') CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT 'NEW',
`agent_name` varchar(32) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'agent name',
`comment` varchar(256) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '' COMMENT 'comment for callback',
`msd_data` varchar(512) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NULL DEFAULT '0' COMMENT 'MSD data',
`created_at` bigint(20) NULL DEFAULT NULL COMMENT 'created at time',
`updated_at` bigint(20) NULL DEFAULT NULL COMMENT 'updated at time',
PRIMARY KEY (`ticket_id`) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 103 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci COMMENT = '用户信息表' ROW_FORMAT = Dynamic;
SET FOREIGN_KEY_CHECKS = 1;

View File

@@ -1,10 +1,13 @@
package cdr
import (
"encoding/json"
"fmt"
"net/http"
"strings"
"time"
ueCallBackTicket "be.ems/features/ue/mf_callback_ticket"
"be.ems/lib/config"
"be.ems/lib/core/ctx"
"be.ems/lib/dborm"
@@ -73,5 +76,125 @@ func PostCDREventFrom(w http.ResponseWriter, r *http.Request) {
}
}
// MF网元类型特殊处理, 未接电话的回拨工单流转处理
if neTypeLower == "mf" && cdrEvent.CDR["agentName"] == "" {
// 发送到MF网元
// 构造网元MF的API地址
url := fmt.Sprintf("http://%s:%d/ne/config/data?neType=%s&neId=%s&paramName=agents",
neInfo.IP, neInfo.Port, neInfo.NeType, neInfo.NeId)
// 发送HTTP请求获取座席列表
resp, err := http.Get(url)
if err != nil {
log.Error("Failed to get MF agents", err)
services.ResponseInternalServerError500ProcessError(w, err)
return
}
defer resp.Body.Close()
// 解析座席列表响应
var agentResp struct {
Code int `json:"code"`
Data []struct {
Domain string `json:"domain"`
Index int `json:"index"`
Name string `json:"name"`
Online bool `json:"online"`
Password string `json:"password"`
} `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)
services.ResponseInternalServerError500ProcessError(w, err)
return
}
// 调用服务获取最新一个被分配工单的座席和下一个要分配的座席
mfService := ueCallBackTicket.NewCallbackTicketService()
lastAgent, err := mfService.GetLastAssignedAgent()
if err != nil {
log.Error("Failed to get last assigned agent", err)
// 可以继续执行,不返回错误
}
// 选择下一个要分配的座席
selectedAgent := mfService.SelectNextAgent(agentResp.Data, lastAgent)
// 创建回调工单
var updatedAt *int64 = nil
ticket := ueCallBackTicket.CallbackTicket{
CallerNumber: cdrEvent.CDR["callerParty"].(string),
CalleeNumber: cdrEvent.CDR["calledParty"].(string),
Status: ueCallBackTicket.TicketStatusNew.Enum(),
AgentName: selectedAgent,
Comment: "",
MsdData: cdrEvent.CDR["msdData"].(string),
CreatedAt: time.Now().UnixMicro(),
UpdatedAt: updatedAt,
}
if err := mfService.InsertCallbackTicket(ticket); err != nil {
log.Error("Failed to insert MF callback ticket", err)
// services.ResponseInternalServerError500ProcessError(w, err)
// return
}
}
// MF网元类型特殊处理, 处理座席回拨的工单流转
if neTypeLower == "mf" && cdrEvent.CDR["recordType"] == "MTC" {
// 获取座席号码(主叫)和被叫号码
agentNumber, ok1 := cdrEvent.CDR["callerParty"].(string)
callerNumber, ok2 := cdrEvent.CDR["calledParty"].(string)
if !ok1 || !ok2 {
log.Error("Invalid CDR format: missing callerParty or calledParty")
services.ResponseInternalServerError500ProcessError(w, fmt.Errorf("invalid CDR format"))
return
}
// 获取通话时长
callDuration, ok := cdrEvent.CDR["callDuration"].(float64)
if !ok {
// 尝试其他可能的类型
if durationInt, ok := cdrEvent.CDR["callDuration"].(int); ok {
callDuration = float64(durationInt)
} else {
log.Error("Invalid CDR format: callDuration is not a number")
services.ResponseInternalServerError500ProcessError(w, fmt.Errorf("invalid CDR format"))
return
}
}
// 通过座席号码和主叫号码查找符合条件的工单
mfService := ueCallBackTicket.NewCallbackTicketService()
ticket, err := mfService.FindCallbackTicketByAgentAndCaller(agentNumber, callerNumber)
if err != nil {
log.Error("Failed to find callback ticket", err)
services.ResponseInternalServerError500ProcessError(w, err)
return
}
if ticket == nil {
// 没有找到对应的工单,可能是手动呼叫,不处理
log.Warn(fmt.Sprintf("No callback ticket found for agent %s and caller %s", agentNumber, callerNumber))
services.ResponseStatusOK204NoContent(w)
return
}
// 获取通话信息
answerTime, _ := cdrEvent.CDR["answerTime"].(string)
releaseTime, _ := cdrEvent.CDR["releaseTime"].(string)
cause, _ := cdrEvent.CDR["cause"].(string)
// 处理回拨结果并更新工单
if err := mfService.ProcessCallbackResult(ticket, callDuration, answerTime, releaseTime, cause); err != nil {
log.Error("Failed to process callback result", err)
services.ResponseInternalServerError500ProcessError(w, err)
return
}
log.Info(fmt.Sprintf("Successfully processed callback for ticket %d", ticket.TicketId))
}
services.ResponseStatusOK204NoContent(w)
}

View File

@@ -3,6 +3,7 @@
package ue
import (
"be.ems/features/ue/mf_callback_ticket"
"be.ems/features/ue/mf_calling"
"be.ems/lib/log"
"github.com/gin-gonic/gin"
@@ -14,5 +15,6 @@ func InitSubServiceRoute(r *gin.Engine) {
mfGroup := r.Group("/psap/v1/mf")
mf_calling.Register(mfGroup)
mf_callback_ticket.Register(mfGroup)
// register other sub modules routes
}

View File

@@ -0,0 +1,155 @@
package mf_callback_ticket
import (
"strconv"
"be.ems/src/framework/i18n"
"be.ems/src/framework/middleware"
"be.ems/src/framework/utils/ctx"
"be.ems/src/framework/vo/result"
"github.com/gin-gonic/gin"
)
// @Description Register Routes for mf calling
func Register(r *gin.RouterGroup) {
mfCallingGroup := r.Group("/ticket")
{
var m *CallbackTicket
mfCallingGroup.GET("/list",
middleware.PreAuthorize(nil),
m.List,
)
mfCallingGroup.PUT("/:ticketId",
middleware.PreAuthorize(nil),
m.Update,
)
mfCallingGroup.DELETE("/:ticketId",
middleware.PreAuthorize(nil),
m.Delete,
)
mfCallingGroup.GET("/:ticketId",
middleware.PreAuthorize(nil),
m.GetById,
)
}
}
func (m *CallbackTicket) List(c *gin.Context) {
language := ctx.AcceptLanguage(c)
neId := c.Query("neId")
if neId == "" {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
var query CallbackTicketQuery
if err := c.ShouldBindQuery(&query); err != nil {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
service := NewCallbackTicketService()
data, total, err := service.SelectCallbackTicket(query)
if err != nil {
c.JSON(500, result.ErrMsg(err.Error()))
return
}
c.JSON(200, result.Ok(gin.H{
"total": total,
"data": data,
}))
}
// Update 更新回调工单
func (m *CallbackTicket) Update(c *gin.Context) {
language := ctx.AcceptLanguage(c)
// 获取路径参数
ticketId := c.Param("ticketId")
// 绑定请求体
var ticket CallbackTicket
if err := c.ShouldBindJSON(&ticket); err != nil {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
// 检查路径ID和请求体ID是否一致
if ticketId != "" {
id, err := strconv.ParseInt(ticketId, 10, 64)
if err != nil {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
ticket.TicketId = id
}
service := NewCallbackTicketService()
if err := service.UpdateCallbackTicket(ticket); err != nil {
c.JSON(500, result.ErrMsg(err.Error()))
return
}
c.JSON(200, result.Ok(nil))
}
// Delete 删除回调工单
func (m *CallbackTicket) Delete(c *gin.Context) {
language := ctx.AcceptLanguage(c)
// 获取路径参数
ticketId := c.Param("ticketId")
if ticketId == "" {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
id, err := strconv.ParseInt(ticketId, 10, 64)
if err != nil {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
service := NewCallbackTicketService()
if err := service.DeleteCallbackTicket(id); err != nil {
c.JSON(500, result.ErrMsg(err.Error()))
return
}
c.JSON(200, result.Ok(nil))
}
// GetById 根据ID获取回调工单
func (m *CallbackTicket) GetById(c *gin.Context) {
language := ctx.AcceptLanguage(c)
// 获取路径参数
ticketId := c.Param("ticketId")
if ticketId == "" {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
id, err := strconv.ParseInt(ticketId, 10, 64)
if err != nil {
c.JSON(400, result.CodeMsg(400, i18n.TKey(language, "app.common.err400")))
return
}
service := NewCallbackTicketService()
ticket, err := service.SelectCallbackTicketById(id)
if err != nil {
c.JSON(500, result.ErrMsg(err.Error()))
return
}
if ticket == nil {
c.JSON(404, result.CodeMsg(404, i18n.TKey(language, "app.common.err404")))
return
}
c.JSON(200, result.Ok(gin.H{
"ticket": ticket,
}))
}

View File

@@ -0,0 +1,115 @@
package mf_callback_ticket
import (
"fmt"
"strconv"
"strings"
)
type TicketStatus int
// TicketStatus 工单状态
const (
TicketStatusNull TicketStatus = iota // 未知状态
TicketStatusNew
TicketStatusInProgress
// TicketStatusFailed
TicketStatusNoAnswer1
TicketStatusNoAnswer2
// TicketStatusBusy
// TicketStatusCancelled
TicketStatusTimeout
TicketStatusPending
// TicketStatusAnswered
// TicketStatusCompleted
TicketStatusClosed
)
func (ts TicketStatus) Enum() string {
switch ts {
case TicketStatusNull:
return "NULL"
case TicketStatusNew:
return "NEW"
case TicketStatusInProgress:
return "IN_PROGRESS"
case TicketStatusNoAnswer1:
return "NO_ANSWER_1"
case TicketStatusNoAnswer2:
return "NO_ANSWER_2"
case TicketStatusTimeout:
return "TIMEOUT"
case TicketStatusPending:
return "PENDING"
case TicketStatusClosed:
return "CLOSED"
// 如果没有匹配的状态,返回未知状态
default:
return "UNKNOWN"
}
}
func (ts TicketStatus) String() string {
return fmt.Sprintf("%d", ts)
}
// ParseCallTag 将字符串转换为 CallTag 枚举类型
func ParseCallTag(s string) TicketStatus {
if i, err := strconv.Atoi(s); err == nil {
return TicketStatus(i)
}
// 如果转换失败,则按名称匹配(忽略大小写)
switch strings.ToLower(s) {
case "NULL":
return TicketStatusNull
case "NEW":
return TicketStatusNew
case "IN_PROGRESS":
return TicketStatusInProgress
case "NO_ANSWER_1":
return TicketStatusNoAnswer1
case "NO_ANSWER_2":
return TicketStatusNoAnswer2
case "TIMEOUT":
return TicketStatusTimeout
case "PENDING":
return TicketStatusPending
case "CLOSED":
return TicketStatusClosed
case "":
// 如果字符串为空,则返回未知状态
return TicketStatusNull
default:
// 默认返回未知状态
return TicketStatusNull
}
}
// CallbackTicketQuery 查询条件结构体
type CallbackTicketQuery struct {
CallerNumber string `form:"callerNumber"`
AgentName string `form:"agentName"`
Status string `form:"status"`
StartTime string `form:"startTime"` // 创建时间范围-起始
EndTime string `form:"endTime"` // 创建时间范围-结束
PageNum int `form:"pageNum" binding:"required"`
PageSize int `form:"pageSize" binding:"required"`
}
// @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"` // 坐席名称
Comment string `json:"comment" gorm:"column:comment"` // 工单备注
MsdData string `json:"msdData" gorm:"column:msd_data"` // MSD数据
CreatedAt int64 `json:"createdAt" gorm:"column:created_at"` // 创建时间
UpdatedAt *int64 `json:"updatedAt" gorm:"column:updated_at;autoUpdateTime:false"` // 更新时间
}
// TableName 表名称
func (*CallbackTicket) TableName() string {
return "mf_callback_ticket"
}

View File

@@ -0,0 +1,353 @@
package mf_callback_ticket
import (
"fmt"
"time"
"be.ems/lib/dborm"
"gorm.io/gorm"
)
type CallbackTicketService struct {
db *gorm.DB
}
// 构造函数示例
func NewCallbackTicketService() *CallbackTicketService {
db := dborm.DefaultDB()
return &CallbackTicketService{db: db}
}
// SelectCallbackTicket 根据条件分页查询回调工单
func (s *CallbackTicketService) SelectCallbackTicket(query CallbackTicketQuery) ([]CallbackTicket, int, error) {
var tickets []CallbackTicket
var total int64
db := s.db.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.db.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.db.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.db.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.db.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.db.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.db.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.db.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.db.Table("mf_callback_ticket").
Where("agent_name <> ''").
Order("created_at 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 []struct {
Domain string `json:"domain"`
Index int `json:"index"`
Name string `json:"name"`
Online bool `json:"online"`
Password string `json:"password"`
}, lastAgentName string) string {
if len(agents) == 0 {
return ""
}
// 默认选第一个座席
selectedAgent := agents[0].Name
// 如果没有上一个座席,直接返回第一个
if lastAgentName == "" {
return selectedAgent
}
// 找到上一个座席的下一个
foundLastAgent := false
for i, agent := range agents {
if foundLastAgent {
// 找到上一个座席的下一个
return agent.Name
}
if agent.Name == lastAgentName {
foundLastAgent = true
// 如果是最后一个座席,则循环回第一个
if i == len(agents)-1 {
return agents[0].Name
}
}
}
// 如果没找到上一个座席(可能被删除了),使用第一个座席
return selectedAgent
}
// 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.db.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, answerTime 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", answerTime, 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.db.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.db.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) error {
if ticket == nil {
return fmt.Errorf("ticket cannot be nil")
}
now := time.Now().UnixMicro()
updatedTicket := CallbackTicket{
TicketId: ticket.TicketId,
Status: TicketStatusTimeout.Enum(),
Comment: fmt.Sprintf("%s - 工单状态为 %s 处理超时,系统自动更新为超时状态", ticket.Comment, originalStatus),
UpdatedAt: &now,
}
if err := s.db.Table("mf_callback_ticket").
Where("ticket_id = ?", ticket.TicketId).
Updates(updatedTicket).Error; err != nil {
return fmt.Errorf("更新工单 %d 状态失败: %w", ticket.TicketId, err)
}
return nil
}

View File

@@ -16,7 +16,7 @@ func Register(r *gin.RouterGroup) {
mfCallingGroup := r.Group("/callings")
{
var m *MfCallingInfo
mfCallingGroup.GET("/:neId/list",
mfCallingGroup.GET("/list",
middleware.PreAuthorize(nil),
m.List,
)

View File

@@ -0,0 +1,104 @@
package psap_ticket_monitor
import (
ueCallBackTicket "be.ems/features/ue/mf_callback_ticket"
"be.ems/lib/log"
"be.ems/src/framework/cron"
"be.ems/src/framework/logger"
)
var NewProcessor = &PsapTicketMonitor{
callbackTicketService: ueCallBackTicket.NewCallbackTicketService(),
count: 0,
}
// PsapTicketMonitor 工单处理超时监控
type PsapTicketMonitor struct {
callbackTicketService *ueCallBackTicket.CallbackTicketService // 回调工单服务
count int // 执行次数
}
func (s *PsapTicketMonitor) Execute(data any) (any, error) {
s.count++ // 执行次数加一
options := data.(cron.JobData)
sysJob := options.SysJob
logger.Infof("执行工单监控任务 %v 任务ID %s", options.Repeat, sysJob.JobID)
// 返回结果,用于记录执行结果
result := map[string]any{
"count": s.count,
}
// 处理超时的NEW状态工单 (30分钟)
newTicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusNew.Enum(),
30*60*1000000, // 30分钟(微秒)
)
if err != nil {
logger.Errorf("处理NEW状态超时工单失败: %v", err)
}
result["newTicketsUpdated"] = newTicketsUpdated
// 处理超时的IN_PROGRESS状态工单 (60分钟)
inProgressTicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusInProgress.Enum(),
60*60*1000000, // 60分钟(微秒)
)
if err != nil {
logger.Errorf("处理IN_PROGRESS状态超时工单失败: %v", err)
}
result["inProgressTicketsUpdated"] = inProgressTicketsUpdated
// 处理超时的NO_ANSWER_1状态工单 (4小时)
noAnswer1TicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusNoAnswer1.Enum(),
4*60*60*1000000, // 4小时(微秒)
)
if err != nil {
logger.Errorf("处理NO_ANSWER_1状态超时工单失败: %v", err)
}
result["noAnswer1TicketsUpdated"] = noAnswer1TicketsUpdated
// 处理超时的NO_ANSWER_2状态工单 (8小时)
noAnswer2TicketsUpdated, err := s.handleTimeoutTickets(
ueCallBackTicket.TicketStatusNoAnswer2.Enum(),
8*60*60*1000000, // 8小时(微秒)
)
if err != nil {
logger.Errorf("处理NO_ANSWER_2状态超时工单失败: %v", err)
}
result["noAnswer2TicketsUpdated"] = noAnswer2TicketsUpdated
// 汇总结果
totalUpdated := newTicketsUpdated + inProgressTicketsUpdated + noAnswer1TicketsUpdated + noAnswer2TicketsUpdated
result["totalUpdated"] = totalUpdated
logger.Infof("工单监控任务完成,共处理 %d 个超时工单", totalUpdated)
return result, nil
}
// handleTimeoutTickets 处理指定状态的超时工单
func (s *PsapTicketMonitor) handleTimeoutTickets(status string, timeoutMicros int64) (int, error) {
// 查询超时的工单
tickets, err := s.callbackTicketService.FindTimeoutTickets(status, timeoutMicros)
if err != nil {
return 0, err
}
if len(tickets) == 0 {
return 0, nil // 没有超时工单
}
// 更新超时工单状态
var updatedCount int
for _, ticket := range tickets {
if err := s.callbackTicketService.UpdateTicketToTimeout(&ticket, status); err != nil {
log.Errorf("更新工单 %d 状态失败: %v", ticket.TicketId, err)
continue
}
updatedCount++
log.Infof("工单 %d 已更新为超时状态 (原状态: %s)", ticket.TicketId, status)
}
return updatedCount, nil
}