// Package memory 提供 room-service Repository 的测试用内存实现。 package memory import ( "context" "sort" "sync" "hyapp/pkg/xerr" "hyapp/services/room-service/internal/room/outbox" roomservice "hyapp/services/room-service/internal/room/service" ) // Repository 是 room-service 持久化接口的内存实现。 // 它主要服务于首版开发和单测,不试图模拟 MySQL 事务细节。 type Repository struct { // mu 保护所有 map 和序列,测试中可能同时模拟多个 room-service 节点。 mu sync.Mutex // rooms 保存房间基础元数据。 rooms map[string]roomservice.RoomMeta // commandSeen 保存 room_id -> command_id 幂等集合。 commandSeen map[string]map[string]bool // commandLogs 保存可回放命令日志。 commandLogs map[string][]roomservice.CommandRecord // snapshots 保存每个房间最新快照。 snapshots map[string]roomservice.SnapshotRecord // outboxByID 保存 event_id -> outbox record。 outboxByID map[string]outbox.Record // outboxSequence 保留插入顺序,模拟 MySQL created_at 顺序扫描。 outboxSequence []string } // NewRepository 创建空的内存仓储。 func NewRepository() *Repository { return &Repository{ rooms: make(map[string]roomservice.RoomMeta), commandSeen: make(map[string]map[string]bool), commandLogs: make(map[string][]roomservice.CommandRecord), snapshots: make(map[string]roomservice.SnapshotRecord), outboxByID: make(map[string]outbox.Record), } } // SaveRoomMeta 保存房间元数据。 func (r *Repository) SaveRoomMeta(_ context.Context, meta roomservice.RoomMeta) error { r.mu.Lock() defer r.mu.Unlock() if _, exists := r.rooms[meta.RoomID]; exists { // 内存实现显式返回冲突,帮助测试发现重复创建。 return xerr.New(xerr.Conflict, "room meta already exists") } r.rooms[meta.RoomID] = meta return nil } // GetRoomMeta 查询房间元数据。 func (r *Repository) GetRoomMeta(_ context.Context, roomID string) (roomservice.RoomMeta, bool, error) { r.mu.Lock() defer r.mu.Unlock() meta, exists := r.rooms[roomID] return meta, exists, nil } // HasCommand 查询命令是否已被执行过。 func (r *Repository) HasCommand(_ context.Context, roomID string, commandID string) (bool, error) { r.mu.Lock() defer r.mu.Unlock() seenByRoom := r.commandSeen[roomID] // nil map 表示该房间还没有任何成功命令。 return seenByRoom != nil && seenByRoom[commandID], nil } // SaveCommand 追加一条成功命令日志。 func (r *Repository) SaveCommand(_ context.Context, record roomservice.CommandRecord) error { r.mu.Lock() defer r.mu.Unlock() if r.commandSeen[record.RoomID] == nil { // 首条命令时懒创建房间级幂等集合。 r.commandSeen[record.RoomID] = make(map[string]bool) } if r.commandSeen[record.RoomID][record.CommandID] { // 测试实现返回冲突;生产 MySQL 使用 ON DUPLICATE KEY 保持幂等。 return xerr.New(xerr.Conflict, "duplicate command") } r.commandSeen[record.RoomID][record.CommandID] = true r.commandLogs[record.RoomID] = append(r.commandLogs[record.RoomID], record) sort.Slice(r.commandLogs[record.RoomID], func(i int, j int) bool { // 恢复依赖 room_version 顺序,内存实现写入后立即排序。 return r.commandLogs[record.RoomID][i].RoomVersion < r.commandLogs[record.RoomID][j].RoomVersion }) return nil } // ListCommandsAfter 返回快照版本之后的全部命令。 func (r *Repository) ListCommandsAfter(_ context.Context, roomID string, roomVersion int64) ([]roomservice.CommandRecord, error) { r.mu.Lock() defer r.mu.Unlock() records := make([]roomservice.CommandRecord, 0) for _, record := range r.commandLogs[roomID] { if record.RoomVersion > roomVersion { // 返回快照版本之后的全部命令,调用方负责过滤 Replayable。 records = append(records, record) } } return records, nil } // SaveSnapshot 保存当前最新快照。 func (r *Repository) SaveSnapshot(_ context.Context, snapshot roomservice.SnapshotRecord) error { r.mu.Lock() defer r.mu.Unlock() current, exists := r.snapshots[snapshot.RoomID] if exists && current.RoomVersion > snapshot.RoomVersion { // 禁止旧快照覆盖新快照,保持和 MySQL IF 逻辑一致。 return nil } r.snapshots[snapshot.RoomID] = snapshot return nil } // GetLatestSnapshot 返回房间最新快照。 func (r *Repository) GetLatestSnapshot(_ context.Context, roomID string) (roomservice.SnapshotRecord, bool, error) { r.mu.Lock() defer r.mu.Unlock() snapshot, exists := r.snapshots[roomID] return snapshot, exists, nil } // SaveOutbox 追加待投递 outbox 事件。 func (r *Repository) SaveOutbox(_ context.Context, records []outbox.Record) error { r.mu.Lock() defer r.mu.Unlock() for _, record := range records { if _, exists := r.outboxByID[record.EventID]; exists { // outbox event_id 幂等,重复保存直接跳过。 continue } r.outboxByID[record.EventID] = record r.outboxSequence = append(r.outboxSequence, record.EventID) } return nil } // ListPendingOutbox 返回待投递事件。 func (r *Repository) ListPendingOutbox(_ context.Context, limit int) ([]outbox.Record, error) { r.mu.Lock() defer r.mu.Unlock() result := make([]outbox.Record, 0, limit) for _, eventID := range r.outboxSequence { record := r.outboxByID[eventID] if record.Status != outbox.StatusPending { // 只返回待投递事件,published 事件保留在 map 中便于测试检查状态。 continue } result = append(result, record) if limit > 0 && len(result) >= limit { // limit<=0 表示不限制;正数 limit 达到后停止扫描。 break } } return result, nil } // MarkOutboxPublished 标记事件投递成功。 func (r *Repository) MarkOutboxPublished(_ context.Context, eventID string) error { r.mu.Lock() defer r.mu.Unlock() record := r.outboxByID[eventID] // 内存实现不校验 eventID 是否存在,保持测试路径简单。 record.Status = outbox.StatusPublished record.LastError = "" r.outboxByID[eventID] = record return nil } // MarkOutboxFailed 标记事件投递失败。 func (r *Repository) MarkOutboxFailed(_ context.Context, eventID string, lastErr string) error { r.mu.Lock() defer r.mu.Unlock() record := r.outboxByID[eventID] // 失败保持 pending,并累计重试次数,模拟 MySQL 更新语义。 record.Status = outbox.StatusPending record.LastError = lastErr record.RetryCount++ r.outboxByID[eventID] = record return nil }