169 lines
6.1 KiB
Go
169 lines
6.1 KiB
Go
// Package store 提供 im-service 领域层当前使用的内存消息存储和顺序号分配。
|
||
package store
|
||
|
||
import (
|
||
"fmt"
|
||
"sync"
|
||
)
|
||
|
||
// Message 是 im-service 内部统一使用的消息实体。
|
||
type Message struct {
|
||
// MessageID 是 im-service 生成的消息唯一标识,ACK 和幂等响应都依赖它。
|
||
MessageID string `json:"message_id"`
|
||
// MessageType 只能使用项目约定的 room_chat_message、room_system_message、peer_message、system_notice。
|
||
MessageType string `json:"message_type"`
|
||
// FromUserID 是发送方或房间系统事件 actor;系统通知可以为空。
|
||
FromUserID int64 `json:"from_user_id,omitempty"`
|
||
// ToUserID 是单聊接收方、系统通知接收方或房间系统事件目标。
|
||
ToUserID int64 `json:"to_user_id,omitempty"`
|
||
// RoomID 标识消息所属房间;单聊和全局系统通知不需要填写。
|
||
RoomID string `json:"room_id,omitempty"`
|
||
// RoomSeq 是 im-service 分配的房间统一时间线序号,room-service 不生成该字段。
|
||
RoomSeq int64 `json:"room_seq,omitempty"`
|
||
// EventID 是 room-service 房间系统事件的幂等键,公屏消息不需要填写。
|
||
EventID string `json:"event_id,omitempty"`
|
||
// Content 是当前 v1 的文本载荷,富媒体和礼物表现通过 Attributes 扩展。
|
||
Content string `json:"content,omitempty"`
|
||
// Attributes 保存业务扩展字段,避免在 v1 消息实体里提前展开过多类型。
|
||
Attributes map[string]string `json:"attributes,omitempty"`
|
||
// RequiresACK 只对 peer_message 和 system_notice 生效,房间消息不做逐用户 ACK。
|
||
RequiresACK bool `json:"requires_ack"`
|
||
// CreatedAtMS 使用毫秒时间戳,便于 WS JSON 和历史拉取直接消费。
|
||
CreatedAtMS int64 `json:"created_at_ms"`
|
||
}
|
||
|
||
// MemoryStore 负责消息顺序号、历史窗口和 ACK 待确认集合。
|
||
type MemoryStore struct {
|
||
// mu 保护所有 map 和切片;当前 store 是进程内同步实现,不跨节点共享。
|
||
mu sync.Mutex
|
||
|
||
// roomSeq 保存每个房间的下一条时间线基础值,NextRoomSeq 在锁内递增。
|
||
roomSeq map[string]int64
|
||
// roomHistory 保存房间最近窗口消息,用于重进房补拉,不承担永久归档。
|
||
roomHistory map[string][]Message
|
||
// peerHistory 保存 v1 单聊基础历史,key 为稳定的双人 pairKey。
|
||
peerHistory map[string][]Message
|
||
// systemHistory 保存用户系统通知历史,当前主要服务 ACK 和后续补拉扩展。
|
||
systemHistory map[int64][]Message
|
||
// pendingACK 保存 user_id -> message_id -> message,只有需要 ACK 的消息进入这里。
|
||
pendingACK map[int64]map[string]Message
|
||
}
|
||
|
||
// NewMemoryStore 初始化内存消息存储。
|
||
func NewMemoryStore() *MemoryStore {
|
||
return &MemoryStore{
|
||
roomSeq: make(map[string]int64),
|
||
roomHistory: make(map[string][]Message),
|
||
peerHistory: make(map[string][]Message),
|
||
systemHistory: make(map[int64][]Message),
|
||
pendingACK: make(map[int64]map[string]Message),
|
||
}
|
||
}
|
||
|
||
// NextRoomSeq 为指定房间分配新的统一时间线顺序号。
|
||
func (s *MemoryStore) NextRoomSeq(roomID string) int64 {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
// 序号从 1 开始递增,保证房间系统消息和公屏消息共享同一条时间线。
|
||
s.roomSeq[roomID]++
|
||
|
||
return s.roomSeq[roomID]
|
||
}
|
||
|
||
// AppendRoomMessage 把房间消息写入最近窗口。
|
||
func (s *MemoryStore) AppendRoomMessage(roomID string, message Message, limit int) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
s.roomHistory[roomID] = append(s.roomHistory[roomID], message)
|
||
if limit > 0 && len(s.roomHistory[roomID]) > limit {
|
||
// 只保留最近窗口,防止热门房长时间运行导致内存无限增长。
|
||
s.roomHistory[roomID] = append([]Message(nil), s.roomHistory[roomID][len(s.roomHistory[roomID])-limit:]...)
|
||
}
|
||
}
|
||
|
||
// RoomHistory 返回房间最近 N 条消息。
|
||
func (s *MemoryStore) RoomHistory(roomID string, limit int) []Message {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
history := append([]Message(nil), s.roomHistory[roomID]...)
|
||
if limit > 0 && len(history) > limit {
|
||
// 拉取侧 limit 不能扩大 store 保存窗口,只能在当前窗口内裁剪。
|
||
history = history[len(history)-limit:]
|
||
}
|
||
|
||
return history
|
||
}
|
||
|
||
// SavePeerMessage 保存单聊消息历史。
|
||
func (s *MemoryStore) SavePeerMessage(a int64, b int64, message Message) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
// 双人会话使用无方向 key,发送方和接收方读取时都落在同一个历史列表。
|
||
key := pairKey(a, b)
|
||
s.peerHistory[key] = append(s.peerHistory[key], message)
|
||
}
|
||
|
||
// SaveSystemNotice 保存系统通知历史。
|
||
func (s *MemoryStore) SaveSystemNotice(userID int64, message Message) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
// 系统通知按目标用户聚合,后续可直接扩展用户维度分页拉取。
|
||
s.systemHistory[userID] = append(s.systemHistory[userID], message)
|
||
}
|
||
|
||
// AddPendingACK 把需要 ACK 的消息记到目标用户的待确认集合里。
|
||
func (s *MemoryStore) AddPendingACK(userID int64, message Message) {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
if s.pendingACK[userID] == nil {
|
||
// ACK 集合按用户懒创建,避免只有房间消息的用户占用额外 map。
|
||
s.pendingACK[userID] = make(map[string]Message)
|
||
}
|
||
|
||
s.pendingACK[userID][message.MessageID] = message
|
||
}
|
||
|
||
// Ack 标记指定消息已被目标用户确认。
|
||
func (s *MemoryStore) Ack(userID int64, messageID string) bool {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
if s.pendingACK[userID] == nil {
|
||
// 没有待确认集合说明该用户没有需要 ACK 的在线投递。
|
||
return false
|
||
}
|
||
|
||
if _, exists := s.pendingACK[userID][messageID]; !exists {
|
||
// message_id 不属于该用户的待确认集合时,拒绝跨用户 ACK。
|
||
return false
|
||
}
|
||
|
||
// ACK 成功后立即删除,当前版本不保留已读回执复杂状态。
|
||
delete(s.pendingACK[userID], messageID)
|
||
|
||
return true
|
||
}
|
||
|
||
// PendingACK 返回指定用户当前待确认消息数量。
|
||
func (s *MemoryStore) PendingACK(userID int64) int {
|
||
s.mu.Lock()
|
||
defer s.mu.Unlock()
|
||
|
||
return len(s.pendingACK[userID])
|
||
}
|
||
|
||
func pairKey(a int64, b int64) string {
|
||
if a > b {
|
||
// 规范化用户顺序,保证 A->B 和 B->A 写入同一条单聊历史。
|
||
a, b = b, a
|
||
}
|
||
|
||
return fmt.Sprintf("%d:%d", a, b)
|
||
}
|