Files
hfb_sys/backend/internal/modules/chat/message.go
T
ymlandClaude Opus 4.8 ad70b24fb6 提交2: 发布群核心功能实现
- 实现 EnsureListingConversation: 发布时建发布群,发欢迎语+二维码
- 实现 AddRenterToListingConversation: 付款后拉租客进群
- 实现 RemoveRenterFromListingConversation: 订单终态移出租客
- 实现消息可见性过滤: 租客只能看到 joined_at 之后的消息
- listing 模块注入 ListingChatCreator 接口,发布时建群
- ListingDTO 新增 listing_group_conversation_id 字段
- 付款流程改为拉租客进发布群(替代建订单群)
- 订单完成和取消时自动移出租客
- 创建 chat 适配器实现接口解耦

Co-Authored-By: Claude Opus 4.8 <noreply@anthropic.com>
2026-06-17 15:57:10 +08:00

221 lines
6.8 KiB
Go

package chat
import (
"context"
"errors"
"gorm.io/gorm"
"gorm.io/gorm/clause"
"hfb_sys/backend/internal/model"
"hfb_sys/backend/internal/modules/chathub"
"time"
)
func (r *Repository) Messages(ctx context.Context, principal Principal, conversationID uint64, page, pageSize int) (*PaginatedResult, error) {
page, pageSize = normalizePagination(page, pageSize)
db := r.db.WithContext(ctx)
var participant *model.ChatParticipant
// 管理员可以查看任意会话的消息,普通用户需要是 participant
if principal.Type != "admin" {
p, err := r.findParticipant(db, principal, conversationID, false)
if err != nil {
return nil, err
}
participant = p
} else {
// 管理员需要验证会话存在
var count int64
if err := db.Model(&model.ChatConversation{}).Where("id = ?", conversationID).Count(&count).Error; err != nil {
return nil, err
}
if count == 0 {
return nil, ErrConversationNotFound
}
}
// 构建查询
query := db.Model(&model.ChatMessage{}).Where("conversation_id = ?", conversationID)
// 租客只能看到加入时间之后的消息
if principal.Type == "user" && participant != nil && participant.Role == "renter" {
query = query.Where("created_at >= ?", participant.JoinedAt)
}
var total int64
if err := query.Count(&total).Error; err != nil {
return nil, err
}
offset := (page - 1) * pageSize
var rows []model.ChatMessage
if err := query.Order("id ASC").
Offset(offset).
Limit(pageSize).
Find(&rows).Error; err != nil {
return nil, err
}
items, err := r.toMessageDTOs(ctx, principal, rows)
if err != nil {
return nil, err
}
return &PaginatedResult{Items: items, Total: total, Page: page, PageSize: pageSize}, nil
}
func (r *Repository) SendMessage(ctx context.Context, principal Principal, conversationID uint64, req SendMessageRequest) (*MessageDTO, error) {
var messageID uint64
err := r.db.WithContext(ctx).Transaction(func(tx *gorm.DB) error {
var conversation model.ChatConversation
if err := tx.Clauses(clause.Locking{Strength: "UPDATE"}).First(&conversation, conversationID).Error; err != nil {
return err
}
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: senderRole,
ContentType: "text",
Content: req.Content,
AttachmentURLS: encodeStringList(req.AttachmentURLS),
}
if err := tx.Create(&message).Error; err != nil {
return err
}
conversation.LastMessageID = &message.ID
conversation.LastMessagePreview = messagePreview(message.Content, req.AttachmentURLS)
conversation.LastMessageAt = &message.CreatedAt
if err := tx.Save(&conversation).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
})
if err != nil {
return nil, err
}
var message model.ChatMessage
if err := r.db.WithContext(ctx).First(&message, messageID).Error; err != nil {
return nil, err
}
items, err := r.toMessageDTOs(ctx, principal, []model.ChatMessage{message})
if err != nil {
return nil, err
}
if len(items) == 0 {
return nil, ErrConversationNotFound
}
// 推送新消息事件给会话中的在线参与者
if r.hub != nil {
msg := items[0]
event := &chathub.ChatEvent{
Type: "new_message",
ConversationID: conversationID,
Message: &chathub.MessageData{
ID: msg.ID,
ConversationID: msg.ConversationID,
SenderType: msg.SenderType,
SenderID: msg.SenderID,
SenderRole: msg.SenderRole,
SenderName: msg.SenderName,
ContentType: msg.ContentType,
Content: msg.Content,
AttachmentURLS: msg.AttachmentURLS,
CreatedAt: msg.CreatedAt.Format(time.RFC3339),
},
}
r.hub.NotifyConversation(conversationID, event)
// 同时广播给所有在线客服,使「未分配 / 全部」视图实时刷新;
// 参与者客服会重复收到,由前端按消息 id 去重。
r.hub.NotifyAllAdmins(event)
}
return &items[0], nil
}
func (r *Repository) MarkRead(ctx context.Context, principal Principal, conversationID uint64) error {
now := time.Now()
updated := false
err := r.db.WithContext(ctx).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 记录,更新已读时间
updated = true
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
}
updated = true
return tx.Model(participant).Update("last_read_at", now).Error
})
if err != nil {
return err
}
// 已读时间发生变化时,通知会话内其他参与者刷新「已读」状态。
if updated && r.hub != nil {
event := &chathub.ChatEvent{
Type: "conversation_read",
ConversationID: conversationID,
ReaderType: principal.Type,
ReaderID: principal.ID,
ReadAt: now.Format(time.RFC3339),
}
r.hub.NotifyConversation(conversationID, event)
// 同时广播给所有在线客服,覆盖未分配会话中非参与者客服的「已读」刷新。
r.hub.NotifyAllAdmins(event)
}
return nil
}