package chat import ( "context" "errors" "gorm.io/gorm" "gorm.io/gorm/clause" "hfb_sys/backend/internal/model" "time" ) func (r *Repository) ListConversations(ctx context.Context, principal Principal, page, pageSize int) (*PaginatedResult, error) { page, pageSize = normalizePagination(page, pageSize) db := r.db.WithContext(ctx) var total int64 countDB := db.Table("chat_conversations AS c"). Joins("JOIN chat_participants AS cp ON cp.conversation_id = c.id"). Where("cp.participant_type = ? AND cp.participant_id = ?", principal.Type, principal.ID). Where("c.status = ?", "active") if err := countDB.Count(&total).Error; err != nil { return nil, err } var rows []conversationRow offset := (page - 1) * pageSize err := r.conversationQuery(ctx, principal). Where("c.status = ?", "active"). Order("COALESCE(c.last_message_at, c.created_at) DESC, c.id DESC"). Offset(offset). Limit(pageSize). Scan(&rows).Error if err != nil { return nil, err } items := make([]ConversationDTO, 0, len(rows)) for _, row := range rows { items = append(items, row.toDTO(nil)) } return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil } func (r *Repository) FindConversation(ctx context.Context, principal Principal, id uint64) (*ConversationDTO, error) { db := r.db.WithContext(ctx) // 管理员可以查看任意会话,无需是 participant if principal.Type == "admin" { var conversation model.ChatConversation if err := db.First(&conversation, id).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrConversationNotFound } return nil, err } participants, err := r.participants(ctx, conversation.ID) if err != nil { return nil, err } dto := ConversationDTO{ ID: conversation.ID, OrderID: conversation.OrderID, ListingID: conversation.ListingID, 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(ctx, principal).Where("c.id = ?", id).First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrConversationNotFound } return nil, err } participants, err := r.participants(ctx, row.ID) if err != nil { return nil, err } dto := row.toDTO(participants) return &dto, nil } func (r *Repository) FindOrderConversation(ctx context.Context, userID uint64, orderID uint64) (*ConversationDTO, error) { principal := Principal{Type: "user", ID: userID} var order model.RentalOrder if err := r.db.WithContext(ctx).First(&order, orderID).Error; err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrConversationNotFound } return nil, err } if order.ListingID > 0 { var row conversationRow err := r.conversationQuery(ctx, principal). Where("c.listing_id = ? AND c.type = ?", order.ListingID, ConversationTypeListingGroup). First(&row).Error if err == nil { participants, err := r.participants(ctx, row.ID) if err != nil { return nil, err } dto := row.toDTO(participants) return &dto, nil } if err != nil && !errors.Is(err, gorm.ErrRecordNotFound) { return nil, err } } var row conversationRow err := r.conversationQuery(ctx, principal).Where("c.order_id = ?", orderID).First(&row).Error if err != nil { if errors.Is(err, gorm.ErrRecordNotFound) { return nil, ErrConversationNotFound } return nil, err } participants, err := r.participants(ctx, row.ID) if err != nil { return nil, err } dto := row.toDTO(participants) return &dto, nil } func (r *Repository) EnsureSupportConversation(ctx context.Context, userID uint64) (*ConversationDTO, error) { var conversationID uint64 err := r.db.WithContext(ctx).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"). Limit(1). Find(&existing).Error if err != nil { return err } if existing.ID > 0 { conversationID = existing.ID return nil } 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(ctx, Principal{Type: "user", ID: userID}, conversationID) if err != nil { return nil, err } r.NotifyNewConversation(conversationID) return item, nil }