feat: 品牌裁剪弹窗/Markdown 行内短代码与详情视频块等
新增: - BrandCropModal 品牌图裁剪弹窗 + cropImage 工具 - Markdown 行内短代码(inlineShortcodes + remark 插件) - MdDetails 折叠块、MdVideo 视频块组件 - directiveBlocks/inlineShortcodes 测试 其它: - 聊天后端(handler/service chat)调整 - Markdown 渲染链路、编辑器、全局样式与品牌相关面板更新
This commit is contained in:
@@ -387,13 +387,12 @@ func (h *Handlers) SetChatMemberMute(c *gin.Context) {
|
||||
sysContent = targetName + " 的禁言已被解除"
|
||||
}
|
||||
sysMsg, _ := h.Chat.SendSystemMessage(roomID, sysContent)
|
||||
// 广播系统消息到房间(sender_id=0,前端按系统消息渲染)
|
||||
// 广播系统消息到房间(sender_id=0,前端按系统消息渲染);
|
||||
// Data 与普通消息一致直接放消息体(扁平结构),前端按 room_id 匹配后才能即时上屏
|
||||
if sysMsg != nil && sysMsg.ID > 0 {
|
||||
h.Hub.BroadcastRoom(realtime.RoomChat(roomID), realtime.Envelope{
|
||||
Type: realtime.EventChatMessage,
|
||||
Data: gin.H{
|
||||
"message": sysMsg,
|
||||
},
|
||||
Data: sysMsg,
|
||||
})
|
||||
}
|
||||
h.Hub.BroadcastRoom(realtime.RoomChat(roomID), realtime.Envelope{
|
||||
@@ -682,7 +681,9 @@ func (h *Handlers) broadcastChatRecalled(roomID uint, msg *model.ChatMessage, ne
|
||||
}
|
||||
env := realtime.Envelope{Type: realtime.EventChatRecalled, Data: data}
|
||||
h.Hub.BroadcastRoom(realtime.RoomChat(roomID), env)
|
||||
// 未订阅该房间的成员(侧栏在看别的会话)也需要实时改预览
|
||||
// 未订阅该房间的成员(侧栏在看别的会话)也需要实时改预览:
|
||||
// 已撤回正文不得残留在侧栏预览里。预览回滚到上一条真实消息由 RecallMessage
|
||||
// 回退 last_message_id 完成,本推送只负责即时抹掉正文、不产生角标与置顶。
|
||||
if h.Chat == nil {
|
||||
return
|
||||
}
|
||||
|
||||
@@ -131,7 +131,7 @@ func (s *ChatService) Membership(roomID, userID uint) (*model.ChatRoomMember, er
|
||||
return s.membership(s.db, roomID, userID)
|
||||
}
|
||||
|
||||
// unreadCounts 批量计算访问者在给定群集合中的未读数(消息 ID 大于已读水位、且排除本人发送)
|
||||
// unreadCounts 批量计算访问者在给定群集合中的未读数(消息 ID 大于已读水位、且排除本人发送与系统提示)
|
||||
func (s *ChatService) unreadCounts(userID uint, roomIDs []uint) map[uint]int64 {
|
||||
out := make(map[uint]int64)
|
||||
if userID == 0 || len(roomIDs) == 0 {
|
||||
@@ -141,7 +141,7 @@ func (s *ChatService) unreadCounts(userID uint, roomIDs []uint) map[uint]int64 {
|
||||
s.db.Table("chat_messages AS m").
|
||||
Select("m.room_id AS room_id, count(*) AS cnt").
|
||||
Joins("JOIN chat_room_members AS rm ON rm.room_id = m.room_id AND rm.user_id = ?", userID).
|
||||
Where("m.room_id IN ? AND m.id > rm.last_read_message_id AND m.sender_id <> ? AND m.deleted_at IS NULL AND m.recalled_at IS NULL", roomIDs, userID).
|
||||
Where("m.room_id IN ? AND m.id > rm.last_read_message_id AND m.sender_id <> ? AND m.deleted_at IS NULL AND m.recalled_at IS NULL AND m.is_system = ?", roomIDs, userID, false).
|
||||
Group("m.room_id").
|
||||
Scan(&rows)
|
||||
for _, r := range rows {
|
||||
@@ -767,8 +767,10 @@ func (s *ChatService) Kick(operatorID, roomID, targetID uint, oversee bool) erro
|
||||
return s.removeMember(roomID, targetID)
|
||||
}
|
||||
|
||||
// SendSystemMessage 向房间落库一条系统提示消息(sender_id=0,is_system=true),
|
||||
// 并推进房间 last_message_id 与 updated_at。失败返回 nil + error。
|
||||
// SendSystemMessage 向房间落库一条系统提示消息(sender_id=0,is_system=true)。
|
||||
// 只推进房间 updated_at,不推进 last_message_id:系统提示不作为会话预览,
|
||||
// 也不参与排序顶置,避免禁言/解禁等管理动作在会话列表伪装成「新消息」。
|
||||
// 失败返回 nil + error。
|
||||
func (s *ChatService) SendSystemMessage(roomID uint, content string) (*model.ChatMessage, error) {
|
||||
msg := &model.ChatMessage{
|
||||
RoomID: roomID,
|
||||
@@ -782,10 +784,7 @@ func (s *ChatService) SendSystemMessage(roomID uint, content string) (*model.Cha
|
||||
return err
|
||||
}
|
||||
return tx.Model(&model.ChatRoom{}).Where("id = ?", roomID).
|
||||
Updates(map[string]interface{}{
|
||||
"last_message_id": msg.ID,
|
||||
"updated_at": time.Now(),
|
||||
}).Error
|
||||
Update("updated_at", time.Now()).Error
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
@@ -1020,7 +1019,7 @@ func (s *ChatService) SendMessage(senderID, roomID uint, content string, replyTo
|
||||
if err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
if err := s.db.Preload("Sender").First(msg, msg.ID).Error; err != nil {
|
||||
if err := s.db.Preload("Sender").Preload("Sender.Badges.Badge").First(msg, msg.ID).Error; err != nil {
|
||||
return nil, nil, err
|
||||
}
|
||||
for _, uid := range mentionIDs {
|
||||
@@ -1109,7 +1108,8 @@ func (s *ChatService) ListMessages(userID, roomID, beforeID uint, size int, over
|
||||
q = q.Where("id < ?", beforeID)
|
||||
}
|
||||
var msgs []model.ChatMessage
|
||||
if err := q.Order("id DESC").Limit(size + 1).Preload("Sender").Preload("Recaller").Find(&msgs).Error; err != nil {
|
||||
// Sender.Badges.Badge 嵌套预加载:消息气泡需展示发送者等级与徽章(与成员列表一致)
|
||||
if err := q.Order("id DESC").Limit(size + 1).Preload("Sender").Preload("Sender.Badges.Badge").Preload("Recaller").Find(&msgs).Error; err != nil {
|
||||
return nil, false, err
|
||||
}
|
||||
hasMore := len(msgs) > size
|
||||
@@ -1138,7 +1138,7 @@ func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee
|
||||
return nil, false, err
|
||||
}
|
||||
var msg model.ChatMessage
|
||||
if err := s.db.Where("id = ? AND room_id = ?", messageID, roomID).First(&msg).Error; err != nil {
|
||||
if err := s.db.Where("id = ? AND room_id = ?", messageID, roomID).Preload("Sender").First(&msg).Error; err != nil {
|
||||
if errors.Is(err, gorm.ErrRecordNotFound) {
|
||||
return nil, false, ErrChatMsgNotFound
|
||||
}
|
||||
@@ -1147,6 +1147,10 @@ func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee
|
||||
if msg.RecalledAt != nil {
|
||||
return &msg, false, nil // 幂等
|
||||
}
|
||||
// 站长的消息禁止群主/群管/站点消息管撤回(仅全站监管可撤回)
|
||||
if msg.Sender.Role == model.RoleOwner && !oversee {
|
||||
return nil, false, ErrChatRecallDenied
|
||||
}
|
||||
if msg.SenderID == operatorID {
|
||||
if !oversee {
|
||||
if _, err := s.membership(s.db, roomID, operatorID); err != nil {
|
||||
@@ -1165,7 +1169,17 @@ func (s *ChatService) RecallMessage(operatorID, roomID, messageID uint, oversee
|
||||
}
|
||||
msg.RecalledAt = &now
|
||||
msg.RecalledBy = operatorID
|
||||
_ = s.db.Preload("Sender").Preload("Recaller").First(&msg, msg.ID)
|
||||
// 撤回的是房间当前预览消息时,回退 last_message_id 到最近一条未撤回的非系统消息:
|
||||
// 撤回不在会话列表留下「已撤回」预览,也不让房间因此停留在列表前排,避免被当成新消息提醒。
|
||||
var prevID uint
|
||||
_ = s.db.Model(&model.ChatMessage{}).
|
||||
Where("room_id = ? AND deleted_at IS NULL AND recalled_at IS NULL AND is_system = ?", roomID, false).
|
||||
Order("id DESC").Limit(1).
|
||||
Select("id").Row().Scan(&prevID)
|
||||
_ = s.db.Model(&model.ChatRoom{}).
|
||||
Where("id = ? AND last_message_id = ?", roomID, msg.ID).
|
||||
Update("last_message_id", prevID).Error
|
||||
_ = s.db.Preload("Sender").Preload("Sender.Badges.Badge").Preload("Recaller").First(&msg, msg.ID)
|
||||
return &msg, true, nil
|
||||
}
|
||||
|
||||
@@ -1345,13 +1359,13 @@ func (s *ChatService) MarkRead(userID, roomID, messageID uint) error {
|
||||
Update("last_read_message_id", messageID).Error
|
||||
}
|
||||
|
||||
// UnreadSummary 当前用户所有群的未读明细与总数
|
||||
// UnreadSummary 当前用户所有群的未读明细与总数(系统提示消息不计入)
|
||||
func (s *ChatService) UnreadSummary(userID uint) (int64, []UnreadRoom, error) {
|
||||
var rows []UnreadRoom
|
||||
err := s.db.Table("chat_messages AS m").
|
||||
Select("m.room_id AS room_id, count(*) AS cnt").
|
||||
Joins("JOIN chat_room_members AS rm ON rm.room_id = m.room_id AND rm.user_id = ?", userID).
|
||||
Where("m.id > rm.last_read_message_id AND m.sender_id <> ? AND m.deleted_at IS NULL AND m.recalled_at IS NULL", userID).
|
||||
Where("m.id > rm.last_read_message_id AND m.sender_id <> ? AND m.deleted_at IS NULL AND m.recalled_at IS NULL AND m.is_system = ?", userID, false).
|
||||
Group("m.room_id").
|
||||
Scan(&rows).Error
|
||||
if err != nil {
|
||||
|
||||
Reference in New Issue
Block a user