字段补充
This commit is contained in:
parent
05881934ad
commit
6d35f421c0
@ -205,6 +205,9 @@ GET /api/v1/app/voice-room/red-packet/active?roomId=9001
|
|||||||
"roomId": "9001",
|
"roomId": "9001",
|
||||||
"regionId": 310,
|
"regionId": 310,
|
||||||
"senderUserId": 10001,
|
"senderUserId": 10001,
|
||||||
|
"senderAccount": "10001",
|
||||||
|
"senderNickname": "Tom",
|
||||||
|
"senderAvatar": "https://example.com/avatar1.png",
|
||||||
"packetMode": "DELAYED",
|
"packetMode": "DELAYED",
|
||||||
"totalAmount": 15000,
|
"totalAmount": 15000,
|
||||||
"totalCount": 5,
|
"totalCount": 5,
|
||||||
@ -585,7 +588,7 @@ class VoiceRoomRedPacket {
|
|||||||
解析建议:
|
解析建议:
|
||||||
|
|
||||||
- 对 `senderUserId/userId` 这类 ID,兼容 number 和 string,统一转 string。
|
- 对 `senderUserId/userId` 这类 ID,兼容 number 和 string,统一转 string。
|
||||||
- HTTP 发送成功、详情、领取成功里的 `packet` 使用 `senderAccount/senderNickname/senderAvatar`;IM `CREATED/OPEN_DUE` 使用 `actualAccount/userNickname/userAvatar`,落本地模型时归一到发送者字段。
|
- HTTP 发送成功、active、详情、领取成功里的 `packet` 使用 `senderAccount/senderNickname/senderAvatar`;IM `CREATED/OPEN_DUE` 使用 `actualAccount/userNickname/userAvatar`,落本地模型时归一到发送者字段。
|
||||||
- 对金额和时间,兼容 number 和 string,统一转 int。
|
- 对金额和时间,兼容 number 和 string,统一转 int。
|
||||||
- `CREATED` IM 里的 `remainAmount/remainCount` 对应 HTTP 里的 `remainingAmount/remainingCount`。
|
- `CREATED` IM 里的 `remainAmount/remainCount` 对应 HTTP 里的 `remainingAmount/remainingCount`。
|
||||||
- 如果 IM 和 HTTP 字段冲突,以 HTTP 最新查询结果为准。
|
- 如果 IM 和 HTTP 字段冲突,以 HTTP 最新查询结果为准。
|
||||||
|
|||||||
@ -154,6 +154,82 @@ func TestConsumeRedPacketCreatedBuildsFullIMPayload(t *testing.T) {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func TestDelayedRedPacketCreatedSendsRegionAndSchedulesOpenDueIM(t *testing.T) {
|
||||||
|
nowMS := int64(1_779_999_700_000)
|
||||||
|
openAtMS := int64(1_780_000_000_000)
|
||||||
|
repository := newFakeRepository()
|
||||||
|
publisher := &fakePublisher{}
|
||||||
|
service := New(Config{NodeID: "node-a"}, repository, publisher, nil)
|
||||||
|
service.SetClock(func() time.Time { return time.UnixMilli(nowMS) })
|
||||||
|
service.SetSenderProfileSource(fakeSenderProfileSource{profile: SenderProfile{
|
||||||
|
UserID: 10001,
|
||||||
|
Account: "10001",
|
||||||
|
Nickname: "Tom",
|
||||||
|
Avatar: "https://example.com/avatar.png",
|
||||||
|
RegionCode: "AR",
|
||||||
|
}})
|
||||||
|
event := broadcastdomain.RedPacketWalletEvent{
|
||||||
|
AppCode: "lalu",
|
||||||
|
EventID: "wallet-red-packet-created-1",
|
||||||
|
EventType: "WalletRedPacketCreated",
|
||||||
|
PacketID: "VRRP2026052712340001",
|
||||||
|
PacketType: "delayed",
|
||||||
|
RoomID: "9001",
|
||||||
|
RegionID: 310,
|
||||||
|
SenderUserID: 10001,
|
||||||
|
TotalAmount: 15000,
|
||||||
|
TotalCount: 5,
|
||||||
|
RemainingAmount: 15000,
|
||||||
|
RemainingCount: 5,
|
||||||
|
Status: "waiting_open",
|
||||||
|
OpenAtMS: openAtMS,
|
||||||
|
ExpiresAtMS: 1_780_086_400_000,
|
||||||
|
CreatedAtMS: nowMS,
|
||||||
|
}
|
||||||
|
|
||||||
|
if err := service.ConsumeRedPacketWalletEvent(appcode.WithContext(context.Background(), "lalu"), event); err != nil {
|
||||||
|
t.Fatalf("ConsumeRedPacketWalletEvent failed: %v", err)
|
||||||
|
}
|
||||||
|
|
||||||
|
roomCreated := repository.records[event.EventID]
|
||||||
|
regionCreated := repository.records[event.EventID+":region_created"]
|
||||||
|
openDue := repository.records[event.EventID+":open_due"]
|
||||||
|
if roomCreated.GroupID != "9001" || roomCreated.Scope != broadcastdomain.ScopeRoom || roomCreated.NextRetryAtMS != nowMS {
|
||||||
|
t.Fatalf("room created outbox mismatch: %+v", roomCreated)
|
||||||
|
}
|
||||||
|
if regionCreated.GroupID != "hy_lalu_bc_r_310" || regionCreated.Scope != broadcastdomain.ScopeRegion || regionCreated.NextRetryAtMS != nowMS {
|
||||||
|
t.Fatalf("region created outbox mismatch: %+v", regionCreated)
|
||||||
|
}
|
||||||
|
if openDue.GroupID != "9001" || openDue.Scope != broadcastdomain.ScopeRoom || openDue.NextRetryAtMS != openAtMS {
|
||||||
|
t.Fatalf("open due outbox mismatch: %+v", openDue)
|
||||||
|
}
|
||||||
|
regionData := redPacketPayloadData(t, regionCreated.PayloadJSON)
|
||||||
|
if regionData["event"] != "CREATED" || regionData["scope"] != "REGION" || regionData["packetMode"] != "DELAYED" {
|
||||||
|
t.Fatalf("region created payload mismatch: %+v", regionData)
|
||||||
|
}
|
||||||
|
openDueData := redPacketPayloadData(t, openDue.PayloadJSON)
|
||||||
|
if openDueData["event"] != "OPEN_DUE" || openDueData["scope"] != "ROOM" || openDueData["claimStartTime"] != float64(openAtMS) {
|
||||||
|
t.Fatalf("open due payload mismatch: %+v", openDueData)
|
||||||
|
}
|
||||||
|
|
||||||
|
result, err := service.ProcessPendingBroadcasts(appcode.WithContext(context.Background(), "lalu"), WorkerOptions{WorkerID: "worker-a", BatchSize: 10})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessPendingBroadcasts before open failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.SuccessCount != 2 || len(publisher.messages) != 2 || containsCustomMessageEvent(publisher.messages, event.EventID+":open_due") {
|
||||||
|
t.Fatalf("before open only room/region CREATED should be sent: result=%+v messages=%+v", result, publisher.messages)
|
||||||
|
}
|
||||||
|
|
||||||
|
service.SetClock(func() time.Time { return time.UnixMilli(openAtMS) })
|
||||||
|
result, err = service.ProcessPendingBroadcasts(appcode.WithContext(context.Background(), "lalu"), WorkerOptions{WorkerID: "worker-a", BatchSize: 10})
|
||||||
|
if err != nil {
|
||||||
|
t.Fatalf("ProcessPendingBroadcasts after open failed: %v", err)
|
||||||
|
}
|
||||||
|
if result.SuccessCount != 1 || !containsCustomMessageEvent(publisher.messages, event.EventID+":open_due") {
|
||||||
|
t.Fatalf("after open should send OPEN_DUE: result=%+v messages=%+v", result, publisher.messages)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
func TestRemoveRegionBroadcastMemberDeletesOnlyUserMembership(t *testing.T) {
|
func TestRemoveRegionBroadcastMemberDeletesOnlyUserMembership(t *testing.T) {
|
||||||
publisher := &fakePublisher{}
|
publisher := &fakePublisher{}
|
||||||
service := New(Config{NodeID: "node-a"}, newFakeRepository(), publisher, nil)
|
service := New(Config{NodeID: "node-a"}, newFakeRepository(), publisher, nil)
|
||||||
@ -211,6 +287,15 @@ func redPacketPayloadData(t *testing.T, payloadJSON string) map[string]any {
|
|||||||
return payload.Data
|
return payload.Data
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func containsCustomMessageEvent(messages []tencentim.CustomGroupMessage, eventID string) bool {
|
||||||
|
for _, message := range messages {
|
||||||
|
if message.EventID == eventID {
|
||||||
|
return true
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return false
|
||||||
|
}
|
||||||
|
|
||||||
type fakeRepository struct {
|
type fakeRepository struct {
|
||||||
records map[string]broadcastdomain.OutboxRecord
|
records map[string]broadcastdomain.OutboxRecord
|
||||||
}
|
}
|
||||||
@ -227,7 +312,7 @@ func (r *fakeRepository) SaveBroadcastOutbox(_ context.Context, record broadcast
|
|||||||
return true, record, nil
|
return true, record, nil
|
||||||
}
|
}
|
||||||
|
|
||||||
func (r *fakeRepository) ClaimPendingBroadcastOutbox(_ context.Context, workerID string, _ int64, lockUntilMS int64, limit int, _ int) ([]broadcastdomain.OutboxRecord, error) {
|
func (r *fakeRepository) ClaimPendingBroadcastOutbox(_ context.Context, workerID string, nowMS int64, lockUntilMS int64, limit int, _ int) ([]broadcastdomain.OutboxRecord, error) {
|
||||||
records := make([]broadcastdomain.OutboxRecord, 0, limit)
|
records := make([]broadcastdomain.OutboxRecord, 0, limit)
|
||||||
for eventID, record := range r.records {
|
for eventID, record := range r.records {
|
||||||
if len(records) >= limit {
|
if len(records) >= limit {
|
||||||
@ -236,6 +321,9 @@ func (r *fakeRepository) ClaimPendingBroadcastOutbox(_ context.Context, workerID
|
|||||||
if record.Status != broadcastdomain.StatusPending && record.Status != broadcastdomain.StatusRetryable {
|
if record.Status != broadcastdomain.StatusPending && record.Status != broadcastdomain.StatusRetryable {
|
||||||
continue
|
continue
|
||||||
}
|
}
|
||||||
|
if record.NextRetryAtMS > nowMS {
|
||||||
|
continue
|
||||||
|
}
|
||||||
record.Status = broadcastdomain.StatusDelivering
|
record.Status = broadcastdomain.StatusDelivering
|
||||||
record.LockedBy = workerID
|
record.LockedBy = workerID
|
||||||
record.LockedUntilMS = lockUntilMS
|
record.LockedUntilMS = lockUntilMS
|
||||||
|
|||||||
@ -4295,11 +4295,12 @@ func TestVoiceRoomActiveRedPacketRouteIsRegistered(t *testing.T) {
|
|||||||
walletClient := &fakeWalletClient{listRedPacketsResp: &walletv1.ListRedPacketsResponse{
|
walletClient := &fakeWalletClient{listRedPacketsResp: &walletv1.ListRedPacketsResponse{
|
||||||
ServerTimeMs: 4000,
|
ServerTimeMs: 4000,
|
||||||
Packets: []*walletv1.RedPacket{
|
Packets: []*walletv1.RedPacket{
|
||||||
{PacketId: "packet-active", RoomId: "room-1", RegionId: 1001, PacketType: "normal", Status: "active"},
|
{PacketId: "packet-active", RoomId: "room-1", RegionId: 1001, SenderUserId: 10002, PacketType: "normal", Status: "active"},
|
||||||
{PacketId: "packet-finished", RoomId: "room-1", RegionId: 1001, PacketType: "normal", Status: "finished"},
|
{PacketId: "packet-finished", RoomId: "room-1", RegionId: 1001, SenderUserId: 10003, PacketType: "normal", Status: "finished"},
|
||||||
},
|
},
|
||||||
}}
|
}}
|
||||||
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, &fakeUserProfileClient{regionID: 1001})
|
profileClient := &fakeUserProfileClient{regionID: 1001}
|
||||||
|
handler := NewHandlerWithClients(&fakeRoomClient{}, nil, nil, profileClient)
|
||||||
handler.SetWalletClient(walletClient)
|
handler.SetWalletClient(walletClient)
|
||||||
router := handler.Routes(auth.NewVerifier("secret"))
|
router := handler.Routes(auth.NewVerifier("secret"))
|
||||||
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/voice-room/red-packet/active?roomId=room-1", nil)
|
request := httptest.NewRequest(http.MethodGet, "/api/v1/app/voice-room/red-packet/active?roomId=room-1", nil)
|
||||||
@ -4315,6 +4316,9 @@ func TestVoiceRoomActiveRedPacketRouteIsRegistered(t *testing.T) {
|
|||||||
if walletClient.lastListRedPackets == nil || walletClient.lastListRedPackets.GetRoomId() != "room-1" || walletClient.lastListRedPackets.GetViewerUserId() != 42 {
|
if walletClient.lastListRedPackets == nil || walletClient.lastListRedPackets.GetRoomId() != "room-1" || walletClient.lastListRedPackets.GetViewerUserId() != 42 {
|
||||||
t.Fatalf("active red-packet request mismatch: %+v", walletClient.lastListRedPackets)
|
t.Fatalf("active red-packet request mismatch: %+v", walletClient.lastListRedPackets)
|
||||||
}
|
}
|
||||||
|
if profileClient.lastBatch == nil || len(profileClient.lastBatch.GetUserIds()) != 1 || profileClient.lastBatch.GetUserIds()[0] != 10002 {
|
||||||
|
t.Fatalf("active red-packet should batch only visible sender profiles: %+v", profileClient.lastBatch)
|
||||||
|
}
|
||||||
var response httpkit.ResponseEnvelope
|
var response httpkit.ResponseEnvelope
|
||||||
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
if err := json.NewDecoder(recorder.Body).Decode(&response); err != nil {
|
||||||
t.Fatalf("decode active red-packet response failed: %v", err)
|
t.Fatalf("decode active red-packet response failed: %v", err)
|
||||||
@ -4325,7 +4329,7 @@ func TestVoiceRoomActiveRedPacketRouteIsRegistered(t *testing.T) {
|
|||||||
t.Fatalf("active red-packet response mismatch: %+v", response)
|
t.Fatalf("active red-packet response mismatch: %+v", response)
|
||||||
}
|
}
|
||||||
first, ok := packets[0].(map[string]any)
|
first, ok := packets[0].(map[string]any)
|
||||||
if !ok || first["packetNo"] != "packet-active" || first["status"] != "ACTIVE" {
|
if !ok || first["packetNo"] != "packet-active" || first["status"] != "ACTIVE" || first["senderNickname"] != "user-10002" || first["senderAvatar"] != "https://cdn.example/avatar.png" {
|
||||||
t.Fatalf("active packet item mismatch: %+v", first)
|
t.Fatalf("active packet item mismatch: %+v", first)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@ -272,7 +272,7 @@ func (h *Handler) sendVoiceRoomRedPacket(writer http.ResponseWriter, request *ht
|
|||||||
}
|
}
|
||||||
|
|
||||||
func (h *Handler) listActiveRedPackets(writer http.ResponseWriter, request *http.Request) {
|
func (h *Handler) listActiveRedPackets(writer http.ResponseWriter, request *http.Request) {
|
||||||
if h.walletClient == nil {
|
if h.walletClient == nil || h.userProfileClient == nil {
|
||||||
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
httpkit.WriteError(writer, request, http.StatusBadGateway, httpkit.CodeUpstreamError, "upstream service error")
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
@ -295,12 +295,21 @@ func (h *Handler) listActiveRedPackets(writer http.ResponseWriter, request *http
|
|||||||
writeRedPacketRPCError(writer, request, err)
|
writeRedPacketRPCError(writer, request, err)
|
||||||
return
|
return
|
||||||
}
|
}
|
||||||
packets := make([]map[string]any, 0, len(resp.GetPackets()))
|
activePackets := make([]*walletv1.RedPacket, 0, len(resp.GetPackets()))
|
||||||
for _, packet := range resp.GetPackets() {
|
for _, packet := range resp.GetPackets() {
|
||||||
if packet.GetStatus() == "active" || packet.GetStatus() == "waiting_open" {
|
if packet.GetStatus() == "active" || packet.GetStatus() == "waiting_open" {
|
||||||
packets = append(packets, voiceRoomPacketFromProto(packet))
|
activePackets = append(activePackets, packet)
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
profiles, err := h.redPacketSenderProfiles(request, activePackets)
|
||||||
|
if err != nil {
|
||||||
|
httpkit.WriteRPCError(writer, request, err)
|
||||||
|
return
|
||||||
|
}
|
||||||
|
packets := make([]map[string]any, 0, len(activePackets))
|
||||||
|
for _, packet := range activePackets {
|
||||||
|
packets = append(packets, voiceRoomPacketFromProtoWithSender(packet, profiles[packet.GetSenderUserId()]))
|
||||||
|
}
|
||||||
httpkit.WriteOK(writer, request, map[string]any{
|
httpkit.WriteOK(writer, request, map[string]any{
|
||||||
"packets": packets,
|
"packets": packets,
|
||||||
"serverTime": resp.GetServerTimeMs(),
|
"serverTime": resp.GetServerTimeMs(),
|
||||||
@ -494,6 +503,34 @@ func (h *Handler) redPacketClaimProfiles(request *http.Request, claims []*wallet
|
|||||||
return resp.GetUsers(), nil
|
return resp.GetUsers(), nil
|
||||||
}
|
}
|
||||||
|
|
||||||
|
func (h *Handler) redPacketSenderProfiles(request *http.Request, packets []*walletv1.RedPacket) (map[int64]*userv1.User, error) {
|
||||||
|
userIDs := make([]int64, 0, len(packets))
|
||||||
|
seen := make(map[int64]struct{}, len(packets))
|
||||||
|
for _, packet := range packets {
|
||||||
|
userID := packet.GetSenderUserId()
|
||||||
|
if userID <= 0 {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
if _, exists := seen[userID]; exists {
|
||||||
|
continue
|
||||||
|
}
|
||||||
|
seen[userID] = struct{}{}
|
||||||
|
userIDs = append(userIDs, userID)
|
||||||
|
}
|
||||||
|
if len(userIDs) == 0 {
|
||||||
|
return map[int64]*userv1.User{}, nil
|
||||||
|
}
|
||||||
|
// active 列表可能有多个红包,发送者展示资料必须批量拉取,避免房间侧边栏刷新造成 N+1 RPC。
|
||||||
|
resp, err := h.userProfileClient.BatchGetUsers(request.Context(), &userv1.BatchGetUsersRequest{
|
||||||
|
Meta: httpkit.UserMeta(request, ""),
|
||||||
|
UserIds: userIDs,
|
||||||
|
})
|
||||||
|
if err != nil {
|
||||||
|
return nil, err
|
||||||
|
}
|
||||||
|
return resp.GetUsers(), nil
|
||||||
|
}
|
||||||
|
|
||||||
func (h *Handler) redPacketSenderProfile(request *http.Request, packet *walletv1.RedPacket, currentUser *userv1.User) (*userv1.User, error) {
|
func (h *Handler) redPacketSenderProfile(request *http.Request, packet *walletv1.RedPacket, currentUser *userv1.User) (*userv1.User, error) {
|
||||||
senderUserID := packet.GetSenderUserId()
|
senderUserID := packet.GetSenderUserId()
|
||||||
if senderUserID <= 0 {
|
if senderUserID <= 0 {
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user