fix(chat): 修复一键清理未读失败
This commit is contained in:
@@ -82,6 +82,19 @@ func (h *Handler) AdminMarkRead(c *gin.Context) {
|
|||||||
h.markRead(c, Principal{Type: "admin", ID: adminID})
|
h.markRead(c, Principal{Type: "admin", ID: adminID})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) AdminMarkAllRead(c *gin.Context) {
|
||||||
|
adminID, ok := currentAdminID(c)
|
||||||
|
if !ok {
|
||||||
|
response.Unauthorized(c, "缺少管理员上下文")
|
||||||
|
return
|
||||||
|
}
|
||||||
|
if err := h.service.MarkAllAdminConversationsRead(c.Request.Context(), adminID); err != nil {
|
||||||
|
writeChatError(c, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
response.OK(c, gin.H{"read": true})
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) AdminTransfer(c *gin.Context) {
|
func (h *Handler) AdminTransfer(c *gin.Context) {
|
||||||
adminID, ok := currentAdminID(c)
|
adminID, ok := currentAdminID(c)
|
||||||
if !ok {
|
if !ok {
|
||||||
|
|||||||
@@ -239,3 +239,31 @@ func (r *Repository) MarkRead(ctx context.Context, principal Principal, conversa
|
|||||||
}
|
}
|
||||||
return nil
|
return nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// MarkAllAdminConversationsRead advances one admin's read cursor for every conversation in one transaction.
|
||||||
|
func (r *Repository) MarkAllAdminConversationsRead(ctx context.Context, adminID uint64) error {
|
||||||
|
now := time.Now()
|
||||||
|
return r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
|
||||||
|
// 先补齐状态行,再更新游标。拆成两步可兼容本地 MySQL/MariaDB 对
|
||||||
|
// INSERT ... SELECT 同表读取并 ON DUPLICATE KEY UPDATE 的限制。
|
||||||
|
if err := tx.Exec(`
|
||||||
|
INSERT INTO chat_admin_conversation_states
|
||||||
|
(conversation_id, admin_user_id, remark, last_read_message_id, last_read_at)
|
||||||
|
SELECT c.id, ?, COALESCE(existing.remark, ''), COALESCE(c.last_message_id, 0), ?
|
||||||
|
FROM chat_conversations AS c
|
||||||
|
LEFT JOIN chat_admin_conversation_states AS existing
|
||||||
|
ON existing.conversation_id = c.id AND existing.admin_user_id = ?
|
||||||
|
WHERE existing.id IS NULL`, adminID, now, adminID).Error; err != nil {
|
||||||
|
return err
|
||||||
|
}
|
||||||
|
return tx.Exec(`
|
||||||
|
UPDATE chat_admin_conversation_states AS cas
|
||||||
|
JOIN chat_conversations AS c ON c.id = cas.conversation_id
|
||||||
|
SET cas.last_read_message_id = c.last_message_id,
|
||||||
|
cas.last_read_at = ?,
|
||||||
|
cas.updated_at = CURRENT_TIMESTAMP
|
||||||
|
WHERE cas.admin_user_id = ?
|
||||||
|
AND c.last_message_id IS NOT NULL
|
||||||
|
AND cas.last_read_message_id < c.last_message_id`, now, adminID).Error
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|||||||
@@ -131,6 +131,16 @@ func (s *Service) MarkRead(ctx context.Context, principal Principal, conversatio
|
|||||||
return s.repo.MarkRead(ctx, principal, conversationID)
|
return s.repo.MarkRead(ctx, principal, conversationID)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (s *Service) MarkAllAdminConversationsRead(ctx context.Context, adminID uint64) error {
|
||||||
|
if s.repo == nil {
|
||||||
|
return ErrDependencyUnavailable
|
||||||
|
}
|
||||||
|
if adminID == 0 {
|
||||||
|
return ErrPermissionDenied
|
||||||
|
}
|
||||||
|
return s.repo.MarkAllAdminConversationsRead(ctx, adminID)
|
||||||
|
}
|
||||||
|
|
||||||
func (s *Service) TransferConversation(ctx context.Context, principal Principal, conversationID uint64, req TransferRequest) error {
|
func (s *Service) TransferConversation(ctx context.Context, principal Principal, conversationID uint64, req TransferRequest) error {
|
||||||
if s.repo == nil {
|
if s.repo == nil {
|
||||||
return ErrDependencyUnavailable
|
return ErrDependencyUnavailable
|
||||||
|
|||||||
@@ -727,6 +727,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine {
|
|||||||
}
|
}
|
||||||
adminRoutes.GET("/chats", requirePerm("chat:view"), chatHandler.AdminList)
|
adminRoutes.GET("/chats", requirePerm("chat:view"), chatHandler.AdminList)
|
||||||
adminRoutes.GET("/chats/counts", requirePerm("chat:view"), chatHandler.AdminCounts)
|
adminRoutes.GET("/chats/counts", requirePerm("chat:view"), chatHandler.AdminCounts)
|
||||||
|
adminRoutes.POST("/chats/read-all", requirePerm("chat:view"), chatHandler.AdminMarkAllRead)
|
||||||
adminRoutes.GET("/chats/:id", requirePerm("chat:view"), chatHandler.AdminDetail)
|
adminRoutes.GET("/chats/:id", requirePerm("chat:view"), chatHandler.AdminDetail)
|
||||||
adminRoutes.GET("/chats/:id/messages", requirePerm("chat:view"), chatHandler.AdminMessages)
|
adminRoutes.GET("/chats/:id/messages", requirePerm("chat:view"), chatHandler.AdminMessages)
|
||||||
adminRoutes.POST("/chats/:id/messages", requirePerm("chat:send"), chatHandler.AdminSend)
|
adminRoutes.POST("/chats/:id/messages", requirePerm("chat:send"), chatHandler.AdminSend)
|
||||||
|
|||||||
@@ -1,8 +1,8 @@
|
|||||||
<script setup lang="ts">
|
<script setup lang="ts">
|
||||||
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
import { computed, nextTick, onBeforeUnmount, onMounted, ref } from 'vue'
|
||||||
import { useRoute } from 'vue-router'
|
import { useRoute } from 'vue-router'
|
||||||
import { ElMessage } from 'element-plus'
|
import { ElMessage, ElMessageBox } from 'element-plus'
|
||||||
import { Close, Picture } from '@element-plus/icons-vue'
|
import { Check, Close, Picture } from '@element-plus/icons-vue'
|
||||||
import {
|
import {
|
||||||
fetchAdminChat,
|
fetchAdminChat,
|
||||||
fetchAdminChatMessages,
|
fetchAdminChatMessages,
|
||||||
@@ -10,6 +10,7 @@ import {
|
|||||||
fetchAdminChats,
|
fetchAdminChats,
|
||||||
fetchQuickReplies,
|
fetchQuickReplies,
|
||||||
markAdminChatRead,
|
markAdminChatRead,
|
||||||
|
markAllAdminChatsRead,
|
||||||
sendAdminChatMessage,
|
sendAdminChatMessage,
|
||||||
updateChatRemark,
|
updateChatRemark,
|
||||||
type AdminChatCounts,
|
type AdminChatCounts,
|
||||||
@@ -54,6 +55,7 @@ const activePaymentRecords = ref<AdminPayment[]>([])
|
|||||||
const loading = ref(false)
|
const loading = ref(false)
|
||||||
const messageLoading = ref(false)
|
const messageLoading = ref(false)
|
||||||
const sending = ref(false)
|
const sending = ref(false)
|
||||||
|
const clearingAll = ref(false)
|
||||||
const uploading = ref(false)
|
const uploading = ref(false)
|
||||||
const content = ref('')
|
const content = ref('')
|
||||||
const attachments = ref<string[]>([])
|
const attachments = ref<string[]>([])
|
||||||
@@ -265,6 +267,29 @@ function refreshAll() {
|
|||||||
void Promise.all([loadConversations(true), loadCounts(true)])
|
void Promise.all([loadConversations(true), loadCounts(true)])
|
||||||
}
|
}
|
||||||
|
|
||||||
|
async function clearAllUnread() {
|
||||||
|
if (clearingAll.value) return
|
||||||
|
try {
|
||||||
|
await ElMessageBox.confirm(
|
||||||
|
'将把全部客服会话标记为已读,不会删除聊天记录。后续新消息仍会正常提醒。',
|
||||||
|
'一键清理未读提醒',
|
||||||
|
{ type: 'warning', confirmButtonText: '确认清理', cancelButtonText: '取消' }
|
||||||
|
)
|
||||||
|
} catch {
|
||||||
|
return
|
||||||
|
}
|
||||||
|
clearingAll.value = true
|
||||||
|
try {
|
||||||
|
await markAllAdminChatsRead()
|
||||||
|
await Promise.all([loadConversations(true), loadCounts(true)])
|
||||||
|
ElMessage.success('未读提醒已清理')
|
||||||
|
} catch {
|
||||||
|
ElMessage.error('清理失败,请稍后重试')
|
||||||
|
} finally {
|
||||||
|
clearingAll.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
async function loadQuickReplies() {
|
async function loadQuickReplies() {
|
||||||
try {
|
try {
|
||||||
quickReplies.value = await fetchQuickReplies()
|
quickReplies.value = await fetchQuickReplies()
|
||||||
@@ -654,6 +679,7 @@ function firstQueryValue(value: unknown) {
|
|||||||
<div class="head-right">
|
<div class="head-right">
|
||||||
<NotificationSettings scope="admin" />
|
<NotificationSettings scope="admin" />
|
||||||
<el-button @click="quickReplyVisible = true">快捷回复管理</el-button>
|
<el-button @click="quickReplyVisible = true">快捷回复管理</el-button>
|
||||||
|
<el-button :loading="clearingAll" :icon="Check" @click="clearAllUnread">一键清理</el-button>
|
||||||
<el-button :loading="loading" @click="refreshAll">刷新</el-button>
|
<el-button :loading="loading" @click="refreshAll">刷新</el-button>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
|
|||||||
@@ -170,6 +170,11 @@ export async function fetchAdminChatCounts(query: AdminChatQuery = {}) {
|
|||||||
return data.data
|
return data.data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function markAllAdminChatsRead() {
|
||||||
|
const { data } = await apiClient.post<ApiResponse<{ read: boolean }>>('/admin/chats/read-all')
|
||||||
|
return data.data
|
||||||
|
}
|
||||||
|
|
||||||
export async function fetchAdminChat(id: number) {
|
export async function fetchAdminChat(id: number) {
|
||||||
const { data } = await apiClient.get<ApiResponse<ChatConversation>>(`/admin/chats/${id}`)
|
const { data } = await apiClient.get<ApiResponse<ChatConversation>>(`/admin/chats/${id}`)
|
||||||
return data.data
|
return data.data
|
||||||
|
|||||||
Reference in New Issue
Block a user