在线状态
This commit is contained in:
parent
400363ec65
commit
4060aee8af
@ -248,13 +248,19 @@ func (h *Handler) setRobotRoomStatus(c *gin.Context, status string, action strin
|
||||
func parseListQuery(c *gin.Context) (listQuery, bool) {
|
||||
options := shared.ListOptions(c)
|
||||
query := listQuery{
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Keyword: options.Keyword,
|
||||
Status: options.Status,
|
||||
SortBy: firstQueryValue(c, "sortBy", "sort_by"),
|
||||
SortDirection: firstQueryValue(c, "sortDirection", "sort_direction", "order"),
|
||||
Page: options.Page,
|
||||
PageSize: options.PageSize,
|
||||
Keyword: options.Keyword,
|
||||
OwnerUserKeyword: firstQueryValue(c, "ownerUserKeyword", "owner_user_keyword"),
|
||||
Status: options.Status,
|
||||
SortBy: firstQueryValue(c, "sortBy", "sort_by"),
|
||||
SortDirection: firstQueryValue(c, "sortDirection", "sort_direction", "order"),
|
||||
}
|
||||
ownerUserID, ok := parseOptionalInt64Query(c, "ownerUserId", "owner_user_id")
|
||||
if !ok {
|
||||
return listQuery{}, false
|
||||
}
|
||||
query.OwnerUserID = ownerUserID
|
||||
regionID, ok := parseOptionalInt64Query(c, "regionId", "region_id")
|
||||
if !ok {
|
||||
return listQuery{}, false
|
||||
|
||||
@ -8,13 +8,15 @@ import (
|
||||
)
|
||||
|
||||
type listQuery struct {
|
||||
Page int
|
||||
PageSize int
|
||||
Keyword string
|
||||
Status string
|
||||
RegionID int64
|
||||
SortBy string
|
||||
SortDirection string
|
||||
Page int
|
||||
PageSize int
|
||||
Keyword string
|
||||
OwnerUserID int64
|
||||
OwnerUserKeyword string
|
||||
Status string
|
||||
RegionID int64
|
||||
SortBy string
|
||||
SortDirection string
|
||||
}
|
||||
|
||||
type roomPinListQuery struct {
|
||||
|
||||
@ -73,42 +73,37 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
|
||||
return nil, 0, fmt.Errorf("room service client is not configured")
|
||||
}
|
||||
query = normalizeListQuery(query)
|
||||
ownerUserID, ownerMatched, err := s.resolveListOwnerUserID(ctx, query)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if strings.TrimSpace(query.OwnerUserKeyword) != "" && !ownerMatched {
|
||||
return []Room{}, 0, nil
|
||||
}
|
||||
roomKeyword := query.Keyword
|
||||
if ownerUserID == 0 {
|
||||
keywordOwnerUserID, keywordOwnerMatched, err := s.resolveOwnerNumberUserID(ctx, query.Keyword)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if keywordOwnerMatched {
|
||||
ownerUserID = keywordOwnerUserID
|
||||
roomKeyword = ""
|
||||
}
|
||||
}
|
||||
result, err := s.roomClient.ListRooms(ctx, roomclient.ListRoomsRequest{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Keyword: query.Keyword,
|
||||
Keyword: roomKeyword,
|
||||
Status: query.Status,
|
||||
RegionID: query.RegionID,
|
||||
OwnerUserID: ownerUserID,
|
||||
SortBy: query.SortBy,
|
||||
SortDirection: query.SortDirection,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if result.Total == 0 && strings.TrimSpace(query.Keyword) != "" && s.userDB != nil {
|
||||
ownerUserID, ok, err := s.queryOwnerUserIDByDisplayID(ctx, query.Keyword)
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if ok {
|
||||
// 新增置顶弹窗按用户短 ID 找房间;精确解析到 user_id 后交给 room-service owner 过滤,避免字符串 LIKE 误命中。
|
||||
fallback, err := s.roomClient.ListRooms(ctx, roomclient.ListRoomsRequest{
|
||||
Page: query.Page,
|
||||
PageSize: query.PageSize,
|
||||
Status: query.Status,
|
||||
RegionID: query.RegionID,
|
||||
OwnerUserID: ownerUserID,
|
||||
SortBy: query.SortBy,
|
||||
SortDirection: query.SortDirection,
|
||||
})
|
||||
if err != nil {
|
||||
return nil, 0, err
|
||||
}
|
||||
if fallback.Total > 0 {
|
||||
result = fallback
|
||||
}
|
||||
}
|
||||
}
|
||||
items := roomsFromClient(result.Rooms)
|
||||
if err := s.fillRoomDetails(ctx, items); err != nil {
|
||||
return nil, 0, err
|
||||
@ -116,24 +111,50 @@ func (s *Service) ListRooms(ctx context.Context, query listQuery) ([]Room, int64
|
||||
return items, result.Total, nil
|
||||
}
|
||||
|
||||
func (s *Service) queryOwnerUserIDByDisplayID(ctx context.Context, displayUserID string) (int64, bool, error) {
|
||||
displayUserID = strings.TrimSpace(displayUserID)
|
||||
if displayUserID == "" || s.userDB == nil {
|
||||
func (s *Service) resolveListOwnerUserID(ctx context.Context, query listQuery) (int64, bool, error) {
|
||||
if query.OwnerUserID > 0 {
|
||||
return query.OwnerUserID, true, nil
|
||||
}
|
||||
return s.resolveOwnerUserID(ctx, query.OwnerUserKeyword)
|
||||
}
|
||||
|
||||
func (s *Service) resolveOwnerUserID(ctx context.Context, keyword string) (int64, bool, error) {
|
||||
keyword = strings.TrimSpace(keyword)
|
||||
if keyword == "" {
|
||||
return 0, false, nil
|
||||
}
|
||||
matchSQL, matchArgs := shared.UserDisplayIDSQL("u", displayUserID, time.Now().UnixMilli())
|
||||
args := append([]any{appctx.FromContext(ctx)}, matchArgs...)
|
||||
var userID int64
|
||||
err := s.userDB.QueryRowContext(ctx, `
|
||||
if s.userDB == nil {
|
||||
return shared.ResolveExactUserIDOrNumericFallback(ctx, nil, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
|
||||
}
|
||||
// 房主筛选必须先把短号、当前展示号、active 靓号等运营可见号码解析成稳定 user_id,再交给 room-service 按 owner_user_id 精确过滤。
|
||||
return shared.ResolveExactUserID(ctx, s.userDB, appctx.FromContext(ctx), keyword, time.Now().UnixMilli())
|
||||
}
|
||||
|
||||
func (s *Service) resolveOwnerNumberUserID(ctx context.Context, keyword string) (int64, bool, error) {
|
||||
keyword = strings.TrimSpace(keyword)
|
||||
if keyword == "" || s.userDB == nil {
|
||||
return 0, false, nil
|
||||
}
|
||||
nowMS := time.Now().UnixMilli()
|
||||
displaySQL, displayArgs := shared.UserDisplayIDSQL("u", keyword, nowMS)
|
||||
orderSQL, orderArgs := shared.UserIdentityExactOrderSQL("u", "u.user_id", keyword, nowMS)
|
||||
args := append([]any{appctx.FromContext(ctx), keyword}, displayArgs...)
|
||||
args = append(args, orderArgs...)
|
||||
row := s.userDB.QueryRowContext(ctx, `
|
||||
SELECT u.user_id
|
||||
FROM users u
|
||||
WHERE u.app_code = ? AND `+matchSQL+`
|
||||
LIMIT 1
|
||||
`, args...).Scan(&userID)
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, false, nil
|
||||
}
|
||||
if err != nil {
|
||||
WHERE u.app_code = ? AND (CAST(u.user_id AS CHAR) = ? OR `+displaySQL+`)
|
||||
ORDER BY
|
||||
`+orderSQL+`,
|
||||
u.user_id DESC
|
||||
LIMIT 1`,
|
||||
args...,
|
||||
)
|
||||
var userID int64
|
||||
if err := row.Scan(&userID); err != nil {
|
||||
if errors.Is(err, sql.ErrNoRows) {
|
||||
return 0, false, nil
|
||||
}
|
||||
return 0, false, err
|
||||
}
|
||||
return userID, true, nil
|
||||
|
||||
@ -2,6 +2,7 @@ package roomadmin
|
||||
|
||||
import (
|
||||
"context"
|
||||
"database/sql"
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"net/http"
|
||||
@ -9,9 +10,12 @@ import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"github.com/DATA-DOG/go-sqlmock"
|
||||
"github.com/gin-gonic/gin"
|
||||
|
||||
"hyapp-admin-server/internal/appctx"
|
||||
"hyapp-admin-server/internal/integration/robotclient"
|
||||
"hyapp-admin-server/internal/integration/roomclient"
|
||||
)
|
||||
|
||||
func TestNormalizeRoomListSortKeepsOnlyContributionSort(t *testing.T) {
|
||||
@ -52,6 +56,107 @@ func TestNormalizeRoomListSortKeepsOnlyContributionSort(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoomsResolvesOwnerUserKeywordBeforeRoomClient(t *testing.T) {
|
||||
db, sqlMock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sql mock failed: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
const keyword = "VIP2026"
|
||||
const ownerUserID = int64(327702592329093120)
|
||||
expectRoomOwnerIdentityLookup(sqlMock, keyword, ownerUserID)
|
||||
|
||||
roomClient := &recordingRoomClient{}
|
||||
service := NewService(db, nil, roomClient, nil)
|
||||
_, _, err = service.ListRooms(appctx.WithContext(context.Background(), "lalu"), listQuery{
|
||||
OwnerUserKeyword: keyword,
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list rooms failed: %v", err)
|
||||
}
|
||||
if len(roomClient.requests) != 1 {
|
||||
t.Fatalf("expected one room-service request, got %+v", roomClient.requests)
|
||||
}
|
||||
if got := roomClient.requests[0].OwnerUserID; got != ownerUserID {
|
||||
t.Fatalf("owner user id should be resolved before room-service call: got %d want %d", got, ownerUserID)
|
||||
}
|
||||
if roomClient.requests[0].Keyword != "" {
|
||||
t.Fatalf("owner keyword must not be forwarded as room keyword: %+v", roomClient.requests[0])
|
||||
}
|
||||
if err := sqlMock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoomsResolvesRoomKeywordOwnerPrettyIDBeforeRoomClient(t *testing.T) {
|
||||
db, sqlMock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sql mock failed: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
const keyword = "VIP2026"
|
||||
const ownerUserID = int64(327702592329093120)
|
||||
expectRoomOwnerNumberLookup(sqlMock, keyword, ownerUserID)
|
||||
|
||||
roomClient := &recordingRoomClient{}
|
||||
service := NewService(db, nil, roomClient, nil)
|
||||
_, _, err = service.ListRooms(appctx.WithContext(context.Background(), "lalu"), listQuery{
|
||||
Keyword: keyword,
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list rooms failed: %v", err)
|
||||
}
|
||||
if len(roomClient.requests) != 1 {
|
||||
t.Fatalf("expected one room-service request, got %+v", roomClient.requests)
|
||||
}
|
||||
if got := roomClient.requests[0].OwnerUserID; got != ownerUserID {
|
||||
t.Fatalf("room keyword should resolve owner user id before room-service call: got %d want %d", got, ownerUserID)
|
||||
}
|
||||
if roomClient.requests[0].Keyword != "" {
|
||||
t.Fatalf("owner pretty id must not be forwarded as room keyword: %+v", roomClient.requests[0])
|
||||
}
|
||||
if err := sqlMock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestListRoomsReturnsEmptyWhenOwnerUserKeywordHasNoMatch(t *testing.T) {
|
||||
db, sqlMock, err := sqlmock.New()
|
||||
if err != nil {
|
||||
t.Fatalf("create sql mock failed: %v", err)
|
||||
}
|
||||
defer db.Close()
|
||||
|
||||
const keyword = "VIP404"
|
||||
expectRoomOwnerIdentityMiss(sqlMock, keyword)
|
||||
|
||||
roomClient := &recordingRoomClient{}
|
||||
service := NewService(db, nil, roomClient, nil)
|
||||
items, total, err := service.ListRooms(appctx.WithContext(context.Background(), "lalu"), listQuery{
|
||||
OwnerUserKeyword: keyword,
|
||||
Page: 1,
|
||||
PageSize: 20,
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("list rooms failed: %v", err)
|
||||
}
|
||||
if len(items) != 0 || total != 0 {
|
||||
t.Fatalf("unmatched owner keyword should return empty page, got items=%+v total=%d", items, total)
|
||||
}
|
||||
if len(roomClient.requests) != 0 {
|
||||
t.Fatalf("room-service must not be called when owner keyword is known unmatched: %+v", roomClient.requests)
|
||||
}
|
||||
if err := sqlMock.ExpectationsWereMet(); err != nil {
|
||||
t.Fatalf("sql expectations mismatch: %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestRobotRoomProfileFromOwnerUsesOwnerNameAndAvatar(t *testing.T) {
|
||||
name, avatar, err := robotRoomProfileFromOwner(RoomOwner{
|
||||
Username: " Robot Host ",
|
||||
@ -65,6 +170,70 @@ func TestRobotRoomProfileFromOwnerUsesOwnerNameAndAvatar(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func expectRoomOwnerIdentityLookup(sqlMock sqlmock.Sqlmock, keyword string, userID int64) {
|
||||
expectRoomOwnerIdentityQuery(sqlMock, keyword).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
|
||||
}
|
||||
|
||||
func expectRoomOwnerIdentityMiss(sqlMock sqlmock.Sqlmock, keyword string) {
|
||||
expectRoomOwnerIdentityQuery(sqlMock, keyword).
|
||||
WillReturnError(sql.ErrNoRows)
|
||||
}
|
||||
|
||||
func expectRoomOwnerNumberLookup(sqlMock sqlmock.Sqlmock, keyword string, userID int64) {
|
||||
expectRoomOwnerNumberQuery(sqlMock, keyword).
|
||||
WillReturnRows(sqlmock.NewRows([]string{"user_id"}).AddRow(userID))
|
||||
}
|
||||
|
||||
func expectRoomOwnerIdentityQuery(sqlMock sqlmock.Sqlmock, keyword string) *sqlmock.ExpectedQuery {
|
||||
return sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*LIMIT 1`).
|
||||
WithArgs(
|
||||
"lalu",
|
||||
keyword,
|
||||
keyword,
|
||||
keyword,
|
||||
sqlmock.AnyArg(),
|
||||
keyword,
|
||||
"%"+keyword+"%",
|
||||
keyword,
|
||||
keyword,
|
||||
keyword,
|
||||
sqlmock.AnyArg(),
|
||||
keyword,
|
||||
)
|
||||
}
|
||||
|
||||
func expectRoomOwnerNumberQuery(sqlMock sqlmock.Sqlmock, keyword string) *sqlmock.ExpectedQuery {
|
||||
return sqlMock.ExpectQuery(`(?s)SELECT u\.user_id.*FROM users u.*CAST\(u\.user_id AS CHAR\) = \?.*LIMIT 1`).
|
||||
WithArgs(
|
||||
"lalu",
|
||||
keyword,
|
||||
keyword,
|
||||
keyword,
|
||||
sqlmock.AnyArg(),
|
||||
keyword,
|
||||
keyword,
|
||||
keyword,
|
||||
keyword,
|
||||
sqlmock.AnyArg(),
|
||||
keyword,
|
||||
)
|
||||
}
|
||||
|
||||
type recordingRoomClient struct {
|
||||
roomclient.Client
|
||||
requests []roomclient.ListRoomsRequest
|
||||
result *roomclient.ListRoomsResult
|
||||
}
|
||||
|
||||
func (c *recordingRoomClient) ListRooms(_ context.Context, req roomclient.ListRoomsRequest) (*roomclient.ListRoomsResult, error) {
|
||||
c.requests = append(c.requests, req)
|
||||
if c.result != nil {
|
||||
return c.result, nil
|
||||
}
|
||||
return &roomclient.ListRoomsResult{}, nil
|
||||
}
|
||||
|
||||
func TestRobotRoomProfileFromOwnerRequiresNameAndAvatar(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
|
||||
@ -322,7 +322,8 @@ func (s *Service) ChangeMicSeat(ctx context.Context, req *roomv1.ChangeMicSeatRe
|
||||
RoomVersion: current.Version,
|
||||
Attributes: roomMicChangedIMAttributes(micChanged),
|
||||
},
|
||||
}, []outbox.Record{micEvent}, nil
|
||||
directIMRecords: []outbox.Record{micEvent},
|
||||
}, nil, nil
|
||||
})
|
||||
if err != nil {
|
||||
return nil, err
|
||||
|
||||
@ -27,7 +27,8 @@ func TestRoomUserGiftValueFollowsMicChangeAndResetsAfterLeave(t *testing.T) {
|
||||
HeatValue: 117,
|
||||
GiftTypeCode: "normal",
|
||||
}}}
|
||||
svc := newUserGiftValueTestService(repository, wallet, now, "node-user-gift-value")
|
||||
directIM := newRecordingRoomDirectIMPublisher()
|
||||
svc := newUserGiftValueTestService(repository, wallet, now, "node-user-gift-value", directIM)
|
||||
|
||||
roomID := "room-user-gift-value"
|
||||
ownerID := int64(61001)
|
||||
@ -76,8 +77,12 @@ func TestRoomUserGiftValueFollowsMicChangeAndResetsAfterLeave(t *testing.T) {
|
||||
if newSeat := seatByNo(changeResp.GetRoom(), 5); newSeat == nil || newSeat.GetUserId() != targetID || newSeat.GetGiftValue() != 117 {
|
||||
t.Fatalf("gift value must follow user to new seat: %+v", newSeat)
|
||||
}
|
||||
if changed := firstMicChangedEventByAction(t, ctx, repository, "change"); changed == nil || changed.GetTargetGiftValue() != 117 {
|
||||
t.Fatalf("RoomMicChanged/change must carry target gift value: %+v", changed)
|
||||
waitForRoomDirectIMCounts(t, directIM, map[string]int{"RoomMicChanged": 2})
|
||||
if changed := firstDirectMicChangedEventByAction(t, directIM, "change"); changed == nil || changed.GetTargetGiftValue() != 117 {
|
||||
t.Fatalf("direct RoomMicChanged/change must carry target gift value: %+v", changed)
|
||||
}
|
||||
if changed := firstMicChangedEventByAction(t, ctx, repository, "change"); changed != nil {
|
||||
t.Fatalf("RoomMicChanged/change must not be written to durable outbox: %+v", changed)
|
||||
}
|
||||
|
||||
if _, err := svc.LeaveRoom(ctx, &roomv1.LeaveRoomRequest{Meta: rocketMeta(roomID, targetID, "target-heat-leave")}); err != nil {
|
||||
@ -462,3 +467,18 @@ func firstMicChangedEventByAction(t *testing.T, ctx context.Context, repository
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func firstDirectMicChangedEventByAction(t *testing.T, publisher *recordingRoomDirectIMPublisher, action string) *roomeventsv1.RoomMicChanged {
|
||||
t.Helper()
|
||||
|
||||
for _, record := range publisher.recordsByType("RoomMicChanged") {
|
||||
var event roomeventsv1.RoomMicChanged
|
||||
if err := proto.Unmarshal(record.GetBody(), &event); err != nil {
|
||||
t.Fatalf("decode direct mic changed event failed: %v", err)
|
||||
}
|
||||
if event.GetAction() == action {
|
||||
return &event
|
||||
}
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user