272 lines
8.4 KiB
Go
272 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.sender_type <> ?", "system").
|
|
Where("NOT (cm.sender_type = ? AND cm.sender_id = ?)", "admin", principal.ID).
|
|
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
|
|
}
|