package integration import ( "context" "fmt" "google.golang.org/protobuf/proto" roomeventsv1 "hyapp/api/proto/events/room/v1" "hyapp/pkg/tencentim" ) // TencentIMPublisher 是 room-service 到腾讯云 IM 的生产路径发布器。 type TencentIMPublisher struct { // client 持有腾讯云 IM REST API、UserSig 和管理员账号配置。 client *tencentim.RESTClient } // NewTencentIMPublisher 创建腾讯云 IM 房间消息发布器。 func NewTencentIMPublisher(client *tencentim.RESTClient) *TencentIMPublisher { return &TencentIMPublisher{client: client} } // EnsureRoomGroup 确保每个 Room Cell 都有对应腾讯云 IM 群组。 func (p *TencentIMPublisher) EnsureRoomGroup(ctx context.Context, roomID string, ownerUserID int64) error { if p == nil || p.client == nil { // 未启用腾讯云 IM 时保持无副作用,便于本地测试只跑 room-service。 return nil } return p.client.EnsureRoomGroup(ctx, roomID, ownerUserID) } // PublishRoomEvent 把房间系统事件发送到腾讯云 IM 群。 func (p *TencentIMPublisher) PublishRoomEvent(ctx context.Context, event tencentim.RoomEvent) error { if p == nil || p.client == nil { // 未启用腾讯云 IM 时不阻断房间状态提交。 return nil } return p.client.PublishRoomEvent(ctx, event) } // PublishOutboxEvent 补偿投递 room_outbox 中可转成房间系统消息的事件。 func (p *TencentIMPublisher) PublishOutboxEvent(ctx context.Context, envelope *roomeventsv1.EventEnvelope) error { if p == nil || p.client == nil || envelope == nil { return nil } event, publish, err := roomEventFromEnvelope(envelope) if err != nil || !publish { return err } return p.client.PublishRoomEvent(ctx, event) } // noopRoomEventPublisher 是本地和测试路径的房间消息发布占位实现。 type noopRoomEventPublisher struct{} // NewNoopRoomEventPublisher 返回无外部副作用的房间消息发布器。 func NewNoopRoomEventPublisher() RoomEventPublisher { return noopRoomEventPublisher{} } // EnsureRoomGroup 在 noop 实现中不做外部调用。 func (noopRoomEventPublisher) EnsureRoomGroup(context.Context, string, int64) error { return nil } // PublishRoomEvent 在 noop 实现中不做外部调用。 func (noopRoomEventPublisher) PublishRoomEvent(context.Context, tencentim.RoomEvent) error { return nil } // noopOutboxPublisher 是首版异步 outbox 消费占位实现。 type noopOutboxPublisher struct{} // NewNoopOutboxPublisher 返回一个占位 outbox 发布器。 // 当前版本保留 outbox 表和 worker 语义,activity/audit 投递后续替换该实现。 func NewNoopOutboxPublisher() OutboxPublisher { return noopOutboxPublisher{} } // PublishOutboxEvent 直接吞掉 outbox 事件。 func (noopOutboxPublisher) PublishOutboxEvent(context.Context, *roomeventsv1.EventEnvelope) error { // 腾讯云 IM 同步广播不依赖该 outbox;activity/audit 仍会复用同一事件源。 return nil } func roomEventFromEnvelope(envelope *roomeventsv1.EventEnvelope) (tencentim.RoomEvent, bool, error) { base := tencentim.RoomEvent{ EventID: envelope.GetEventId(), RoomID: envelope.GetRoomId(), EventType: eventTypeForClient(envelope.GetEventType()), RoomVersion: envelope.GetRoomVersion(), } switch envelope.GetEventType() { case "RoomUserJoined": var body roomeventsv1.RoomUserJoined if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { return tencentim.RoomEvent{}, false, err } base.ActorUserID = body.GetUserId() base.TargetUserID = body.GetUserId() base.Attributes = map[string]string{"role": body.GetRole()} return base, true, nil case "RoomUserLeft": var body roomeventsv1.RoomUserLeft if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { return tencentim.RoomEvent{}, false, err } base.ActorUserID = body.GetUserId() base.TargetUserID = body.GetUserId() return base, true, nil case "RoomMicChanged": var body roomeventsv1.RoomMicChanged if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { return tencentim.RoomEvent{}, false, err } base.ActorUserID = body.GetActorUserId() base.TargetUserID = body.GetTargetUserId() base.SeatNo = body.GetToSeat() if base.SeatNo == 0 { base.SeatNo = body.GetFromSeat() } base.Attributes = map[string]string{"action": body.GetAction()} return base, true, nil case "RoomUserMuted": var body roomeventsv1.RoomUserMuted if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { return tencentim.RoomEvent{}, false, err } base.ActorUserID = body.GetActorUserId() base.TargetUserID = body.GetTargetUserId() base.Attributes = map[string]string{"muted": fmt.Sprintf("%t", body.GetMuted())} return base, true, nil case "RoomUserKicked": var body roomeventsv1.RoomUserKicked if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { return tencentim.RoomEvent{}, false, err } base.ActorUserID = body.GetActorUserId() base.TargetUserID = body.GetTargetUserId() return base, true, nil case "RoomGiftSent": var body roomeventsv1.RoomGiftSent if err := proto.Unmarshal(envelope.GetBody(), &body); err != nil { return tencentim.RoomEvent{}, false, err } base.ActorUserID = body.GetSenderUserId() base.TargetUserID = body.GetTargetUserId() base.GiftValue = body.GetGiftValue() base.Attributes = map[string]string{ "gift_id": body.GetGiftId(), "gift_count": fmt.Sprintf("%d", body.GetGiftCount()), "billing_receipt_id": body.GetBillingReceiptId(), } return base, true, nil default: // RoomCreated/HeatChanged/RankChanged 不单独推客户端,避免一条命令产生多条系统消息。 return tencentim.RoomEvent{}, false, nil } } func eventTypeForClient(eventType string) string { switch eventType { case "RoomUserJoined": return "room_user_joined" case "RoomUserLeft": return "room_user_left" case "RoomMicChanged": return "room_mic_changed" case "RoomUserMuted": return "room_user_muted" case "RoomUserKicked": return "room_user_kicked" case "RoomGiftSent": return "room_gift_sent" default: return eventType } }