Merge branch 'main' of gitea.haiyihy.com:hy/hyapp-server into codex/hotgame-compat-main-20260610
This commit is contained in:
commit
995b52ce9d
File diff suppressed because it is too large
Load Diff
@ -997,6 +997,17 @@ message SystemEvictUserResponse {
|
||||
string rtc_kick_error = 6;
|
||||
}
|
||||
|
||||
// SendGiftTargetHostScope 是 gateway 在送礼入口按每个接收方固化的主播工资入账快照。
|
||||
message SendGiftTargetHostScope {
|
||||
int64 target_user_id = 1;
|
||||
// target_is_host 由 gateway 根据 user-service active host profile 注入,客户端不能直接声明。
|
||||
bool target_is_host = 2;
|
||||
// target_host_region_id 是主播所属区域,后续结算按该区域匹配 Host & Agency 政策。
|
||||
int64 target_host_region_id = 3;
|
||||
// target_agency_owner_user_id 是送礼瞬间主播上级代理收款用户;为空表示当前无代理或代理关系不可结算。
|
||||
int64 target_agency_owner_user_id = 4;
|
||||
}
|
||||
|
||||
// SendGiftRequest 是首版房间内最重要的变现命令。
|
||||
message SendGiftRequest {
|
||||
RequestMeta meta = 1;
|
||||
@ -1022,6 +1033,8 @@ message SendGiftRequest {
|
||||
string entitlement_id = 13;
|
||||
// source 当前支持 bag;为空按普通金币礼物兼容。
|
||||
string source = 14;
|
||||
// target_host_scopes 覆盖批量 target 的工资入账快照;单目标旧字段仍保留兼容老调用链。
|
||||
repeated SendGiftTargetHostScope target_host_scopes = 15;
|
||||
}
|
||||
|
||||
// SendGiftResponse 返回扣费成功并落到房间后的状态结果。
|
||||
|
||||
@ -442,6 +442,7 @@ type fakeUserHostClient struct {
|
||||
listCoinSellersErr error
|
||||
lastHost *userv1.GetHostProfileRequest
|
||||
hostProfile *userv1.HostProfile
|
||||
hostProfiles map[int64]*userv1.HostProfile
|
||||
hostErr error
|
||||
lastBD *userv1.GetBDProfileRequest
|
||||
bdProfile *userv1.BDProfile
|
||||
@ -1394,6 +1395,9 @@ func (f *fakeUserHostClient) GetHostProfile(_ context.Context, req *userv1.GetHo
|
||||
if f.hostErr != nil {
|
||||
return nil, f.hostErr
|
||||
}
|
||||
if f.hostProfiles != nil {
|
||||
return &userv1.GetHostProfileResponse{HostProfile: f.hostProfiles[req.GetUserId()]}, nil
|
||||
}
|
||||
if f.hostProfile != nil {
|
||||
return &userv1.GetHostProfileResponse{HostProfile: f.hostProfile}, nil
|
||||
}
|
||||
@ -2433,6 +2437,33 @@ func TestSendGiftForwardsMultipleTargetUserIDs(t *testing.T) {
|
||||
}
|
||||
}
|
||||
|
||||
func TestSendGiftInjectsHostPeriodScopeForEachTarget(t *testing.T) {
|
||||
roomClient := &fakeRoomClient{}
|
||||
hostClient := &fakeUserHostClient{hostProfiles: map[int64]*userv1.HostProfile{
|
||||
43: {UserId: 43, Status: "active", RegionId: 8801, CurrentAgencyOwnerUserId: 30001},
|
||||
44: {UserId: 44, Status: "active", RegionId: 8802, CurrentAgencyOwnerUserId: 30002},
|
||||
}}
|
||||
handler := NewHandlerWithClients(roomClient, nil, nil, &fakeUserProfileClient{regionID: 1001})
|
||||
handler.SetUserHostClient(hostClient)
|
||||
router := handler.Routes(auth.NewVerifier("secret"))
|
||||
request := httptest.NewRequest(http.MethodPost, "/api/v1/rooms/gift/send", bytes.NewReader([]byte(`{"room_id":"room-1","command_id":"cmd-gift-multi-host","target_user_ids":[43,44],"gift_id":"rose","gift_count":2}`)))
|
||||
request.Header.Set("Authorization", "Bearer "+signGatewayToken(t, "secret", 42))
|
||||
recorder := httptest.NewRecorder()
|
||||
|
||||
router.ServeHTTP(recorder, request)
|
||||
|
||||
if recorder.Code != http.StatusOK {
|
||||
t.Fatalf("status mismatch: got %d body=%s", recorder.Code, recorder.Body.String())
|
||||
}
|
||||
scopes := roomClient.lastGift.GetTargetHostScopes()
|
||||
if len(scopes) != 2 || !scopes[0].GetTargetIsHost() || scopes[0].GetTargetHostRegionId() != 8801 || !scopes[1].GetTargetIsHost() || scopes[1].GetTargetHostRegionId() != 8802 {
|
||||
t.Fatalf("gateway must inject each target host scope: %+v", scopes)
|
||||
}
|
||||
if !roomClient.lastGift.GetTargetIsHost() || roomClient.lastGift.GetTargetHostRegionId() != 8801 {
|
||||
t.Fatalf("legacy first-target host scope must remain populated: %+v", roomClient.lastGift)
|
||||
}
|
||||
}
|
||||
|
||||
func TestJoinRoomReturnsInitialDataWithIMGroupRTCTokenAndProfiles(t *testing.T) {
|
||||
previousNow := roomapi.TimeNow
|
||||
roomapi.TimeNow = func() time.Time { return time.Unix(1_700_000_000, 0) }
|
||||
|
||||
@ -1558,11 +1558,12 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
targetIsHost, targetHostRegionID, targetAgencyOwnerUserID, err := h.resolveGiftTargetHostScope(request, firstUserID(targetUserIDs))
|
||||
targetHostScopes, err := h.resolveGiftTargetHostScopes(request, targetUserIDs)
|
||||
if err != nil {
|
||||
httpkit.WriteRPCError(writer, request, err)
|
||||
return
|
||||
}
|
||||
firstTargetScope := firstGiftTargetHostScope(targetHostScopes, firstUserID(targetUserIDs))
|
||||
|
||||
resp, err := h.roomClient.SendGift(request.Context(), &roomv1.SendGiftRequest{
|
||||
Meta: httpkit.RoomMeta(request, body.RoomID, body.CommandID),
|
||||
@ -1574,9 +1575,10 @@ func (h *Handler) sendGift(writer http.ResponseWriter, request *http.Request) {
|
||||
PoolId: body.PoolID,
|
||||
SenderCountryId: senderCountryID,
|
||||
SenderRegionId: senderRegionID,
|
||||
TargetIsHost: targetIsHost,
|
||||
TargetHostRegionId: targetHostRegionID,
|
||||
TargetAgencyOwnerUserId: targetAgencyOwnerUserID,
|
||||
TargetIsHost: firstTargetScope.GetTargetIsHost(),
|
||||
TargetHostRegionId: firstTargetScope.GetTargetHostRegionId(),
|
||||
TargetAgencyOwnerUserId: firstTargetScope.GetTargetAgencyOwnerUserId(),
|
||||
TargetHostScopes: targetHostScopes,
|
||||
EntitlementId: strings.TrimSpace(body.EntitlementID),
|
||||
Source: strings.TrimSpace(body.Source),
|
||||
})
|
||||
@ -1627,6 +1629,33 @@ func (h *Handler) resolveGiftTargetHostScope(request *http.Request, targetUserID
|
||||
return true, profile.GetRegionId(), profile.GetCurrentAgencyOwnerUserId(), nil
|
||||
}
|
||||
|
||||
func (h *Handler) resolveGiftTargetHostScopes(request *http.Request, targetUserIDs []int64) ([]*roomv1.SendGiftTargetHostScope, error) {
|
||||
scopes := make([]*roomv1.SendGiftTargetHostScope, 0, len(targetUserIDs))
|
||||
for _, targetUserID := range targetUserIDs {
|
||||
targetIsHost, targetHostRegionID, targetAgencyOwnerUserID, err := h.resolveGiftTargetHostScope(request, targetUserID)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
// 批量送礼必须按接收方分别固化主播快照;只解析第一个 target 会让后续主播丢失工资钻石入账。
|
||||
scopes = append(scopes, &roomv1.SendGiftTargetHostScope{
|
||||
TargetUserId: targetUserID,
|
||||
TargetIsHost: targetIsHost,
|
||||
TargetHostRegionId: targetHostRegionID,
|
||||
TargetAgencyOwnerUserId: targetAgencyOwnerUserID,
|
||||
})
|
||||
}
|
||||
return scopes, nil
|
||||
}
|
||||
|
||||
func firstGiftTargetHostScope(scopes []*roomv1.SendGiftTargetHostScope, targetUserID int64) *roomv1.SendGiftTargetHostScope {
|
||||
for _, scope := range scopes {
|
||||
if scope.GetTargetUserId() == targetUserID {
|
||||
return scope
|
||||
}
|
||||
}
|
||||
return &roomv1.SendGiftTargetHostScope{TargetUserId: targetUserID}
|
||||
}
|
||||
|
||||
// batchRoomDisplayProfiles 返回房间和公屏专用展示资料。
|
||||
func (h *Handler) batchRoomDisplayProfiles(writer http.ResponseWriter, request *http.Request) {
|
||||
if h.userProfileClient == nil {
|
||||
|
||||
@ -430,6 +430,8 @@ type SendGift struct {
|
||||
TargetHostRegionID int64 `json:"target_host_region_id,omitempty"`
|
||||
// TargetAgencyOwnerUserID 是 gateway 从 user-service active host profile 注入的代理收款人快照。
|
||||
TargetAgencyOwnerUserID int64 `json:"target_agency_owner_user_id,omitempty"`
|
||||
// TargetHostScopes 是 gateway 为批量送礼逐接收方固化的主播工资入账快照。
|
||||
TargetHostScopes []GiftTargetHostScope `json:"target_host_scopes,omitempty"`
|
||||
// BillingReceiptID 是 wallet-service 成功落账后的回执,用于排障和恢复审计。
|
||||
BillingReceiptID string `json:"billing_receipt_id,omitempty"`
|
||||
// CoinSpent 是 sender COIN 实际扣减值,来自 wallet-service 服务端价格。
|
||||
@ -493,6 +495,14 @@ type SendGift struct {
|
||||
// Type 返回命令类型。
|
||||
func (SendGift) Type() string { return "send_gift" }
|
||||
|
||||
// GiftTargetHostScope 记录一个接收方在 gateway 入站时看到的主播工资入账资格。
|
||||
type GiftTargetHostScope struct {
|
||||
TargetUserID int64 `json:"target_user_id"`
|
||||
TargetIsHost bool `json:"target_is_host,omitempty"`
|
||||
TargetHostRegionID int64 `json:"target_host_region_id,omitempty"`
|
||||
TargetAgencyOwnerUserID int64 `json:"target_agency_owner_user_id,omitempty"`
|
||||
}
|
||||
|
||||
// RocketRewardGrant 记录发射命令中已经结算出的资源组发放结果。
|
||||
type RocketRewardGrant struct {
|
||||
RewardRole string `json:"reward_role"`
|
||||
|
||||
@ -83,6 +83,7 @@ func (s *Service) sendGift(ctx context.Context, req *roomv1.SendGiftRequest, rob
|
||||
// 工资区域由 gateway 从 user-service host profile 注入;房间 visible_region_id 只用于房间/礼物可见性。
|
||||
TargetHostRegionID: req.GetTargetHostRegionId(),
|
||||
TargetAgencyOwnerUserID: req.GetTargetAgencyOwnerUserId(),
|
||||
TargetHostScopes: giftTargetHostScopesFromProto(req.GetTargetHostScopes()),
|
||||
RobotGift: robotOptions.Enabled && !robotOptions.RealRoomHeat,
|
||||
RobotWalletGift: robotOptions.Enabled,
|
||||
SyntheticLuckyGift: robotOptions.Enabled && strings.TrimSpace(req.GetPoolId()) != "",
|
||||
@ -482,6 +483,7 @@ func (s *Service) debitGiftTargets(ctx context.Context, cmd command.SendGift, ro
|
||||
return billing, []giftTargetBilling{{TargetUserID: cmd.TargetUserID, CommandID: cmd.ID(), Billing: billing}}, nil
|
||||
}
|
||||
if len(cmd.TargetUserIDs) == 1 {
|
||||
targetScope := giftTargetHostScopeFor(cmd, cmd.TargetUserID)
|
||||
// 单目标保留原 wallet command_id,避免改变已有幂等键、账务回执和排障路径。
|
||||
billing, err := s.wallet.DebitGift(ctx, &walletv1.DebitGiftRequest{
|
||||
CommandId: cmd.ID(),
|
||||
@ -493,9 +495,9 @@ func (s *Service) debitGiftTargets(ctx context.Context, cmd command.SendGift, ro
|
||||
AppCode: appcode.FromContext(ctx),
|
||||
RegionId: roomMeta.VisibleRegionID,
|
||||
SenderRegionId: cmd.SenderRegionID,
|
||||
TargetIsHost: cmd.TargetIsHost,
|
||||
TargetHostRegionId: cmd.TargetHostRegionID,
|
||||
TargetAgencyOwnerUserId: cmd.TargetAgencyOwnerUserID,
|
||||
TargetIsHost: targetScope.TargetIsHost,
|
||||
TargetHostRegionId: targetScope.TargetHostRegionID,
|
||||
TargetAgencyOwnerUserId: targetScope.TargetAgencyOwnerUserID,
|
||||
EntitlementId: cmd.EntitlementID,
|
||||
ChargeSource: cmd.Source,
|
||||
})
|
||||
@ -507,15 +509,16 @@ func (s *Service) debitGiftTargets(ctx context.Context, cmd command.SendGift, ro
|
||||
|
||||
targets := make([]*walletv1.DebitGiftTarget, 0, len(cmd.TargetUserIDs))
|
||||
for _, targetUserID := range cmd.TargetUserIDs {
|
||||
targetScope := giftTargetHostScopeFor(cmd, targetUserID)
|
||||
target := &walletv1.DebitGiftTarget{
|
||||
CommandId: giftTargetCommandID(cmd.ID(), targetUserID, len(cmd.TargetUserIDs)),
|
||||
TargetUserId: targetUserID,
|
||||
}
|
||||
if targetUserID == cmd.TargetUserID && cmd.TargetIsHost {
|
||||
// 现有 room proto 只有单个 host scope;多目标时只允许把该快照用于对应的第一个目标,不能错误扩散到其他接收方。
|
||||
if targetScope.TargetIsHost {
|
||||
// 每个目标只使用 gateway 为该 target 固化的主播快照,避免批量送礼时第一个接收方以外的主播丢工资钻石。
|
||||
target.TargetIsHost = true
|
||||
target.TargetHostRegionId = cmd.TargetHostRegionID
|
||||
target.TargetAgencyOwnerUserId = cmd.TargetAgencyOwnerUserID
|
||||
target.TargetHostRegionId = targetScope.TargetHostRegionID
|
||||
target.TargetAgencyOwnerUserId = targetScope.TargetAgencyOwnerUserID
|
||||
}
|
||||
targets = append(targets, target)
|
||||
}
|
||||
@ -758,6 +761,43 @@ func normalizeGiftTargetUserIDs(targetUserID int64, targetUserIDs []int64) []int
|
||||
return ids
|
||||
}
|
||||
|
||||
func giftTargetHostScopesFromProto(scopes []*roomv1.SendGiftTargetHostScope) []command.GiftTargetHostScope {
|
||||
result := make([]command.GiftTargetHostScope, 0, len(scopes))
|
||||
seen := make(map[int64]bool, len(scopes))
|
||||
for _, scope := range scopes {
|
||||
targetUserID := scope.GetTargetUserId()
|
||||
if targetUserID <= 0 || seen[targetUserID] {
|
||||
continue
|
||||
}
|
||||
seen[targetUserID] = true
|
||||
result = append(result, command.GiftTargetHostScope{
|
||||
TargetUserID: targetUserID,
|
||||
TargetIsHost: scope.GetTargetIsHost(),
|
||||
TargetHostRegionID: scope.GetTargetHostRegionId(),
|
||||
TargetAgencyOwnerUserID: scope.GetTargetAgencyOwnerUserId(),
|
||||
})
|
||||
}
|
||||
return result
|
||||
}
|
||||
|
||||
func giftTargetHostScopeFor(cmd command.SendGift, targetUserID int64) command.GiftTargetHostScope {
|
||||
for _, scope := range cmd.TargetHostScopes {
|
||||
if scope.TargetUserID == targetUserID {
|
||||
return scope
|
||||
}
|
||||
}
|
||||
if targetUserID == cmd.TargetUserID && cmd.TargetIsHost {
|
||||
// 老 gateway 只会传单目标字段;保留这个兜底,避免滚动发布期间新 room-service 丢旧请求的主播入账。
|
||||
return command.GiftTargetHostScope{
|
||||
TargetUserID: targetUserID,
|
||||
TargetIsHost: true,
|
||||
TargetHostRegionID: cmd.TargetHostRegionID,
|
||||
TargetAgencyOwnerUserID: cmd.TargetAgencyOwnerUserID,
|
||||
}
|
||||
}
|
||||
return command.GiftTargetHostScope{TargetUserID: targetUserID}
|
||||
}
|
||||
|
||||
func giftTargetCommandID(commandID string, targetUserID int64, targetCount int) string {
|
||||
if targetCount <= 1 {
|
||||
return commandID
|
||||
|
||||
@ -131,6 +131,10 @@ func TestSendGiftBatchDebitsMultipleTargets(t *testing.T) {
|
||||
TargetUserIds: []int64{firstTargetID, secondTargetID},
|
||||
GiftId: "rose",
|
||||
GiftCount: 2,
|
||||
TargetHostScopes: []*roomv1.SendGiftTargetHostScope{
|
||||
{TargetUserId: firstTargetID, TargetIsHost: true, TargetHostRegionId: 8801, TargetAgencyOwnerUserId: 30001},
|
||||
{TargetUserId: secondTargetID, TargetIsHost: true, TargetHostRegionId: 8802, TargetAgencyOwnerUserId: 30002},
|
||||
},
|
||||
})
|
||||
if err != nil {
|
||||
t.Fatalf("send multi-target gift failed: %v", err)
|
||||
@ -153,6 +157,10 @@ func TestSendGiftBatchDebitsMultipleTargets(t *testing.T) {
|
||||
if wallet.lastBatch.GetTargets()[0].GetCommandId() != "cmd-gift-multi:target:202" || wallet.lastBatch.GetTargets()[1].GetCommandId() != "cmd-gift-multi:target:303" {
|
||||
t.Fatalf("wallet batch target command ids mismatch: %+v", wallet.lastBatch.GetTargets())
|
||||
}
|
||||
if !wallet.lastBatch.GetTargets()[0].GetTargetIsHost() || wallet.lastBatch.GetTargets()[0].GetTargetHostRegionId() != 8801 || wallet.lastBatch.GetTargets()[0].GetTargetAgencyOwnerUserId() != 30001 ||
|
||||
!wallet.lastBatch.GetTargets()[1].GetTargetIsHost() || wallet.lastBatch.GetTargets()[1].GetTargetHostRegionId() != 8802 || wallet.lastBatch.GetTargets()[1].GetTargetAgencyOwnerUserId() != 30002 {
|
||||
t.Fatalf("wallet batch target host scopes mismatch: %+v", wallet.lastBatch.GetTargets())
|
||||
}
|
||||
|
||||
events := roomGiftSentEvents(t, ctx, repository)
|
||||
if len(events) != 2 {
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user