package service import ( "fmt" "strings" "time" roomeventsv1 "hyapp.local/api/proto/events/room/v1" "hyapp/services/room-service/internal/room/command" "hyapp/services/room-service/internal/room/outbox" "hyapp/services/room-service/internal/room/state" ) // roomMicChangedEvent 建立 RoomMicChanged 事实;展示快照只来自命令入口, // room-service 不反查 user-service,保证 Room Cell 主链路仍只维护房态。 func roomMicChangedEvent(event roomeventsv1.RoomMicChanged, profile command.UserDisplayProfile) *roomeventsv1.RoomMicChanged { event.TargetNickname = strings.TrimSpace(profile.Nickname) event.TargetAvatar = strings.TrimSpace(profile.Avatar) event.TargetDisplayUserId = strings.TrimSpace(profile.DisplayUserID) event.TargetPrettyDisplayUserId = strings.TrimSpace(profile.PrettyDisplayUserID) return &event } // roomMicChangedIMAttributes 是 direct IM 和 outbox 补偿的唯一麦位 attributes 映射。 // 目标用户展示字段同时写 target_*;actor 与 target 相同时再写旧客户端已认识的 nickname/avatar。 func roomMicChangedIMAttributes(event *roomeventsv1.RoomMicChanged) map[string]string { if event == nil { return map[string]string{} } values := map[string]string{ "action": strings.TrimSpace(event.GetAction()), "mic_session_id": strings.TrimSpace(event.GetMicSessionId()), "publish_state": strings.TrimSpace(event.GetPublishState()), "reason": strings.TrimSpace(event.GetReason()), "publish_deadline_ms": fmt.Sprintf("%d", event.GetPublishDeadlineMs()), "publish_event_time_ms": fmt.Sprintf("%d", event.GetPublishEventTimeMs()), "mic_muted": fmt.Sprintf("%t", event.GetMicMuted()), "seat_status": strings.TrimSpace(event.GetSeatStatus()), "target_gift_value": fmt.Sprintf("%d", event.GetTargetGiftValue()), "target_nickname": strings.TrimSpace(event.GetTargetNickname()), "target_avatar": strings.TrimSpace(event.GetTargetAvatar()), "target_display_user_id": strings.TrimSpace(event.GetTargetDisplayUserId()), "target_pretty_display_user_id": strings.TrimSpace(event.GetTargetPrettyDisplayUserId()), } if event.GetActorUserId() == event.GetTargetUserId() { values["nickname"] = values["target_nickname"] values["avatar"] = values["target_avatar"] values["display_user_id"] = values["target_display_user_id"] values["pretty_display_user_id"] = values["target_pretty_display_user_id"] } for key, value := range values { if value == "" { delete(values, key) } } return values } // buildMicDownEventForSeat records every non-MicDown release path as the same // RoomMicChanged/down fact, so downstream duration consumers never need to infer // mic closure from leave/kick/close side effects. func buildMicDownEventForSeat(roomID string, roomVersion int64, now time.Time, actorUserID int64, targetUserID int64, seatNo int32, micSessionID string, reason string) (outbox.Record, error) { return outbox.Build(roomID, "RoomMicChanged", roomVersion, now, roomMicChangedEvent(roomeventsv1.RoomMicChanged{ ActorUserId: actorUserID, TargetUserId: targetUserID, FromSeat: seatNo, ToSeat: 0, Action: "down", MicSessionId: micSessionID, PublishState: state.MicPublishIdle, Reason: reason, SeatStatus: state.MicSeatStatusEmpty, }, command.UserDisplayProfile{})) }