fix(room): isolate room list by region and sort by country
This commit is contained in:
parent
87b3ebf167
commit
2a488daeed
@ -8367,9 +8367,11 @@ type ListRoomsRequest struct {
|
||||
Limit int32 `protobuf:"varint,6,opt,name=limit,proto3" json:"limit,omitempty"`
|
||||
Query string `protobuf:"bytes,7,opt,name=query,proto3" json:"query,omitempty"`
|
||||
// country_code 是可选国家筛选值;gateway 只能传当前用户区域国家列表内的国家码。
|
||||
CountryCode string `protobuf:"bytes,8,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
CountryCode string `protobuf:"bytes,8,opt,name=country_code,json=countryCode,proto3" json:"country_code,omitempty"`
|
||||
// viewer_country_code 是当前用户国家,用于区域内国家优先排序,不作为客户端可选筛选条件。
|
||||
ViewerCountryCode string `protobuf:"bytes,9,opt,name=viewer_country_code,json=viewerCountryCode,proto3" json:"viewer_country_code,omitempty"`
|
||||
unknownFields protoimpl.UnknownFields
|
||||
sizeCache protoimpl.SizeCache
|
||||
}
|
||||
|
||||
func (x *ListRoomsRequest) Reset() {
|
||||
@ -8458,6 +8460,13 @@ func (x *ListRoomsRequest) GetCountryCode() string {
|
||||
return ""
|
||||
}
|
||||
|
||||
func (x *ListRoomsRequest) GetViewerCountryCode() string {
|
||||
if x != nil {
|
||||
return x.ViewerCountryCode
|
||||
}
|
||||
return ""
|
||||
}
|
||||
|
||||
// ListRoomFeedsRequest 查询 Mine 页 visited/friend/following/followed 房间流。
|
||||
// 它和公共房间发现列表分离,避免把用户关系流误建模成房间全集过滤。
|
||||
type ListRoomFeedsRequest struct {
|
||||
@ -10948,7 +10957,7 @@ const file_proto_room_v1_room_proto_rawDesc = "" +
|
||||
"\x1aVerifyRoomPresenceResponse\x12\x18\n" +
|
||||
"\apresent\x18\x01 \x01(\bR\apresent\x12\x16\n" +
|
||||
"\x06reason\x18\x02 \x01(\tR\x06reason\x12!\n" +
|
||||
"\froom_version\x18\x03 \x01(\x03R\vroomVersion\"\x8d\x02\n" +
|
||||
"\froom_version\x18\x03 \x01(\x03R\vroomVersion\"\xbd\x02\n" +
|
||||
"\x10ListRoomsRequest\x12.\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12$\n" +
|
||||
"\x0eviewer_user_id\x18\x02 \x01(\x03R\fviewerUserId\x12*\n" +
|
||||
@ -10957,7 +10966,8 @@ const file_proto_room_v1_room_proto_rawDesc = "" +
|
||||
"\x06cursor\x18\x05 \x01(\tR\x06cursor\x12\x14\n" +
|
||||
"\x05limit\x18\x06 \x01(\x05R\x05limit\x12\x14\n" +
|
||||
"\x05query\x18\a \x01(\tR\x05query\x12!\n" +
|
||||
"\fcountry_code\x18\b \x01(\tR\vcountryCode\"\xb7\x02\n" +
|
||||
"\fcountry_code\x18\b \x01(\tR\vcountryCode\x12.\n" +
|
||||
"\x13viewer_country_code\x18\t \x01(\tR\x11viewerCountryCode\"\xb7\x02\n" +
|
||||
"\x14ListRoomFeedsRequest\x12.\n" +
|
||||
"\x04meta\x18\x01 \x01(\v2\x1a.hyapp.room.v1.RequestMetaR\x04meta\x12$\n" +
|
||||
"\x0eviewer_user_id\x18\x02 \x01(\x03R\fviewerUserId\x12*\n" +
|
||||
|
||||
@ -1003,6 +1003,8 @@ message ListRoomsRequest {
|
||||
string query = 7;
|
||||
// country_code 是可选国家筛选值;gateway 只能传当前用户区域国家列表内的国家码。
|
||||
string country_code = 8;
|
||||
// viewer_country_code 是当前用户国家,用于区域内国家优先排序,不作为客户端可选筛选条件。
|
||||
string viewer_country_code = 9;
|
||||
}
|
||||
|
||||
// ListRoomFeedsRequest 查询 Mine 页 visited/friend/following/followed 房间流。
|
||||
|
||||
@ -17,6 +17,7 @@ const (
|
||||
roomPinStatusAll = "all"
|
||||
roomPinTypeRegion = "region"
|
||||
roomPinTypeGlobal = "global"
|
||||
roomPinTypeCountry = "country"
|
||||
defaultRoomPinDays = int64(30)
|
||||
maxRoomPinDays = int64(3650)
|
||||
roomPinDayMillis = int64(24 * 60 * 60 * 1000)
|
||||
@ -142,6 +143,8 @@ func normalizeRoomPinType(pinType string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(pinType)) {
|
||||
case roomPinTypeGlobal:
|
||||
return roomPinTypeGlobal
|
||||
case roomPinTypeCountry:
|
||||
return roomPinTypeCountry
|
||||
case roomPinTypeRegion:
|
||||
return roomPinTypeRegion
|
||||
default:
|
||||
|
||||
@ -2641,7 +2641,7 @@ func TestListRoomsUsesUserRegionFromUserService(t *testing.T) {
|
||||
if queryClient.lastList == nil {
|
||||
t.Fatal("room query client was not called")
|
||||
}
|
||||
if queryClient.lastList.GetViewerUserId() != 42 || queryClient.lastList.GetVisibleRegionId() != 1001 {
|
||||
if queryClient.lastList.GetViewerUserId() != 42 || queryClient.lastList.GetVisibleRegionId() != 1001 || queryClient.lastList.GetViewerCountryCode() != "US" {
|
||||
t.Fatalf("ListRooms must use authenticated user and server-side region: %+v", queryClient.lastList)
|
||||
}
|
||||
if queryClient.lastList.GetTab() != "hot" || queryClient.lastList.GetLimit() != 2 || queryClient.lastList.GetCursor() != "cursor-1" || queryClient.lastList.GetQuery() != "room" || queryClient.lastList.GetCountryCode() != "US" {
|
||||
|
||||
@ -108,14 +108,15 @@ func (h *Handler) listRooms(writer http.ResponseWriter, request *http.Request) {
|
||||
}
|
||||
|
||||
resp, err := h.roomQueryClient.ListRooms(request.Context(), &roomv1.ListRoomsRequest{
|
||||
Meta: httpkit.RoomMeta(request, "", ""),
|
||||
ViewerUserId: viewerUserID,
|
||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||
Tab: tab,
|
||||
Cursor: request.URL.Query().Get("cursor"),
|
||||
Limit: limit,
|
||||
Query: roomListQuery(request),
|
||||
CountryCode: countryCode,
|
||||
Meta: httpkit.RoomMeta(request, "", ""),
|
||||
ViewerUserId: viewerUserID,
|
||||
VisibleRegionId: userResp.GetUser().GetRegionId(),
|
||||
ViewerCountryCode: userResp.GetUser().GetCountry(),
|
||||
Tab: tab,
|
||||
Cursor: request.URL.Query().Get("cursor"),
|
||||
Limit: limit,
|
||||
Query: roomListQuery(request),
|
||||
CountryCode: countryCode,
|
||||
})
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
|
||||
@ -183,7 +183,7 @@ CREATE TABLE IF NOT EXISTS room_region_pins (
|
||||
id BIGINT NOT NULL AUTO_INCREMENT COMMENT '主键 ID',
|
||||
app_code VARCHAR(32) NOT NULL COMMENT '应用编码,用于多租户隔离',
|
||||
visible_region_id BIGINT NOT NULL COMMENT '可见区域 ID',
|
||||
pin_type VARCHAR(32) NOT NULL DEFAULT 'region' COMMENT '置顶类型:region 区域置顶,global 全区置顶',
|
||||
pin_type VARCHAR(32) NOT NULL DEFAULT 'region' COMMENT '置顶类型:region 区域置顶,country 国家置顶,global 历史全区置顶',
|
||||
room_id VARCHAR(64) NOT NULL COMMENT '房间 ID',
|
||||
weight BIGINT NOT NULL DEFAULT 0 COMMENT '权重',
|
||||
status VARCHAR(32) NOT NULL COMMENT '业务状态',
|
||||
|
||||
@ -13,9 +13,10 @@ import (
|
||||
var adminSeatCandidates = []int32{10, 15, 20, 25, 30}
|
||||
|
||||
const (
|
||||
roomPinTypeRegion = "region"
|
||||
roomPinTypeGlobal = "global"
|
||||
roomPinDayMillis = int64(24 * 60 * 60 * 1000)
|
||||
roomPinTypeRegion = "region"
|
||||
roomPinTypeGlobal = "global"
|
||||
roomPinTypeCountry = "country"
|
||||
roomPinDayMillis = int64(24 * 60 * 60 * 1000)
|
||||
)
|
||||
|
||||
func (s *Service) AdminGetRoomSeatConfig(ctx context.Context, _ *roomv1.AdminGetRoomSeatConfigRequest) (*roomv1.AdminGetRoomSeatConfigResponse, error) {
|
||||
@ -156,6 +157,8 @@ func normalizeRoomPinType(pinType string) string {
|
||||
switch strings.ToLower(strings.TrimSpace(pinType)) {
|
||||
case roomPinTypeGlobal:
|
||||
return roomPinTypeGlobal
|
||||
case roomPinTypeCountry:
|
||||
return roomPinTypeCountry
|
||||
default:
|
||||
// 空值和未知值都回落到区域置顶,兼容旧后台只传 room_id/duration_days 的请求。
|
||||
return roomPinTypeRegion
|
||||
|
||||
@ -37,6 +37,7 @@ type roomListCursor struct {
|
||||
Tab string `json:"tab"`
|
||||
Query string `json:"query,omitempty"`
|
||||
CountryCode string `json:"country_code,omitempty"`
|
||||
ViewerCountry string `json:"viewer_country,omitempty"`
|
||||
SortScore int64 `json:"sort_score,omitempty"`
|
||||
CreatedAtMS int64 `json:"created_at_ms,omitempty"`
|
||||
UpdatedAtMS int64 `json:"updated_at_ms,omitempty"`
|
||||
@ -68,7 +69,8 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
|
||||
return nil, err
|
||||
}
|
||||
countryCode := normalizeRoomCountryCode(req.GetCountryCode())
|
||||
cursor, err := decodeRoomListCursor(tab, query, countryCode, req.GetCursor())
|
||||
viewerCountryCode := normalizeRoomCountryCode(req.GetViewerCountryCode())
|
||||
cursor, err := decodeRoomListCursor(tab, query, countryCode, viewerCountryCode, req.GetCursor())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -79,6 +81,7 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
|
||||
Tab: tab,
|
||||
Query: query,
|
||||
CountryCode: countryCode,
|
||||
ViewerCountryCode: viewerCountryCode,
|
||||
Limit: limit + 1,
|
||||
CursorSortScore: cursor.SortScore,
|
||||
CursorCreatedAtMS: cursor.CreatedAtMS,
|
||||
@ -93,7 +96,7 @@ func (s *Service) ListRooms(ctx context.Context, req *roomv1.ListRoomsRequest) (
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return roomListResponseFromEntries(tab, query, countryCode, entries, limit), nil
|
||||
return roomListResponseFromEntries(tab, query, countryCode, viewerCountryCode, entries, limit), nil
|
||||
}
|
||||
|
||||
// ListRoomFeeds 查询 Mine 页 visited/friend/following/followed 房间流。
|
||||
@ -115,7 +118,7 @@ func (s *Service) ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRe
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
cursor, err := decodeRoomListCursor(tab, query, "", req.GetCursor())
|
||||
cursor, err := decodeRoomListCursor(tab, query, "", "", req.GetCursor())
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
@ -163,7 +166,7 @@ func (s *Service) ListRoomFeeds(ctx context.Context, req *roomv1.ListRoomFeedsRe
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return roomListResponseFromEntries(tab, query, "", entries, limit), nil
|
||||
return roomListResponseFromEntries(tab, query, "", "", entries, limit), nil
|
||||
}
|
||||
|
||||
// GetMyRoom 查询当前用户自己创建的房间,不依赖 room_list_entries 投影命中。
|
||||
@ -196,12 +199,12 @@ func (s *Service) GetMyRoom(ctx context.Context, req *roomv1.GetMyRoomRequest) (
|
||||
}, nil
|
||||
}
|
||||
|
||||
func roomListResponseFromEntries(tab string, query string, countryCode string, entries []RoomListEntry, limit int) *roomv1.ListRoomsResponse {
|
||||
func roomListResponseFromEntries(tab string, query string, countryCode string, viewerCountryCode string, entries []RoomListEntry, limit int) *roomv1.ListRoomsResponse {
|
||||
nextCursor := ""
|
||||
if len(entries) > limit {
|
||||
// 多查一条用于判断是否存在下一页;返回数据只包含请求的 limit 条。
|
||||
last := entries[limit-1]
|
||||
nextCursor = encodeRoomListCursor(tab, query, countryCode, last)
|
||||
nextCursor = encodeRoomListCursor(tab, query, countryCode, viewerCountryCode, last)
|
||||
entries = entries[:limit]
|
||||
}
|
||||
|
||||
@ -406,10 +409,10 @@ func normalizeRoomCountryCode(countryCode string) string {
|
||||
}
|
||||
|
||||
// decodeRoomListCursor 校验不透明 cursor 和当前 tab 一致,禁止客户端跨排序维度复用游标。
|
||||
func decodeRoomListCursor(tab string, query string, countryCode string, encoded string) (roomListCursor, error) {
|
||||
func decodeRoomListCursor(tab string, query string, countryCode string, viewerCountryCode string, encoded string) (roomListCursor, error) {
|
||||
encoded = strings.TrimSpace(encoded)
|
||||
if encoded == "" {
|
||||
return roomListCursor{Tab: tab, Query: query, CountryCode: countryCode}, nil
|
||||
return roomListCursor{Tab: tab, Query: query, CountryCode: countryCode, ViewerCountry: viewerCountryCode}, nil
|
||||
}
|
||||
|
||||
payload, err := base64.RawURLEncoding.DecodeString(encoded)
|
||||
@ -421,7 +424,7 @@ func decodeRoomListCursor(tab string, query string, countryCode string, encoded
|
||||
if err := json.Unmarshal(payload, &cursor); err != nil {
|
||||
return roomListCursor{}, xerr.New(xerr.InvalidArgument, "cursor is invalid")
|
||||
}
|
||||
if cursor.Tab != tab || cursor.Query != query || cursor.CountryCode != countryCode || cursor.RoomID == "" {
|
||||
if cursor.Tab != tab || cursor.Query != query || cursor.CountryCode != countryCode || cursor.ViewerCountry != viewerCountryCode || cursor.RoomID == "" {
|
||||
return roomListCursor{}, xerr.New(xerr.InvalidArgument, "cursor is invalid")
|
||||
}
|
||||
|
||||
@ -430,11 +433,12 @@ func decodeRoomListCursor(tab string, query string, countryCode string, encoded
|
||||
|
||||
// encodeRoomListCursor 把最后一条列表记录编码成下一页边界。
|
||||
// cursor 不暴露契约语义,后续调整排序字段时可以只兼容服务端解析逻辑。
|
||||
func encodeRoomListCursor(tab string, query string, countryCode string, entry RoomListEntry) string {
|
||||
func encodeRoomListCursor(tab string, query string, countryCode string, viewerCountryCode string, entry RoomListEntry) string {
|
||||
cursor := roomListCursor{
|
||||
Tab: tab,
|
||||
Query: query,
|
||||
CountryCode: countryCode,
|
||||
ViewerCountry: viewerCountryCode,
|
||||
SortScore: entry.SortScore,
|
||||
CreatedAtMS: entry.CreatedAtMS,
|
||||
UpdatedAtMS: entry.UpdatedAtMS,
|
||||
|
||||
@ -64,7 +64,7 @@ func TestRegionalPinOrdersPublicRoomList(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForHot(t *testing.T) {
|
||||
func TestPinnedRoomListOrdersRegionCountryThenCountryBucketsForHot(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
svc := roomservice.New(roomservice.Config{
|
||||
@ -74,42 +74,46 @@ func TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForHot(t *testing.T) {
|
||||
SnapshotEveryN: 1,
|
||||
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||
|
||||
createPinnedListRoom(t, ctx, svc, "room-global-pin", "global-pin", 3101, 9002)
|
||||
createPinnedListRoom(t, ctx, svc, "room-region-pin", "region-pin", 3102, 9001)
|
||||
createPinnedListRoom(t, ctx, svc, "room-local-online", "local-online", 3103, 9001)
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-online", "other-online", 3104, 9002)
|
||||
createPinnedListRoom(t, ctx, svc, "room-local-empty", "local-empty", 3105, 9001)
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-empty", "other-empty", 3106, 9002)
|
||||
repository.SetRoomSortScore("room-global-pin", 1)
|
||||
repository.SetRoomSortScore("room-region-pin", 2)
|
||||
repository.SetRoomSortScore("room-local-online", 100)
|
||||
repository.SetRoomSortScore("room-other-online", 10000)
|
||||
repository.SetRoomSortScore("room-local-empty", 100000)
|
||||
repository.SetRoomSortScore("room-other-empty", 200000)
|
||||
repository.SetRoomPresence("room-local-online", 1, 0)
|
||||
repository.SetRoomPresence("room-other-online", 1, 0)
|
||||
repository.SetRoomPresence("room-local-empty", 0, 0)
|
||||
repository.SetRoomPresence("room-other-empty", 0, 0)
|
||||
createPinnedListRoom(t, ctx, svc, "room-region-pin", "region-pin", 3101, 9001, "AE")
|
||||
createPinnedListRoom(t, ctx, svc, "room-country-pin", "country-pin", 3102, 9001, "US")
|
||||
createPinnedListRoom(t, ctx, svc, "room-viewer-country-online", "viewer-online", 3103, 9001, "US")
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-country-online", "other-online", 3104, 9001, "AE")
|
||||
createPinnedListRoom(t, ctx, svc, "room-viewer-country-empty", "viewer-empty", 3105, 9001, "US")
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-country-empty", "other-empty", 3106, 9001, "AE")
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-region-online", "other-region", 3107, 9002, "US")
|
||||
repository.SetRoomSortScore("room-region-pin", 1)
|
||||
repository.SetRoomSortScore("room-country-pin", 2)
|
||||
repository.SetRoomSortScore("room-viewer-country-online", 100)
|
||||
repository.SetRoomSortScore("room-other-country-online", 10000)
|
||||
repository.SetRoomSortScore("room-viewer-country-empty", 100000)
|
||||
repository.SetRoomSortScore("room-other-country-empty", 200000)
|
||||
repository.SetRoomSortScore("room-other-region-online", 300000)
|
||||
repository.SetRoomPresence("room-viewer-country-online", 1, 0)
|
||||
repository.SetRoomPresence("room-other-country-online", 1, 0)
|
||||
repository.SetRoomPresence("room-viewer-country-empty", 0, 0)
|
||||
repository.SetRoomPresence("room-other-country-empty", 0, 0)
|
||||
repository.SetRoomPresence("room-other-region-online", 1, 0)
|
||||
expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli()
|
||||
repository.PinRoomWithType("room-global-pin", "global", 0, 1, expiresAtMS)
|
||||
repository.PinRoom("room-region-pin", 9001, 99, expiresAtMS)
|
||||
repository.PinRoomWithType("room-country-pin", "country", 9001, 88, expiresAtMS)
|
||||
|
||||
page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
|
||||
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
|
||||
ViewerUserId: 4001,
|
||||
VisibleRegionId: 9001,
|
||||
Tab: "hot",
|
||||
Limit: 6,
|
||||
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
|
||||
ViewerUserId: 4001,
|
||||
VisibleRegionId: 9001,
|
||||
ViewerCountryCode: "US",
|
||||
Tab: "hot",
|
||||
Limit: 6,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list hot rooms failed: %v", err)
|
||||
}
|
||||
if got := roomIDs(page.GetRooms()); len(got) != 6 || got[0] != "room-global-pin" || got[1] != "room-region-pin" || got[2] != "room-local-online" || got[3] != "room-other-online" || got[4] != "room-local-empty" || got[5] != "room-other-empty" {
|
||||
if got := roomIDs(page.GetRooms()); len(got) != 6 || got[0] != "room-region-pin" || got[1] != "room-country-pin" || got[2] != "room-viewer-country-online" || got[3] != "room-other-country-online" || got[4] != "room-viewer-country-empty" || got[5] != "room-other-country-empty" {
|
||||
t.Fatalf("hot order mismatch: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
func TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForNew(t *testing.T) {
|
||||
func TestPinnedRoomListOrdersRegionCountryThenCountryBucketsForNew(t *testing.T) {
|
||||
ctx := context.Background()
|
||||
repository := mysqltest.NewRepository(t)
|
||||
svc := roomservice.New(roomservice.Config{
|
||||
@ -119,31 +123,34 @@ func TestPinnedRoomListOrdersGlobalRegionLocalThenOtherForNew(t *testing.T) {
|
||||
SnapshotEveryN: 1,
|
||||
}, router.NewMemoryDirectory(), repository, followTestWallet{}, integration.NewNoopRoomEventPublisher(), integration.NewNoopOutboxPublisher())
|
||||
|
||||
createPinnedListRoom(t, ctx, svc, "room-global-new", "global-new", 3201, 9002)
|
||||
createPinnedListRoom(t, ctx, svc, "room-region-new", "region-new", 3202, 9001)
|
||||
createPinnedListRoom(t, ctx, svc, "room-local-newer", "local-newer", 3203, 9001)
|
||||
createPinnedListRoom(t, ctx, svc, "room-local-older", "local-older", 3204, 9001)
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-newest", "other-newest", 3205, 9002)
|
||||
repository.SetRoomCreatedAt("room-global-new", 10)
|
||||
createPinnedListRoom(t, ctx, svc, "room-region-new", "region-new", 3201, 9001, "AE")
|
||||
createPinnedListRoom(t, ctx, svc, "room-country-new", "country-new", 3202, 9001, "US")
|
||||
createPinnedListRoom(t, ctx, svc, "room-viewer-newer", "viewer-newer", 3203, 9001, "US")
|
||||
createPinnedListRoom(t, ctx, svc, "room-viewer-older", "viewer-older", 3204, 9001, "US")
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-newest", "other-newest", 3205, 9001, "AE")
|
||||
createPinnedListRoom(t, ctx, svc, "room-other-region-newest", "other-region-newest", 3206, 9002, "US")
|
||||
repository.SetRoomCreatedAt("room-region-new", 20)
|
||||
repository.SetRoomCreatedAt("room-local-newer", 300)
|
||||
repository.SetRoomCreatedAt("room-local-older", 100)
|
||||
repository.SetRoomCreatedAt("room-country-new", 10)
|
||||
repository.SetRoomCreatedAt("room-viewer-newer", 300)
|
||||
repository.SetRoomCreatedAt("room-viewer-older", 100)
|
||||
repository.SetRoomCreatedAt("room-other-newest", 1000)
|
||||
repository.SetRoomCreatedAt("room-other-region-newest", 2000)
|
||||
expiresAtMS := time.Now().UTC().Add(24 * time.Hour).UnixMilli()
|
||||
repository.PinRoomWithType("room-global-new", "global", 0, 1, expiresAtMS)
|
||||
repository.PinRoom("room-region-new", 9001, 99, expiresAtMS)
|
||||
repository.PinRoomWithType("room-country-new", "country", 9001, 88, expiresAtMS)
|
||||
|
||||
page, err := svc.ListRooms(ctx, &roomv1.ListRoomsRequest{
|
||||
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
|
||||
ViewerUserId: 4001,
|
||||
VisibleRegionId: 9001,
|
||||
Tab: "new",
|
||||
Limit: 5,
|
||||
Meta: &roomv1.RequestMeta{AppCode: appcode.Default},
|
||||
ViewerUserId: 4001,
|
||||
VisibleRegionId: 9001,
|
||||
ViewerCountryCode: "US",
|
||||
Tab: "new",
|
||||
Limit: 5,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list new rooms failed: %v", err)
|
||||
}
|
||||
if got := roomIDs(page.GetRooms()); len(got) != 5 || got[0] != "room-global-new" || got[1] != "room-region-new" || got[2] != "room-local-newer" || got[3] != "room-local-older" || got[4] != "room-other-newest" {
|
||||
if got := roomIDs(page.GetRooms()); len(got) != 5 || got[0] != "room-region-new" || got[1] != "room-country-new" || got[2] != "room-viewer-newer" || got[3] != "room-viewer-older" || got[4] != "room-other-newest" {
|
||||
t.Fatalf("new order mismatch: %+v", got)
|
||||
}
|
||||
}
|
||||
|
||||
@ -423,6 +423,8 @@ type RoomListQuery struct {
|
||||
Query string
|
||||
// CountryCode 非空时只返回房主国家命中的房间;gateway 必须先校验它属于当前用户区域。
|
||||
CountryCode string
|
||||
// ViewerCountryCode 是当前用户国家;公共列表不按它过滤,只用它在区域内把本国家房间排到同状态房间前。
|
||||
ViewerCountryCode string
|
||||
// Limit 是实际查询数量,service 层会限制最大值。
|
||||
Limit int
|
||||
// CursorSortScore 是 hot tab 的游标 score。
|
||||
|
||||
@ -1937,7 +1937,7 @@ func (r *Repository) UpsertRoomListEntry(ctx context.Context, entry roomservice.
|
||||
return err
|
||||
}
|
||||
|
||||
// ListRoomListEntries 读取公共发现列表读模型,并按查询区域把全区置顶、区域置顶、本区房和外区房排成稳定分页序列。
|
||||
// ListRoomListEntries 读取公共发现列表读模型,并先按查询区域隔离,再在区域内部按置顶、用户国家和在线状态排成稳定分页序列。
|
||||
func (r *Repository) ListRoomListEntries(ctx context.Context, query roomservice.RoomListQuery) ([]roomservice.RoomListEntry, error) {
|
||||
if query.Limit <= 0 {
|
||||
query.Limit = 20
|
||||
@ -2354,16 +2354,22 @@ func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) {
|
||||
if regionID < 0 {
|
||||
regionID = 0
|
||||
}
|
||||
regionLiteral := strconv.FormatInt(regionID, 10)
|
||||
pinnedExpr := "CASE WHEN gp.room_id IS NULL AND rp.room_id IS NULL THEN 0 ELSE 1 END"
|
||||
newPinRankExpr := "CASE WHEN gp.room_id IS NOT NULL THEN 0 WHEN rp.room_id IS NOT NULL THEN 1 WHEN r.visible_region_id = " + regionLiteral + " THEN 2 ELSE 3 END"
|
||||
hotPinRankExpr := "CASE WHEN gp.room_id IS NOT NULL THEN 0 WHEN rp.room_id IS NOT NULL THEN 1 WHEN r.visible_region_id = " + regionLiteral + " AND r.online_count > 0 THEN 2 WHEN r.visible_region_id <> " + regionLiteral + " AND r.online_count > 0 THEN 3 WHEN r.visible_region_id = " + regionLiteral + " THEN 4 ELSE 5 END"
|
||||
pinRankExpr := hotPinRankExpr
|
||||
if query.Tab == "new" {
|
||||
pinRankExpr = newPinRankExpr
|
||||
viewerCountryCode := normalizeRoomListSQLCountryCode(query.ViewerCountryCode)
|
||||
filterCountryCode := normalizeRoomListSQLCountryCode(query.CountryCode)
|
||||
sortCountryCode := viewerCountryCode
|
||||
if filterCountryCode != "" {
|
||||
// 国家 tab 已经把结果集收敛到一个国家,国家置顶和国家优先桶都应按该 tab 国家解释。
|
||||
sortCountryCode = filterCountryCode
|
||||
}
|
||||
pinWeightExpr := "CASE WHEN gp.room_id IS NOT NULL THEN gp.weight WHEN rp.room_id IS NOT NULL THEN rp.weight ELSE 0 END"
|
||||
pinExpiresExpr := "CASE WHEN gp.room_id IS NOT NULL THEN gp.expires_at_ms WHEN rp.room_id IS NOT NULL THEN rp.expires_at_ms ELSE 0 END"
|
||||
normalRankExpr := "CASE WHEN r.online_count > 0 THEN 2 ELSE 4 END"
|
||||
if sortCountryCode != "" {
|
||||
countryLiteral := roomListSQLStringLiteral(sortCountryCode)
|
||||
normalRankExpr = "CASE WHEN r.owner_country_code = " + countryLiteral + " AND r.online_count > 0 THEN 2 WHEN r.owner_country_code <> " + countryLiteral + " AND r.online_count > 0 THEN 3 WHEN r.owner_country_code = " + countryLiteral + " THEN 4 ELSE 5 END"
|
||||
}
|
||||
pinnedExpr := "CASE WHEN gp.room_id IS NULL AND rp.room_id IS NULL AND cp.room_id IS NULL THEN 0 ELSE 1 END"
|
||||
pinRankExpr := "CASE WHEN gp.room_id IS NOT NULL THEN 0 WHEN rp.room_id IS NOT NULL THEN 0 WHEN cp.room_id IS NOT NULL THEN 1 ELSE " + normalRankExpr + " END"
|
||||
pinWeightExpr := "CASE WHEN gp.room_id IS NOT NULL THEN gp.weight WHEN rp.room_id IS NOT NULL THEN rp.weight WHEN cp.room_id IS NOT NULL THEN cp.weight ELSE 0 END"
|
||||
pinExpiresExpr := "CASE WHEN gp.room_id IS NOT NULL THEN gp.expires_at_ms WHEN rp.room_id IS NOT NULL THEN rp.expires_at_ms WHEN cp.room_id IS NOT NULL THEN cp.expires_at_ms ELSE 0 END"
|
||||
selectSQL := `
|
||||
SELECT r.app_code, r.room_id, r.room_short_id, r.visible_region_id, r.owner_country_code, r.owner_user_id, r.title, r.cover_url, r.mode, r.status, r.locked,
|
||||
r.heat, r.online_count, r.seat_count, r.occupied_seat_count, r.sort_score, r.created_at_ms, r.updated_at_ms,
|
||||
@ -2384,9 +2390,18 @@ func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) {
|
||||
AND rp.room_id = r.room_id
|
||||
AND rp.status = ?
|
||||
AND rp.pinned_at_ms <= ?
|
||||
AND rp.expires_at_ms > ?`
|
||||
where := []string{"r.app_code = ?", "r.status = ?"}
|
||||
args := []any{"active", query.NowMS, query.NowMS, regionID, "active", query.NowMS, query.NowMS, appcode.Normalize(query.AppCode), "active"}
|
||||
AND rp.expires_at_ms > ?
|
||||
LEFT JOIN room_region_pins cp
|
||||
ON cp.app_code = r.app_code
|
||||
AND cp.pin_type = 'country'
|
||||
AND cp.visible_region_id = ?
|
||||
AND cp.room_id = r.room_id
|
||||
AND r.owner_country_code = ?
|
||||
AND cp.status = ?
|
||||
AND cp.pinned_at_ms <= ?
|
||||
AND cp.expires_at_ms > ?`
|
||||
where := []string{"r.app_code = ?", "r.status = ?", "r.visible_region_id = ?"}
|
||||
args := []any{"active", query.NowMS, query.NowMS, regionID, "active", query.NowMS, query.NowMS, regionID, sortCountryCode, "active", query.NowMS, query.NowMS, appcode.Normalize(query.AppCode), "active", regionID}
|
||||
if query.OwnerUserID > 0 {
|
||||
where = append(where, "r.owner_user_id = ?")
|
||||
args = append(args, query.OwnerUserID)
|
||||
@ -2417,6 +2432,23 @@ func buildRoomListQuerySQL(query roomservice.RoomListQuery) (string, []any) {
|
||||
return selectSQL + "\n\tWHERE " + strings.Join(where, " AND ") + "\n\tORDER BY " + pinRankExpr + " ASC, " + pinWeightExpr + " DESC, " + pinExpiresExpr + " DESC, r.sort_score DESC, r.room_id ASC\n\tLIMIT ?", args
|
||||
}
|
||||
|
||||
func normalizeRoomListSQLCountryCode(countryCode string) string {
|
||||
countryCode = strings.ToUpper(strings.TrimSpace(countryCode))
|
||||
if len(countryCode) < 2 || len(countryCode) > 3 {
|
||||
return ""
|
||||
}
|
||||
for _, ch := range countryCode {
|
||||
if ch < 'A' || ch > 'Z' {
|
||||
return ""
|
||||
}
|
||||
}
|
||||
return countryCode
|
||||
}
|
||||
|
||||
func roomListSQLStringLiteral(value string) string {
|
||||
return "'" + strings.ReplaceAll(value, "'", "''") + "'"
|
||||
}
|
||||
|
||||
func (r *Repository) AdminListRooms(ctx context.Context, query roomservice.AdminRoomListQuery) ([]roomservice.AdminRoomListEntry, int64, error) {
|
||||
if query.Page <= 0 {
|
||||
query.Page = 1
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user