管理员可以看到所有会话了
This commit is contained in:
@@ -149,6 +149,38 @@ func (r *Repository) ListConversations(principal Principal, page, pageSize int)
|
||||
}
|
||||
|
||||
func (r *Repository) FindConversation(principal Principal, id uint64) (*ConversationDTO, error) {
|
||||
// 管理员可以查看任意会话,无需是 participant
|
||||
if principal.Type == "admin" {
|
||||
var conversation model.ChatConversation
|
||||
if err := r.db.First(&conversation, id).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, ErrConversationNotFound
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
participants, err := r.participants(conversation.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
dto := ConversationDTO{
|
||||
ID: conversation.ID,
|
||||
OrderID: conversation.OrderID,
|
||||
Type: conversation.Type,
|
||||
Title: conversation.Title,
|
||||
Status: conversation.Status,
|
||||
Role: "admin", // 管理员角色
|
||||
Participants: participants,
|
||||
LastMessageID: conversation.LastMessageID,
|
||||
LastMessagePreview: conversation.LastMessagePreview,
|
||||
LastMessageAt: conversation.LastMessageAt,
|
||||
UnreadCount: 0, // 管理员不计未读
|
||||
CreatedAt: conversation.CreatedAt,
|
||||
UpdatedAt: conversation.UpdatedAt,
|
||||
}
|
||||
return &dto, nil
|
||||
}
|
||||
|
||||
// 普通用户需要是 participant
|
||||
var row conversationRow
|
||||
err := r.conversationQuery(principal).Where("c.id = ?", id).First(&row).Error
|
||||
if err != nil {
|
||||
@@ -269,9 +301,23 @@ func (r *Repository) EnsureSupportConversation(userID uint64) (*ConversationDTO,
|
||||
|
||||
func (r *Repository) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) {
|
||||
page, pageSize = normalizePagination(page, pageSize)
|
||||
if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil {
|
||||
return nil, err
|
||||
|
||||
// 管理员可以查看任意会话的消息,普通用户需要是 participant
|
||||
if principal.Type != "admin" {
|
||||
if _, err := r.findParticipant(r.db, principal, conversationID, false); err != nil {
|
||||
return nil, err
|
||||
}
|
||||
} else {
|
||||
// 管理员需要验证会话存在
|
||||
var count int64
|
||||
if err := r.db.Model(&model.ChatConversation{}).Where("id = ?", conversationID).Count(&count).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
if count == 0 {
|
||||
return nil, ErrConversationNotFound
|
||||
}
|
||||
}
|
||||
|
||||
var total int64
|
||||
if err := r.db.Model(&model.ChatMessage{}).Where("conversation_id = ?", conversationID).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
@@ -295,10 +341,6 @@ func (r *Repository) Messages(principal Principal, conversationID uint64, page,
|
||||
func (r *Repository) SendMessage(principal Principal, conversationID uint64, req SendMessageRequest) (*MessageDTO, error) {
|
||||
var messageID uint64
|
||||
err := r.db.Transaction(func(tx *gorm.DB) error {
|
||||
participant, err := r.findParticipant(tx, principal, conversationID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
var conversation model.ChatConversation
|
||||
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&conversation, conversationID).Error; err != nil {
|
||||
return err
|
||||
@@ -306,11 +348,41 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req
|
||||
if conversation.Status != "active" {
|
||||
return ErrPermissionDenied
|
||||
}
|
||||
|
||||
var senderRole string
|
||||
var participant *model.ChatParticipant
|
||||
|
||||
// 管理员可以在任意会话发送消息,无需是 participant
|
||||
if principal.Type == "admin" {
|
||||
// 尝试查找管理员的 participant 记录
|
||||
var p model.ChatParticipant
|
||||
err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?",
|
||||
conversationID, principal.Type, principal.ID).First(&p).Error
|
||||
if err == nil {
|
||||
// 管理员是 participant,使用其角色
|
||||
participant = &p
|
||||
senderRole = p.Role
|
||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 管理员不是 participant,使用特殊角色 "admin"
|
||||
senderRole = "admin"
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
} else {
|
||||
// 普通用户必须是 participant
|
||||
p, err := r.findParticipant(tx, principal, conversationID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
participant = p
|
||||
senderRole = p.Role
|
||||
}
|
||||
|
||||
message := model.ChatMessage{
|
||||
ConversationID: conversation.ID,
|
||||
SenderType: principal.Type,
|
||||
SenderID: principal.ID,
|
||||
SenderRole: participant.Role,
|
||||
SenderRole: senderRole,
|
||||
ContentType: "text",
|
||||
Content: req.Content,
|
||||
AttachmentURLS: encodeStringList(req.AttachmentURLS),
|
||||
@@ -324,10 +396,15 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req
|
||||
if err := tx.Save(&conversation).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
now := time.Now()
|
||||
if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil {
|
||||
return err
|
||||
|
||||
// 更新 participant 的已读时间(仅当是 participant 时)
|
||||
if participant != nil {
|
||||
now := time.Now()
|
||||
if err := tx.Model(participant).Update("last_read_at", now).Error; err != nil {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
messageID = message.ID
|
||||
return nil
|
||||
})
|
||||
@@ -370,6 +447,24 @@ func (r *Repository) SendMessage(principal Principal, conversationID uint64, req
|
||||
|
||||
func (r *Repository) MarkRead(principal Principal, conversationID uint64) error {
|
||||
return r.db.Transaction(func(tx *gorm.DB) error {
|
||||
// 管理员可以不是 participant,直接返回成功
|
||||
if principal.Type == "admin" {
|
||||
// 尝试查找 participant 记录,如果有就更新
|
||||
var participant model.ChatParticipant
|
||||
err := tx.Where("conversation_id = ? AND participant_type = ? AND participant_id = ?",
|
||||
conversationID, principal.Type, principal.ID).First(&participant).Error
|
||||
if err == nil {
|
||||
// 有 participant 记录,更新已读时间
|
||||
now := time.Now()
|
||||
return tx.Model(&participant).Update("last_read_at", now).Error
|
||||
} else if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
// 没有 participant 记录,直接返回成功(管理员无需记录已读)
|
||||
return nil
|
||||
}
|
||||
return err
|
||||
}
|
||||
|
||||
// 普通用户必须是 participant
|
||||
participant, err := r.findParticipant(tx, principal, conversationID, true)
|
||||
if err != nil {
|
||||
return err
|
||||
@@ -475,6 +570,10 @@ func (r *Repository) toMessageDTOs(principal Principal, rows []model.ChatMessage
|
||||
}
|
||||
if row.SenderType == "admin" {
|
||||
name = adminNames[row.SenderID]
|
||||
// 如果角色是 "admin"(不是 participant 的管理员),在名字后添加标识
|
||||
if row.SenderRole == "admin" {
|
||||
name = name + " (管理员)"
|
||||
}
|
||||
}
|
||||
items = append(items, MessageDTO{
|
||||
ID: row.ID,
|
||||
@@ -830,8 +929,50 @@ func (r *Repository) GetAvailableSupportAdmins() ([]SupportAdminDTO, error) {
|
||||
// ListConversationsWithFilter 支持筛选的会话列表
|
||||
func (r *Repository) ListConversationsWithFilter(principal Principal, page, pageSize int, filter string) (*PaginatedResult, error) {
|
||||
page, pageSize = normalizePagination(page, pageSize)
|
||||
var total int64
|
||||
|
||||
// 管理员在"全部"模式下直接查询所有会话
|
||||
if principal.Type == "admin" && filter == "all" {
|
||||
var total int64
|
||||
if err := r.db.Model(&model.ChatConversation{}).Count(&total).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
var conversations []model.ChatConversation
|
||||
offset := (page - 1) * pageSize
|
||||
if err := r.db.Order("COALESCE(last_message_at, created_at) DESC, id DESC").
|
||||
Offset(offset).
|
||||
Limit(pageSize).
|
||||
Find(&conversations).Error; err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
items := make([]ConversationDTO, 0, len(conversations))
|
||||
for _, conv := range conversations {
|
||||
participants, err := r.participants(conv.ID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
items = append(items, ConversationDTO{
|
||||
ID: conv.ID,
|
||||
OrderID: conv.OrderID,
|
||||
Type: conv.Type,
|
||||
Title: conv.Title,
|
||||
Status: conv.Status,
|
||||
Role: "admin", // 管理员角色
|
||||
Participants: participants,
|
||||
LastMessageID: conv.LastMessageID,
|
||||
LastMessagePreview: conv.LastMessagePreview,
|
||||
LastMessageAt: conv.LastMessageAt,
|
||||
UnreadCount: 0, // 管理员不计未读
|
||||
CreatedAt: conv.CreatedAt,
|
||||
UpdatedAt: conv.UpdatedAt,
|
||||
})
|
||||
}
|
||||
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
|
||||
}
|
||||
|
||||
// 其他情况使用原有逻辑
|
||||
var total int64
|
||||
countDB := r.db.Table("chat_conversations AS c").
|
||||
Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id")
|
||||
|
||||
@@ -844,12 +985,8 @@ func (r *Repository) ListConversationsWithFilter(principal Principal, page, page
|
||||
countDB = countDB.Where("c.id NOT IN (?)",
|
||||
r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support"))
|
||||
default:
|
||||
// 全部会话(admin 可以看所有)
|
||||
if principal.Type == "admin" {
|
||||
// 管理员看所有会话
|
||||
} else {
|
||||
countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
||||
}
|
||||
// 普通用户的全部会话
|
||||
countDB = countDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
||||
}
|
||||
|
||||
if err := countDB.Count(&total).Error; err != nil {
|
||||
@@ -867,11 +1004,8 @@ func (r *Repository) ListConversationsWithFilter(principal Principal, page, page
|
||||
queryDB = queryDB.Where("c.id NOT IN (?)",
|
||||
r.db.Table("chat_participants").Select("conversation_id").Where("participant_type = ? AND role = ?", "admin", "support"))
|
||||
default:
|
||||
if principal.Type == "admin" {
|
||||
// 管理员看所有会话
|
||||
} else {
|
||||
queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
||||
}
|
||||
// 普通用户的全部会话
|
||||
queryDB = queryDB.Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID)
|
||||
}
|
||||
|
||||
err := queryDB.
|
||||
|
||||
@@ -276,12 +276,17 @@ function roleLabel(role: string) {
|
||||
support: '客服',
|
||||
customer: '咨询',
|
||||
system: '系统',
|
||||
admin: '管理员',
|
||||
}
|
||||
return map[role] || '成员'
|
||||
}
|
||||
|
||||
function senderLabel(item: ChatMessage) {
|
||||
if (item.sender_type === 'system') return '系统'
|
||||
// 管理员消息特殊标识
|
||||
if (item.sender_role === 'admin') {
|
||||
return `管理员 · ${item.sender_name}`
|
||||
}
|
||||
return `${roleLabel(item.sender_role)} · ${item.sender_name}`
|
||||
}
|
||||
|
||||
@@ -374,13 +379,19 @@ function getSupportName(item: ChatConversation) {
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
class="message-row"
|
||||
:class="{ self: item.is_self, system: item.sender_type === 'system' }"
|
||||
:class="{
|
||||
self: item.is_self,
|
||||
system: item.sender_type === 'system',
|
||||
admin: item.sender_role === 'admin'
|
||||
}"
|
||||
>
|
||||
<template v-if="item.sender_type === 'system'">
|
||||
<span>{{ item.content }}</span>
|
||||
</template>
|
||||
<template v-else>
|
||||
<small>{{ senderLabel(item) }} · {{ formatDateMinute(item.created_at) }}</small>
|
||||
<small :class="{ 'admin-label': item.sender_role === 'admin' }">
|
||||
{{ senderLabel(item) }} · {{ formatDateMinute(item.created_at) }}
|
||||
</small>
|
||||
<p v-if="item.content">{{ item.content }}</p>
|
||||
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
||||
<ChatAttachmentImage
|
||||
@@ -678,6 +689,16 @@ function getSupportName(item: ChatConversation) {
|
||||
background: #dff5eb;
|
||||
}
|
||||
|
||||
.message-row.admin p {
|
||||
background: #fff4e6;
|
||||
border-left: 3px solid #ff9800;
|
||||
}
|
||||
|
||||
.admin-label {
|
||||
color: #ff9800 !important;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-attachments {
|
||||
display: grid;
|
||||
justify-items: start;
|
||||
|
||||
@@ -6,7 +6,7 @@ export interface ChatParticipant {
|
||||
conversation_id: number
|
||||
participant_type: 'user' | 'admin'
|
||||
participant_id: number
|
||||
role: 'renter' | 'owner' | 'support' | 'customer'
|
||||
role: 'renter' | 'owner' | 'support' | 'customer' | 'admin'
|
||||
remark: string
|
||||
display_name: string
|
||||
avatar_url: string
|
||||
@@ -20,7 +20,7 @@ export interface ChatConversation {
|
||||
type: string
|
||||
title: string
|
||||
status: string
|
||||
role: 'renter' | 'owner' | 'support' | 'customer'
|
||||
role: 'renter' | 'owner' | 'support' | 'customer' | 'admin'
|
||||
participants?: ChatParticipant[]
|
||||
last_message_id?: number
|
||||
last_message_preview: string
|
||||
@@ -35,7 +35,7 @@ export interface ChatMessage {
|
||||
conversation_id: number
|
||||
sender_type: 'user' | 'admin' | 'system'
|
||||
sender_id: number
|
||||
sender_role: 'renter' | 'owner' | 'support' | 'customer' | 'system'
|
||||
sender_role: 'renter' | 'owner' | 'support' | 'customer' | 'system' | 'admin'
|
||||
sender_name: string
|
||||
sender_avatar: string
|
||||
is_self: boolean
|
||||
|
||||
@@ -204,12 +204,17 @@ function roleLabel(role: string) {
|
||||
support: '客服',
|
||||
customer: '咨询',
|
||||
system: '系统',
|
||||
admin: '管理员',
|
||||
}
|
||||
return map[role] || '成员'
|
||||
}
|
||||
|
||||
function senderLabel(message: ChatMessage) {
|
||||
if (message.sender_type === 'system') return '系统'
|
||||
// 管理员消息特殊标识
|
||||
if (message.sender_role === 'admin') {
|
||||
return `管理员 · ${message.sender_name}`
|
||||
}
|
||||
return `${roleLabel(message.sender_role)} · ${message.sender_name}`
|
||||
}
|
||||
|
||||
@@ -255,7 +260,11 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
class="message-row"
|
||||
:class="{ self: item.is_self, system: item.sender_type === 'system' }"
|
||||
:class="{
|
||||
self: item.is_self,
|
||||
system: item.sender_type === 'system',
|
||||
admin: item.sender_role === 'admin' && !item.is_self
|
||||
}"
|
||||
>
|
||||
<template v-if="item.sender_type === 'system'">
|
||||
<span class="system-message">{{ item.content }}</span>
|
||||
@@ -263,7 +272,9 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
<template v-else>
|
||||
<div class="avatar" :class="{ self: item.is_self }">{{ roleLabel(item.sender_role).slice(0, 1) }}</div>
|
||||
<div class="bubble-wrap" :class="{ self: item.is_self }">
|
||||
<span class="sender-name">{{ senderLabel(item) }}</span>
|
||||
<span class="sender-name" :class="{ 'admin-label': item.sender_role === 'admin' && !item.is_self }">
|
||||
{{ senderLabel(item) }}
|
||||
</span>
|
||||
<div v-if="item.content" class="bubble">{{ item.content }}</div>
|
||||
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
||||
<ChatAttachmentImage
|
||||
@@ -469,6 +480,20 @@ function handleKeydown(e: KeyboardEvent) {
|
||||
color: #0f5132;
|
||||
}
|
||||
|
||||
.message-row.admin .bubble {
|
||||
background: #fff4e6;
|
||||
border-left: 3px solid #ff9800;
|
||||
}
|
||||
|
||||
.message-row.admin .avatar {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
.admin-label {
|
||||
color: #ff9800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-attachments {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
@@ -196,12 +196,17 @@ function roleLabel(role: string) {
|
||||
support: '客服',
|
||||
customer: '咨询',
|
||||
system: '系统',
|
||||
admin: '管理员',
|
||||
}
|
||||
return map[role] || '成员'
|
||||
}
|
||||
|
||||
function senderLabel(message: ChatMessage) {
|
||||
if (message.sender_type === 'system') return '系统'
|
||||
// 管理员消息特殊标识
|
||||
if (message.sender_role === 'admin') {
|
||||
return `管理员 · ${message.sender_name}`
|
||||
}
|
||||
return `${roleLabel(message.sender_role)} · ${message.sender_name}`
|
||||
}
|
||||
</script>
|
||||
@@ -228,7 +233,11 @@ function senderLabel(message: ChatMessage) {
|
||||
v-for="item in messages"
|
||||
:key="item.id"
|
||||
class="message-row"
|
||||
:class="{ self: item.is_self, system: item.sender_type === 'system' }"
|
||||
:class="{
|
||||
self: item.is_self,
|
||||
system: item.sender_type === 'system',
|
||||
admin: item.sender_role === 'admin' && !item.is_self
|
||||
}"
|
||||
>
|
||||
<template v-if="item.sender_type === 'system'">
|
||||
<span class="system-message">{{ item.content }}</span>
|
||||
@@ -236,7 +245,9 @@ function senderLabel(message: ChatMessage) {
|
||||
<template v-else>
|
||||
<div class="avatar">{{ roleLabel(item.sender_role).slice(0, 1) }}</div>
|
||||
<div class="bubble-wrap">
|
||||
<span class="sender-name">{{ senderLabel(item) }}</span>
|
||||
<span class="sender-name" :class="{ 'admin-label': item.sender_role === 'admin' && !item.is_self }">
|
||||
{{ senderLabel(item) }}
|
||||
</span>
|
||||
<div v-if="item.content" class="bubble">{{ item.content }}</div>
|
||||
<div v-if="item.attachment_urls.length > 0" class="message-attachments">
|
||||
<ChatAttachmentImage
|
||||
@@ -421,6 +432,20 @@ function senderLabel(message: ChatMessage) {
|
||||
background: #dff5eb;
|
||||
}
|
||||
|
||||
.message-row.admin .bubble {
|
||||
background: #fff4e6;
|
||||
border-left: 3px solid #ff9800;
|
||||
}
|
||||
|
||||
.message-row.admin .avatar {
|
||||
background: #ff9800;
|
||||
}
|
||||
|
||||
.admin-label {
|
||||
color: #ff9800;
|
||||
font-weight: 600;
|
||||
}
|
||||
|
||||
.message-attachments {
|
||||
display: grid;
|
||||
gap: 6px;
|
||||
|
||||
Reference in New Issue
Block a user