feat: callback ticket features
This commit is contained in:
37
database/install/mf_callback_ticket.sql
Executable file
37
database/install/mf_callback_ticket.sql
Executable 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;
|
||||
@@ -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¶mName=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)
|
||||
}
|
||||
|
||||
@@ -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
|
||||
}
|
||||
|
||||
155
features/ue/mf_callback_ticket/controller.go
Normal file
155
features/ue/mf_callback_ticket/controller.go
Normal 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,
|
||||
}))
|
||||
}
|
||||
115
features/ue/mf_callback_ticket/model.go
Normal file
115
features/ue/mf_callback_ticket/model.go
Normal 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"
|
||||
}
|
||||
353
features/ue/mf_callback_ticket/service.go
Normal file
353
features/ue/mf_callback_ticket/service.go
Normal 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
|
||||
}
|
||||
@@ -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,
|
||||
)
|
||||
|
||||
@@ -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
|
||||
}
|
||||
Reference in New Issue
Block a user