From 2346f2e0640c905b5c0b2cc70b63f4bd73642276 Mon Sep 17 00:00:00 2001 From: yml2213 Date: Fri, 29 May 2026 10:15:21 +0800 Subject: [PATCH] =?UTF-8?q?=E4=BC=98=E5=8C=96=E5=92=A8=E8=AF=A2=E5=AE=A2?= =?UTF-8?q?=E6=9C=8D?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- backend/internal/model/chat.go | 2 +- backend/internal/modules/chat/dto.go | 2 +- backend/internal/modules/chat/handler.go | 14 +++ backend/internal/modules/chat/repository.go | 87 ++++++++++++++++++- backend/internal/modules/chat/service.go | 10 +++ backend/internal/router/router.go | 1 + backend/migrations/000001_init.sql | 2 +- backend/migrations/000004_support_chat.sql | 2 + frontend/src/api/chats.ts | 13 ++- frontend/src/layouts/AppLayout.vue | 27 +++++- frontend/src/views/account/ChatView.vue | 5 +- frontend/src/views/account/MessagesView.vue | 10 +-- frontend/src/views/admin/AdminChatsView.vue | 9 +- frontend/src/views/mobile/MobileChatView.vue | 14 ++- frontend/src/views/mobile/MobileHomeView.vue | 29 ++++++- .../src/views/mobile/MobileMessagesView.vue | 7 +- 16 files changed, 202 insertions(+), 32 deletions(-) create mode 100644 backend/migrations/000004_support_chat.sql diff --git a/backend/internal/model/chat.go b/backend/internal/model/chat.go index 27af0f6..e114e96 100644 --- a/backend/internal/model/chat.go +++ b/backend/internal/model/chat.go @@ -8,7 +8,7 @@ import ( type ChatConversation struct { ID uint64 `gorm:"primaryKey" json:"id"` - OrderID uint64 `gorm:"not null;uniqueIndex" json:"order_id"` + OrderID *uint64 `gorm:"uniqueIndex" json:"order_id"` Type string `gorm:"size:32;not null;default:'order_group'" json:"type"` Title string `gorm:"size:128;not null" json:"title"` Status string `gorm:"size:32;not null;default:'active'" json:"status"` diff --git a/backend/internal/modules/chat/dto.go b/backend/internal/modules/chat/dto.go index 66d54a5..60fc1d6 100644 --- a/backend/internal/modules/chat/dto.go +++ b/backend/internal/modules/chat/dto.go @@ -9,7 +9,7 @@ type Principal struct { type ConversationDTO struct { ID uint64 `json:"id"` - OrderID uint64 `json:"order_id"` + OrderID *uint64 `json:"order_id"` Type string `json:"type"` Title string `json:"title"` Status string `json:"status"` diff --git a/backend/internal/modules/chat/handler.go b/backend/internal/modules/chat/handler.go index d0a769a..229c50a 100644 --- a/backend/internal/modules/chat/handler.go +++ b/backend/internal/modules/chat/handler.go @@ -81,6 +81,20 @@ func (h *Handler) OrderConversation(c *gin.Context) { response.OK(c, item) } +func (h *Handler) EnsureSupportConversation(c *gin.Context) { + userID, ok := currentUserID(c) + if !ok { + response.Unauthorized(c, "缺少用户上下文") + return + } + item, err := h.service.EnsureSupportConversation(userID) + if err != nil { + writeChatError(c, err) + return + } + response.OK(c, item) +} + func (h *Handler) Messages(c *gin.Context) { userID, ok := currentUserID(c) if !ok { diff --git a/backend/internal/modules/chat/repository.go b/backend/internal/modules/chat/repository.go index 8d7bba0..88e480e 100644 --- a/backend/internal/modules/chat/repository.go +++ b/backend/internal/modules/chat/repository.go @@ -43,7 +43,7 @@ func EnsureOrderConversation(tx *gorm.DB, order model.RentalOrder) (*model.ChatC now := time.Now() conversation := model.ChatConversation{ - OrderID: order.ID, + OrderID: &order.ID, Type: "order_group", Title: orderConversationTitle(order), Status: "active", @@ -182,6 +182,89 @@ func (r *Repository) FindOrderConversation(userID uint64, orderID uint64) (*Conv return &dto, nil } +func (r *Repository) EnsureSupportConversation(userID uint64) (*ConversationDTO, error) { + var conversationID uint64 + err := r.db.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). + Order("c.id ASC"). + First(&existing).Error + if err == nil { + conversationID = existing.ID + return nil + } + if !errors.Is(err, gorm.ErrRecordNotFound) { + return err + } + + now := time.Now() + conversation := model.ChatConversation{ + Type: "general_support", + Title: "平台客服", + Status: "active", + } + if err := tx.Create(&conversation).Error; err != nil { + return err + } + + participants := []model.ChatParticipant{ + { + ConversationID: conversation.ID, + ParticipantType: "user", + ParticipantID: userID, + Role: "customer", + JoinedAt: now, + }, + } + if supportID := defaultSupportAdminID(tx); supportID > 0 { + participants = append(participants, model.ChatParticipant{ + ConversationID: conversation.ID, + ParticipantType: "admin", + ParticipantID: supportID, + Role: "support", + JoinedAt: now, + }) + } + for _, participant := range participants { + if err := tx.Clauses(clause.OnConflict{DoNothing: true}).Create(&participant).Error; err != nil { + return err + } + } + + message := model.ChatMessage{ + ConversationID: conversation.ID, + SenderType: "system", + SenderRole: "system", + ContentType: "system", + Content: "您好,客服会尽快回复,请直接描述您遇到的问题。", + AttachmentURLS: emptyJSONList(), + } + if err := tx.Create(&message).Error; err != nil { + return err + } + conversation.LastMessageID = &message.ID + conversation.LastMessagePreview = truncatePreview(message.Content) + conversation.LastMessageAt = &message.CreatedAt + if err := tx.Save(&conversation).Error; err != nil { + return err + } + conversationID = conversation.ID + return nil + }) + if err != nil { + return nil, err + } + item, err := r.FindConversation(Principal{Type: "user", ID: userID}, conversationID) + if err != nil { + return nil, err + } + r.NotifyNewConversation(conversationID) + return item, nil +} + 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 { @@ -473,7 +556,7 @@ func (r *Repository) adminNames(ids []uint64) (map[uint64]string, error) { type conversationRow struct { ID uint64 - OrderID uint64 + OrderID *uint64 Type string Title string Status string diff --git a/backend/internal/modules/chat/service.go b/backend/internal/modules/chat/service.go index 1a7170b..ccc01a2 100644 --- a/backend/internal/modules/chat/service.go +++ b/backend/internal/modules/chat/service.go @@ -41,6 +41,16 @@ func (s *Service) FindOrderConversation(userID uint64, orderID uint64) (*Convers return s.repo.FindOrderConversation(userID, orderID) } +func (s *Service) EnsureSupportConversation(userID uint64) (*ConversationDTO, error) { + if s.repo == nil { + return nil, ErrDependencyUnavailable + } + if userID == 0 { + return nil, ErrPermissionDenied + } + return s.repo.EnsureSupportConversation(userID) +} + func (s *Service) Messages(principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) { if s.repo == nil { return nil, ErrDependencyUnavailable diff --git a/backend/internal/router/router.go b/backend/internal/router/router.go index 63a41ad..e2cae3b 100644 --- a/backend/internal/router/router.go +++ b/backend/internal/router/router.go @@ -247,6 +247,7 @@ func New(cfg config.Config, deps Dependencies, logger *zap.Logger) *gin.Engine { if chatHubHandler != nil { chatRoutes.GET("/events", chatHubHandler.UserEvents) } + chatRoutes.POST("/support", chatHandler.EnsureSupportConversation) chatRoutes.GET("", chatHandler.List) chatRoutes.GET("/:id", chatHandler.Detail) chatRoutes.GET("/:id/messages", chatHandler.Messages) diff --git a/backend/migrations/000001_init.sql b/backend/migrations/000001_init.sql index df6009a..dfcc693 100644 --- a/backend/migrations/000001_init.sql +++ b/backend/migrations/000001_init.sql @@ -315,7 +315,7 @@ CREATE TABLE system_configs ( CREATE TABLE chat_conversations ( id BIGINT UNSIGNED PRIMARY KEY AUTO_INCREMENT, - order_id BIGINT UNSIGNED NOT NULL, + order_id BIGINT UNSIGNED NULL, type VARCHAR(32) NOT NULL DEFAULT 'order_group', title VARCHAR(128) NOT NULL, status VARCHAR(32) NOT NULL DEFAULT 'active', diff --git a/backend/migrations/000004_support_chat.sql b/backend/migrations/000004_support_chat.sql new file mode 100644 index 0000000..759b8a5 --- /dev/null +++ b/backend/migrations/000004_support_chat.sql @@ -0,0 +1,2 @@ +ALTER TABLE chat_conversations + MODIFY order_id BIGINT UNSIGNED NULL; diff --git a/frontend/src/api/chats.ts b/frontend/src/api/chats.ts index 6820816..aea6b56 100644 --- a/frontend/src/api/chats.ts +++ b/frontend/src/api/chats.ts @@ -6,7 +6,7 @@ export interface ChatParticipant { conversation_id: number participant_type: 'user' | 'admin' participant_id: number - role: 'renter' | 'owner' | 'support' + role: 'renter' | 'owner' | 'support' | 'customer' remark: string display_name: string avatar_url: string @@ -16,11 +16,11 @@ export interface ChatParticipant { export interface ChatConversation { id: number - order_id: number + order_id: number | null type: string title: string status: string - role: 'renter' | 'owner' | 'support' + role: 'renter' | 'owner' | 'support' | 'customer' 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' | 'system' + sender_role: 'renter' | 'owner' | 'support' | 'customer' | 'system' sender_name: string sender_avatar: string is_self: boolean @@ -62,6 +62,11 @@ export async function fetchOrderChat(orderId: number) { return data.data } +export async function ensureSupportChat() { + const { data } = await apiClient.post>('/chats/support') + return data.data +} + export async function fetchChatMessages(id: number, page = 1, pageSize = 100) { const { data } = await apiClient.get>>(`/chats/${id}/messages`, { params: { page, page_size: pageSize }, diff --git a/frontend/src/layouts/AppLayout.vue b/frontend/src/layouts/AppLayout.vue index 05e83b0..cd3064f 100644 --- a/frontend/src/layouts/AppLayout.vue +++ b/frontend/src/layouts/AppLayout.vue @@ -1,6 +1,7 @@