843 lines
28 KiB
Go
843 lines
28 KiB
Go
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_id;presence 和麦位状态由 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)
|
||
}
|