支持客服入口按业务场景分流到客服分组
联系客服支持 scene(general/mohong),按用户与场景复用会话;摸大红入口走摸大红分组选人。
This commit is contained in:
@@ -58,6 +58,7 @@ func (r *Repository) FindConversation(ctx context.Context, principal Principal,
|
||||
OrderID: conversation.OrderID,
|
||||
ListingID: conversation.ListingID,
|
||||
Type: conversation.Type,
|
||||
SupportScene: conversation.SupportScene,
|
||||
Title: conversation.Title,
|
||||
Status: conversation.Status,
|
||||
Role: "admin", // 管理员角色
|
||||
@@ -131,20 +132,43 @@ func (r *Repository) FindOrderConversation(ctx context.Context, userID uint64, o
|
||||
dto := row.toDTO(participants)
|
||||
return &dto, nil
|
||||
}
|
||||
func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint64) (*ConversationDTO, error) {
|
||||
func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint64, scene string) (*ConversationDTO, error) {
|
||||
scene, title, groupCode, welcome := resolveSupportScene(scene)
|
||||
var conversationID uint64
|
||||
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||
var existing model.ChatConversation
|
||||
err := tx.Table("chat_conversations AS c").
|
||||
Select("c.*").
|
||||
Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id").
|
||||
Where("c.type = ? AND cp.participant_type = ? AND cp.participant_id = ?", "general_support", "user", userID).
|
||||
Where(
|
||||
"c.type = ? AND c.support_scene = ? AND cp.participant_type = ? AND cp.participant_id = ?",
|
||||
ConversationTypeGeneralSupport, scene, "user", userID,
|
||||
).
|
||||
Order("c.id ASC").
|
||||
Limit(1).
|
||||
Find(&existing).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
// 兼容旧数据:scene=general 时也匹配未回填 support_scene 的历史会话
|
||||
if existing.ID == 0 && scene == SupportSceneGeneral {
|
||||
err = tx.Table("chat_conversations AS c").
|
||||
Select("c.*").
|
||||
Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id").
|
||||
Where(
|
||||
"c.type = ? AND (c.support_scene = '' OR c.support_scene IS NULL) AND cp.participant_type = ? AND cp.participant_id = ?",
|
||||
ConversationTypeGeneralSupport, "user", userID,
|
||||
).
|
||||
Order("c.id ASC").
|
||||
Limit(1).
|
||||
Find(&existing).Error
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if existing.ID > 0 && existing.SupportScene == "" {
|
||||
_ = tx.Model(&existing).Update("support_scene", SupportSceneGeneral).Error
|
||||
}
|
||||
}
|
||||
if existing.ID > 0 {
|
||||
conversationID = existing.ID
|
||||
return nil
|
||||
@@ -152,9 +176,10 @@ func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint6
|
||||
|
||||
now := time.Now()
|
||||
conversation := model.ChatConversation{
|
||||
Type: "general_support",
|
||||
Title: "平台客服",
|
||||
Status: "active",
|
||||
Type: ConversationTypeGeneralSupport,
|
||||
SupportScene: scene,
|
||||
Title: title,
|
||||
Status: "active",
|
||||
}
|
||||
if err := tx.Create(&conversation).Error; err != nil {
|
||||
return err
|
||||
@@ -169,7 +194,7 @@ func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint6
|
||||
JoinedAt: now,
|
||||
},
|
||||
}
|
||||
if supportID := defaultSupportAdminID(tx); supportID > 0 {
|
||||
if supportID := pickSupportAdminForScene(tx, groupCode); supportID > 0 {
|
||||
participants = append(participants, model.ChatParticipant{
|
||||
ConversationID: conversation.ID,
|
||||
ParticipantType: "admin",
|
||||
@@ -189,7 +214,7 @@ func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint6
|
||||
SenderType: "system",
|
||||
SenderRole: "system",
|
||||
ContentType: "system",
|
||||
Content: "您好,客服会尽快回复,请直接描述您遇到的问题。",
|
||||
Content: welcome,
|
||||
AttachmentURLS: emptyJSONList(),
|
||||
}
|
||||
if err := tx.Create(&message).Error; err != nil {
|
||||
|
||||
@@ -17,6 +17,7 @@ type ConversationDTO struct {
|
||||
LatestHandoffStatus string `json:"latest_handoff_status,omitempty"`
|
||||
LatestRefundStatus string `json:"latest_refund_status,omitempty"`
|
||||
Type string `json:"type"`
|
||||
SupportScene string `json:"support_scene,omitempty"`
|
||||
Title string `json:"title"`
|
||||
Status string `json:"status"`
|
||||
Role string `json:"role"`
|
||||
@@ -32,6 +33,12 @@ type ConversationDTO struct {
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
// EnsureSupportRequest 创建/复用客服会话。
|
||||
type EnsureSupportRequest struct {
|
||||
// Scene 业务场景:general(默认)/ mohong(摸大红)
|
||||
Scene string `json:"scene"`
|
||||
}
|
||||
|
||||
type ParticipantDTO struct {
|
||||
ID uint64 `json:"id"`
|
||||
ConversationID uint64 `json:"conversation_id"`
|
||||
|
||||
@@ -62,7 +62,10 @@ func (h *Handler) EnsureSupportConversation(c *gin.Context) {
|
||||
response.Unauthorized(c, "缺少用户上下文")
|
||||
return
|
||||
}
|
||||
item, err := h.service.EnsureSupportConversation(c.Request.Context(), userID)
|
||||
var req EnsureSupportRequest
|
||||
// scene 可选;空 body 仍按平台客服处理
|
||||
_ = c.ShouldBindJSON(&req)
|
||||
item, err := h.service.EnsureSupportConversation(c.Request.Context(), userID, req.Scene)
|
||||
if err != nil {
|
||||
writeChatError(c, err)
|
||||
return
|
||||
|
||||
@@ -21,6 +21,7 @@ type conversationRow struct {
|
||||
LatestHandoffStatus string
|
||||
LatestRefundStatus string
|
||||
Type string
|
||||
SupportScene string
|
||||
Title string
|
||||
Status string
|
||||
Role string
|
||||
@@ -37,7 +38,7 @@ type conversationRow struct {
|
||||
|
||||
func (r *Repository) conversationQuery(ctx context.Context, principal Principal) *gorm.DB {
|
||||
return r.db.WithContext(ctx).Table("chat_conversations AS c").
|
||||
Select(`c.id, c.order_id, c.listing_id, c.type, c.title, c.status, c.last_message_id,
|
||||
Select(`c.id, c.order_id, c.listing_id, c.type, c.support_scene, c.title, c.status, c.last_message_id,
|
||||
c.last_message_preview, c.last_message_at, c.created_at, c.updated_at, cp.role,
|
||||
(
|
||||
SELECT COUNT(1)
|
||||
@@ -300,6 +301,7 @@ func (row conversationRow) toDTO(participants []ParticipantDTO) ConversationDTO
|
||||
LatestHandoffStatus: row.LatestHandoffStatus,
|
||||
LatestRefundStatus: row.LatestRefundStatus,
|
||||
Type: row.Type,
|
||||
SupportScene: row.SupportScene,
|
||||
Title: row.Title,
|
||||
Status: row.Status,
|
||||
Role: row.Role,
|
||||
|
||||
@@ -54,14 +54,14 @@ func (s *Service) FindOrderConversation(ctx context.Context, userID uint64, orde
|
||||
return s.repo.FindOrderConversation(ctx, userID, orderID)
|
||||
}
|
||||
|
||||
func (s *Service) EnsureSupportConversation(ctx context.Context, userID uint64) (*ConversationDTO, error) {
|
||||
func (s *Service) EnsureSupportConversation(ctx context.Context, userID uint64, scene string) (*ConversationDTO, error) {
|
||||
if s.repo == nil {
|
||||
return nil, ErrDependencyUnavailable
|
||||
}
|
||||
if userID == 0 {
|
||||
return nil, ErrPermissionDenied
|
||||
}
|
||||
return s.repo.EnsureSupportConversation(ctx, userID)
|
||||
return s.repo.EnsureSupportConversation(ctx, userID, scene)
|
||||
}
|
||||
|
||||
func (s *Service) Messages(ctx context.Context, principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
|
||||
@@ -213,7 +213,7 @@ func (r *Repository) listAdminConversations(ctx context.Context, principal Princ
|
||||
var rows []conversationRow
|
||||
offset := (page - 1) * pageSize
|
||||
queryDB := r.adminConversationBase(ctx, principal, keyword).
|
||||
Select(`c.id, c.order_id, c.listing_id, c.type, c.title, c.status, c.last_message_id,
|
||||
Select(`c.id, c.order_id, c.listing_id, c.type, c.support_scene, c.title, c.status, c.last_message_id,
|
||||
c.last_message_preview, c.last_message_at, c.created_at, c.updated_at,
|
||||
COALESCE(cp_me.role, 'admin') AS role,
|
||||
lo.id AS latest_order_id, lo.order_no AS latest_order_no, lo.status AS latest_order_status,
|
||||
|
||||
@@ -0,0 +1,44 @@
|
||||
package chat
|
||||
|
||||
import (
|
||||
"strings"
|
||||
|
||||
"hfb_sys/backend/internal/modules/supportgroup"
|
||||
|
||||
"gorm.io/gorm"
|
||||
)
|
||||
|
||||
// 客服入口业务场景(与前端 POST /chats/support 的 scene 对齐)。
|
||||
const (
|
||||
SupportSceneGeneral = "general"
|
||||
SupportSceneMohong = "mohong"
|
||||
)
|
||||
|
||||
// resolveSupportScene 规范化场景并返回标题、客服分组 code、欢迎语。
|
||||
// groupCode 为空时表示不走分组,使用默认客服选取逻辑。
|
||||
func resolveSupportScene(raw string) (scene, title, groupCode, welcome string) {
|
||||
switch strings.ToLower(strings.TrimSpace(raw)) {
|
||||
case SupportSceneMohong:
|
||||
return SupportSceneMohong,
|
||||
"摸大红客服",
|
||||
supportgroup.GroupCodeMohong,
|
||||
"您好,这里是摸大红客服。请直接描述问题,可将订单信息复制后发送。"
|
||||
default:
|
||||
// 未知场景回落平台客服,避免随意 scene 绕开分配策略
|
||||
return SupportSceneGeneral,
|
||||
"平台客服",
|
||||
"",
|
||||
"您好,客服会尽快回复,请直接描述您遇到的问题。"
|
||||
}
|
||||
}
|
||||
|
||||
// pickSupportAdminForScene 按分组选人;分组无成员或失败时回退默认客服逻辑。
|
||||
func pickSupportAdminForScene(tx *gorm.DB, groupCode string) uint64 {
|
||||
groupCode = strings.TrimSpace(groupCode)
|
||||
if groupCode != "" {
|
||||
if id, err := supportgroup.PickSupportAdmin(tx, groupCode); err == nil && id > 0 {
|
||||
return id
|
||||
}
|
||||
}
|
||||
return defaultSupportAdminID(tx)
|
||||
}
|
||||
Reference in New Issue
Block a user