fix(chat): 回复后再移出待处理
This commit is contained in:
@@ -99,6 +99,9 @@ func TestListAdminConversationsUsesPersonalStateAndBatchData(t *testing.T) {
|
||||
if item.UnreadCount != 1 {
|
||||
t.Fatalf("管理员未读数 = %d, want 1", item.UnreadCount)
|
||||
}
|
||||
if !item.NeedsReply {
|
||||
t.Fatal("用户消息未回复时应保持待处理")
|
||||
}
|
||||
counts, err := repo.AdminConversationCounts(t.Context(), Principal{Type: "admin", ID: admin.ID}, adminChatFilterMine, adminChatStageAll, "")
|
||||
if err != nil {
|
||||
t.Fatalf("查询客服统计失败: %v", err)
|
||||
@@ -106,4 +109,23 @@ func TestListAdminConversationsUsesPersonalStateAndBatchData(t *testing.T) {
|
||||
if counts.Ownership[adminChatFilterMine] != 1 || counts.Stages[adminChatStageAll] != 1 {
|
||||
t.Fatalf("聚合统计异常: ownership=%v stages=%v", counts.Ownership, counts.Stages)
|
||||
}
|
||||
|
||||
adminMessage := model.ChatMessage{ConversationID: conversation.ID, SenderType: "admin", SenderID: admin.ID, SenderRole: "support", ContentType: "text", Content: "客服回复", CreatedAt: now.Add(2 * time.Second)}
|
||||
if err := db.Create(&adminMessage).Error; err != nil {
|
||||
t.Fatalf("创建客服回复失败: %v", err)
|
||||
}
|
||||
if err := db.Model(&conversation).Updates(map[string]interface{}{
|
||||
"last_message_id": adminMessage.ID,
|
||||
"last_message_preview": adminMessage.Content,
|
||||
"last_message_at": adminMessage.CreatedAt,
|
||||
}).Error; err != nil {
|
||||
t.Fatalf("更新客服回复摘要失败: %v", err)
|
||||
}
|
||||
result, err = repo.ListConversationsWithFilter(t.Context(), Principal{Type: "admin", ID: admin.ID}, 1, 20, adminChatFilterMine, adminChatStagePending, "")
|
||||
if err != nil {
|
||||
t.Fatalf("查询已回复待处理会话失败: %v", err)
|
||||
}
|
||||
if result.Total != 0 {
|
||||
t.Fatalf("客服回复后待处理数量 = %d, want 0", result.Total)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -30,6 +30,7 @@ type ConversationDTO struct {
|
||||
LastSenderID uint64 `json:"last_sender_id,omitempty"`
|
||||
LastSenderRole string `json:"last_sender_role,omitempty"`
|
||||
UnreadCount int64 `json:"unread_count"`
|
||||
NeedsReply bool `json:"needs_reply"`
|
||||
CreatedAt time.Time `json:"created_at"`
|
||||
UpdatedAt time.Time `json:"updated_at"`
|
||||
}
|
||||
|
||||
@@ -33,6 +33,7 @@ type conversationRow struct {
|
||||
LastSenderID uint64
|
||||
LastSenderRole string
|
||||
UnreadCount int64
|
||||
NeedsReply int64
|
||||
CreatedAt time.Time
|
||||
UpdatedAt time.Time
|
||||
}
|
||||
@@ -333,6 +334,7 @@ func (row conversationRow) toDTO(participants []ParticipantDTO) ConversationDTO
|
||||
LastSenderID: row.LastSenderID,
|
||||
LastSenderRole: row.LastSenderRole,
|
||||
UnreadCount: row.UnreadCount,
|
||||
NeedsReply: row.NeedsReply > 0,
|
||||
CreatedAt: row.CreatedAt,
|
||||
UpdatedAt: row.UpdatedAt,
|
||||
}
|
||||
|
||||
@@ -239,6 +239,7 @@ func (r *Repository) listAdminConversations(ctx context.Context, principal Princ
|
||||
COALESCE(explicit_lo.handoff_status, listing_lo.handoff_status) AS latest_handoff_status,
|
||||
COALESCE(explicit_lo.refund_status, listing_lo.refund_status) AS latest_refund_status,
|
||||
lm.sender_type AS last_sender_type, lm.sender_id AS last_sender_id, lm.sender_role AS last_sender_role,
|
||||
CASE WHEN ` + adminNeedsReplyExpression() + ` THEN 1 ELSE 0 END AS needs_reply,
|
||||
(
|
||||
SELECT COUNT(1)
|
||||
FROM chat_messages AS cm
|
||||
@@ -414,13 +415,7 @@ func adminChatStageCondition(stage string, principal Principal) (string, []inter
|
||||
latestRefundStatus := "COALESCE(explicit_lo.refund_status, listing_lo.refund_status)"
|
||||
switch stage {
|
||||
case adminChatStagePending:
|
||||
return `EXISTS (
|
||||
SELECT 1
|
||||
FROM chat_messages AS cm_pending
|
||||
WHERE cm_pending.conversation_id = c.id
|
||||
AND (cm_pending.admin_attention_type <> '' OR cm_pending.sender_type = 'user')
|
||||
AND cm_pending.id > COALESCE(cas.last_read_message_id, 0)
|
||||
)`, nil
|
||||
return adminNeedsReplyExpression(), nil
|
||||
case adminChatStageUnjoined:
|
||||
return latestID + " IS NULL", nil
|
||||
case adminChatStageHandoff:
|
||||
@@ -438,6 +433,20 @@ func adminChatStageCondition(stage string, principal Principal) (string, []inter
|
||||
}
|
||||
}
|
||||
|
||||
func adminNeedsReplyExpression() string {
|
||||
return `COALESCE((
|
||||
SELECT MAX(cm_attention.id)
|
||||
FROM chat_messages AS cm_attention
|
||||
WHERE cm_attention.conversation_id = c.id
|
||||
AND (cm_attention.admin_attention_type <> '' OR cm_attention.sender_type = 'user')
|
||||
), 0) > COALESCE((
|
||||
SELECT MAX(cm_admin.id)
|
||||
FROM chat_messages AS cm_admin
|
||||
WHERE cm_admin.conversation_id = c.id
|
||||
AND cm_admin.sender_type = 'admin'
|
||||
), 0)`
|
||||
}
|
||||
|
||||
// ArchiveListingConversation 将商品关联的发布群标记为已归档(解散),并写入系统提示。
|
||||
// 用于客服封存订单时同事务解散群聊;发布群不存在时静默跳过,不阻断封存流程。
|
||||
// 归档后 SendMessage 的 status 校验会阻断所有成员继续发言。
|
||||
|
||||
Reference in New Issue
Block a user