package integration import ( "context" "hash/fnv" "sync" roomeventsv1 "hyapp.local/api/proto/events/room/v1" ) // ShardedOutboxPublisher 只在 IM bridge 侧使用:不同 room_id 可以并发,同一个房间仍串行调用下游发布器。 type ShardedOutboxPublisher struct { inner OutboxPublisher locks []sync.Mutex } // NewShardedOutboxPublisher 用 room_id 分片保护下游发布器的房间内顺序;concurrency<=1 时保持历史串行实现。 func NewShardedOutboxPublisher(inner OutboxPublisher, concurrency int) OutboxPublisher { if inner == nil { return NewNoopOutboxPublisher() } if concurrency <= 1 { return inner } return &ShardedOutboxPublisher{ inner: inner, locks: make([]sync.Mutex, concurrency), } } // PublishOutboxEvent 对同一 room_id 使用同一把锁,避免 RocketMQ consumer 并发后把礼物和麦位展示乱序发给腾讯 IM。 func (p *ShardedOutboxPublisher) PublishOutboxEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) error { if p == nil || p.inner == nil || envelope == nil { return nil } if len(p.locks) == 0 { return p.inner.PublishOutboxEvent(ctx, envelope) } lock := &p.locks[p.shardIndex(envelope.GetRoomId())] lock.Lock() defer lock.Unlock() return p.inner.PublishOutboxEvent(ctx, envelope) } func (p *ShardedOutboxPublisher) shardIndex(roomID string) uint32 { if len(p.locks) == 1 { return 0 } hash := fnv.New32a() _, _ = hash.Write([]byte(roomID)) return hash.Sum32() % uint32(len(p.locks)) }