2026-05-12 09:53:20 +08:00

843 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

package http
import (
"encoding/base64"
"encoding/json"
"hyapp/services/gateway-service/internal/transport/http/httpkit"
"net/http"
"strconv"
"strings"
roomv1 "hyapp.local/api/proto/room/v1"
userv1 "hyapp.local/api/proto/user/v1"
"hyapp/pkg/appcode"
"hyapp/pkg/roomid"
"hyapp/pkg/xerr"
"hyapp/services/gateway-service/internal/auth"
)
const (
defaultRoomFeedRelationPageSize = 100
maxRoomFeedRelationScanCount = 500
)
type roomFeedRelationCursor struct {
Tab string `json:"tab"`
Query string `json:"query,omitempty"`
UpdatedAtMS int64 `json:"updated_at_ms,omitempty"`
SubjectUserID int64 `json:"subject_user_id,omitempty"`
RoomID string `json:"room_id"`
}
// listRooms 按当前登录用户的服务端 region_id 查询房间发现列表。
func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
if h.userProfileClient == nil || h.roomQueryClient == nil {
// 列表入口必须同时依赖 user-service 区域归属和 room-service 读模型。
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
tab, ok := parsePublicRoomListTab(request.URL.Query().Get("tab"))
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
viewerUserID := auth.UserIDFromContext(request.Context())
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: authRequestMeta(request, ""),
UserId: viewerUserID,
})
if err != nil {
// user-service 是 region_id 的唯一 owner查询失败不能用 IP 国家兜底。
httpkit.WriteRPCError(writer, request, err)
return
}
resp, err := h.roomQueryClient.ListRooms(request.Context(), &roomv1.ListRoomsRequest{
Meta: meta(request, "", ""),
ViewerUserId: viewerUserID,
VisibleRegionId: userResp.GetUser().GetRegionId(),
Tab: tab,
Cursor: request.URL.Query().Get("cursor"),
Limit: limit,
Query: roomListQuery(request),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
}
// listRoomFeeds 查询 Mine 页 visited/friend/following 用户房间流。
func (h *Handler) listRoomFeeds(writer http.ResponseWriter, request *http.Request) {
if h.userProfileClient == nil || h.roomQueryClient == nil {
// feed 同样需要 user-service 区域归属,避免跨区域露出用户关系流房间。
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
limit, ok := parseRoomListLimit(request.URL.Query().Get("limit"))
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
tab, ok := parseRoomFeedTab(request.URL.Query().Get("tab"))
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
viewerUserID := auth.UserIDFromContext(request.Context())
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: authRequestMeta(request, ""),
UserId: viewerUserID,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
relatedUsers, ok, err := h.roomFeedRelatedUsers(request, tab, roomListQuery(request), request.URL.Query().Get("cursor"))
if !ok {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
resp, err := h.roomQueryClient.ListRoomFeeds(request.Context(), &roomv1.ListRoomFeedsRequest{
Meta: meta(request, "", ""),
ViewerUserId: viewerUserID,
VisibleRegionId: userResp.GetUser().GetRegionId(),
Tab: tab,
Cursor: request.URL.Query().Get("cursor"),
Limit: limit,
Query: roomListQuery(request),
RelatedUsers: relatedUsers,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, roomListDataFromProto(resp))
}
func (h *Handler) roomFeedRelatedUsers(request *http.Request, tab string, query string, rawCursor string) ([]*roomv1.RoomFeedRelatedUser, bool, error) {
if tab == "visited" {
return nil, true, nil
}
if h.userSocialClient == nil {
// friend/following 必须以 user-service 关系事实为准,不能退回旧 room feed 投影。
return nil, true, xerr.New(xerr.Unavailable, "user social client is not configured")
}
cursor, ok := decodeRoomFeedRelationCursor(tab, query, rawCursor)
if !ok {
return nil, false, nil
}
viewerUserID := auth.UserIDFromContext(request.Context())
relatedUsers := make([]*roomv1.RoomFeedRelatedUser, 0, defaultRoomFeedRelationPageSize)
for len(relatedUsers) < maxRoomFeedRelationScanCount {
batch, nextCursor, err := h.roomFeedRelatedUserBatch(request, tab, viewerUserID, cursor)
if err != nil {
return nil, true, err
}
relatedUsers = append(relatedUsers, batch...)
if len(batch) < defaultRoomFeedRelationPageSize {
break
}
cursor = nextCursor
}
return relatedUsers, true, nil
}
func (h *Handler) roomFeedRelatedUserBatch(request *http.Request, tab string, viewerUserID int64, cursor roomFeedRelationCursor) ([]*roomv1.RoomFeedRelatedUser, roomFeedRelationCursor, error) {
switch tab {
case "following":
resp, err := h.userSocialClient.ListFollowing(request.Context(), &userv1.ListFollowingRequest{
Meta: authRequestMeta(request, ""),
UserId: viewerUserID,
Page: 1,
PageSize: defaultRoomFeedRelationPageSize,
CursorUpdatedAtMs: cursor.UpdatedAtMS,
CursorUserId: cursor.SubjectUserID,
})
if err != nil {
return nil, cursor, err
}
relatedUsers := make([]*roomv1.RoomFeedRelatedUser, 0, len(resp.GetRecords()))
for _, record := range resp.GetRecords() {
relatedUsers = append(relatedUsers, &roomv1.RoomFeedRelatedUser{
UserId: record.GetFolloweeUserId(),
RelationUpdatedAtMs: record.GetFollowedAtMs(),
})
cursor.UpdatedAtMS = record.GetFollowedAtMs()
cursor.SubjectUserID = record.GetFolloweeUserId()
}
return relatedUsers, cursor, nil
case "friend":
resp, err := h.userSocialClient.ListFriends(request.Context(), &userv1.ListFriendsRequest{
Meta: authRequestMeta(request, ""),
UserId: viewerUserID,
Page: 1,
PageSize: defaultRoomFeedRelationPageSize,
CursorUpdatedAtMs: cursor.UpdatedAtMS,
CursorUserId: cursor.SubjectUserID,
})
if err != nil {
return nil, cursor, err
}
relatedUsers := make([]*roomv1.RoomFeedRelatedUser, 0, len(resp.GetRecords()))
for _, record := range resp.GetRecords() {
relatedUsers = append(relatedUsers, &roomv1.RoomFeedRelatedUser{
UserId: record.GetFriendUserId(),
RelationUpdatedAtMs: record.GetFriendedAtMs(),
})
cursor.UpdatedAtMS = record.GetFriendedAtMs()
cursor.SubjectUserID = record.GetFriendUserId()
}
return relatedUsers, cursor, nil
default:
return nil, cursor, xerr.New(xerr.InvalidArgument, "tab is invalid")
}
}
func decodeRoomFeedRelationCursor(tab string, query string, raw string) (roomFeedRelationCursor, bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
return roomFeedRelationCursor{Tab: tab, Query: query}, true
}
payload, err := base64.RawURLEncoding.DecodeString(raw)
if err != nil {
return roomFeedRelationCursor{}, false
}
var cursor roomFeedRelationCursor
if err := json.Unmarshal(payload, &cursor); err != nil {
return roomFeedRelationCursor{}, false
}
if cursor.Tab != tab || cursor.Query != query || cursor.RoomID == "" || cursor.UpdatedAtMS <= 0 || cursor.SubjectUserID <= 0 {
return roomFeedRelationCursor{}, false
}
return cursor, true
}
func parsePublicRoomListTab(raw string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "", "hot":
return "hot", true
case "new":
return "new", true
default:
return "", false
}
}
func parseRoomFeedTab(raw string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(raw)) {
case "visited":
return "visited", true
case "friend":
return "friend", true
case "following":
return "following", true
default:
return "", false
}
}
func roomListQuery(request *http.Request) string {
if query := strings.TrimSpace(request.URL.Query().Get("q")); query != "" {
return query
}
return strings.TrimSpace(request.URL.Query().Get("query"))
}
// parseRoomListLimit 只做 HTTP 字符串到数字的传输层解析。
// 默认值和最大值仍由 room-service 统一裁剪,避免 gateway 和查询服务出现双份策略。
func parseRoomListLimit(raw string) (int32, bool) {
raw = strings.TrimSpace(raw)
if raw == "" {
return 0, true
}
value, err := strconv.Atoi(raw)
if err != nil {
return 0, false
}
return int32(value), true
}
// getMyRoom 返回 Mine 顶部“我的房间”卡片。
// 这个入口直接查 room-service 的 owner 权威数据,不依赖发现页 room_list_entries 投影是否命中。
func (h *Handler) getMyRoom(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
resp, err := h.roomQueryClient.GetMyRoom(request.Context(), &roomv1.GetMyRoomRequest{
Meta: meta(request, "", ""),
OwnerUserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, myRoomDataFromProto(resp))
}
// getCurrentRoom 查询当前登录用户是否仍有可恢复房间。
// gateway 只传鉴权 user_idpresence 和麦位状态由 room-service 用读模型和 Room Cell 快照判断。
func (h *Handler) getCurrentRoom(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
resp, err := h.roomQueryClient.GetCurrentRoom(request.Context(), &roomv1.GetCurrentRoomRequest{
Meta: meta(request, "", ""),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, currentRoomDataFromProto(resp))
}
// currentRoomData 是 gateway 的外部 JSON 契约,显式保留 false/0 字段。
// 不能直接把 protobuf response 写出去,否则 proto3 的 omitempty 会让 has_current_room=false 消失。
type currentRoomData struct {
HasCurrentRoom bool `json:"has_current_room"`
RoomID string `json:"room_id"`
RoomVersion int64 `json:"room_version"`
Role string `json:"role"`
MicSessionID string `json:"mic_session_id"`
PublishState string `json:"publish_state"`
NeedJoinIMGroup bool `json:"need_join_im_group"`
NeedRTCToken bool `json:"need_rtc_token"`
ServerTimeMS int64 `json:"server_time_ms"`
}
func currentRoomDataFromProto(resp *roomv1.GetCurrentRoomResponse) currentRoomData {
if resp == nil {
return currentRoomData{}
}
return currentRoomData{
HasCurrentRoom: resp.GetHasCurrentRoom(),
RoomID: resp.GetRoomId(),
RoomVersion: resp.GetRoomVersion(),
Role: resp.GetRole(),
MicSessionID: resp.GetMicSessionId(),
PublishState: resp.GetPublishState(),
NeedJoinIMGroup: resp.GetNeedJoinImGroup(),
NeedRTCToken: resp.GetNeedRtcToken(),
ServerTimeMS: resp.GetServerTimeMs(),
}
}
// getRoomSnapshot 主动拉取房间页完整快照。
// 该接口只读 Room Cell/snapshot不刷新 heartbeat也不隐式 JoinRoom。
func (h *Handler) getRoomSnapshot(writer http.ResponseWriter, request *http.Request) {
if h.roomQueryClient == nil {
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
roomID := strings.TrimSpace(request.URL.Query().Get("room_id"))
if !roomid.ValidStringID(roomID) {
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
resp, err := h.roomQueryClient.GetRoomSnapshot(request.Context(), &roomv1.GetRoomSnapshotRequest{
Meta: meta(request, roomID, ""),
RoomId: roomID,
ViewerUserId: auth.UserIDFromContext(request.Context()),
})
write(writer, request, roomSnapshotDataFromProto(resp), err)
}
// createRoom 把创建房间 HTTP JSON 请求转换为 room-service CreateRoom 命令。
func (h *Handler) createRoom(writer http.ResponseWriter, request *http.Request) {
var body struct {
SeatCount int32 `json:"seat_count"`
Mode string `json:"mode"`
RoomName string `json:"room_name"`
RoomAvatar string `json:"room_avatar"`
RoomDescription string `json:"room_description"`
}
if !decode(writer, request, &body) {
return
}
body.Mode = strings.TrimSpace(body.Mode)
body.RoomName = strings.TrimSpace(body.RoomName)
body.RoomAvatar = strings.TrimSpace(body.RoomAvatar)
body.RoomDescription = strings.TrimSpace(body.RoomDescription)
if body.SeatCount < 0 || body.Mode == "" || body.RoomName == "" {
// seat_count 可以省略走后台默认值mode 和 room_name 仍是创建页的最小必填信息。
httpkit.WriteError(writer, request, http.StatusBadRequest, httpkit.CodeInvalidArgument, "invalid argument")
return
}
if h.userProfileClient == nil {
// CreateRoom 的 visible_region_id 必须来自 user-service不能让客户端传入或 gateway 猜测。
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
return
}
userResp, err := h.userProfileClient.GetUser(request.Context(), &userv1.GetUserRequest{
Meta: authRequestMeta(request, ""),
UserId: auth.UserIDFromContext(request.Context()),
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
roomID := appcode.NewScopedID(appcode.FromContext(request.Context()))
roomShortID := userResp.GetUser().GetDisplayUserId()
resp, err := h.roomClient.CreateRoom(request.Context(), &roomv1.CreateRoomRequest{
Meta: meta(request, roomID, ""),
SeatCount: body.SeatCount,
Mode: body.Mode,
VisibleRegionId: userResp.GetUser().GetRegionId(),
RoomName: body.RoomName,
RoomAvatar: body.RoomAvatar,
RoomDescription: body.RoomDescription,
RoomShortId: roomShortID,
})
write(writer, request, resp, err)
}
// updateRoomProfile 把 App 房间资料编辑请求转换为 room-service UpdateRoomProfile 命令。
func (h *Handler) updateRoomProfile(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
RoomName *string `json:"room_name"`
RoomAvatar *string `json:"room_avatar"`
RoomDescription *string `json:"room_description"`
SeatCount *int32 `json:"seat_count"`
}
if !decode(writer, request, &body) {
return
}
body.RoomID = strings.TrimSpace(body.RoomID)
body.CommandID = strings.TrimSpace(body.CommandID)
trimOptionalString(body.RoomName)
trimOptionalString(body.RoomAvatar)
trimOptionalString(body.RoomDescription)
resp, err := h.roomClient.UpdateRoomProfile(request.Context(), &roomv1.UpdateRoomProfileRequest{
Meta: meta(request, body.RoomID, body.CommandID),
RoomName: body.RoomName,
RoomAvatar: body.RoomAvatar,
RoomDescription: body.RoomDescription,
SeatCount: body.SeatCount,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, updateRoomProfileDataFromProto(resp))
}
func trimOptionalString(value *string) {
if value == nil {
return
}
*value = strings.TrimSpace(*value)
}
// joinRoom 把进房 HTTP JSON 请求转换为 room-service JoinRoom 命令,并组装房间首屏初始化包。
func (h *Handler) joinRoom(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
Role string `json:"role"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.JoinRoom(request.Context(), &roomv1.JoinRoomRequest{
Meta: meta(request, body.RoomID, body.CommandID),
Role: body.Role,
})
if err != nil {
httpkit.WriteRPCError(writer, request, err)
return
}
httpkit.WriteOK(writer, request, h.joinRoomInitialData(request, body.RoomID, resp))
}
func (h *Handler) joinRoomInitialData(request *http.Request, requestedRoomID string, resp *roomv1.JoinRoomResponse) joinRoomData {
if resp == nil {
return joinRoomData{}
}
viewerUserID := auth.UserIDFromContext(request.Context())
snapshot := resp.GetRoom()
roomID := snapshot.GetRoomId()
if roomID == "" {
// 老测试桩或异常上游可能只返回 result用 JoinRoom meta 兜底,避免 HTTP DTO 生成崩溃。
roomID = strings.TrimSpace(requestedRoomID)
}
return joinRoomData{
Result: commandResultDataFromProto(resp.GetResult()),
Room: roomInitialRoomDataFromSnapshot(snapshot, roomID),
Viewer: roomViewerDataFromProto(resp.GetUser(), viewerUserID),
Seats: roomSeatDataFromSnapshot(snapshot),
ContributionRank: roomRankDataFromSnapshot(snapshot, joinRoomContributionRankLimit),
Profiles: h.roomInitialProfiles(request, snapshot, viewerUserID),
IM: roomIMData{GroupID: roomIMGroupID(roomID), NeedJoinGroup: true},
RTC: h.joinRoomRTCData(roomID, viewerUserID),
ServerTimeMS: resp.GetResult().GetServerTimeMs(),
}
}
func (h *Handler) joinRoomRTCData(roomID string, viewerUserID int64) roomRTCData {
rtc := roomRTCData{NeedToken: true}
token, err := h.generateTencentRTCToken(viewerUserID, roomID)
if err != nil {
// RTC 票据是进房后的音频能力,不应该回滚已经成功的业务 presence。
rtc.Available = false
rtc.Reason = "rtc_token_unavailable"
return rtc
}
rtc.Available = true
rtc.Token = &token
return rtc
}
func (h *Handler) roomInitialProfiles(request *http.Request, snapshot *roomv1.RoomSnapshot, viewerUserID int64) []roomUserProfileData {
if h.userProfileClient == nil {
return nil
}
userIDs := roomInitialProfileUserIDs(snapshot, viewerUserID, joinRoomContributionRankLimit)
if len(userIDs) == 0 {
return nil
}
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
Meta: authRequestMeta(request, ""),
UserIds: userIDs,
})
if err != nil {
// 用户资料只服务首屏展示,失败时让客户端用 /users/profiles:batch 补偿,避免阻塞进房。
return nil
}
profiles := make([]roomUserProfileData, 0, len(userIDs))
for _, userID := range userIDs {
user := resp.GetUsers()[userID]
if user == nil {
continue
}
profiles = append(profiles, roomUserProfileData{
UserID: strconv.FormatInt(user.GetUserId(), 10),
DisplayUserID: user.GetDisplayUserId(),
Username: user.GetUsername(),
Avatar: user.GetAvatar(),
})
}
return profiles
}
// roomHeartbeat 把客户端显式心跳转换为 room-service presence refresh 命令。
func (h *Handler) roomHeartbeat(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.RoomHeartbeat(request.Context(), &roomv1.RoomHeartbeatRequest{
Meta: meta(request, body.RoomID, body.CommandID),
})
write(writer, request, resp, err)
}
// leaveRoom 把退房 HTTP JSON 请求转换为 room-service LeaveRoom 命令。
func (h *Handler) leaveRoom(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.LeaveRoom(request.Context(), &roomv1.LeaveRoomRequest{
Meta: meta(request, body.RoomID, body.CommandID),
})
write(writer, request, resp, err)
}
// micUp 把上麦 HTTP JSON 请求转换为 room-service MicUp 命令。
func (h *Handler) micUp(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
SeatNo int32 `json:"seat_no"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.MicUp(request.Context(), &roomv1.MicUpRequest{
Meta: meta(request, body.RoomID, body.CommandID),
SeatNo: body.SeatNo,
})
write(writer, request, resp, err)
}
// micDown 把下麦 HTTP JSON 请求转换为 room-service MicDown 命令。
func (h *Handler) micDown(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
Reason string `json:"reason"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.MicDown(request.Context(), &roomv1.MicDownRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
Reason: body.Reason,
})
write(writer, request, resp, err)
}
// changeMicSeat 把换麦位 HTTP JSON 请求转换为 room-service ChangeMicSeat 命令。
func (h *Handler) changeMicSeat(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
SeatNo int32 `json:"seat_no"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.ChangeMicSeat(request.Context(), &roomv1.ChangeMicSeatRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
SeatNo: body.SeatNo,
})
write(writer, request, resp, err)
}
// confirmMicPublishing 把客户端 SDK 发流成功回调转换为 room-service 发流确认命令。
func (h *Handler) confirmMicPublishing(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
MicSessionID string `json:"mic_session_id"`
RoomVersion int64 `json:"room_version"`
EventTimeMS int64 `json:"event_time_ms"`
Source string `json:"source"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.ConfirmMicPublishing(request.Context(), &roomv1.ConfirmMicPublishingRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
MicSessionId: body.MicSessionID,
RoomVersion: body.RoomVersion,
EventTimeMs: body.EventTimeMS,
Source: body.Source,
})
write(writer, request, resp, err)
}
// setMicSeatLock 把锁麦 HTTP JSON 请求转换为 room-service SetMicSeatLock 命令。
func (h *Handler) setMicSeatLock(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
SeatNo int32 `json:"seat_no"`
Locked bool `json:"locked"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.SetMicSeatLock(request.Context(), &roomv1.SetMicSeatLockRequest{
Meta: meta(request, body.RoomID, body.CommandID),
SeatNo: body.SeatNo,
Locked: body.Locked,
})
write(writer, request, resp, err)
}
// setChatEnabled 把公屏开关 HTTP JSON 请求转换为 room-service SetChatEnabled 命令。
func (h *Handler) setChatEnabled(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
Enabled bool `json:"enabled"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.SetChatEnabled(request.Context(), &roomv1.SetChatEnabledRequest{
Meta: meta(request, body.RoomID, body.CommandID),
Enabled: body.Enabled,
})
write(writer, request, resp, err)
}
// setRoomAdmin 把管理员增删 HTTP JSON 请求转换为 room-service SetRoomAdmin 命令。
func (h *Handler) setRoomAdmin(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
Enabled bool `json:"enabled"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.SetRoomAdmin(request.Context(), &roomv1.SetRoomAdminRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
Enabled: body.Enabled,
})
write(writer, request, resp, err)
}
// transferRoomHost 把主持转移 HTTP JSON 请求转换为 room-service TransferRoomHost 命令。
func (h *Handler) transferRoomHost(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.TransferRoomHost(request.Context(), &roomv1.TransferRoomHostRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
})
write(writer, request, resp, err)
}
// muteUser 把禁言 HTTP JSON 请求转换为 room-service MuteUser 命令。
func (h *Handler) muteUser(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
Muted bool `json:"muted"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.MuteUser(request.Context(), &roomv1.MuteUserRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
Muted: body.Muted,
})
write(writer, request, resp, err)
}
// kickUser 把踢人 HTTP JSON 请求转换为 room-service KickUser 命令。
func (h *Handler) kickUser(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.KickUser(request.Context(), &roomv1.KickUserRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
})
write(writer, request, resp, err)
}
// unbanUser 把解封 HTTP JSON 请求转换为 room-service UnbanUser 命令。
func (h *Handler) unbanUser(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.UnbanUser(request.Context(), &roomv1.UnbanUserRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
})
write(writer, request, resp, err)
}
// sendGift 把送礼 HTTP JSON 请求转换为 room-service SendGift 命令。
// 扣费、幂等、房间表现和 outbox 仍由 room-service 的命令链路统一处理。
func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
var body struct {
RoomID string `json:"room_id"`
CommandID string `json:"command_id"`
TargetUserID int64 `json:"target_user_id"`
GiftID string `json:"gift_id"`
GiftCount int32 `json:"gift_count"`
}
if !decode(writer, request, &body) {
return
}
resp, err := h.roomClient.SendGift(request.Context(), &roomv1.SendGiftRequest{
Meta: meta(request, body.RoomID, body.CommandID),
TargetUserId: body.TargetUserID,
GiftId: body.GiftID,
GiftCount: body.GiftCount,
})
write(writer, request, resp, err)
}