fix(chat): 回复后再移出待处理

This commit is contained in:
yml2213
2026-08-25 20:27:26 +08:00
parent 16237dcefc
commit cc91979b34
6 changed files with 44 additions and 9 deletions
@@ -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)
}
}
+1
View File
@@ -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,
}
+16 -7
View File
@@ -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 校验会阻断所有成员继续发言。
@@ -564,7 +564,7 @@ function stageCount(key: string) {
}
function conversationStageLabel(item: ChatConversation) {
if (item.unread_count > 0) return '待处理'
if (item.needs_reply) return '待处理'
if (!item.latest_order_id) return '无订单'
if (item.latest_order_status === 'pending_handoff') return '待交接'
if (['renting', 'overdue'].includes(item.latest_order_status || '')) return '使用中'
@@ -584,7 +584,7 @@ function conversationStageLabel(item: ChatConversation) {
}
function conversationStageClass(item: ChatConversation) {
if (item.unread_count > 0) return 'pending'
if (item.needs_reply) return 'pending'
if (!item.latest_order_id) return 'unjoined'
if (item.latest_order_status === 'pending_handoff') return 'handoff'
if (['renting', 'overdue'].includes(item.latest_order_status || '')) return 'renting'
+1
View File
@@ -39,6 +39,7 @@ export interface ChatConversation {
last_sender_id?: number
last_sender_role?: string
unread_count: number
needs_reply: boolean
created_at: string
updated_at: string
}