|
|
|
@@ -27,6 +27,11 @@ type SessionListItem struct {
|
|
|
|
|
UnreadCount int `json:"unread_count"`
|
|
|
|
|
LastMessage string `json:"last_message"`
|
|
|
|
|
LastMessageAt *time.Time `json:"last_message_at,omitempty"`
|
|
|
|
|
MessageCount int `json:"message_count"`
|
|
|
|
|
CustomerName string `json:"customer_name"`
|
|
|
|
|
AgentName string `json:"agent_name"`
|
|
|
|
|
ChannelName string `json:"channel_name"`
|
|
|
|
|
ChannelType string `json:"channel_type"`
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
type CreateNoteReq struct {
|
|
|
|
@@ -169,33 +174,125 @@ func (h *SessionHandler) List(c *gin.Context) {
|
|
|
|
|
page, pageSize := middleware.GetPageParams(c)
|
|
|
|
|
status := c.Query("status")
|
|
|
|
|
priority := c.Query("priority")
|
|
|
|
|
agentID := c.Query("agent_id")
|
|
|
|
|
channelID := c.Query("channel_id")
|
|
|
|
|
search := strings.TrimSpace(c.Query("search"))
|
|
|
|
|
from := c.Query("from")
|
|
|
|
|
to := c.Query("to")
|
|
|
|
|
|
|
|
|
|
var sessions []model.Session
|
|
|
|
|
var total int64
|
|
|
|
|
|
|
|
|
|
query := model.DB.Where("tenant_id = ?", tenantID)
|
|
|
|
|
query := model.DB.Model(&model.Session{}).Where("sessions.tenant_id = ?", tenantID)
|
|
|
|
|
if middleware.GetRole(c) == "agent" {
|
|
|
|
|
query = query.Where("agent_id = ? OR status = ?", middleware.GetUserID(c), "waiting")
|
|
|
|
|
query = query.Where("sessions.agent_id = ? OR sessions.status = ?", middleware.GetUserID(c), "waiting")
|
|
|
|
|
}
|
|
|
|
|
if status != "" {
|
|
|
|
|
query = query.Where("status = ?", status)
|
|
|
|
|
query = query.Where("sessions.status = ?", status)
|
|
|
|
|
}
|
|
|
|
|
if priority != "" {
|
|
|
|
|
query = query.Where("priority = ?", priority)
|
|
|
|
|
query = query.Where("sessions.priority = ?", priority)
|
|
|
|
|
}
|
|
|
|
|
if agentID != "" {
|
|
|
|
|
query = query.Where("sessions.agent_id = ?", agentID)
|
|
|
|
|
}
|
|
|
|
|
if channelID != "" {
|
|
|
|
|
query = query.Where("sessions.channel_id = ?", channelID)
|
|
|
|
|
}
|
|
|
|
|
if from != "" {
|
|
|
|
|
if t, err := time.ParseInLocation("2006-01-02", from, time.Local); err == nil {
|
|
|
|
|
query = query.Where("sessions.created_at >= ?", t)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if to != "" {
|
|
|
|
|
if t, err := time.ParseInLocation("2006-01-02", to, time.Local); err == nil {
|
|
|
|
|
query = query.Where("sessions.created_at < ?", t.Add(24*time.Hour))
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if search != "" {
|
|
|
|
|
like := "%" + search + "%"
|
|
|
|
|
query = query.Joins("LEFT JOIN customers ON customers.id = sessions.customer_id").
|
|
|
|
|
Where("customers.name LIKE ? OR CAST(sessions.id AS TEXT) LIKE ? OR COALESCE(sessions.end_reason, '') LIKE ?", like, like, like)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
query.Model(&model.Session{}).Count(&total)
|
|
|
|
|
if err := query.Order("created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&sessions).Error; err != nil {
|
|
|
|
|
query.Count(&total)
|
|
|
|
|
if err := query.Order("sessions.created_at desc").Offset((page - 1) * pageSize).Limit(pageSize).Find(&sessions).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询会话失败"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// 批量补全客户 / 客服 / 渠道 / 消息数
|
|
|
|
|
customerIDs := make([]uint, 0)
|
|
|
|
|
agentIDs := make([]uint, 0)
|
|
|
|
|
channelIDs := make([]uint, 0)
|
|
|
|
|
sessionIDs := make([]uint, 0, len(sessions))
|
|
|
|
|
for _, s := range sessions {
|
|
|
|
|
sessionIDs = append(sessionIDs, s.ID)
|
|
|
|
|
customerIDs = append(customerIDs, s.CustomerID)
|
|
|
|
|
channelIDs = append(channelIDs, s.ChannelID)
|
|
|
|
|
if s.AgentID != nil {
|
|
|
|
|
agentIDs = append(agentIDs, *s.AgentID)
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
customerMap := map[uint]model.Customer{}
|
|
|
|
|
if len(customerIDs) > 0 {
|
|
|
|
|
var customers []model.Customer
|
|
|
|
|
model.DB.Where("id IN ?", uniqueUint(customerIDs)).Find(&customers)
|
|
|
|
|
for _, cu := range customers {
|
|
|
|
|
customerMap[cu.ID] = cu
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
agentMap := map[uint]model.User{}
|
|
|
|
|
if len(agentIDs) > 0 {
|
|
|
|
|
var users []model.User
|
|
|
|
|
model.DB.Where("id IN ?", uniqueUint(agentIDs)).Find(&users)
|
|
|
|
|
for _, u := range users {
|
|
|
|
|
agentMap[u.ID] = u
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
channelMap := map[uint]model.Channel{}
|
|
|
|
|
if len(channelIDs) > 0 {
|
|
|
|
|
var channels []model.Channel
|
|
|
|
|
model.DB.Where("id IN ?", uniqueUint(channelIDs)).Find(&channels)
|
|
|
|
|
for _, ch := range channels {
|
|
|
|
|
channelMap[ch.ID] = ch
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
msgCountMap := map[uint]int{}
|
|
|
|
|
if len(sessionIDs) > 0 {
|
|
|
|
|
type row struct {
|
|
|
|
|
SessionID uint
|
|
|
|
|
Cnt int
|
|
|
|
|
}
|
|
|
|
|
var rows []row
|
|
|
|
|
model.DB.Model(&model.Message{}).
|
|
|
|
|
Select("session_id, COUNT(*) as cnt").
|
|
|
|
|
Where("session_id IN ?", sessionIDs).
|
|
|
|
|
Group("session_id").
|
|
|
|
|
Scan(&rows)
|
|
|
|
|
for _, r := range rows {
|
|
|
|
|
msgCountMap[r.SessionID] = r.Cnt
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
items := make([]SessionListItem, 0, len(sessions))
|
|
|
|
|
for _, session := range sessions {
|
|
|
|
|
item := SessionListItem{Session: session}
|
|
|
|
|
item := SessionListItem{Session: session, MessageCount: msgCountMap[session.ID]}
|
|
|
|
|
if middleware.GetRole(c) == "agent" && session.AgentID != nil && *session.AgentID == middleware.GetUserID(c) {
|
|
|
|
|
item.UnreadCount = unreadCount(session)
|
|
|
|
|
}
|
|
|
|
|
if cu, ok := customerMap[session.CustomerID]; ok {
|
|
|
|
|
item.CustomerName = cu.Name
|
|
|
|
|
}
|
|
|
|
|
if session.AgentID != nil {
|
|
|
|
|
if u, ok := agentMap[*session.AgentID]; ok {
|
|
|
|
|
item.AgentName = u.Nickname
|
|
|
|
|
}
|
|
|
|
|
}
|
|
|
|
|
if ch, ok := channelMap[session.ChannelID]; ok {
|
|
|
|
|
item.ChannelName = ch.Name
|
|
|
|
|
item.ChannelType = ch.Type
|
|
|
|
|
}
|
|
|
|
|
var lastMsg model.Message
|
|
|
|
|
if err := model.DB.Where("session_id = ?", session.ID).Order("seq desc").First(&lastMsg).Error; err == nil {
|
|
|
|
|
if lastMsg.Type == "image" {
|
|
|
|
@@ -216,6 +313,22 @@ func (h *SessionHandler) List(c *gin.Context) {
|
|
|
|
|
middleware.JSONList(c, items, total, page, pageSize)
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func uniqueUint(ids []uint) []uint {
|
|
|
|
|
seen := make(map[uint]struct{}, len(ids))
|
|
|
|
|
out := make([]uint, 0, len(ids))
|
|
|
|
|
for _, id := range ids {
|
|
|
|
|
if id == 0 {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
if _, ok := seen[id]; ok {
|
|
|
|
|
continue
|
|
|
|
|
}
|
|
|
|
|
seen[id] = struct{}{}
|
|
|
|
|
out = append(out, id)
|
|
|
|
|
}
|
|
|
|
|
return out
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
func (h *SessionHandler) Get(c *gin.Context) {
|
|
|
|
|
id := c.Param("id")
|
|
|
|
|
|
|
|
|
@@ -299,10 +412,14 @@ func (h *SessionHandler) ListAvailableAgents(c *gin.Context) {
|
|
|
|
|
Nickname string `json:"nickname"`
|
|
|
|
|
Status string `json:"status"`
|
|
|
|
|
}
|
|
|
|
|
// all=1 返回租户全部坐席(含离线),用于对话记录筛选;默认仅在线(工作台转接)
|
|
|
|
|
q := model.DB.Where("tenant_id = ? AND role IN ?", middleware.GetTenantID(c), []string{"agent", "supervisor", "admin"})
|
|
|
|
|
if c.Query("all") != "1" {
|
|
|
|
|
q = q.Where("role = ? AND status = ?", "agent", "online")
|
|
|
|
|
}
|
|
|
|
|
var users []model.User
|
|
|
|
|
if err := model.DB.Where("tenant_id = ? AND role = ? AND status = ?", middleware.GetTenantID(c), "agent", "online").
|
|
|
|
|
Order("nickname asc").Find(&users).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询在线客服失败"})
|
|
|
|
|
if err := q.Order("nickname asc").Find(&users).Error; err != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "查询客服失败"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
items := make([]agentItem, 0, len(users))
|
|
|
|
@@ -523,3 +640,71 @@ func (h *SessionHandler) UpdatePriority(c *gin.Context) {
|
|
|
|
|
|
|
|
|
|
middleware.JSON(c, gin.H{"message": "已更新"})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// Archive 将已结束会话归档(主管/管理员)
|
|
|
|
|
func (h *SessionHandler) Archive(c *gin.Context) {
|
|
|
|
|
if !isTenantManager(c) {
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅主管或管理员可归档"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
session, ok := loadTenantSession(c, c.Param("id"))
|
|
|
|
|
if !ok {
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if session.Status != "ended" {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "仅已结束的会话可归档"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
result := model.DB.Model(&model.Session{}).
|
|
|
|
|
Where("id = ? AND tenant_id = ? AND status = ?", session.ID, session.TenantID, "ended").
|
|
|
|
|
Update("status", "archived")
|
|
|
|
|
if result.Error != nil || result.RowsAffected == 0 {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "归档失败"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
model.DB.Create(&model.SessionEvent{
|
|
|
|
|
SessionID: session.ID,
|
|
|
|
|
OperatorID: middleware.GetUserID(c),
|
|
|
|
|
Action: "archive",
|
|
|
|
|
Detail: "会话已归档",
|
|
|
|
|
})
|
|
|
|
|
session.Status = "archived"
|
|
|
|
|
broadcastSessionUpdate(session)
|
|
|
|
|
middleware.JSON(c, gin.H{"message": "已归档", "session": session})
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
// BatchArchive 批量归档已结束会话
|
|
|
|
|
func (h *SessionHandler) BatchArchive(c *gin.Context) {
|
|
|
|
|
if !isTenantManager(c) {
|
|
|
|
|
c.JSON(http.StatusForbidden, gin.H{"code": 403, "message": "仅主管或管理员可归档"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
var req struct {
|
|
|
|
|
IDs []uint `json:"ids" binding:"required"`
|
|
|
|
|
}
|
|
|
|
|
if err := c.ShouldBindJSON(&req); err != nil || len(req.IDs) == 0 {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "请选择要归档的会话"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
if len(req.IDs) > 100 {
|
|
|
|
|
c.JSON(http.StatusBadRequest, gin.H{"code": 400, "message": "单次最多归档 100 条"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
tenantID := middleware.GetTenantID(c)
|
|
|
|
|
result := model.DB.Model(&model.Session{}).
|
|
|
|
|
Where("tenant_id = ? AND status = ? AND id IN ?", tenantID, "ended", req.IDs).
|
|
|
|
|
Update("status", "archived")
|
|
|
|
|
if result.Error != nil {
|
|
|
|
|
c.JSON(http.StatusInternalServerError, gin.H{"code": 500, "message": "批量归档失败"})
|
|
|
|
|
return
|
|
|
|
|
}
|
|
|
|
|
for _, id := range req.IDs {
|
|
|
|
|
model.DB.Create(&model.SessionEvent{
|
|
|
|
|
SessionID: id,
|
|
|
|
|
OperatorID: middleware.GetUserID(c),
|
|
|
|
|
Action: "archive",
|
|
|
|
|
Detail: "会话已归档",
|
|
|
|
|
})
|
|
|
|
|
}
|
|
|
|
|
middleware.JSON(c, gin.H{"message": "已归档", "count": result.RowsAffected})
|
|
|
|
|
}
|
|
|
|
|