Files
hfb_sys/backend/internal/modules/chat/conversation.go
T

271 lines
8.4 KiB
Go

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
}
var state model.ChatAdminConversationState
if err := db.Where("conversation_id = ? AND admin_user_id = ?", conversation.ID, principal.ID).First(&state).Error; err != nil && !errors.Is(err, gorm.ErrRecordNotFound) {
return nil, err
}
var unreadCount int64
if err := db.Table("chat_messages AS cm").
Where("cm.conversation_id = ?", conversation.ID).
Where("(cm.admin_attention_type <> ? OR cm.sender_type = ?)", "", "user").
Where("cm.id > ?", state.LastReadMessageID).
Count(&unreadCount).Error; err != nil {
return nil, err
}
dto := ConversationDTO{
ID: conversation.ID,
OrderID: conversation.OrderID,
ListingID: conversation.ListingID,
Type: conversation.Type,
SupportScene: conversation.SupportScene,
Title: conversation.Title,
Status: conversation.Status,
Role: "admin", // 管理员角色
AdminRemark: state.Remark,
Participants: participants,
LastMessageID: conversation.LastMessageID,
LastMessagePreview: conversation.LastMessagePreview,
LastMessageAt: conversation.LastMessageAt,
UnreadCount: unreadCount,
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, scene string) (*ConversationDTO, error) {
scene, title, groupCode, welcome := resolveSupportScene(scene)
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 c.support_scene = ? AND cp.participant_type = ? AND cp.participant_id = ?",
ConversationTypeGeneralSupport, scene, "user", userID,
).
Order("c.id ASC").
Limit(1).
Find(&existing).Error
if err != nil {
return err
}
// 兼容旧数据:scene=general 时也匹配未回填 support_scene 的历史会话
if existing.ID == 0 && scene == SupportSceneGeneral {
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 (c.support_scene = '' OR c.support_scene IS NULL) AND cp.participant_type = ? AND cp.participant_id = ?",
ConversationTypeGeneralSupport, "user", userID,
).
Order("c.id ASC").
Limit(1).
Find(&existing).Error
if err != nil {
return err
}
if existing.ID > 0 && existing.SupportScene == "" {
_ = tx.Model(&existing).Update("support_scene", SupportSceneGeneral).Error
}
}
// 兼容旧摸大红场景码 mohong → crash
if existing.ID == 0 && scene == SupportSceneCrash {
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 c.support_scene = ? AND cp.participant_type = ? AND cp.participant_id = ?",
ConversationTypeGeneralSupport, SupportSceneMohong, "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: ConversationTypeGeneralSupport,
SupportScene: scene,
Title: 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 := pickSupportAdminForScene(tx, groupCode); 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: welcome,
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
}