diff --git a/api/proto/luckygift/v1/luckygift.pb.go b/api/proto/luckygift/v1/luckygift.pb.go index deb70ece..e98f7e06 100644 --- a/api/proto/luckygift/v1/luckygift.pb.go +++ b/api/proto/luckygift/v1/luckygift.pb.go @@ -3059,6 +3059,1168 @@ func (x *AdjustLuckyGiftPoolBalanceResponse) GetIdempotentReplay() bool { return false } +// 用户画像协议追加在文件尾,避免改变既有 message index。外部策略哈希只存在于 owner 数据库, +// 本协议永远只下发 external_user_id,防止运营误把不可逆内部键当成外部用户身份。 +type LuckyGiftUserProfileWindow struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Draws int64 `protobuf:"varint,1,opt,name=draws,proto3" json:"draws,omitempty"` + WagerCoins int64 `protobuf:"varint,2,opt,name=wager_coins,json=wagerCoins,proto3" json:"wager_coins,omitempty"` + PayoutCoins int64 `protobuf:"varint,3,opt,name=payout_coins,json=payoutCoins,proto3" json:"payout_coins,omitempty"` + NetCoins int64 `protobuf:"varint,4,opt,name=net_coins,json=netCoins,proto3" json:"net_coins,omitempty"` + RtpPpm int64 `protobuf:"varint,5,opt,name=rtp_ppm,json=rtpPpm,proto3" json:"rtp_ppm,omitempty"` + HasRtp bool `protobuf:"varint,6,opt,name=has_rtp,json=hasRtp,proto3" json:"has_rtp,omitempty"` +} + +func (x *LuckyGiftUserProfileWindow) Reset() { + *x = LuckyGiftUserProfileWindow{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[32] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LuckyGiftUserProfileWindow) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LuckyGiftUserProfileWindow) ProtoMessage() {} + +func (x *LuckyGiftUserProfileWindow) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[32] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LuckyGiftUserProfileWindow.ProtoReflect.Descriptor instead. +func (*LuckyGiftUserProfileWindow) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{32} +} + +func (x *LuckyGiftUserProfileWindow) GetDraws() int64 { + if x != nil { + return x.Draws + } + return 0 +} + +func (x *LuckyGiftUserProfileWindow) GetWagerCoins() int64 { + if x != nil { + return x.WagerCoins + } + return 0 +} + +func (x *LuckyGiftUserProfileWindow) GetPayoutCoins() int64 { + if x != nil { + return x.PayoutCoins + } + return 0 +} + +func (x *LuckyGiftUserProfileWindow) GetNetCoins() int64 { + if x != nil { + return x.NetCoins + } + return 0 +} + +func (x *LuckyGiftUserProfileWindow) GetRtpPpm() int64 { + if x != nil { + return x.RtpPpm + } + return 0 +} + +func (x *LuckyGiftUserProfileWindow) GetHasRtp() bool { + if x != nil { + return x.HasRtp + } + return false +} + +type LuckyGiftUserProfile struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + AppCode string `protobuf:"bytes,1,opt,name=app_code,json=appCode,proto3" json:"app_code,omitempty"` + PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"` + IdentityType string `protobuf:"bytes,3,opt,name=identity_type,json=identityType,proto3" json:"identity_type,omitempty"` + InternalUserId int64 `protobuf:"varint,4,opt,name=internal_user_id,json=internalUserId,proto3" json:"internal_user_id,omitempty"` + ExternalUserId string `protobuf:"bytes,5,opt,name=external_user_id,json=externalUserId,proto3" json:"external_user_id,omitempty"` + RuleVersion int64 `protobuf:"varint,6,opt,name=rule_version,json=ruleVersion,proto3" json:"rule_version,omitempty"` + StrategyVersion string `protobuf:"bytes,7,opt,name=strategy_version,json=strategyVersion,proto3" json:"strategy_version,omitempty"` + Stage string `protobuf:"bytes,8,opt,name=stage,proto3" json:"stage,omitempty"` + PaidDraws int64 `protobuf:"varint,9,opt,name=paid_draws,json=paidDraws,proto3" json:"paid_draws,omitempty"` + EquivalentDraws int64 `protobuf:"varint,10,opt,name=equivalent_draws,json=equivalentDraws,proto3" json:"equivalent_draws,omitempty"` + LossStreak int64 `protobuf:"varint,11,opt,name=loss_streak,json=lossStreak,proto3" json:"loss_streak,omitempty"` + PendingSpendJackpotTokens int64 `protobuf:"varint,12,opt,name=pending_spend_jackpot_tokens,json=pendingSpendJackpotTokens,proto3" json:"pending_spend_jackpot_tokens,omitempty"` + GuaranteeDrawsRemaining int64 `protobuf:"varint,13,opt,name=guarantee_draws_remaining,json=guaranteeDrawsRemaining,proto3" json:"guarantee_draws_remaining,omitempty"` + DownweightActive bool `protobuf:"varint,14,opt,name=downweight_active,json=downweightActive,proto3" json:"downweight_active,omitempty"` + DownweightFactorPpm int64 `protobuf:"varint,15,opt,name=downweight_factor_ppm,json=downweightFactorPpm,proto3" json:"downweight_factor_ppm,omitempty"` + OneHour *LuckyGiftUserProfileWindow `protobuf:"bytes,16,opt,name=one_hour,json=oneHour,proto3" json:"one_hour,omitempty"` + TwentyFourHours *LuckyGiftUserProfileWindow `protobuf:"bytes,17,opt,name=twenty_four_hours,json=twentyFourHours,proto3" json:"twenty_four_hours,omitempty"` + FortyEightHours *LuckyGiftUserProfileWindow `protobuf:"bytes,18,opt,name=forty_eight_hours,json=fortyEightHours,proto3" json:"forty_eight_hours,omitempty"` + Lifetime *LuckyGiftUserProfileWindow `protobuf:"bytes,19,opt,name=lifetime,proto3" json:"lifetime,omitempty"` + BaseRewardCoins int64 `protobuf:"varint,20,opt,name=base_reward_coins,json=baseRewardCoins,proto3" json:"base_reward_coins,omitempty"` + OrdinaryWinCount int64 `protobuf:"varint,21,opt,name=ordinary_win_count,json=ordinaryWinCount,proto3" json:"ordinary_win_count,omitempty"` + HighMultiplierOrdinaryWinCount int64 `protobuf:"varint,22,opt,name=high_multiplier_ordinary_win_count,json=highMultiplierOrdinaryWinCount,proto3" json:"high_multiplier_ordinary_win_count,omitempty"` + RtpCompensationJackpotCount int64 `protobuf:"varint,23,opt,name=rtp_compensation_jackpot_count,json=rtpCompensationJackpotCount,proto3" json:"rtp_compensation_jackpot_count,omitempty"` + CumulativeSpendJackpotCount int64 `protobuf:"varint,24,opt,name=cumulative_spend_jackpot_count,json=cumulativeSpendJackpotCount,proto3" json:"cumulative_spend_jackpot_count,omitempty"` + GuaranteeHitCount int64 `protobuf:"varint,25,opt,name=guarantee_hit_count,json=guaranteeHitCount,proto3" json:"guarantee_hit_count,omitempty"` + ZeroDrawCount int64 `protobuf:"varint,26,opt,name=zero_draw_count,json=zeroDrawCount,proto3" json:"zero_draw_count,omitempty"` + GrantedDrawCount int64 `protobuf:"varint,27,opt,name=granted_draw_count,json=grantedDrawCount,proto3" json:"granted_draw_count,omitempty"` + PendingDrawCount int64 `protobuf:"varint,28,opt,name=pending_draw_count,json=pendingDrawCount,proto3" json:"pending_draw_count,omitempty"` + FailedDrawCount int64 `protobuf:"varint,29,opt,name=failed_draw_count,json=failedDrawCount,proto3" json:"failed_draw_count,omitempty"` + MaxMultiplierPpm int64 `protobuf:"varint,30,opt,name=max_multiplier_ppm,json=maxMultiplierPpm,proto3" json:"max_multiplier_ppm,omitempty"` + MaxRewardCoins int64 `protobuf:"varint,31,opt,name=max_reward_coins,json=maxRewardCoins,proto3" json:"max_reward_coins,omitempty"` + FirstDrawAtMs int64 `protobuf:"varint,32,opt,name=first_draw_at_ms,json=firstDrawAtMs,proto3" json:"first_draw_at_ms,omitempty"` + LastDrawAtMs int64 `protobuf:"varint,33,opt,name=last_draw_at_ms,json=lastDrawAtMs,proto3" json:"last_draw_at_ms,omitempty"` + AggregatedAtMs int64 `protobuf:"varint,34,opt,name=aggregated_at_ms,json=aggregatedAtMs,proto3" json:"aggregated_at_ms,omitempty"` + DataLagMs int64 `protobuf:"varint,35,opt,name=data_lag_ms,json=dataLagMs,proto3" json:"data_lag_ms,omitempty"` + User_24HRtpThresholdPpm int64 `protobuf:"varint,36,opt,name=user_24h_rtp_threshold_ppm,json=user24hRtpThresholdPpm,proto3" json:"user_24h_rtp_threshold_ppm,omitempty"` +} + +func (x *LuckyGiftUserProfile) Reset() { + *x = LuckyGiftUserProfile{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[33] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LuckyGiftUserProfile) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LuckyGiftUserProfile) ProtoMessage() {} + +func (x *LuckyGiftUserProfile) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[33] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LuckyGiftUserProfile.ProtoReflect.Descriptor instead. +func (*LuckyGiftUserProfile) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{33} +} + +func (x *LuckyGiftUserProfile) GetAppCode() string { + if x != nil { + return x.AppCode + } + return "" +} + +func (x *LuckyGiftUserProfile) GetPoolId() string { + if x != nil { + return x.PoolId + } + return "" +} + +func (x *LuckyGiftUserProfile) GetIdentityType() string { + if x != nil { + return x.IdentityType + } + return "" +} + +func (x *LuckyGiftUserProfile) GetInternalUserId() int64 { + if x != nil { + return x.InternalUserId + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetExternalUserId() string { + if x != nil { + return x.ExternalUserId + } + return "" +} + +func (x *LuckyGiftUserProfile) GetRuleVersion() int64 { + if x != nil { + return x.RuleVersion + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetStrategyVersion() string { + if x != nil { + return x.StrategyVersion + } + return "" +} + +func (x *LuckyGiftUserProfile) GetStage() string { + if x != nil { + return x.Stage + } + return "" +} + +func (x *LuckyGiftUserProfile) GetPaidDraws() int64 { + if x != nil { + return x.PaidDraws + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetEquivalentDraws() int64 { + if x != nil { + return x.EquivalentDraws + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetLossStreak() int64 { + if x != nil { + return x.LossStreak + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetPendingSpendJackpotTokens() int64 { + if x != nil { + return x.PendingSpendJackpotTokens + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetGuaranteeDrawsRemaining() int64 { + if x != nil { + return x.GuaranteeDrawsRemaining + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetDownweightActive() bool { + if x != nil { + return x.DownweightActive + } + return false +} + +func (x *LuckyGiftUserProfile) GetDownweightFactorPpm() int64 { + if x != nil { + return x.DownweightFactorPpm + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetOneHour() *LuckyGiftUserProfileWindow { + if x != nil { + return x.OneHour + } + return nil +} + +func (x *LuckyGiftUserProfile) GetTwentyFourHours() *LuckyGiftUserProfileWindow { + if x != nil { + return x.TwentyFourHours + } + return nil +} + +func (x *LuckyGiftUserProfile) GetFortyEightHours() *LuckyGiftUserProfileWindow { + if x != nil { + return x.FortyEightHours + } + return nil +} + +func (x *LuckyGiftUserProfile) GetLifetime() *LuckyGiftUserProfileWindow { + if x != nil { + return x.Lifetime + } + return nil +} + +func (x *LuckyGiftUserProfile) GetBaseRewardCoins() int64 { + if x != nil { + return x.BaseRewardCoins + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetOrdinaryWinCount() int64 { + if x != nil { + return x.OrdinaryWinCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetHighMultiplierOrdinaryWinCount() int64 { + if x != nil { + return x.HighMultiplierOrdinaryWinCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetRtpCompensationJackpotCount() int64 { + if x != nil { + return x.RtpCompensationJackpotCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetCumulativeSpendJackpotCount() int64 { + if x != nil { + return x.CumulativeSpendJackpotCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetGuaranteeHitCount() int64 { + if x != nil { + return x.GuaranteeHitCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetZeroDrawCount() int64 { + if x != nil { + return x.ZeroDrawCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetGrantedDrawCount() int64 { + if x != nil { + return x.GrantedDrawCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetPendingDrawCount() int64 { + if x != nil { + return x.PendingDrawCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetFailedDrawCount() int64 { + if x != nil { + return x.FailedDrawCount + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetMaxMultiplierPpm() int64 { + if x != nil { + return x.MaxMultiplierPpm + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetMaxRewardCoins() int64 { + if x != nil { + return x.MaxRewardCoins + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetFirstDrawAtMs() int64 { + if x != nil { + return x.FirstDrawAtMs + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetLastDrawAtMs() int64 { + if x != nil { + return x.LastDrawAtMs + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetAggregatedAtMs() int64 { + if x != nil { + return x.AggregatedAtMs + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetDataLagMs() int64 { + if x != nil { + return x.DataLagMs + } + return 0 +} + +func (x *LuckyGiftUserProfile) GetUser_24HRtpThresholdPpm() int64 { + if x != nil { + return x.User_24HRtpThresholdPpm + } + return 0 +} + +type ListLuckyGiftUserProfilesRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"` + IdentityType string `protobuf:"bytes,3,opt,name=identity_type,json=identityType,proto3" json:"identity_type,omitempty"` + InternalUserId int64 `protobuf:"varint,4,opt,name=internal_user_id,json=internalUserId,proto3" json:"internal_user_id,omitempty"` + ExternalUserId string `protobuf:"bytes,5,opt,name=external_user_id,json=externalUserId,proto3" json:"external_user_id,omitempty"` + Stage string `protobuf:"bytes,6,opt,name=stage,proto3" json:"stage,omitempty"` + Status string `protobuf:"bytes,7,opt,name=status,proto3" json:"status,omitempty"` + SortBy string `protobuf:"bytes,8,opt,name=sort_by,json=sortBy,proto3" json:"sort_by,omitempty"` + SortDirection string `protobuf:"bytes,9,opt,name=sort_direction,json=sortDirection,proto3" json:"sort_direction,omitempty"` + Page int32 `protobuf:"varint,10,opt,name=page,proto3" json:"page,omitempty"` + PageSize int32 `protobuf:"varint,11,opt,name=page_size,json=pageSize,proto3" json:"page_size,omitempty"` + // cursor 是 owner service 生成的不透明 keyset 游标;page 仅保留旧客户端协议兼容,服务端不再执行 OFFSET。 + Cursor string `protobuf:"bytes,12,opt,name=cursor,proto3" json:"cursor,omitempty"` +} + +func (x *ListLuckyGiftUserProfilesRequest) Reset() { + *x = ListLuckyGiftUserProfilesRequest{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[34] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListLuckyGiftUserProfilesRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListLuckyGiftUserProfilesRequest) ProtoMessage() {} + +func (x *ListLuckyGiftUserProfilesRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[34] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListLuckyGiftUserProfilesRequest.ProtoReflect.Descriptor instead. +func (*ListLuckyGiftUserProfilesRequest) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{34} +} + +func (x *ListLuckyGiftUserProfilesRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *ListLuckyGiftUserProfilesRequest) GetPoolId() string { + if x != nil { + return x.PoolId + } + return "" +} + +func (x *ListLuckyGiftUserProfilesRequest) GetIdentityType() string { + if x != nil { + return x.IdentityType + } + return "" +} + +func (x *ListLuckyGiftUserProfilesRequest) GetInternalUserId() int64 { + if x != nil { + return x.InternalUserId + } + return 0 +} + +func (x *ListLuckyGiftUserProfilesRequest) GetExternalUserId() string { + if x != nil { + return x.ExternalUserId + } + return "" +} + +func (x *ListLuckyGiftUserProfilesRequest) GetStage() string { + if x != nil { + return x.Stage + } + return "" +} + +func (x *ListLuckyGiftUserProfilesRequest) GetStatus() string { + if x != nil { + return x.Status + } + return "" +} + +func (x *ListLuckyGiftUserProfilesRequest) GetSortBy() string { + if x != nil { + return x.SortBy + } + return "" +} + +func (x *ListLuckyGiftUserProfilesRequest) GetSortDirection() string { + if x != nil { + return x.SortDirection + } + return "" +} + +func (x *ListLuckyGiftUserProfilesRequest) GetPage() int32 { + if x != nil { + return x.Page + } + return 0 +} + +func (x *ListLuckyGiftUserProfilesRequest) GetPageSize() int32 { + if x != nil { + return x.PageSize + } + return 0 +} + +func (x *ListLuckyGiftUserProfilesRequest) GetCursor() string { + if x != nil { + return x.Cursor + } + return "" +} + +type ListLuckyGiftUserProfilesResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Profiles []*LuckyGiftUserProfile `protobuf:"bytes,1,rep,name=profiles,proto3" json:"profiles,omitempty"` + Total int64 `protobuf:"varint,2,opt,name=total,proto3" json:"total,omitempty"` + SnapshotAtMs int64 `protobuf:"varint,3,opt,name=snapshot_at_ms,json=snapshotAtMs,proto3" json:"snapshot_at_ms,omitempty"` + NextCursor string `protobuf:"bytes,4,opt,name=next_cursor,json=nextCursor,proto3" json:"next_cursor,omitempty"` + HasMore bool `protobuf:"varint,5,opt,name=has_more,json=hasMore,proto3" json:"has_more,omitempty"` +} + +func (x *ListLuckyGiftUserProfilesResponse) Reset() { + *x = ListLuckyGiftUserProfilesResponse{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[35] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *ListLuckyGiftUserProfilesResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*ListLuckyGiftUserProfilesResponse) ProtoMessage() {} + +func (x *ListLuckyGiftUserProfilesResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[35] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use ListLuckyGiftUserProfilesResponse.ProtoReflect.Descriptor instead. +func (*ListLuckyGiftUserProfilesResponse) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{35} +} + +func (x *ListLuckyGiftUserProfilesResponse) GetProfiles() []*LuckyGiftUserProfile { + if x != nil { + return x.Profiles + } + return nil +} + +func (x *ListLuckyGiftUserProfilesResponse) GetTotal() int64 { + if x != nil { + return x.Total + } + return 0 +} + +func (x *ListLuckyGiftUserProfilesResponse) GetSnapshotAtMs() int64 { + if x != nil { + return x.SnapshotAtMs + } + return 0 +} + +func (x *ListLuckyGiftUserProfilesResponse) GetNextCursor() string { + if x != nil { + return x.NextCursor + } + return "" +} + +func (x *ListLuckyGiftUserProfilesResponse) GetHasMore() bool { + if x != nil { + return x.HasMore + } + return false +} + +type GetLuckyGiftUserProfileRequest struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Meta *RequestMeta `protobuf:"bytes,1,opt,name=meta,proto3" json:"meta,omitempty"` + PoolId string `protobuf:"bytes,2,opt,name=pool_id,json=poolId,proto3" json:"pool_id,omitempty"` + IdentityType string `protobuf:"bytes,3,opt,name=identity_type,json=identityType,proto3" json:"identity_type,omitempty"` + InternalUserId int64 `protobuf:"varint,4,opt,name=internal_user_id,json=internalUserId,proto3" json:"internal_user_id,omitempty"` + ExternalUserId string `protobuf:"bytes,5,opt,name=external_user_id,json=externalUserId,proto3" json:"external_user_id,omitempty"` + JackpotPage int32 `protobuf:"varint,6,opt,name=jackpot_page,json=jackpotPage,proto3" json:"jackpot_page,omitempty"` + JackpotPageSize int32 `protobuf:"varint,7,opt,name=jackpot_page_size,json=jackpotPageSize,proto3" json:"jackpot_page_size,omitempty"` + // jackpot_cursor 由上一页 next_jackpot_cursor 原样返回;jackpot_page 仅保留协议兼容。 + JackpotCursor string `protobuf:"bytes,8,opt,name=jackpot_cursor,json=jackpotCursor,proto3" json:"jackpot_cursor,omitempty"` +} + +func (x *GetLuckyGiftUserProfileRequest) Reset() { + *x = GetLuckyGiftUserProfileRequest{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[36] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLuckyGiftUserProfileRequest) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLuckyGiftUserProfileRequest) ProtoMessage() {} + +func (x *GetLuckyGiftUserProfileRequest) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[36] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLuckyGiftUserProfileRequest.ProtoReflect.Descriptor instead. +func (*GetLuckyGiftUserProfileRequest) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{36} +} + +func (x *GetLuckyGiftUserProfileRequest) GetMeta() *RequestMeta { + if x != nil { + return x.Meta + } + return nil +} + +func (x *GetLuckyGiftUserProfileRequest) GetPoolId() string { + if x != nil { + return x.PoolId + } + return "" +} + +func (x *GetLuckyGiftUserProfileRequest) GetIdentityType() string { + if x != nil { + return x.IdentityType + } + return "" +} + +func (x *GetLuckyGiftUserProfileRequest) GetInternalUserId() int64 { + if x != nil { + return x.InternalUserId + } + return 0 +} + +func (x *GetLuckyGiftUserProfileRequest) GetExternalUserId() string { + if x != nil { + return x.ExternalUserId + } + return "" +} + +func (x *GetLuckyGiftUserProfileRequest) GetJackpotPage() int32 { + if x != nil { + return x.JackpotPage + } + return 0 +} + +func (x *GetLuckyGiftUserProfileRequest) GetJackpotPageSize() int32 { + if x != nil { + return x.JackpotPageSize + } + return 0 +} + +func (x *GetLuckyGiftUserProfileRequest) GetJackpotCursor() string { + if x != nil { + return x.JackpotCursor + } + return "" +} + +type LuckyGiftUserMultiplierStat struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + HitType string `protobuf:"bytes,1,opt,name=hit_type,json=hitType,proto3" json:"hit_type,omitempty"` + MultiplierPpm int64 `protobuf:"varint,2,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"` + DrawCount int64 `protobuf:"varint,3,opt,name=draw_count,json=drawCount,proto3" json:"draw_count,omitempty"` + WagerCoins int64 `protobuf:"varint,4,opt,name=wager_coins,json=wagerCoins,proto3" json:"wager_coins,omitempty"` + PayoutCoins int64 `protobuf:"varint,5,opt,name=payout_coins,json=payoutCoins,proto3" json:"payout_coins,omitempty"` +} + +func (x *LuckyGiftUserMultiplierStat) Reset() { + *x = LuckyGiftUserMultiplierStat{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[37] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LuckyGiftUserMultiplierStat) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LuckyGiftUserMultiplierStat) ProtoMessage() {} + +func (x *LuckyGiftUserMultiplierStat) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[37] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LuckyGiftUserMultiplierStat.ProtoReflect.Descriptor instead. +func (*LuckyGiftUserMultiplierStat) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{37} +} + +func (x *LuckyGiftUserMultiplierStat) GetHitType() string { + if x != nil { + return x.HitType + } + return "" +} + +func (x *LuckyGiftUserMultiplierStat) GetMultiplierPpm() int64 { + if x != nil { + return x.MultiplierPpm + } + return 0 +} + +func (x *LuckyGiftUserMultiplierStat) GetDrawCount() int64 { + if x != nil { + return x.DrawCount + } + return 0 +} + +func (x *LuckyGiftUserMultiplierStat) GetWagerCoins() int64 { + if x != nil { + return x.WagerCoins + } + return 0 +} + +func (x *LuckyGiftUserMultiplierStat) GetPayoutCoins() int64 { + if x != nil { + return x.PayoutCoins + } + return 0 +} + +type LuckyGiftUserJackpotCondition struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Name string `protobuf:"bytes,1,opt,name=name,proto3" json:"name,omitempty"` + Numerator int64 `protobuf:"varint,2,opt,name=numerator,proto3" json:"numerator,omitempty"` + Denominator int64 `protobuf:"varint,3,opt,name=denominator,proto3" json:"denominator,omitempty"` + RatioPpm int64 `protobuf:"varint,4,opt,name=ratio_ppm,json=ratioPpm,proto3" json:"ratio_ppm,omitempty"` + LimitPpm int64 `protobuf:"varint,5,opt,name=limit_ppm,json=limitPpm,proto3" json:"limit_ppm,omitempty"` + FactorPpm int64 `protobuf:"varint,6,opt,name=factor_ppm,json=factorPpm,proto3" json:"factor_ppm,omitempty"` + Passed bool `protobuf:"varint,7,opt,name=passed,proto3" json:"passed,omitempty"` + Reason string `protobuf:"bytes,8,opt,name=reason,proto3" json:"reason,omitempty"` +} + +func (x *LuckyGiftUserJackpotCondition) Reset() { + *x = LuckyGiftUserJackpotCondition{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[38] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LuckyGiftUserJackpotCondition) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LuckyGiftUserJackpotCondition) ProtoMessage() {} + +func (x *LuckyGiftUserJackpotCondition) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[38] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LuckyGiftUserJackpotCondition.ProtoReflect.Descriptor instead. +func (*LuckyGiftUserJackpotCondition) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{38} +} + +func (x *LuckyGiftUserJackpotCondition) GetName() string { + if x != nil { + return x.Name + } + return "" +} + +func (x *LuckyGiftUserJackpotCondition) GetNumerator() int64 { + if x != nil { + return x.Numerator + } + return 0 +} + +func (x *LuckyGiftUserJackpotCondition) GetDenominator() int64 { + if x != nil { + return x.Denominator + } + return 0 +} + +func (x *LuckyGiftUserJackpotCondition) GetRatioPpm() int64 { + if x != nil { + return x.RatioPpm + } + return 0 +} + +func (x *LuckyGiftUserJackpotCondition) GetLimitPpm() int64 { + if x != nil { + return x.LimitPpm + } + return 0 +} + +func (x *LuckyGiftUserJackpotCondition) GetFactorPpm() int64 { + if x != nil { + return x.FactorPpm + } + return 0 +} + +func (x *LuckyGiftUserJackpotCondition) GetPassed() bool { + if x != nil { + return x.Passed + } + return false +} + +func (x *LuckyGiftUserJackpotCondition) GetReason() string { + if x != nil { + return x.Reason + } + return "" +} + +type LuckyGiftUserJackpotHit struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + DrawId string `protobuf:"bytes,1,opt,name=draw_id,json=drawId,proto3" json:"draw_id,omitempty"` + RequestId string `protobuf:"bytes,2,opt,name=request_id,json=requestId,proto3" json:"request_id,omitempty"` + RuleVersion int64 `protobuf:"varint,3,opt,name=rule_version,json=ruleVersion,proto3" json:"rule_version,omitempty"` + Stage string `protobuf:"bytes,4,opt,name=stage,proto3" json:"stage,omitempty"` + Mechanism string `protobuf:"bytes,5,opt,name=mechanism,proto3" json:"mechanism,omitempty"` + ReasonCode string `protobuf:"bytes,6,opt,name=reason_code,json=reasonCode,proto3" json:"reason_code,omitempty"` + ReasonSummary string `protobuf:"bytes,7,opt,name=reason_summary,json=reasonSummary,proto3" json:"reason_summary,omitempty"` + TierId string `protobuf:"bytes,8,opt,name=tier_id,json=tierId,proto3" json:"tier_id,omitempty"` + MultiplierPpm int64 `protobuf:"varint,9,opt,name=multiplier_ppm,json=multiplierPpm,proto3" json:"multiplier_ppm,omitempty"` + WagerCoins int64 `protobuf:"varint,10,opt,name=wager_coins,json=wagerCoins,proto3" json:"wager_coins,omitempty"` + PayoutCoins int64 `protobuf:"varint,11,opt,name=payout_coins,json=payoutCoins,proto3" json:"payout_coins,omitempty"` + OccurredAtMs int64 `protobuf:"varint,12,opt,name=occurred_at_ms,json=occurredAtMs,proto3" json:"occurred_at_ms,omitempty"` + Conditions []*LuckyGiftUserJackpotCondition `protobuf:"bytes,13,rep,name=conditions,proto3" json:"conditions,omitempty"` + // event_key 只标识这条画像大奖事实,供前端稳定追加去重;不包含用户策略键或用户哈希。 + EventKey string `protobuf:"bytes,14,opt,name=event_key,json=eventKey,proto3" json:"event_key,omitempty"` +} + +func (x *LuckyGiftUserJackpotHit) Reset() { + *x = LuckyGiftUserJackpotHit{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[39] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *LuckyGiftUserJackpotHit) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*LuckyGiftUserJackpotHit) ProtoMessage() {} + +func (x *LuckyGiftUserJackpotHit) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[39] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use LuckyGiftUserJackpotHit.ProtoReflect.Descriptor instead. +func (*LuckyGiftUserJackpotHit) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{39} +} + +func (x *LuckyGiftUserJackpotHit) GetDrawId() string { + if x != nil { + return x.DrawId + } + return "" +} + +func (x *LuckyGiftUserJackpotHit) GetRequestId() string { + if x != nil { + return x.RequestId + } + return "" +} + +func (x *LuckyGiftUserJackpotHit) GetRuleVersion() int64 { + if x != nil { + return x.RuleVersion + } + return 0 +} + +func (x *LuckyGiftUserJackpotHit) GetStage() string { + if x != nil { + return x.Stage + } + return "" +} + +func (x *LuckyGiftUserJackpotHit) GetMechanism() string { + if x != nil { + return x.Mechanism + } + return "" +} + +func (x *LuckyGiftUserJackpotHit) GetReasonCode() string { + if x != nil { + return x.ReasonCode + } + return "" +} + +func (x *LuckyGiftUserJackpotHit) GetReasonSummary() string { + if x != nil { + return x.ReasonSummary + } + return "" +} + +func (x *LuckyGiftUserJackpotHit) GetTierId() string { + if x != nil { + return x.TierId + } + return "" +} + +func (x *LuckyGiftUserJackpotHit) GetMultiplierPpm() int64 { + if x != nil { + return x.MultiplierPpm + } + return 0 +} + +func (x *LuckyGiftUserJackpotHit) GetWagerCoins() int64 { + if x != nil { + return x.WagerCoins + } + return 0 +} + +func (x *LuckyGiftUserJackpotHit) GetPayoutCoins() int64 { + if x != nil { + return x.PayoutCoins + } + return 0 +} + +func (x *LuckyGiftUserJackpotHit) GetOccurredAtMs() int64 { + if x != nil { + return x.OccurredAtMs + } + return 0 +} + +func (x *LuckyGiftUserJackpotHit) GetConditions() []*LuckyGiftUserJackpotCondition { + if x != nil { + return x.Conditions + } + return nil +} + +func (x *LuckyGiftUserJackpotHit) GetEventKey() string { + if x != nil { + return x.EventKey + } + return "" +} + +type GetLuckyGiftUserProfileResponse struct { + state protoimpl.MessageState + sizeCache protoimpl.SizeCache + unknownFields protoimpl.UnknownFields + + Profile *LuckyGiftUserProfile `protobuf:"bytes,1,opt,name=profile,proto3" json:"profile,omitempty"` + MultiplierDistribution []*LuckyGiftUserMultiplierStat `protobuf:"bytes,2,rep,name=multiplier_distribution,json=multiplierDistribution,proto3" json:"multiplier_distribution,omitempty"` + JackpotHits []*LuckyGiftUserJackpotHit `protobuf:"bytes,3,rep,name=jackpot_hits,json=jackpotHits,proto3" json:"jackpot_hits,omitempty"` + JackpotHitTotal int64 `protobuf:"varint,4,opt,name=jackpot_hit_total,json=jackpotHitTotal,proto3" json:"jackpot_hit_total,omitempty"` + SnapshotAtMs int64 `protobuf:"varint,5,opt,name=snapshot_at_ms,json=snapshotAtMs,proto3" json:"snapshot_at_ms,omitempty"` + JackpotPage int32 `protobuf:"varint,6,opt,name=jackpot_page,json=jackpotPage,proto3" json:"jackpot_page,omitempty"` + JackpotPageSize int32 `protobuf:"varint,7,opt,name=jackpot_page_size,json=jackpotPageSize,proto3" json:"jackpot_page_size,omitempty"` + NextJackpotCursor string `protobuf:"bytes,8,opt,name=next_jackpot_cursor,json=nextJackpotCursor,proto3" json:"next_jackpot_cursor,omitempty"` + JackpotHasMore bool `protobuf:"varint,9,opt,name=jackpot_has_more,json=jackpotHasMore,proto3" json:"jackpot_has_more,omitempty"` +} + +func (x *GetLuckyGiftUserProfileResponse) Reset() { + *x = GetLuckyGiftUserProfileResponse{} + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[40] + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + ms.StoreMessageInfo(mi) +} + +func (x *GetLuckyGiftUserProfileResponse) String() string { + return protoimpl.X.MessageStringOf(x) +} + +func (*GetLuckyGiftUserProfileResponse) ProtoMessage() {} + +func (x *GetLuckyGiftUserProfileResponse) ProtoReflect() protoreflect.Message { + mi := &file_proto_luckygift_v1_luckygift_proto_msgTypes[40] + if x != nil { + ms := protoimpl.X.MessageStateOf(protoimpl.Pointer(x)) + if ms.LoadMessageInfo() == nil { + ms.StoreMessageInfo(mi) + } + return ms + } + return mi.MessageOf(x) +} + +// Deprecated: Use GetLuckyGiftUserProfileResponse.ProtoReflect.Descriptor instead. +func (*GetLuckyGiftUserProfileResponse) Descriptor() ([]byte, []int) { + return file_proto_luckygift_v1_luckygift_proto_rawDescGZIP(), []int{40} +} + +func (x *GetLuckyGiftUserProfileResponse) GetProfile() *LuckyGiftUserProfile { + if x != nil { + return x.Profile + } + return nil +} + +func (x *GetLuckyGiftUserProfileResponse) GetMultiplierDistribution() []*LuckyGiftUserMultiplierStat { + if x != nil { + return x.MultiplierDistribution + } + return nil +} + +func (x *GetLuckyGiftUserProfileResponse) GetJackpotHits() []*LuckyGiftUserJackpotHit { + if x != nil { + return x.JackpotHits + } + return nil +} + +func (x *GetLuckyGiftUserProfileResponse) GetJackpotHitTotal() int64 { + if x != nil { + return x.JackpotHitTotal + } + return 0 +} + +func (x *GetLuckyGiftUserProfileResponse) GetSnapshotAtMs() int64 { + if x != nil { + return x.SnapshotAtMs + } + return 0 +} + +func (x *GetLuckyGiftUserProfileResponse) GetJackpotPage() int32 { + if x != nil { + return x.JackpotPage + } + return 0 +} + +func (x *GetLuckyGiftUserProfileResponse) GetJackpotPageSize() int32 { + if x != nil { + return x.JackpotPageSize + } + return 0 +} + +func (x *GetLuckyGiftUserProfileResponse) GetNextJackpotCursor() string { + if x != nil { + return x.NextJackpotCursor + } + return "" +} + +func (x *GetLuckyGiftUserProfileResponse) GetJackpotHasMore() bool { + if x != nil { + return x.JackpotHasMore + } + return false +} + var File_proto_luckygift_v1_luckygift_proto protoreflect.FileDescriptor var file_proto_luckygift_v1_luckygift_proto_rawDesc = []byte{ @@ -3698,100 +4860,399 @@ var file_proto_luckygift_v1_luckygift_proto_rawDesc = []byte{ 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x04, 0x70, 0x6f, 0x6f, 0x6c, 0x12, 0x2b, 0x0a, 0x11, 0x69, 0x64, 0x65, 0x6d, 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x74, 0x5f, 0x72, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x18, 0x03, 0x20, 0x01, 0x28, 0x08, 0x52, 0x10, 0x69, 0x64, 0x65, 0x6d, - 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x32, 0xf7, 0x03, 0x0a, - 0x10, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, - 0x65, 0x12, 0x67, 0x0a, 0x0e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, - 0x69, 0x66, 0x74, 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, - 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, - 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, - 0x66, 0x74, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x14, 0x45, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, - 0x61, 0x77, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, - 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, - 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, - 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, - 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19, 0x42, 0x61, 0x74, 0x63, 0x68, 0x45, - 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, - 0x72, 0x61, 0x77, 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, - 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, - 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, - 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x42, - 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, - 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, - 0x12, 0x74, 0x0a, 0x17, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x45, 0x78, 0x74, 0x65, 0x72, - 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x12, 0x2b, 0x2e, 0x68, 0x79, + 0x70, 0x6f, 0x74, 0x65, 0x6e, 0x74, 0x52, 0x65, 0x70, 0x6c, 0x61, 0x79, 0x22, 0xc5, 0x01, 0x0a, + 0x1a, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x12, 0x14, 0x0a, 0x05, 0x64, + 0x72, 0x61, 0x77, 0x73, 0x18, 0x01, 0x20, 0x01, 0x28, 0x03, 0x52, 0x05, 0x64, 0x72, 0x61, 0x77, + 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, + 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x77, 0x61, 0x67, 0x65, 0x72, 0x43, 0x6f, 0x69, + 0x6e, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x5f, 0x63, 0x6f, 0x69, + 0x6e, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6f, 0x75, 0x74, + 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x1b, 0x0a, 0x09, 0x6e, 0x65, 0x74, 0x5f, 0x63, 0x6f, 0x69, + 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6e, 0x65, 0x74, 0x43, 0x6f, 0x69, + 0x6e, 0x73, 0x12, 0x17, 0x0a, 0x07, 0x72, 0x74, 0x70, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x06, 0x72, 0x74, 0x70, 0x50, 0x70, 0x6d, 0x12, 0x17, 0x0a, 0x07, 0x68, + 0x61, 0x73, 0x5f, 0x72, 0x74, 0x70, 0x18, 0x06, 0x20, 0x01, 0x28, 0x08, 0x52, 0x06, 0x68, 0x61, + 0x73, 0x52, 0x74, 0x70, 0x22, 0xfd, 0x0d, 0x0a, 0x14, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, + 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x19, 0x0a, + 0x08, 0x61, 0x70, 0x70, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x07, 0x61, 0x70, 0x70, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, + 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, + 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, + 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, + 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x28, 0x0a, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x73, 0x65, + 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x78, 0x74, 0x65, + 0x72, 0x6e, 0x61, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x75, + 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x18, 0x06, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0b, 0x72, 0x75, 0x6c, 0x65, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x29, 0x0a, + 0x10, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, 0x79, 0x5f, 0x76, 0x65, 0x72, 0x73, 0x69, 0x6f, + 0x6e, 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0f, 0x73, 0x74, 0x72, 0x61, 0x74, 0x65, 0x67, + 0x79, 0x56, 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, + 0x65, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x1d, + 0x0a, 0x0a, 0x70, 0x61, 0x69, 0x64, 0x5f, 0x64, 0x72, 0x61, 0x77, 0x73, 0x18, 0x09, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x09, 0x70, 0x61, 0x69, 0x64, 0x44, 0x72, 0x61, 0x77, 0x73, 0x12, 0x29, 0x0a, + 0x10, 0x65, 0x71, 0x75, 0x69, 0x76, 0x61, 0x6c, 0x65, 0x6e, 0x74, 0x5f, 0x64, 0x72, 0x61, 0x77, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x65, 0x71, 0x75, 0x69, 0x76, 0x61, 0x6c, + 0x65, 0x6e, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x12, 0x1f, 0x0a, 0x0b, 0x6c, 0x6f, 0x73, 0x73, + 0x5f, 0x73, 0x74, 0x72, 0x65, 0x61, 0x6b, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x6c, + 0x6f, 0x73, 0x73, 0x53, 0x74, 0x72, 0x65, 0x61, 0x6b, 0x12, 0x3f, 0x0a, 0x1c, 0x70, 0x65, 0x6e, + 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x73, 0x70, 0x65, 0x6e, 0x64, 0x5f, 0x6a, 0x61, 0x63, 0x6b, 0x70, + 0x6f, 0x74, 0x5f, 0x74, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x19, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x53, 0x70, 0x65, 0x6e, 0x64, 0x4a, 0x61, 0x63, + 0x6b, 0x70, 0x6f, 0x74, 0x54, 0x6f, 0x6b, 0x65, 0x6e, 0x73, 0x12, 0x3a, 0x0a, 0x19, 0x67, 0x75, + 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x5f, 0x64, 0x72, 0x61, 0x77, 0x73, 0x5f, 0x72, 0x65, + 0x6d, 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x18, 0x0d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x17, 0x67, + 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65, 0x6d, + 0x61, 0x69, 0x6e, 0x69, 0x6e, 0x67, 0x12, 0x2b, 0x0a, 0x11, 0x64, 0x6f, 0x77, 0x6e, 0x77, 0x65, + 0x69, 0x67, 0x68, 0x74, 0x5f, 0x61, 0x63, 0x74, 0x69, 0x76, 0x65, 0x18, 0x0e, 0x20, 0x01, 0x28, + 0x08, 0x52, 0x10, 0x64, 0x6f, 0x77, 0x6e, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x41, 0x63, 0x74, + 0x69, 0x76, 0x65, 0x12, 0x32, 0x0a, 0x15, 0x64, 0x6f, 0x77, 0x6e, 0x77, 0x65, 0x69, 0x67, 0x68, + 0x74, 0x5f, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x0f, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x13, 0x64, 0x6f, 0x77, 0x6e, 0x77, 0x65, 0x69, 0x67, 0x68, 0x74, 0x46, 0x61, + 0x63, 0x74, 0x6f, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x49, 0x0a, 0x08, 0x6f, 0x6e, 0x65, 0x5f, 0x68, + 0x6f, 0x75, 0x72, 0x18, 0x10, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x07, 0x6f, 0x6e, 0x65, 0x48, 0x6f, + 0x75, 0x72, 0x12, 0x5a, 0x0a, 0x11, 0x74, 0x77, 0x65, 0x6e, 0x74, 0x79, 0x5f, 0x66, 0x6f, 0x75, + 0x72, 0x5f, 0x68, 0x6f, 0x75, 0x72, 0x73, 0x18, 0x11, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x0f, 0x74, + 0x77, 0x65, 0x6e, 0x74, 0x79, 0x46, 0x6f, 0x75, 0x72, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, 0x5a, + 0x0a, 0x11, 0x66, 0x6f, 0x72, 0x74, 0x79, 0x5f, 0x65, 0x69, 0x67, 0x68, 0x74, 0x5f, 0x68, 0x6f, + 0x75, 0x72, 0x73, 0x18, 0x12, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, + 0x69, 0x6c, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x0f, 0x66, 0x6f, 0x72, 0x74, 0x79, + 0x45, 0x69, 0x67, 0x68, 0x74, 0x48, 0x6f, 0x75, 0x72, 0x73, 0x12, 0x4a, 0x0a, 0x08, 0x6c, 0x69, + 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x18, 0x13, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x2e, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x57, 0x69, 0x6e, 0x64, 0x6f, 0x77, 0x52, 0x08, 0x6c, 0x69, + 0x66, 0x65, 0x74, 0x69, 0x6d, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x62, 0x61, 0x73, 0x65, 0x5f, 0x72, + 0x65, 0x77, 0x61, 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x14, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0f, 0x62, 0x61, 0x73, 0x65, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, + 0x6e, 0x73, 0x12, 0x2c, 0x0a, 0x12, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x5f, 0x77, + 0x69, 0x6e, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x15, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, + 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x57, 0x69, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, + 0x12, 0x4a, 0x0a, 0x22, 0x68, 0x69, 0x67, 0x68, 0x5f, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, + 0x69, 0x65, 0x72, 0x5f, 0x6f, 0x72, 0x64, 0x69, 0x6e, 0x61, 0x72, 0x79, 0x5f, 0x77, 0x69, 0x6e, + 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x16, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1e, 0x68, 0x69, + 0x67, 0x68, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x4f, 0x72, 0x64, 0x69, + 0x6e, 0x61, 0x72, 0x79, 0x57, 0x69, 0x6e, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x43, 0x0a, 0x1e, + 0x72, 0x74, 0x70, 0x5f, 0x63, 0x6f, 0x6d, 0x70, 0x65, 0x6e, 0x73, 0x61, 0x74, 0x69, 0x6f, 0x6e, + 0x5f, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x17, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, 0x72, 0x74, 0x70, 0x43, 0x6f, 0x6d, 0x70, 0x65, 0x6e, 0x73, + 0x61, 0x74, 0x69, 0x6f, 0x6e, 0x4a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x43, 0x6f, 0x75, 0x6e, + 0x74, 0x12, 0x43, 0x0a, 0x1e, 0x63, 0x75, 0x6d, 0x75, 0x6c, 0x61, 0x74, 0x69, 0x76, 0x65, 0x5f, + 0x73, 0x70, 0x65, 0x6e, 0x64, 0x5f, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, 0x63, 0x6f, + 0x75, 0x6e, 0x74, 0x18, 0x18, 0x20, 0x01, 0x28, 0x03, 0x52, 0x1b, 0x63, 0x75, 0x6d, 0x75, 0x6c, + 0x61, 0x74, 0x69, 0x76, 0x65, 0x53, 0x70, 0x65, 0x6e, 0x64, 0x4a, 0x61, 0x63, 0x6b, 0x70, 0x6f, + 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2e, 0x0a, 0x13, 0x67, 0x75, 0x61, 0x72, 0x61, 0x6e, + 0x74, 0x65, 0x65, 0x5f, 0x68, 0x69, 0x74, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x19, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x11, 0x67, 0x75, 0x61, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x65, 0x48, 0x69, + 0x74, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x26, 0x0a, 0x0f, 0x7a, 0x65, 0x72, 0x6f, 0x5f, 0x64, + 0x72, 0x61, 0x77, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x1a, 0x20, 0x01, 0x28, 0x03, 0x52, + 0x0d, 0x7a, 0x65, 0x72, 0x6f, 0x44, 0x72, 0x61, 0x77, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, + 0x0a, 0x12, 0x67, 0x72, 0x61, 0x6e, 0x74, 0x65, 0x64, 0x5f, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x63, + 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x1b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x67, 0x72, 0x61, 0x6e, + 0x74, 0x65, 0x64, 0x44, 0x72, 0x61, 0x77, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, + 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, 0x67, 0x5f, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x63, 0x6f, 0x75, + 0x6e, 0x74, 0x18, 0x1c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x10, 0x70, 0x65, 0x6e, 0x64, 0x69, 0x6e, + 0x67, 0x44, 0x72, 0x61, 0x77, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2a, 0x0a, 0x11, 0x66, 0x61, + 0x69, 0x6c, 0x65, 0x64, 0x5f, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, + 0x1d, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x66, 0x61, 0x69, 0x6c, 0x65, 0x64, 0x44, 0x72, 0x61, + 0x77, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x2c, 0x0a, 0x12, 0x6d, 0x61, 0x78, 0x5f, 0x6d, 0x75, + 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x1e, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x10, 0x6d, 0x61, 0x78, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, + 0x72, 0x50, 0x70, 0x6d, 0x12, 0x28, 0x0a, 0x10, 0x6d, 0x61, 0x78, 0x5f, 0x72, 0x65, 0x77, 0x61, + 0x72, 0x64, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x1f, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, + 0x6d, 0x61, 0x78, 0x52, 0x65, 0x77, 0x61, 0x72, 0x64, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x27, + 0x0a, 0x10, 0x66, 0x69, 0x72, 0x73, 0x74, 0x5f, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x61, 0x74, 0x5f, + 0x6d, 0x73, 0x18, 0x20, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, 0x66, 0x69, 0x72, 0x73, 0x74, 0x44, + 0x72, 0x61, 0x77, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x25, 0x0a, 0x0f, 0x6c, 0x61, 0x73, 0x74, 0x5f, + 0x64, 0x72, 0x61, 0x77, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x21, 0x20, 0x01, 0x28, 0x03, + 0x52, 0x0c, 0x6c, 0x61, 0x73, 0x74, 0x44, 0x72, 0x61, 0x77, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x28, + 0x0a, 0x10, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, 0x61, 0x74, 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, + 0x6d, 0x73, 0x18, 0x22, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x61, 0x67, 0x67, 0x72, 0x65, 0x67, + 0x61, 0x74, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x1e, 0x0a, 0x0b, 0x64, 0x61, 0x74, 0x61, + 0x5f, 0x6c, 0x61, 0x67, 0x5f, 0x6d, 0x73, 0x18, 0x23, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x64, + 0x61, 0x74, 0x61, 0x4c, 0x61, 0x67, 0x4d, 0x73, 0x12, 0x3a, 0x0a, 0x1a, 0x75, 0x73, 0x65, 0x72, + 0x5f, 0x32, 0x34, 0x68, 0x5f, 0x72, 0x74, 0x70, 0x5f, 0x74, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, + 0x6c, 0x64, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x24, 0x20, 0x01, 0x28, 0x03, 0x52, 0x16, 0x75, 0x73, + 0x65, 0x72, 0x32, 0x34, 0x68, 0x52, 0x74, 0x70, 0x54, 0x68, 0x72, 0x65, 0x73, 0x68, 0x6f, 0x6c, + 0x64, 0x50, 0x70, 0x6d, 0x22, 0xa0, 0x03, 0x0a, 0x20, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, + 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, 0x0a, 0x04, 0x6d, 0x65, 0x74, + 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, + 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x52, 0x65, 0x71, + 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x12, 0x17, + 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, 0x69, 0x64, 0x65, 0x6e, 0x74, + 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0c, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, 0x65, 0x12, 0x28, 0x0a, 0x10, + 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, + 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, + 0x61, 0x6c, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x0e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, + 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x16, 0x0a, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, + 0x18, 0x07, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x73, 0x74, 0x61, 0x74, 0x75, 0x73, 0x12, 0x17, + 0x0a, 0x07, 0x73, 0x6f, 0x72, 0x74, 0x5f, 0x62, 0x79, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x73, 0x6f, 0x72, 0x74, 0x42, 0x79, 0x12, 0x25, 0x0a, 0x0e, 0x73, 0x6f, 0x72, 0x74, 0x5f, + 0x64, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x18, 0x09, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0d, 0x73, 0x6f, 0x72, 0x74, 0x44, 0x69, 0x72, 0x65, 0x63, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x12, + 0x0a, 0x04, 0x70, 0x61, 0x67, 0x65, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x05, 0x52, 0x04, 0x70, 0x61, + 0x67, 0x65, 0x12, 0x1b, 0x0a, 0x09, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, + 0x0b, 0x20, 0x01, 0x28, 0x05, 0x52, 0x08, 0x70, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, + 0x16, 0x0a, 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x06, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x22, 0xe1, 0x01, 0x0a, 0x21, 0x4c, 0x69, 0x73, 0x74, + 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x44, 0x0a, + 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x18, 0x01, 0x20, 0x03, 0x28, 0x0b, 0x32, + 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x08, 0x70, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x73, 0x12, 0x14, 0x0a, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x18, 0x02, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x05, 0x74, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x6e, 0x61, + 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0c, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x41, 0x74, 0x4d, 0x73, 0x12, + 0x1f, 0x0a, 0x0b, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x04, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x0a, 0x6e, 0x65, 0x78, 0x74, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, + 0x12, 0x19, 0x0a, 0x08, 0x68, 0x61, 0x73, 0x5f, 0x6d, 0x6f, 0x72, 0x65, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x08, 0x52, 0x07, 0x68, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x22, 0xdd, 0x02, 0x0a, 0x1e, + 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, + 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x12, 0x33, + 0x0a, 0x04, 0x6d, 0x65, 0x74, 0x61, 0x18, 0x01, 0x20, 0x01, 0x28, 0x0b, 0x32, 0x1f, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x4d, 0x65, 0x74, 0x61, 0x52, 0x04, 0x6d, + 0x65, 0x74, 0x61, 0x12, 0x17, 0x0a, 0x07, 0x70, 0x6f, 0x6f, 0x6c, 0x5f, 0x69, 0x64, 0x18, 0x02, + 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x70, 0x6f, 0x6f, 0x6c, 0x49, 0x64, 0x12, 0x23, 0x0a, 0x0d, + 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x03, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0c, 0x69, 0x64, 0x65, 0x6e, 0x74, 0x69, 0x74, 0x79, 0x54, 0x79, 0x70, + 0x65, 0x12, 0x28, 0x0a, 0x10, 0x69, 0x6e, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x73, + 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0e, 0x69, 0x6e, 0x74, + 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x55, 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x28, 0x0a, 0x10, 0x65, + 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x5f, 0x75, 0x73, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, + 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0e, 0x65, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x55, + 0x73, 0x65, 0x72, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, + 0x5f, 0x70, 0x61, 0x67, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x6a, 0x61, 0x63, + 0x6b, 0x70, 0x6f, 0x74, 0x50, 0x61, 0x67, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6a, 0x61, 0x63, 0x6b, + 0x70, 0x6f, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x05, 0x52, 0x0f, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x50, 0x61, 0x67, 0x65, + 0x53, 0x69, 0x7a, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, + 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x0d, 0x6a, 0x61, + 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x22, 0xc2, 0x01, 0x0a, 0x1b, + 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x75, 0x6c, + 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x12, 0x19, 0x0a, 0x08, 0x68, + 0x69, 0x74, 0x5f, 0x74, 0x79, 0x70, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x07, 0x68, + 0x69, 0x74, 0x54, 0x79, 0x70, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, + 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0d, + 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, 0x70, 0x6d, 0x12, 0x1d, 0x0a, + 0x0a, 0x64, 0x72, 0x61, 0x77, 0x5f, 0x63, 0x6f, 0x75, 0x6e, 0x74, 0x18, 0x03, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x09, 0x64, 0x72, 0x61, 0x77, 0x43, 0x6f, 0x75, 0x6e, 0x74, 0x12, 0x1f, 0x0a, 0x0b, + 0x77, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x04, 0x20, 0x01, 0x28, + 0x03, 0x52, 0x0a, 0x77, 0x61, 0x67, 0x65, 0x72, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x21, 0x0a, + 0x0c, 0x70, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x5f, 0x63, 0x6f, 0x69, 0x6e, 0x73, 0x18, 0x05, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x73, + 0x22, 0xfc, 0x01, 0x0a, 0x1d, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, + 0x65, 0x72, 0x4a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, + 0x6f, 0x6e, 0x12, 0x12, 0x0a, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, + 0x52, 0x04, 0x6e, 0x61, 0x6d, 0x65, 0x12, 0x1c, 0x0a, 0x09, 0x6e, 0x75, 0x6d, 0x65, 0x72, 0x61, + 0x74, 0x6f, 0x72, 0x18, 0x02, 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x6e, 0x75, 0x6d, 0x65, 0x72, + 0x61, 0x74, 0x6f, 0x72, 0x12, 0x20, 0x0a, 0x0b, 0x64, 0x65, 0x6e, 0x6f, 0x6d, 0x69, 0x6e, 0x61, + 0x74, 0x6f, 0x72, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x64, 0x65, 0x6e, 0x6f, 0x6d, + 0x69, 0x6e, 0x61, 0x74, 0x6f, 0x72, 0x12, 0x1b, 0x0a, 0x09, 0x72, 0x61, 0x74, 0x69, 0x6f, 0x5f, + 0x70, 0x70, 0x6d, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x72, 0x61, 0x74, 0x69, 0x6f, + 0x50, 0x70, 0x6d, 0x12, 0x1b, 0x0a, 0x09, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x5f, 0x70, 0x70, 0x6d, + 0x18, 0x05, 0x20, 0x01, 0x28, 0x03, 0x52, 0x08, 0x6c, 0x69, 0x6d, 0x69, 0x74, 0x50, 0x70, 0x6d, + 0x12, 0x1d, 0x0a, 0x0a, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x06, + 0x20, 0x01, 0x28, 0x03, 0x52, 0x09, 0x66, 0x61, 0x63, 0x74, 0x6f, 0x72, 0x50, 0x70, 0x6d, 0x12, + 0x16, 0x0a, 0x06, 0x70, 0x61, 0x73, 0x73, 0x65, 0x64, 0x18, 0x07, 0x20, 0x01, 0x28, 0x08, 0x52, + 0x06, 0x70, 0x61, 0x73, 0x73, 0x65, 0x64, 0x12, 0x16, 0x0a, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, + 0x6e, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x22, + 0x8a, 0x04, 0x0a, 0x17, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x4a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x48, 0x69, 0x74, 0x12, 0x17, 0x0a, 0x07, 0x64, + 0x72, 0x61, 0x77, 0x5f, 0x69, 0x64, 0x18, 0x01, 0x20, 0x01, 0x28, 0x09, 0x52, 0x06, 0x64, 0x72, + 0x61, 0x77, 0x49, 0x64, 0x12, 0x1d, 0x0a, 0x0a, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x5f, + 0x69, 0x64, 0x18, 0x02, 0x20, 0x01, 0x28, 0x09, 0x52, 0x09, 0x72, 0x65, 0x71, 0x75, 0x65, 0x73, + 0x74, 0x49, 0x64, 0x12, 0x21, 0x0a, 0x0c, 0x72, 0x75, 0x6c, 0x65, 0x5f, 0x76, 0x65, 0x72, 0x73, + 0x69, 0x6f, 0x6e, 0x18, 0x03, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x72, 0x75, 0x6c, 0x65, 0x56, + 0x65, 0x72, 0x73, 0x69, 0x6f, 0x6e, 0x12, 0x14, 0x0a, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x18, + 0x04, 0x20, 0x01, 0x28, 0x09, 0x52, 0x05, 0x73, 0x74, 0x61, 0x67, 0x65, 0x12, 0x1c, 0x0a, 0x09, + 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x18, 0x05, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x09, 0x6d, 0x65, 0x63, 0x68, 0x61, 0x6e, 0x69, 0x73, 0x6d, 0x12, 0x1f, 0x0a, 0x0b, 0x72, 0x65, + 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x63, 0x6f, 0x64, 0x65, 0x18, 0x06, 0x20, 0x01, 0x28, 0x09, 0x52, + 0x0a, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x43, 0x6f, 0x64, 0x65, 0x12, 0x25, 0x0a, 0x0e, 0x72, + 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x5f, 0x73, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x18, 0x07, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x0d, 0x72, 0x65, 0x61, 0x73, 0x6f, 0x6e, 0x53, 0x75, 0x6d, 0x6d, 0x61, + 0x72, 0x79, 0x12, 0x17, 0x0a, 0x07, 0x74, 0x69, 0x65, 0x72, 0x5f, 0x69, 0x64, 0x18, 0x08, 0x20, + 0x01, 0x28, 0x09, 0x52, 0x06, 0x74, 0x69, 0x65, 0x72, 0x49, 0x64, 0x12, 0x25, 0x0a, 0x0e, 0x6d, + 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x5f, 0x70, 0x70, 0x6d, 0x18, 0x09, 0x20, + 0x01, 0x28, 0x03, 0x52, 0x0d, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, 0x65, 0x72, 0x50, + 0x70, 0x6d, 0x12, 0x1f, 0x0a, 0x0b, 0x77, 0x61, 0x67, 0x65, 0x72, 0x5f, 0x63, 0x6f, 0x69, 0x6e, + 0x73, 0x18, 0x0a, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0a, 0x77, 0x61, 0x67, 0x65, 0x72, 0x43, 0x6f, + 0x69, 0x6e, 0x73, 0x12, 0x21, 0x0a, 0x0c, 0x70, 0x61, 0x79, 0x6f, 0x75, 0x74, 0x5f, 0x63, 0x6f, + 0x69, 0x6e, 0x73, 0x18, 0x0b, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0b, 0x70, 0x61, 0x79, 0x6f, 0x75, + 0x74, 0x43, 0x6f, 0x69, 0x6e, 0x73, 0x12, 0x24, 0x0a, 0x0e, 0x6f, 0x63, 0x63, 0x75, 0x72, 0x72, + 0x65, 0x64, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x0c, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0c, + 0x6f, 0x63, 0x63, 0x75, 0x72, 0x72, 0x65, 0x64, 0x41, 0x74, 0x4d, 0x73, 0x12, 0x51, 0x0a, 0x0a, + 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x18, 0x0d, 0x20, 0x03, 0x28, 0x0b, + 0x32, 0x31, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, + 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x4a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x43, 0x6f, 0x6e, 0x64, 0x69, 0x74, + 0x69, 0x6f, 0x6e, 0x52, 0x0a, 0x63, 0x6f, 0x6e, 0x64, 0x69, 0x74, 0x69, 0x6f, 0x6e, 0x73, 0x12, + 0x1b, 0x0a, 0x09, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x5f, 0x6b, 0x65, 0x79, 0x18, 0x0e, 0x20, 0x01, + 0x28, 0x09, 0x52, 0x08, 0x65, 0x76, 0x65, 0x6e, 0x74, 0x4b, 0x65, 0x79, 0x22, 0x9a, 0x04, 0x0a, + 0x1f, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, + 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x42, 0x0a, 0x07, 0x70, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x18, 0x01, 0x20, 0x01, 0x28, + 0x0b, 0x32, 0x28, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, + 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x07, 0x70, 0x72, 0x6f, + 0x66, 0x69, 0x6c, 0x65, 0x12, 0x68, 0x0a, 0x17, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, + 0x65, 0x72, 0x5f, 0x64, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x18, + 0x02, 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, + 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, + 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, + 0x65, 0x72, 0x53, 0x74, 0x61, 0x74, 0x52, 0x16, 0x6d, 0x75, 0x6c, 0x74, 0x69, 0x70, 0x6c, 0x69, + 0x65, 0x72, 0x44, 0x69, 0x73, 0x74, 0x72, 0x69, 0x62, 0x75, 0x74, 0x69, 0x6f, 0x6e, 0x12, 0x4e, + 0x0a, 0x0c, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, 0x68, 0x69, 0x74, 0x73, 0x18, 0x03, + 0x20, 0x03, 0x28, 0x0b, 0x32, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, + 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, + 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x4a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x48, 0x69, + 0x74, 0x52, 0x0b, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x48, 0x69, 0x74, 0x73, 0x12, 0x2a, + 0x0a, 0x11, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, 0x68, 0x69, 0x74, 0x5f, 0x74, 0x6f, + 0x74, 0x61, 0x6c, 0x18, 0x04, 0x20, 0x01, 0x28, 0x03, 0x52, 0x0f, 0x6a, 0x61, 0x63, 0x6b, 0x70, + 0x6f, 0x74, 0x48, 0x69, 0x74, 0x54, 0x6f, 0x74, 0x61, 0x6c, 0x12, 0x24, 0x0a, 0x0e, 0x73, 0x6e, + 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x5f, 0x61, 0x74, 0x5f, 0x6d, 0x73, 0x18, 0x05, 0x20, 0x01, + 0x28, 0x03, 0x52, 0x0c, 0x73, 0x6e, 0x61, 0x70, 0x73, 0x68, 0x6f, 0x74, 0x41, 0x74, 0x4d, 0x73, + 0x12, 0x21, 0x0a, 0x0c, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, 0x70, 0x61, 0x67, 0x65, + 0x18, 0x06, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0b, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x50, + 0x61, 0x67, 0x65, 0x12, 0x2a, 0x0a, 0x11, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, 0x70, + 0x61, 0x67, 0x65, 0x5f, 0x73, 0x69, 0x7a, 0x65, 0x18, 0x07, 0x20, 0x01, 0x28, 0x05, 0x52, 0x0f, + 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x50, 0x61, 0x67, 0x65, 0x53, 0x69, 0x7a, 0x65, 0x12, + 0x2e, 0x0a, 0x13, 0x6e, 0x65, 0x78, 0x74, 0x5f, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, + 0x63, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x18, 0x08, 0x20, 0x01, 0x28, 0x09, 0x52, 0x11, 0x6e, 0x65, + 0x78, 0x74, 0x4a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x43, 0x75, 0x72, 0x73, 0x6f, 0x72, 0x12, + 0x28, 0x0a, 0x10, 0x6a, 0x61, 0x63, 0x6b, 0x70, 0x6f, 0x74, 0x5f, 0x68, 0x61, 0x73, 0x5f, 0x6d, + 0x6f, 0x72, 0x65, 0x18, 0x09, 0x20, 0x01, 0x28, 0x08, 0x52, 0x0e, 0x6a, 0x61, 0x63, 0x6b, 0x70, + 0x6f, 0x74, 0x48, 0x61, 0x73, 0x4d, 0x6f, 0x72, 0x65, 0x32, 0xf7, 0x03, 0x0a, 0x10, 0x4c, 0x75, + 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x67, + 0x0a, 0x0e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, + 0x12, 0x29, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, + 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79, + 0x47, 0x69, 0x66, 0x74, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, - 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x43, 0x68, 0x65, 0x63, 0x6b, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x52, + 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x14, 0x45, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x12, + 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, + 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, + 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, + 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19, 0x42, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x65, 0x63, + 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, + 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, + 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, 0x68, 0x45, 0x78, 0x65, 0x63, 0x75, + 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, + 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x42, 0x61, 0x74, 0x63, + 0x68, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, + 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x74, 0x0a, + 0x17, 0x45, 0x78, 0x65, 0x63, 0x75, 0x74, 0x65, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, + 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x12, 0x2b, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, - 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x32, 0x98, 0x07, 0x0a, 0x15, 0x41, 0x64, 0x6d, 0x69, 0x6e, - 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, - 0x12, 0x73, 0x0a, 0x12, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, - 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, - 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, - 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, - 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, - 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, - 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, - 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, - 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x30, - 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, - 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, - 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, - 0x1a, 0x31, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, - 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x75, 0x63, 0x6b, - 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x79, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, - 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x2f, 0x2e, 0x68, 0x79, - 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, - 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, - 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, - 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, - 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x73, - 0x0a, 0x12, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, - 0x72, 0x61, 0x77, 0x73, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, - 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, - 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, - 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, - 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, - 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, - 0x32, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, - 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, - 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, - 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, + 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x2c, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, + 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x45, 0x78, 0x74, 0x65, 0x72, + 0x6e, 0x61, 0x6c, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x52, 0x65, 0x73, 0x70, 0x6f, + 0x6e, 0x73, 0x65, 0x32, 0xa8, 0x09, 0x0a, 0x15, 0x41, 0x64, 0x6d, 0x69, 0x6e, 0x4c, 0x75, 0x63, + 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x53, 0x65, 0x72, 0x76, 0x69, 0x63, 0x65, 0x12, 0x73, 0x0a, + 0x12, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, + 0x66, 0x69, 0x67, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, - 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, - 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, - 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, - 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, - 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, - 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, - 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x68, + 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, + 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, + 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, + 0x73, 0x65, 0x12, 0x7c, 0x0a, 0x15, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x75, 0x63, 0x6b, + 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x12, 0x30, 0x2e, 0x68, 0x79, + 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, + 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, + 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x31, 0x2e, + 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, + 0x76, 0x31, 0x2e, 0x55, 0x70, 0x73, 0x65, 0x72, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, + 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x79, 0x0a, 0x14, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, + 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, 0x67, 0x73, 0x12, 0x2f, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, + 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, + 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, 0x69, + 0x67, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x30, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x43, 0x6f, 0x6e, 0x66, + 0x69, 0x67, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x73, 0x0a, 0x12, 0x4c, + 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, + 0x73, 0x12, 0x2d, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, + 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, + 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x2e, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, + 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, + 0x69, 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x82, 0x01, 0x0a, 0x17, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, + 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x12, 0x32, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, - 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, - 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, - 0x6e, 0x73, 0x65, 0x12, 0x8b, 0x01, 0x0a, 0x1a, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, + 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x44, 0x72, + 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, + 0x1a, 0x33, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, + 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, + 0x66, 0x74, 0x44, 0x72, 0x61, 0x77, 0x53, 0x75, 0x6d, 0x6d, 0x61, 0x72, 0x79, 0x52, 0x65, 0x73, + 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x12, 0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, - 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, - 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, - 0x63, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x68, 0x79, 0x61, 0x70, - 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, - 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, - 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, - 0x65, 0x42, 0x30, 0x5a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, - 0x2f, 0x61, 0x70, 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x75, 0x63, 0x6b, 0x79, - 0x67, 0x69, 0x66, 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, - 0x74, 0x76, 0x31, 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, + 0x63, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, + 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, + 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, + 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, + 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, + 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, + 0x12, 0x8b, 0x01, 0x0a, 0x1a, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, + 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x12, + 0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, + 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x6a, 0x75, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, + 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, + 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x36, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, + 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x41, 0x64, 0x6a, 0x75, + 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x50, 0x6f, 0x6f, 0x6c, 0x42, + 0x61, 0x6c, 0x61, 0x6e, 0x63, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x88, + 0x01, 0x0a, 0x19, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, + 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x12, 0x34, 0x2e, 0x68, + 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, + 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, + 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x73, 0x52, 0x65, 0x71, 0x75, 0x65, + 0x73, 0x74, 0x1a, 0x35, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, + 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x4c, 0x69, 0x73, 0x74, 0x4c, 0x75, 0x63, 0x6b, + 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, + 0x73, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x12, 0x82, 0x01, 0x0a, 0x17, 0x47, 0x65, + 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, + 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x12, 0x32, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x75, + 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, 0x65, 0x74, 0x4c, 0x75, + 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, 0x72, 0x6f, 0x66, 0x69, + 0x6c, 0x65, 0x52, 0x65, 0x71, 0x75, 0x65, 0x73, 0x74, 0x1a, 0x33, 0x2e, 0x68, 0x79, 0x61, 0x70, + 0x70, 0x2e, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x2e, 0x76, 0x31, 0x2e, 0x47, + 0x65, 0x74, 0x4c, 0x75, 0x63, 0x6b, 0x79, 0x47, 0x69, 0x66, 0x74, 0x55, 0x73, 0x65, 0x72, 0x50, + 0x72, 0x6f, 0x66, 0x69, 0x6c, 0x65, 0x52, 0x65, 0x73, 0x70, 0x6f, 0x6e, 0x73, 0x65, 0x42, 0x30, + 0x5a, 0x2e, 0x68, 0x79, 0x61, 0x70, 0x70, 0x2e, 0x6c, 0x6f, 0x63, 0x61, 0x6c, 0x2f, 0x61, 0x70, + 0x69, 0x2f, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x2f, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, + 0x74, 0x2f, 0x76, 0x31, 0x3b, 0x6c, 0x75, 0x63, 0x6b, 0x79, 0x67, 0x69, 0x66, 0x74, 0x76, 0x31, + 0x62, 0x06, 0x70, 0x72, 0x6f, 0x74, 0x6f, 0x33, } var ( @@ -3806,7 +5267,7 @@ func file_proto_luckygift_v1_luckygift_proto_rawDescGZIP() []byte { return file_proto_luckygift_v1_luckygift_proto_rawDescData } -var file_proto_luckygift_v1_luckygift_proto_msgTypes = make([]protoimpl.MessageInfo, 32) +var file_proto_luckygift_v1_luckygift_proto_msgTypes = make([]protoimpl.MessageInfo, 41) var file_proto_luckygift_v1_luckygift_proto_goTypes = []any{ (*RequestMeta)(nil), // 0: hyapp.luckygift.v1.RequestMeta (*LuckyGiftMeta)(nil), // 1: hyapp.luckygift.v1.LuckyGiftMeta @@ -3840,6 +5301,15 @@ var file_proto_luckygift_v1_luckygift_proto_goTypes = []any{ (*AdjustLuckyGiftPoolBalanceRequest)(nil), // 29: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest (*LuckyGiftPoolAdjustment)(nil), // 30: hyapp.luckygift.v1.LuckyGiftPoolAdjustment (*AdjustLuckyGiftPoolBalanceResponse)(nil), // 31: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse + (*LuckyGiftUserProfileWindow)(nil), // 32: hyapp.luckygift.v1.LuckyGiftUserProfileWindow + (*LuckyGiftUserProfile)(nil), // 33: hyapp.luckygift.v1.LuckyGiftUserProfile + (*ListLuckyGiftUserProfilesRequest)(nil), // 34: hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest + (*ListLuckyGiftUserProfilesResponse)(nil), // 35: hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse + (*GetLuckyGiftUserProfileRequest)(nil), // 36: hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest + (*LuckyGiftUserMultiplierStat)(nil), // 37: hyapp.luckygift.v1.LuckyGiftUserMultiplierStat + (*LuckyGiftUserJackpotCondition)(nil), // 38: hyapp.luckygift.v1.LuckyGiftUserJackpotCondition + (*LuckyGiftUserJackpotHit)(nil), // 39: hyapp.luckygift.v1.LuckyGiftUserJackpotHit + (*GetLuckyGiftUserProfileResponse)(nil), // 40: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse } var file_proto_luckygift_v1_luckygift_proto_depIdxs = []int32{ 0, // 0: hyapp.luckygift.v1.LuckyGiftMeta.meta:type_name -> hyapp.luckygift.v1.RequestMeta @@ -3868,33 +5338,48 @@ var file_proto_luckygift_v1_luckygift_proto_depIdxs = []int32{ 0, // 23: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta 30, // 24: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.adjustment:type_name -> hyapp.luckygift.v1.LuckyGiftPoolAdjustment 23, // 25: hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse.pool:type_name -> hyapp.luckygift.v1.LuckyGiftPoolBalance - 5, // 26: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.luckygift.v1.CheckLuckyGiftRequest - 8, // 27: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest - 10, // 28: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest - 12, // 29: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:input_type -> hyapp.luckygift.v1.ExternalGiftDrawRequest - 14, // 30: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.luckygift.v1.GetLuckyGiftConfigRequest - 16, // 31: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest - 18, // 32: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsRequest - 20, // 33: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsRequest - 24, // 34: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:input_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest - 26, // 35: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:input_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest - 29, // 36: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:input_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest - 6, // 37: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.luckygift.v1.CheckLuckyGiftResponse - 9, // 38: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse - 11, // 39: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse - 13, // 40: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:output_type -> hyapp.luckygift.v1.ExternalGiftDrawResponse - 15, // 41: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.luckygift.v1.GetLuckyGiftConfigResponse - 17, // 42: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse - 19, // 43: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsResponse - 21, // 44: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsResponse - 25, // 45: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:output_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse - 27, // 46: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:output_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse - 31, // 47: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:output_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse - 37, // [37:48] is the sub-list for method output_type - 26, // [26:37] is the sub-list for method input_type - 26, // [26:26] is the sub-list for extension type_name - 26, // [26:26] is the sub-list for extension extendee - 0, // [0:26] is the sub-list for field type_name + 32, // 26: hyapp.luckygift.v1.LuckyGiftUserProfile.one_hour:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 32, // 27: hyapp.luckygift.v1.LuckyGiftUserProfile.twenty_four_hours:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 32, // 28: hyapp.luckygift.v1.LuckyGiftUserProfile.forty_eight_hours:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 32, // 29: hyapp.luckygift.v1.LuckyGiftUserProfile.lifetime:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfileWindow + 0, // 30: hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 33, // 31: hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse.profiles:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfile + 0, // 32: hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest.meta:type_name -> hyapp.luckygift.v1.RequestMeta + 38, // 33: hyapp.luckygift.v1.LuckyGiftUserJackpotHit.conditions:type_name -> hyapp.luckygift.v1.LuckyGiftUserJackpotCondition + 33, // 34: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.profile:type_name -> hyapp.luckygift.v1.LuckyGiftUserProfile + 37, // 35: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.multiplier_distribution:type_name -> hyapp.luckygift.v1.LuckyGiftUserMultiplierStat + 39, // 36: hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse.jackpot_hits:type_name -> hyapp.luckygift.v1.LuckyGiftUserJackpotHit + 5, // 37: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:input_type -> hyapp.luckygift.v1.CheckLuckyGiftRequest + 8, // 38: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawRequest + 10, // 39: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:input_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawRequest + 12, // 40: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:input_type -> hyapp.luckygift.v1.ExternalGiftDrawRequest + 14, // 41: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:input_type -> hyapp.luckygift.v1.GetLuckyGiftConfigRequest + 16, // 42: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:input_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigRequest + 18, // 43: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:input_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsRequest + 20, // 44: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:input_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsRequest + 24, // 45: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:input_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryRequest + 26, // 46: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:input_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesRequest + 29, // 47: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:input_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceRequest + 34, // 48: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftUserProfiles:input_type -> hyapp.luckygift.v1.ListLuckyGiftUserProfilesRequest + 36, // 49: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftUserProfile:input_type -> hyapp.luckygift.v1.GetLuckyGiftUserProfileRequest + 6, // 50: hyapp.luckygift.v1.LuckyGiftService.CheckLuckyGift:output_type -> hyapp.luckygift.v1.CheckLuckyGiftResponse + 9, // 51: hyapp.luckygift.v1.LuckyGiftService.ExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.ExecuteLuckyGiftDrawResponse + 11, // 52: hyapp.luckygift.v1.LuckyGiftService.BatchExecuteLuckyGiftDraw:output_type -> hyapp.luckygift.v1.BatchExecuteLuckyGiftDrawResponse + 13, // 53: hyapp.luckygift.v1.LuckyGiftService.ExecuteExternalGiftDraw:output_type -> hyapp.luckygift.v1.ExternalGiftDrawResponse + 15, // 54: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftConfig:output_type -> hyapp.luckygift.v1.GetLuckyGiftConfigResponse + 17, // 55: hyapp.luckygift.v1.AdminLuckyGiftService.UpsertLuckyGiftConfig:output_type -> hyapp.luckygift.v1.UpsertLuckyGiftConfigResponse + 19, // 56: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftConfigs:output_type -> hyapp.luckygift.v1.ListLuckyGiftConfigsResponse + 21, // 57: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftDraws:output_type -> hyapp.luckygift.v1.ListLuckyGiftDrawsResponse + 25, // 58: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftDrawSummary:output_type -> hyapp.luckygift.v1.GetLuckyGiftDrawSummaryResponse + 27, // 59: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftPoolBalances:output_type -> hyapp.luckygift.v1.ListLuckyGiftPoolBalancesResponse + 31, // 60: hyapp.luckygift.v1.AdminLuckyGiftService.AdjustLuckyGiftPoolBalance:output_type -> hyapp.luckygift.v1.AdjustLuckyGiftPoolBalanceResponse + 35, // 61: hyapp.luckygift.v1.AdminLuckyGiftService.ListLuckyGiftUserProfiles:output_type -> hyapp.luckygift.v1.ListLuckyGiftUserProfilesResponse + 40, // 62: hyapp.luckygift.v1.AdminLuckyGiftService.GetLuckyGiftUserProfile:output_type -> hyapp.luckygift.v1.GetLuckyGiftUserProfileResponse + 50, // [50:63] is the sub-list for method output_type + 37, // [37:50] is the sub-list for method input_type + 37, // [37:37] is the sub-list for extension type_name + 37, // [37:37] is the sub-list for extension extendee + 0, // [0:37] is the sub-list for field type_name } func init() { file_proto_luckygift_v1_luckygift_proto_init() } @@ -3908,7 +5393,7 @@ func file_proto_luckygift_v1_luckygift_proto_init() { GoPackagePath: reflect.TypeOf(x{}).PkgPath(), RawDescriptor: file_proto_luckygift_v1_luckygift_proto_rawDesc, NumEnums: 0, - NumMessages: 32, + NumMessages: 41, NumExtensions: 0, NumServices: 2, }, diff --git a/api/proto/luckygift/v1/luckygift.proto b/api/proto/luckygift/v1/luckygift.proto index 516a3ebc..2563a93f 100644 --- a/api/proto/luckygift/v1/luckygift.proto +++ b/api/proto/luckygift/v1/luckygift.proto @@ -324,6 +324,8 @@ service AdminLuckyGiftService { rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse); rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse); rpc AdjustLuckyGiftPoolBalance(AdjustLuckyGiftPoolBalanceRequest) returns (AdjustLuckyGiftPoolBalanceResponse); + rpc ListLuckyGiftUserProfiles(ListLuckyGiftUserProfilesRequest) returns (ListLuckyGiftUserProfilesResponse); + rpc GetLuckyGiftUserProfile(GetLuckyGiftUserProfileRequest) returns (GetLuckyGiftUserProfileResponse); } // LuckyGiftHit 只返回批量逐份开奖中实际中奖的精简位置,不复制全部审计明细。 @@ -368,3 +370,138 @@ message AdjustLuckyGiftPoolBalanceResponse { LuckyGiftPoolBalance pool = 2; bool idempotent_replay = 3; } + +// 用户画像协议追加在文件尾,避免改变既有 message index。外部策略哈希只存在于 owner 数据库, +// 本协议永远只下发 external_user_id,防止运营误把不可逆内部键当成外部用户身份。 +message LuckyGiftUserProfileWindow { + int64 draws = 1; + int64 wager_coins = 2; + int64 payout_coins = 3; + int64 net_coins = 4; + int64 rtp_ppm = 5; + bool has_rtp = 6; +} + +message LuckyGiftUserProfile { + string app_code = 1; + string pool_id = 2; + string identity_type = 3; + int64 internal_user_id = 4; + string external_user_id = 5; + int64 rule_version = 6; + string strategy_version = 7; + string stage = 8; + int64 paid_draws = 9; + int64 equivalent_draws = 10; + int64 loss_streak = 11; + int64 pending_spend_jackpot_tokens = 12; + int64 guarantee_draws_remaining = 13; + bool downweight_active = 14; + int64 downweight_factor_ppm = 15; + LuckyGiftUserProfileWindow one_hour = 16; + LuckyGiftUserProfileWindow twenty_four_hours = 17; + LuckyGiftUserProfileWindow forty_eight_hours = 18; + LuckyGiftUserProfileWindow lifetime = 19; + int64 base_reward_coins = 20; + int64 ordinary_win_count = 21; + int64 high_multiplier_ordinary_win_count = 22; + int64 rtp_compensation_jackpot_count = 23; + int64 cumulative_spend_jackpot_count = 24; + int64 guarantee_hit_count = 25; + int64 zero_draw_count = 26; + int64 granted_draw_count = 27; + int64 pending_draw_count = 28; + int64 failed_draw_count = 29; + int64 max_multiplier_ppm = 30; + int64 max_reward_coins = 31; + int64 first_draw_at_ms = 32; + int64 last_draw_at_ms = 33; + int64 aggregated_at_ms = 34; + int64 data_lag_ms = 35; + int64 user_24h_rtp_threshold_ppm = 36; +} + +message ListLuckyGiftUserProfilesRequest { + RequestMeta meta = 1; + string pool_id = 2; + string identity_type = 3; + int64 internal_user_id = 4; + string external_user_id = 5; + string stage = 6; + string status = 7; + string sort_by = 8; + string sort_direction = 9; + int32 page = 10; + int32 page_size = 11; + // cursor 是 owner service 生成的不透明 keyset 游标;page 仅保留旧客户端协议兼容,服务端不再执行 OFFSET。 + string cursor = 12; +} + +message ListLuckyGiftUserProfilesResponse { + repeated LuckyGiftUserProfile profiles = 1; + int64 total = 2; + int64 snapshot_at_ms = 3; + string next_cursor = 4; + bool has_more = 5; +} + +message GetLuckyGiftUserProfileRequest { + RequestMeta meta = 1; + string pool_id = 2; + string identity_type = 3; + int64 internal_user_id = 4; + string external_user_id = 5; + int32 jackpot_page = 6; + int32 jackpot_page_size = 7; + // jackpot_cursor 由上一页 next_jackpot_cursor 原样返回;jackpot_page 仅保留协议兼容。 + string jackpot_cursor = 8; +} + +message LuckyGiftUserMultiplierStat { + string hit_type = 1; + int64 multiplier_ppm = 2; + int64 draw_count = 3; + int64 wager_coins = 4; + int64 payout_coins = 5; +} + +message LuckyGiftUserJackpotCondition { + string name = 1; + int64 numerator = 2; + int64 denominator = 3; + int64 ratio_ppm = 4; + int64 limit_ppm = 5; + int64 factor_ppm = 6; + bool passed = 7; + string reason = 8; +} + +message LuckyGiftUserJackpotHit { + string draw_id = 1; + string request_id = 2; + int64 rule_version = 3; + string stage = 4; + string mechanism = 5; + string reason_code = 6; + string reason_summary = 7; + string tier_id = 8; + int64 multiplier_ppm = 9; + int64 wager_coins = 10; + int64 payout_coins = 11; + int64 occurred_at_ms = 12; + repeated LuckyGiftUserJackpotCondition conditions = 13; + // event_key 只标识这条画像大奖事实,供前端稳定追加去重;不包含用户策略键或用户哈希。 + string event_key = 14; +} + +message GetLuckyGiftUserProfileResponse { + LuckyGiftUserProfile profile = 1; + repeated LuckyGiftUserMultiplierStat multiplier_distribution = 2; + repeated LuckyGiftUserJackpotHit jackpot_hits = 3; + int64 jackpot_hit_total = 4; + int64 snapshot_at_ms = 5; + int32 jackpot_page = 6; + int32 jackpot_page_size = 7; + string next_jackpot_cursor = 8; + bool jackpot_has_more = 9; +} diff --git a/api/proto/luckygift/v1/luckygift_grpc.pb.go b/api/proto/luckygift/v1/luckygift_grpc.pb.go index 7e666eca..5c43a7e3 100644 --- a/api/proto/luckygift/v1/luckygift_grpc.pb.go +++ b/api/proto/luckygift/v1/luckygift_grpc.pb.go @@ -242,6 +242,8 @@ const ( AdminLuckyGiftService_GetLuckyGiftDrawSummary_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftDrawSummary" AdminLuckyGiftService_ListLuckyGiftPoolBalances_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftPoolBalances" AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/AdjustLuckyGiftPoolBalance" + AdminLuckyGiftService_ListLuckyGiftUserProfiles_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/ListLuckyGiftUserProfiles" + AdminLuckyGiftService_GetLuckyGiftUserProfile_FullMethodName = "/hyapp.luckygift.v1.AdminLuckyGiftService/GetLuckyGiftUserProfile" ) // AdminLuckyGiftServiceClient is the client API for AdminLuckyGiftService service. @@ -255,6 +257,8 @@ type AdminLuckyGiftServiceClient interface { GetLuckyGiftDrawSummary(ctx context.Context, in *GetLuckyGiftDrawSummaryRequest, opts ...grpc.CallOption) (*GetLuckyGiftDrawSummaryResponse, error) ListLuckyGiftPoolBalances(ctx context.Context, in *ListLuckyGiftPoolBalancesRequest, opts ...grpc.CallOption) (*ListLuckyGiftPoolBalancesResponse, error) AdjustLuckyGiftPoolBalance(ctx context.Context, in *AdjustLuckyGiftPoolBalanceRequest, opts ...grpc.CallOption) (*AdjustLuckyGiftPoolBalanceResponse, error) + ListLuckyGiftUserProfiles(ctx context.Context, in *ListLuckyGiftUserProfilesRequest, opts ...grpc.CallOption) (*ListLuckyGiftUserProfilesResponse, error) + GetLuckyGiftUserProfile(ctx context.Context, in *GetLuckyGiftUserProfileRequest, opts ...grpc.CallOption) (*GetLuckyGiftUserProfileResponse, error) } type adminLuckyGiftServiceClient struct { @@ -335,6 +339,26 @@ func (c *adminLuckyGiftServiceClient) AdjustLuckyGiftPoolBalance(ctx context.Con return out, nil } +func (c *adminLuckyGiftServiceClient) ListLuckyGiftUserProfiles(ctx context.Context, in *ListLuckyGiftUserProfilesRequest, opts ...grpc.CallOption) (*ListLuckyGiftUserProfilesResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(ListLuckyGiftUserProfilesResponse) + err := c.cc.Invoke(ctx, AdminLuckyGiftService_ListLuckyGiftUserProfiles_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + +func (c *adminLuckyGiftServiceClient) GetLuckyGiftUserProfile(ctx context.Context, in *GetLuckyGiftUserProfileRequest, opts ...grpc.CallOption) (*GetLuckyGiftUserProfileResponse, error) { + cOpts := append([]grpc.CallOption{grpc.StaticMethod()}, opts...) + out := new(GetLuckyGiftUserProfileResponse) + err := c.cc.Invoke(ctx, AdminLuckyGiftService_GetLuckyGiftUserProfile_FullMethodName, in, out, cOpts...) + if err != nil { + return nil, err + } + return out, nil +} + // AdminLuckyGiftServiceServer is the server API for AdminLuckyGiftService service. // All implementations must embed UnimplementedAdminLuckyGiftServiceServer // for forward compatibility. @@ -346,6 +370,8 @@ type AdminLuckyGiftServiceServer interface { GetLuckyGiftDrawSummary(context.Context, *GetLuckyGiftDrawSummaryRequest) (*GetLuckyGiftDrawSummaryResponse, error) ListLuckyGiftPoolBalances(context.Context, *ListLuckyGiftPoolBalancesRequest) (*ListLuckyGiftPoolBalancesResponse, error) AdjustLuckyGiftPoolBalance(context.Context, *AdjustLuckyGiftPoolBalanceRequest) (*AdjustLuckyGiftPoolBalanceResponse, error) + ListLuckyGiftUserProfiles(context.Context, *ListLuckyGiftUserProfilesRequest) (*ListLuckyGiftUserProfilesResponse, error) + GetLuckyGiftUserProfile(context.Context, *GetLuckyGiftUserProfileRequest) (*GetLuckyGiftUserProfileResponse, error) mustEmbedUnimplementedAdminLuckyGiftServiceServer() } @@ -377,6 +403,12 @@ func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftPoolBalances(contex func (UnimplementedAdminLuckyGiftServiceServer) AdjustLuckyGiftPoolBalance(context.Context, *AdjustLuckyGiftPoolBalanceRequest) (*AdjustLuckyGiftPoolBalanceResponse, error) { return nil, status.Error(codes.Unimplemented, "method AdjustLuckyGiftPoolBalance not implemented") } +func (UnimplementedAdminLuckyGiftServiceServer) ListLuckyGiftUserProfiles(context.Context, *ListLuckyGiftUserProfilesRequest) (*ListLuckyGiftUserProfilesResponse, error) { + return nil, status.Error(codes.Unimplemented, "method ListLuckyGiftUserProfiles not implemented") +} +func (UnimplementedAdminLuckyGiftServiceServer) GetLuckyGiftUserProfile(context.Context, *GetLuckyGiftUserProfileRequest) (*GetLuckyGiftUserProfileResponse, error) { + return nil, status.Error(codes.Unimplemented, "method GetLuckyGiftUserProfile not implemented") +} func (UnimplementedAdminLuckyGiftServiceServer) mustEmbedUnimplementedAdminLuckyGiftServiceServer() {} func (UnimplementedAdminLuckyGiftServiceServer) testEmbeddedByValue() {} @@ -524,6 +556,42 @@ func _AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_Handler(srv interface{}, return interceptor(ctx, in, info, handler) } +func _AdminLuckyGiftService_ListLuckyGiftUserProfiles_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(ListLuckyGiftUserProfilesRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftUserProfiles(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminLuckyGiftService_ListLuckyGiftUserProfiles_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminLuckyGiftServiceServer).ListLuckyGiftUserProfiles(ctx, req.(*ListLuckyGiftUserProfilesRequest)) + } + return interceptor(ctx, in, info, handler) +} + +func _AdminLuckyGiftService_GetLuckyGiftUserProfile_Handler(srv interface{}, ctx context.Context, dec func(interface{}) error, interceptor grpc.UnaryServerInterceptor) (interface{}, error) { + in := new(GetLuckyGiftUserProfileRequest) + if err := dec(in); err != nil { + return nil, err + } + if interceptor == nil { + return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftUserProfile(ctx, in) + } + info := &grpc.UnaryServerInfo{ + Server: srv, + FullMethod: AdminLuckyGiftService_GetLuckyGiftUserProfile_FullMethodName, + } + handler := func(ctx context.Context, req interface{}) (interface{}, error) { + return srv.(AdminLuckyGiftServiceServer).GetLuckyGiftUserProfile(ctx, req.(*GetLuckyGiftUserProfileRequest)) + } + return interceptor(ctx, in, info, handler) +} + // AdminLuckyGiftService_ServiceDesc is the grpc.ServiceDesc for AdminLuckyGiftService service. // It's only intended for direct use with grpc.RegisterService, // and not to be introspected or modified (even as a copy) @@ -559,6 +627,14 @@ var AdminLuckyGiftService_ServiceDesc = grpc.ServiceDesc{ MethodName: "AdjustLuckyGiftPoolBalance", Handler: _AdminLuckyGiftService_AdjustLuckyGiftPoolBalance_Handler, }, + { + MethodName: "ListLuckyGiftUserProfiles", + Handler: _AdminLuckyGiftService_ListLuckyGiftUserProfiles_Handler, + }, + { + MethodName: "GetLuckyGiftUserProfile", + Handler: _AdminLuckyGiftService_GetLuckyGiftUserProfile_Handler, + }, }, Streams: []grpc.StreamDesc{}, Metadata: "proto/luckygift/v1/luckygift.proto", diff --git a/server/admin/cmd/server/main.go b/server/admin/cmd/server/main.go index 5902ca64..fba94b91 100644 --- a/server/admin/cmd/server/main.go +++ b/server/admin/cmd/server/main.go @@ -351,7 +351,8 @@ func main() { databimodule.WithLegacyApps(databiLegacyApps(cfg.DashboardExternalSources, cfg.MoneyRegionSources)...), databimodule.WithLegacyRegionCatalogs(databiLegacyRegionCatalogs(moneyRegionSources)...), ) - luckyGiftHandler := luckygiftmodule.New(luckygiftadmin.NewGRPC(luckyGiftConn), cfg.LuckyGiftService.RequestTimeout, auditHandler) + luckyGiftHandler := luckygiftmodule.New(luckygiftadmin.NewGRPC(luckyGiftConn), cfg.LuckyGiftService.RequestTimeout, auditHandler). + BindUserClient(userclient.NewGRPC(userConn)) handlers := router.Handlers{ Audit: auditHandler, Auth: authmodule.New(store, auth, auditHandler, cfg), diff --git a/server/admin/internal/integration/luckygiftadmin/client.go b/server/admin/internal/integration/luckygiftadmin/client.go index 0643d936..072eb3cf 100644 --- a/server/admin/internal/integration/luckygiftadmin/client.go +++ b/server/admin/internal/integration/luckygiftadmin/client.go @@ -16,6 +16,8 @@ type Client interface { ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error) + ListLuckyGiftUserProfiles(ctx context.Context, req *luckygiftv1.ListLuckyGiftUserProfilesRequest) (*luckygiftv1.ListLuckyGiftUserProfilesResponse, error) + GetLuckyGiftUserProfile(ctx context.Context, req *luckygiftv1.GetLuckyGiftUserProfileRequest) (*luckygiftv1.GetLuckyGiftUserProfileResponse, error) ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error) AdjustLuckyGiftPoolBalance(ctx context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) } @@ -48,6 +50,14 @@ func (c *GRPCClient) GetLuckyGiftDrawSummary(ctx context.Context, req *luckygift return c.client.GetLuckyGiftDrawSummary(ctx, req) } +func (c *GRPCClient) ListLuckyGiftUserProfiles(ctx context.Context, req *luckygiftv1.ListLuckyGiftUserProfilesRequest) (*luckygiftv1.ListLuckyGiftUserProfilesResponse, error) { + return c.client.ListLuckyGiftUserProfiles(ctx, req) +} + +func (c *GRPCClient) GetLuckyGiftUserProfile(ctx context.Context, req *luckygiftv1.GetLuckyGiftUserProfileRequest) (*luckygiftv1.GetLuckyGiftUserProfileResponse, error) { + return c.client.GetLuckyGiftUserProfile(ctx, req) +} + func (c *GRPCClient) ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error) { return c.client.ListLuckyGiftPoolBalances(ctx, req) } diff --git a/server/admin/internal/modules/luckygift/handler.go b/server/admin/internal/modules/luckygift/handler.go index 2d9e9a47..fdefd2a9 100644 --- a/server/admin/internal/modules/luckygift/handler.go +++ b/server/admin/internal/modules/luckygift/handler.go @@ -9,6 +9,7 @@ import ( "time" "hyapp-admin-server/internal/integration/luckygiftadmin" + "hyapp-admin-server/internal/integration/userclient" "hyapp-admin-server/internal/middleware" "hyapp-admin-server/internal/modules/shared" "hyapp-admin-server/internal/response" @@ -21,6 +22,7 @@ import ( type Handler struct { luckyGift luckygiftadmin.Client + users userclient.Client requestTimeout time.Duration audit shared.OperationLogger } @@ -32,6 +34,13 @@ func New(luckyGift luckygiftadmin.Client, requestTimeout time.Duration, audit sh return &Handler{luckyGift: luckyGift, requestTimeout: requestTimeout, audit: audit} } +// BindUserClient 注入 user-service 主数据读取能力。幸运礼物 owner 只保存不可变的内部 user_id, +// 短号、默认号、靓号和用户资料必须在 admin-server 展示边界实时解析,不能复制进幸运礼物画像表。 +func (h *Handler) BindUserClient(users userclient.Client) *Handler { + h.users = users + return h +} + type configRequest struct { AppCode string `json:"app_code"` PoolID string `json:"pool_id"` diff --git a/server/admin/internal/modules/luckygift/user_profile.go b/server/admin/internal/modules/luckygift/user_profile.go new file mode 100644 index 00000000..e8cc7007 --- /dev/null +++ b/server/admin/internal/modules/luckygift/user_profile.go @@ -0,0 +1,621 @@ +package luckygift + +import ( + "context" + "fmt" + "strconv" + "strings" + + "hyapp-admin-server/internal/appctx" + "hyapp-admin-server/internal/integration/userclient" + "hyapp-admin-server/internal/middleware" + "hyapp-admin-server/internal/modules/shared" + "hyapp-admin-server/internal/response" + luckygiftv1 "hyapp.local/api/proto/luckygift/v1" + + "github.com/gin-gonic/gin" + "google.golang.org/grpc/codes" + grpcstatus "google.golang.org/grpc/status" +) + +type luckyGiftInternalUserDTO struct { + // UserID 使用字符串返回,避免浏览器把 64 位长号转成 JavaScript number 后丢失末位精度。 + UserID string `json:"user_id"` + DisplayUserID string `json:"display_user_id"` + DefaultDisplayID string `json:"default_display_user_id"` + PrettyID string `json:"pretty_id"` + PrettyDisplayUserID string `json:"pretty_display_user_id"` + Name string `json:"name"` + Avatar string `json:"avatar"` + Status string `json:"status"` +} + +type luckyGiftUserProfileWindowDTO struct { + Draws int64 `json:"draws"` + WagerCoins int64 `json:"wager_coins"` + PayoutCoins int64 `json:"payout_coins"` + NetCoins int64 `json:"net_coins"` + RTPPPM int64 `json:"rtp_ppm"` + HasRTP bool `json:"has_rtp"` +} + +type luckyGiftUserProfileDTO struct { + AppCode string `json:"app_code"` + PoolID string `json:"pool_id"` + IdentityType string `json:"identity_type"` + InternalUserID string `json:"internal_user_id,omitempty"` + ExternalUserID string `json:"external_user_id,omitempty"` + InternalUser *luckyGiftInternalUserDTO `json:"internal_user,omitempty"` + + RuleVersion int64 `json:"rule_version"` + StrategyVersion string `json:"strategy_version"` + Stage string `json:"stage"` + PaidDraws int64 `json:"paid_draws"` + EquivalentDraws int64 `json:"equivalent_draws"` + LossStreak int64 `json:"loss_streak"` + PendingSpendJackpotTokens int64 `json:"pending_spend_jackpot_tokens"` + GuaranteeDrawsRemaining int64 `json:"guarantee_draws_remaining"` + DownweightActive bool `json:"downweight_active"` + DownweightFactorPPM int64 `json:"downweight_factor_ppm"` + User24HourRTPThresholdPPM int64 `json:"user_24h_rtp_threshold_ppm"` + CurrentRTPPPM int64 `json:"current_rtp_ppm"` + HasCurrentRTP bool `json:"has_current_rtp"` + OneHour luckyGiftUserProfileWindowDTO `json:"one_hour"` + TwentyFourHours luckyGiftUserProfileWindowDTO `json:"twenty_four_hours"` + FortyEightHours luckyGiftUserProfileWindowDTO `json:"forty_eight_hours"` + Lifetime luckyGiftUserProfileWindowDTO `json:"lifetime"` + + BaseRewardCoins int64 `json:"base_reward_coins"` + OrdinaryWinCount int64 `json:"ordinary_win_count"` + HighMultiplierOrdinaryWinCount int64 `json:"high_multiplier_ordinary_win_count"` + RTPCompensationJackpotCount int64 `json:"rtp_compensation_jackpot_count"` + CumulativeSpendJackpotCount int64 `json:"cumulative_spend_jackpot_count"` + JackpotWinCount int64 `json:"jackpot_win_count"` + GuaranteeHitCount int64 `json:"guarantee_hit_count"` + ZeroDrawCount int64 `json:"zero_draw_count"` + GrantedDrawCount int64 `json:"granted_draw_count"` + PendingDrawCount int64 `json:"pending_draw_count"` + FailedDrawCount int64 `json:"failed_draw_count"` + MaxMultiplierPPM int64 `json:"max_multiplier_ppm"` + MaxRewardCoins int64 `json:"max_reward_coins"` + FirstDrawAtMS int64 `json:"first_draw_at_ms"` + LastDrawAtMS int64 `json:"last_draw_at_ms"` + AggregatedAtMS int64 `json:"aggregated_at_ms"` + DataLagMS int64 `json:"data_lag_ms"` +} + +type luckyGiftUserProfilePageDTO struct { + Items []luckyGiftUserProfileDTO `json:"items"` + Page int `json:"page"` + PageSize int `json:"page_size"` + Total int64 `json:"total"` + SnapshotAtMS int64 `json:"snapshot_at_ms"` + NextCursor string `json:"next_cursor,omitempty"` + HasMore bool `json:"has_more"` +} + +type luckyGiftUserMultiplierStatDTO struct { + HitType string `json:"hit_type"` + HitTypeLabel string `json:"hit_type_label"` + MultiplierPPM int64 `json:"multiplier_ppm"` + DrawCount int64 `json:"draw_count"` + WagerCoins int64 `json:"wager_coins"` + PayoutCoins int64 `json:"payout_coins"` +} + +type luckyGiftUserJackpotConditionDTO struct { + Name string `json:"name"` + Label string `json:"label"` + Actual string `json:"actual"` + Threshold string `json:"threshold"` + Operator string `json:"operator"` + Numerator int64 `json:"numerator"` + Denominator int64 `json:"denominator"` + RatioPPM int64 `json:"ratio_ppm"` + LimitPPM int64 `json:"limit_ppm"` + FactorPPM int64 `json:"factor_ppm"` + Factor string `json:"factor"` + Passed bool `json:"passed"` + Reason string `json:"reason"` +} + +type luckyGiftUserJackpotHitDTO struct { + EventKey string `json:"event_key"` + DrawID string `json:"draw_id"` + RequestID string `json:"request_id"` + RuleVersion int64 `json:"rule_version"` + Stage string `json:"stage"` + Mechanism string `json:"mechanism"` + MechanismLabel string `json:"mechanism_label"` + ReasonCode string `json:"reason_code"` + ReasonSummary string `json:"reason_summary"` + TierID string `json:"tier_id"` + MultiplierPPM int64 `json:"multiplier_ppm"` + WagerCoins int64 `json:"wager_coins"` + PayoutCoins int64 `json:"payout_coins"` + OccurredAtMS int64 `json:"occurred_at_ms"` + Conditions []luckyGiftUserJackpotConditionDTO `json:"conditions"` +} + +type luckyGiftUserProfileDetailDTO struct { + Profile luckyGiftUserProfileDTO `json:"profile"` + MultiplierDistribution []luckyGiftUserMultiplierStatDTO `json:"multiplier_distribution"` + JackpotHits []luckyGiftUserJackpotHitDTO `json:"jackpot_hits"` + JackpotHitTotal int64 `json:"jackpot_hit_total"` + JackpotPage int32 `json:"jackpot_page"` + JackpotPageSize int32 `json:"jackpot_page_size"` + NextJackpotCursor string `json:"next_jackpot_cursor,omitempty"` + JackpotHasMore bool `json:"jackpot_has_more"` + SnapshotAtMS int64 `json:"snapshot_at_ms"` +} + +// ListLuckyGiftUserProfiles 查询已经由 lucky-gift-service 增量物化的用户画像。列表只按 app+pool +// 读取聚合表;内部账号的短号/靓号解析和资料补全统一走 user-service,不在 HTTP 请求里扫描开奖事实表。 +func (h *Handler) ListLuckyGiftUserProfiles(c *gin.Context) { + appCode, poolID, ok := requiredLuckyGiftProfileScope(c) + if !ok { + return + } + identityType, ok := normalizeLuckyGiftProfileIdentity(firstNonEmpty(c.Query("identity_type"), c.Query("source"))) + if !ok { + response.BadRequest(c, "identity_type must be all, internal or external") + return + } + options := shared.ListOptions(c) + if options.PageSize > 100 { + options.PageSize = 100 + } + ctx, cancel := h.luckyGiftContext(c) + defer cancel() + ctx = appctx.WithContext(ctx, appCode) + + internalUserID := parseOptionalInt64(firstNonEmpty(c.Query("internal_user_id"), c.Query("user_id"))) + externalUserID := strings.TrimSpace(c.Query("external_user_id")) + keyword := strings.TrimSpace(c.Query("user_keyword")) + if keyword != "" { + resolvedInternalID, resolvedExternalID, internalMatched, err := h.resolveLuckyGiftProfileKeyword(ctx, appCode, identityType, keyword, middleware.CurrentRequestID(c)) + if err != nil { + response.ServerError(c, "搜索用户失败") + return + } + internalUserID, externalUserID = resolvedInternalID, resolvedExternalID + if identityType == "internal" && !internalMatched { + // 未解析的内部短号/长号/靓号必须返回空页,不能把“查无此人”退化为无条件全量列表。 + response.OK(c, luckyGiftUserProfilePageDTO{Items: []luckyGiftUserProfileDTO{}, Page: options.Page, PageSize: options.PageSize}) + return + } + } + + resp, err := h.luckyGift.ListLuckyGiftUserProfiles(ctx, &luckygiftv1.ListLuckyGiftUserProfilesRequest{ + Meta: h.metaForApp(c, appCode), + PoolId: poolID, + IdentityType: identityType, + InternalUserId: internalUserID, + ExternalUserId: externalUserID, + Stage: strings.TrimSpace(c.Query("stage")), + Status: strings.TrimSpace(c.Query("status")), + SortBy: strings.TrimSpace(c.Query("sort_by")), + SortDirection: strings.TrimSpace(c.Query("sort_direction")), + Page: int32(options.Page), + PageSize: int32(options.PageSize), + Cursor: strings.TrimSpace(c.Query("cursor")), + }) + if err != nil { + h.writeLuckyGiftUserProfileError(c, err, "获取幸运礼物用户画像失败") + return + } + + users, err := h.loadLuckyGiftInternalUsers(ctx, resp.GetProfiles(), middleware.CurrentRequestID(c)) + if err != nil { + response.ServerError(c, "获取用户资料失败") + return + } + items := make([]luckyGiftUserProfileDTO, 0, len(resp.GetProfiles())) + for _, profile := range resp.GetProfiles() { + items = append(items, luckyGiftUserProfileFromProto(profile, users[profile.GetInternalUserId()])) + } + response.OK(c, luckyGiftUserProfilePageDTO{ + Items: items, Page: options.Page, PageSize: options.PageSize, + Total: resp.GetTotal(), SnapshotAtMS: resp.GetSnapshotAtMs(), + NextCursor: resp.GetNextCursor(), HasMore: resp.GetHasMore(), + }) +} + +// GetLuckyGiftUserProfile 返回单个用户的倍率分布和大奖解释。大奖明细只在展开行时请求, +// 避免列表页同时解码所有用户的条件 trace,保证高密度页面仍保持稳定响应时间。 +func (h *Handler) GetLuckyGiftUserProfile(c *gin.Context) { + appCode, poolID, ok := requiredLuckyGiftProfileScope(c) + if !ok { + return + } + identityType, ok := normalizeLuckyGiftProfileIdentity(firstNonEmpty(c.Query("identity_type"), c.Query("source"))) + if !ok || identityType == "" { + response.BadRequest(c, "identity_type must be internal or external") + return + } + ctx, cancel := h.luckyGiftContext(c) + defer cancel() + ctx = appctx.WithContext(ctx, appCode) + + internalUserID := parseOptionalInt64(firstNonEmpty(c.Query("internal_user_id"), c.Query("user_id"))) + externalUserID := strings.TrimSpace(c.Query("external_user_id")) + if keyword := strings.TrimSpace(c.Query("user_keyword")); keyword != "" { + resolvedInternalID, resolvedExternalID, internalMatched, err := h.resolveLuckyGiftProfileKeyword(ctx, appCode, identityType, keyword, middleware.CurrentRequestID(c)) + if err != nil { + response.ServerError(c, "搜索用户失败") + return + } + internalUserID, externalUserID = resolvedInternalID, resolvedExternalID + if identityType == "internal" && !internalMatched { + response.NotFound(c, "未找到该用户的幸运礼物画像") + return + } + } + if (identityType == "internal" && internalUserID <= 0) || (identityType == "external" && externalUserID == "") { + response.BadRequest(c, "internal_user_id or external_user_id is required") + return + } + jackpotPage := parseLuckyGiftProfilePositiveInt(c.Query("jackpot_page"), 1, 1_000_000) + jackpotPageSize := parseLuckyGiftProfilePositiveInt(c.Query("jackpot_page_size"), 20, 50) + + resp, err := h.luckyGift.GetLuckyGiftUserProfile(ctx, &luckygiftv1.GetLuckyGiftUserProfileRequest{ + Meta: h.metaForApp(c, appCode), PoolId: poolID, IdentityType: identityType, + InternalUserId: internalUserID, ExternalUserId: externalUserID, + JackpotPage: int32(jackpotPage), JackpotPageSize: int32(jackpotPageSize), + JackpotCursor: strings.TrimSpace(c.Query("jackpot_cursor")), + }) + if err != nil { + h.writeLuckyGiftUserProfileError(c, err, "获取幸运礼物用户画像详情失败") + return + } + users, err := h.loadLuckyGiftInternalUsers(ctx, []*luckygiftv1.LuckyGiftUserProfile{resp.GetProfile()}, middleware.CurrentRequestID(c)) + if err != nil { + response.ServerError(c, "获取用户资料失败") + return + } + detail := luckyGiftUserProfileDetailDTO{ + Profile: luckyGiftUserProfileFromProto(resp.GetProfile(), users[resp.GetProfile().GetInternalUserId()]), + MultiplierDistribution: make([]luckyGiftUserMultiplierStatDTO, 0, len(resp.GetMultiplierDistribution())), + JackpotHits: make([]luckyGiftUserJackpotHitDTO, 0, len(resp.GetJackpotHits())), + JackpotHitTotal: resp.GetJackpotHitTotal(), JackpotPage: resp.GetJackpotPage(), + JackpotPageSize: resp.GetJackpotPageSize(), NextJackpotCursor: resp.GetNextJackpotCursor(), + JackpotHasMore: resp.GetJackpotHasMore(), SnapshotAtMS: resp.GetSnapshotAtMs(), + } + for _, stat := range resp.GetMultiplierDistribution() { + detail.MultiplierDistribution = append(detail.MultiplierDistribution, luckyGiftUserMultiplierStatFromProto(stat)) + } + for _, hit := range resp.GetJackpotHits() { + detail.JackpotHits = append(detail.JackpotHits, luckyGiftUserJackpotHitFromProto(hit)) + } + response.OK(c, detail) +} + +func parseLuckyGiftProfilePositiveInt(value string, fallback, maximum int) int { + parsed, err := strconv.Atoi(strings.TrimSpace(value)) + if err != nil || parsed <= 0 { + return fallback + } + if parsed > maximum { + return maximum + } + return parsed +} + +func requiredLuckyGiftProfileScope(c *gin.Context) (string, string, bool) { + appCode := strings.ToLower(strings.TrimSpace(c.Query("app_code"))) + poolID := strings.TrimSpace(c.Query("pool_id")) + if appCode == "" || poolID == "" { + response.BadRequest(c, "app_code and pool_id are required") + return "", "", false + } + return appCode, poolID, true +} + +func normalizeLuckyGiftProfileIdentity(value string) (string, bool) { + switch strings.ToLower(strings.TrimSpace(value)) { + case "", "all": + // owner 服务用空值表达“内部与外部”,避免引入第三种持久身份类型。 + return "", true + case "internal", "external": + return strings.ToLower(strings.TrimSpace(value)), true + default: + return "", false + } +} + +func (h *Handler) resolveLuckyGiftProfileKeyword(ctx context.Context, appCode, identityType, keyword, requestID string) (int64, string, bool, error) { + var externalUserID string + if identityType == "" || identityType == "external" { + // 外部接入没有 HyApp 用户主数据,原样按 external_user_id 精确匹配;稳定策略哈希永不出 owner 服务。 + externalUserID = keyword + } + if identityType == "external" { + return 0, externalUserID, false, nil + } + if h.users == nil { + return 0, externalUserID, false, fmt.Errorf("lucky gift user profile resolver is not configured") + } + identity, err := h.users.ResolveDisplayUserID(appctx.WithContext(ctx, appCode), userclient.ResolveDisplayUserIDRequest{ + RequestID: requestID, Caller: "admin-server", DisplayUserID: keyword, + }) + if err == nil && identity != nil && identity.UserID > 0 { + return identity.UserID, externalUserID, true, nil + } + if err != nil { + switch grpcstatus.Code(err) { + case codes.NotFound: + // 继续尝试把纯数字解释为系统长 user_id。 + case codes.InvalidArgument: + // all 模式下 keyword 也可能是任意 external_user_id;内部展示号格式不合法只代表内部未命中,不能阻断外部精确搜索。 + return 0, externalUserID, false, nil + default: + return 0, externalUserID, false, err + } + } + + // 纯数字先按展示号解析;只有展示号确实不存在时才回退为 64 位长 user_id,避免短号与长号碰撞时查错人。 + longUserID, parseErr := strconv.ParseInt(keyword, 10, 64) + if parseErr != nil || longUserID <= 0 { + return 0, externalUserID, false, nil + } + user, getErr := h.users.GetUser(appctx.WithContext(ctx, appCode), userclient.GetUserRequest{ + RequestID: requestID, Caller: "admin-server", UserID: longUserID, + }) + if getErr == nil && user != nil { + return user.UserID, externalUserID, true, nil + } + if getErr != nil && grpcstatus.Code(getErr) != codes.NotFound { + return 0, externalUserID, false, getErr + } + return 0, externalUserID, false, nil +} + +func (h *Handler) loadLuckyGiftInternalUsers(ctx context.Context, profiles []*luckygiftv1.LuckyGiftUserProfile, requestID string) (map[int64]*userclient.User, error) { + ids := make([]int64, 0, len(profiles)) + seen := make(map[int64]struct{}, len(profiles)) + for _, profile := range profiles { + if profile == nil || profile.GetIdentityType() != "internal" || profile.GetInternalUserId() <= 0 { + continue + } + if _, exists := seen[profile.GetInternalUserId()]; exists { + continue + } + seen[profile.GetInternalUserId()] = struct{}{} + ids = append(ids, profile.GetInternalUserId()) + } + if len(ids) == 0 { + return map[int64]*userclient.User{}, nil + } + if h.users == nil { + return nil, fmt.Errorf("lucky gift user profile user client is not configured") + } + return h.users.BatchGetUsers(ctx, userclient.BatchGetUsersRequest{RequestID: requestID, Caller: "admin-server", UserIDs: ids}) +} + +func luckyGiftUserProfileFromProto(profile *luckygiftv1.LuckyGiftUserProfile, user *userclient.User) luckyGiftUserProfileDTO { + if profile == nil { + return luckyGiftUserProfileDTO{} + } + jackpotCount := profile.GetRtpCompensationJackpotCount() + profile.GetCumulativeSpendJackpotCount() + dto := luckyGiftUserProfileDTO{ + AppCode: profile.GetAppCode(), PoolID: profile.GetPoolId(), IdentityType: profile.GetIdentityType(), + RuleVersion: profile.GetRuleVersion(), + StrategyVersion: profile.GetStrategyVersion(), Stage: profile.GetStage(), PaidDraws: profile.GetPaidDraws(), + EquivalentDraws: profile.GetEquivalentDraws(), LossStreak: profile.GetLossStreak(), + PendingSpendJackpotTokens: profile.GetPendingSpendJackpotTokens(), GuaranteeDrawsRemaining: profile.GetGuaranteeDrawsRemaining(), + DownweightActive: profile.GetDownweightActive(), DownweightFactorPPM: profile.GetDownweightFactorPpm(), + User24HourRTPThresholdPPM: profile.GetUser_24HRtpThresholdPpm(), + OneHour: luckyGiftUserProfileWindowFromProto(profile.GetOneHour()), + TwentyFourHours: luckyGiftUserProfileWindowFromProto(profile.GetTwentyFourHours()), + FortyEightHours: luckyGiftUserProfileWindowFromProto(profile.GetFortyEightHours()), + Lifetime: luckyGiftUserProfileWindowFromProto(profile.GetLifetime()), + BaseRewardCoins: profile.GetBaseRewardCoins(), OrdinaryWinCount: profile.GetOrdinaryWinCount(), + HighMultiplierOrdinaryWinCount: profile.GetHighMultiplierOrdinaryWinCount(), + RTPCompensationJackpotCount: profile.GetRtpCompensationJackpotCount(), + CumulativeSpendJackpotCount: profile.GetCumulativeSpendJackpotCount(), JackpotWinCount: jackpotCount, + GuaranteeHitCount: profile.GetGuaranteeHitCount(), ZeroDrawCount: profile.GetZeroDrawCount(), + GrantedDrawCount: profile.GetGrantedDrawCount(), PendingDrawCount: profile.GetPendingDrawCount(), FailedDrawCount: profile.GetFailedDrawCount(), + MaxMultiplierPPM: profile.GetMaxMultiplierPpm(), MaxRewardCoins: profile.GetMaxRewardCoins(), + FirstDrawAtMS: profile.GetFirstDrawAtMs(), LastDrawAtMS: profile.GetLastDrawAtMs(), + AggregatedAtMS: profile.GetAggregatedAtMs(), DataLagMS: profile.GetDataLagMs(), + CurrentRTPPPM: profile.GetTwentyFourHours().GetRtpPpm(), HasCurrentRTP: profile.GetTwentyFourHours().GetHasRtp(), + } + if profile.GetIdentityType() == "internal" && profile.GetInternalUserId() > 0 { + // 外部画像即使 owner 发生协议回归误填了 internal_user_id,也不得在 admin HTTP 边界透出策略哈希。 + dto.InternalUserID = strconv.FormatInt(profile.GetInternalUserId(), 10) + dto.InternalUser = luckyGiftInternalUserFromClient(profile.GetInternalUserId(), user) + } else if profile.GetIdentityType() == "external" { + dto.ExternalUserID = profile.GetExternalUserId() + } + return dto +} + +func luckyGiftInternalUserFromClient(userID int64, user *userclient.User) *luckyGiftInternalUserDTO { + dto := &luckyGiftInternalUserDTO{UserID: strconv.FormatInt(userID, 10)} + if user == nil { + return dto + } + dto.DisplayUserID = user.DisplayUserID + dto.DefaultDisplayID = user.DefaultDisplayUserID + dto.PrettyID = user.PrettyID + dto.PrettyDisplayUserID = user.PrettyDisplayUserID + dto.Name = user.Username + dto.Avatar = user.Avatar + dto.Status = user.Status + return dto +} + +func luckyGiftUserProfileWindowFromProto(window *luckygiftv1.LuckyGiftUserProfileWindow) luckyGiftUserProfileWindowDTO { + if window == nil { + return luckyGiftUserProfileWindowDTO{} + } + return luckyGiftUserProfileWindowDTO{ + Draws: window.GetDraws(), WagerCoins: window.GetWagerCoins(), PayoutCoins: window.GetPayoutCoins(), + NetCoins: window.GetNetCoins(), RTPPPM: window.GetRtpPpm(), HasRTP: window.GetHasRtp(), + } +} + +func luckyGiftUserMultiplierStatFromProto(stat *luckygiftv1.LuckyGiftUserMultiplierStat) luckyGiftUserMultiplierStatDTO { + if stat == nil { + return luckyGiftUserMultiplierStatDTO{} + } + return luckyGiftUserMultiplierStatDTO{ + HitType: stat.GetHitType(), HitTypeLabel: luckyGiftHitTypeLabel(stat.GetHitType()), + MultiplierPPM: stat.GetMultiplierPpm(), DrawCount: stat.GetDrawCount(), + WagerCoins: stat.GetWagerCoins(), PayoutCoins: stat.GetPayoutCoins(), + } +} + +func luckyGiftUserJackpotHitFromProto(hit *luckygiftv1.LuckyGiftUserJackpotHit) luckyGiftUserJackpotHitDTO { + if hit == nil { + return luckyGiftUserJackpotHitDTO{} + } + conditions := make([]luckyGiftUserJackpotConditionDTO, 0, len(hit.GetConditions())) + for _, condition := range hit.GetConditions() { + conditions = append(conditions, luckyGiftUserJackpotConditionFromProto(condition)) + } + return luckyGiftUserJackpotHitDTO{ + EventKey: hit.GetEventKey(), DrawID: hit.GetDrawId(), RequestID: hit.GetRequestId(), RuleVersion: hit.GetRuleVersion(), Stage: hit.GetStage(), + Mechanism: hit.GetMechanism(), MechanismLabel: luckyGiftJackpotMechanismLabel(hit.GetMechanism()), + ReasonCode: hit.GetReasonCode(), ReasonSummary: luckyGiftJackpotReasonSummary(hit), TierID: hit.GetTierId(), + MultiplierPPM: hit.GetMultiplierPpm(), WagerCoins: hit.GetWagerCoins(), PayoutCoins: hit.GetPayoutCoins(), + OccurredAtMS: hit.GetOccurredAtMs(), Conditions: conditions, + } +} + +func luckyGiftUserJackpotConditionFromProto(condition *luckygiftv1.LuckyGiftUserJackpotCondition) luckyGiftUserJackpotConditionDTO { + if condition == nil { + return luckyGiftUserJackpotConditionDTO{} + } + actual, threshold, operator := luckyGiftJackpotConditionValues(condition) + return luckyGiftUserJackpotConditionDTO{ + Name: condition.GetName(), Label: luckyGiftJackpotConditionLabel(condition.GetName()), + Actual: actual, Threshold: threshold, Operator: operator, + Numerator: condition.GetNumerator(), Denominator: condition.GetDenominator(), + RatioPPM: condition.GetRatioPpm(), LimitPPM: condition.GetLimitPpm(), FactorPPM: condition.GetFactorPpm(), + Factor: luckyGiftPercent(condition.GetFactorPpm()), Passed: condition.GetPassed(), Reason: condition.GetReason(), + } +} + +func luckyGiftJackpotConditionValues(condition *luckygiftv1.LuckyGiftUserJackpotCondition) (string, string, string) { + name := condition.GetName() + switch name { + case "global_rtp", "spend_global_rtp", "user_round_rtp", "user_48h_rtp", "user_day_rtp", "user_72h_rtp": + return fmt.Sprintf("%s(返奖 %d / 消耗 %d)", luckyGiftPercent(condition.GetRatioPpm()), condition.GetNumerator(), condition.GetDenominator()), luckyGiftPercent(condition.GetLimitPpm()), "≤" + case "user_24h_rtp_downweight": + return fmt.Sprintf("%s(返奖 %d / 消耗 %d)", luckyGiftPercent(condition.GetRatioPpm()), condition.GetNumerator(), condition.GetDenominator()), luckyGiftPercent(condition.GetLimitPpm()), ">" + case "user_registered_48h": + return luckyGiftDurationHours(condition.GetNumerator()), luckyGiftDurationHours(condition.GetDenominator()), "≥" + case "daily_jackpot_limit", "spend_daily_jackpot_limit": + return fmt.Sprintf("%d 次", condition.GetNumerator()), fmt.Sprintf("%d 次", condition.GetLimitPpm()), "<" + case "settled_round_qualification", "payable_jackpot_candidate": + if condition.GetPassed() { + return "已满足", "必须满足", "=" + } + return "未满足", "必须满足", "=" + default: + if condition.GetLimitPpm() != 0 || condition.GetRatioPpm() != 0 { + return luckyGiftPercent(condition.GetRatioPpm()), luckyGiftPercent(condition.GetLimitPpm()), "≤" + } + if condition.GetPassed() { + return "已满足", "必须满足", "=" + } + return "未满足", "必须满足", "=" + } +} + +func luckyGiftJackpotConditionLabel(name string) string { + labels := map[string]string{ + "global_rtp": "全奖池当前返奖率", + "spend_global_rtp": "累计消费大奖检查时的全奖池返奖率", + "settled_round_qualification": "上一结算轮次是否达到补偿条件", + "user_round_rtp": "用户当前结算轮次返奖率", + "user_48h_rtp": "用户近 48 小时返奖率", + "user_registered_48h": "用户注册是否已满 48 小时", + "user_24h_rtp_downweight": "用户近 24 小时普通中奖降权", + "user_day_rtp": "用户当天返奖率", + "user_72h_rtp": "用户近 72 小时返奖率", + "daily_jackpot_limit": "用户当天已中大奖次数", + "spend_daily_jackpot_limit": "累计消费大奖的当天次数限制", + "payable_jackpot_candidate": "奖池余额是否足够支付所选大奖", + } + if label := labels[name]; label != "" { + return label + } + return strings.ReplaceAll(strings.TrimSpace(name), "_", " ") +} + +func luckyGiftJackpotMechanismLabel(mechanism string) string { + switch mechanism { + case "rtp_compensation", "jackpot_mechanism_1": + return "RTP 补偿大奖" + case "spend_milestone", "jackpot_mechanism_2": + return "累计消费大奖" + default: + return "大奖" + } +} + +func luckyGiftJackpotReasonSummary(hit *luckygiftv1.LuckyGiftUserJackpotHit) string { + switch hit.GetReasonCode() { + case "rtp_compensation_jackpot": + return "用户与奖池均满足补偿条件,且奖池余额足够支付,因此触发 RTP 补偿大奖。" + case "milestone_token_jackpot": + return "用户累计消费已获得一次大奖机会,且奖池余额足够支付,因此触发累计消费大奖。" + } + if summary := strings.TrimSpace(hit.GetReasonSummary()); summary != "" { + return summary + } + return "本次开奖满足下列全部条件,因此命中该大奖。" +} + +func luckyGiftHitTypeLabel(hitType string) string { + switch hitType { + case "zero": + return "未中奖" + case "ordinary": + return "普通奖" + case "rtp_compensation": + return "RTP 补偿大奖" + case "spend_milestone": + return "累计消费大奖" + default: + return hitType + } +} + +func luckyGiftPercent(ppm int64) string { + value := strconv.FormatFloat(float64(ppm)/10_000, 'f', 2, 64) + value = strings.TrimRight(strings.TrimRight(value, "0"), ".") + if value == "" { + value = "0" + } + return value + "%" +} + +func luckyGiftDurationHours(milliseconds int64) string { + value := strconv.FormatFloat(float64(milliseconds)/3_600_000, 'f', 1, 64) + value = strings.TrimRight(strings.TrimRight(value, "0"), ".") + return value + " 小时" +} + +func firstNonEmpty(values ...string) string { + for _, value := range values { + if strings.TrimSpace(value) != "" { + return value + } + } + return "" +} + +func (h *Handler) writeLuckyGiftUserProfileError(c *gin.Context, err error, fallback string) { + status := grpcstatus.Convert(err) + switch status.Code() { + case codes.InvalidArgument: + response.BadRequest(c, status.Message()) + case codes.NotFound: + response.NotFound(c, "未找到该用户的幸运礼物画像") + default: + response.ServerError(c, fallback) + } +} diff --git a/server/admin/internal/modules/opscenter/routes.go b/server/admin/internal/modules/opscenter/routes.go index 4b7dfb7b..94f14c33 100644 --- a/server/admin/internal/modules/opscenter/routes.go +++ b/server/admin/internal/modules/opscenter/routes.go @@ -24,6 +24,8 @@ func RegisterRoutes(protected *gin.RouterGroup, h *Handler) { group.GET("/lucky-gifts/configs", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftConfigs) group.GET("/lucky-gifts/draws", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftDraws) group.GET("/lucky-gifts/summary", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftDrawSummary) + group.GET("/lucky-gifts/user-profiles", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftUserProfiles) + group.GET("/lucky-gifts/user-profile", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.GetLuckyGiftUserProfile) group.GET("/lucky-gifts/pools", middleware.RequirePermission("lucky-gift:view"), h.luckyGift.ListLuckyGiftPoolBalances) group.POST("/lucky-gifts/pools/credit", middleware.RequirePermission("lucky-gift:pool-credit"), h.luckyGift.CreditLuckyGiftPoolBalance) group.POST("/lucky-gifts/pools/debit", middleware.RequirePermission("lucky-gift:pool-debit"), h.luckyGift.DebitLuckyGiftPoolBalance) diff --git a/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql b/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql index 34ff03d8..a262a0a7 100644 --- a/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql +++ b/services/lucky-gift-service/deploy/mysql/initdb/001_lucky_gift_service.sql @@ -403,6 +403,118 @@ CREATE TABLE IF NOT EXISTS lucky_draw_pool_stat_cursors ( PRIMARY KEY (app_code, cursor_name) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物后台统计快照游标'; +CREATE TABLE IF NOT EXISTS lucky_gift_user_profiles ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3;两套奖池画像严格隔离', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '内部 user_id 文本或外部 external_user_id', + strategy_user_id BIGINT NOT NULL COMMENT '仅供本服务关联策略状态,绝不下发', + internal_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '内部真实 user_id', + external_user_id VARCHAR(128) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '外部原始用户 ID', + rule_version BIGINT NOT NULL DEFAULT 0 COMMENT '最近规则版本', + current_stage VARCHAR(32) NOT NULL DEFAULT '' COMMENT '最近阶段', + total_draws BIGINT NOT NULL DEFAULT 0 COMMENT '累计子抽次数', + total_wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '累计消耗金币', + total_payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '累计返奖金币', + lifetime_rtp_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '累计 RTP,物化后用于无 filesort 游标排序', + total_base_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '累计基础返奖', + ordinary_win_count BIGINT NOT NULL DEFAULT 0 COMMENT '普通非零奖次数', + high_multiplier_ordinary_win_count BIGINT NOT NULL DEFAULT 0 COMMENT '高倍率普通奖次数', + rtp_compensation_jackpot_count BIGINT NOT NULL DEFAULT 0 COMMENT 'RTP 补偿大奖次数', + cumulative_spend_jackpot_count BIGINT NOT NULL DEFAULT 0 COMMENT '累计消费大奖次数', + jackpot_count BIGINT NOT NULL DEFAULT 0 COMMENT '两类独立大奖累计次数,物化后用于排序和筛选', + guarantee_hit_count BIGINT NOT NULL DEFAULT 0 COMMENT '连续未中保护命中次数', + zero_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '未中奖次数', + granted_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '已发放次数', + pending_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '待发放次数', + failed_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '发放失败次数', + max_multiplier_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '最高倍率', + max_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '最高单抽返奖', + last_downweight_active TINYINT(1) NOT NULL DEFAULT 0 COMMENT '最近是否触发 24h RTP 降权', + current_equivalent_draws BIGINT NOT NULL DEFAULT 0 COMMENT '当前运行状态的等价抽数快照', + current_loss_streak BIGINT NOT NULL DEFAULT 0 COMMENT '当前连续未中奖次数快照', + current_pending_spend_jackpot_tokens BIGINT NOT NULL DEFAULT 0 COMMENT '当前累计消费大奖资格快照', + first_draw_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '首次抽奖时间', + last_draw_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最近抽奖时间', + aggregated_at_ms BIGINT NOT NULL COMMENT '最近聚合时间', + created_at_ms BIGINT NOT NULL COMMENT '创建时间', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间', + PRIMARY KEY (app_code, pool_id, strategy_version, identity_type, identity_key), + KEY idx_lucky_user_profile_sort_last (app_code, pool_id, strategy_version, last_draw_at_ms DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_rtp (app_code, pool_id, strategy_version, lifetime_rtp_ppm DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_wager (app_code, pool_id, strategy_version, total_wager_coins DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_payout (app_code, pool_id, strategy_version, total_payout_coins DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_jackpot (app_code, pool_id, strategy_version, jackpot_count DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_loss (app_code, pool_id, strategy_version, current_loss_streak DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_stage (app_code, pool_id, strategy_version, current_stage, identity_type, identity_key), + KEY idx_lucky_user_profile_pending (app_code, pool_id, strategy_version, current_pending_spend_jackpot_tokens DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_internal (app_code, pool_id, strategy_version, internal_user_id), + KEY idx_lucky_user_profile_external (app_code, pool_id, strategy_version, external_user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像累计快照'; + +CREATE TABLE IF NOT EXISTS lucky_gift_user_profile_events ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + source_type VARCHAR(24) NOT NULL COMMENT 'internal/external_item/external_legacy', + source_draw_id VARCHAR(128) NOT NULL COMMENT '来源抽奖事实 ID', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '用户身份键', + draw_count BIGINT NOT NULL DEFAULT 1 COMMENT '代表的子抽次数;fixed_v2 外部整单固定为 1', + wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '消耗金币', + payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '返奖金币', + base_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '基础返奖', + multiplier_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '命中倍率', + hit_type VARCHAR(32) NOT NULL COMMENT 'zero/ordinary/rtp_compensation/spend_milestone', + reward_status VARCHAR(32) NOT NULL COMMENT 'pending/granted/failed', + occurred_at_ms BIGINT NOT NULL COMMENT '支付事实时间', + aggregation_token VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首次插入者的聚合批次令牌', + created_at_ms BIGINT NOT NULL COMMENT '读模型写入时间', + PRIMARY KEY (app_code, source_type, source_draw_id), + KEY idx_lucky_user_profile_event_window (app_code, pool_id, strategy_version, identity_type, identity_key, occurred_at_ms), + KEY idx_lucky_user_profile_event_token (app_code, source_type, aggregation_token) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像窗口事件'; + +CREATE TABLE IF NOT EXISTS lucky_gift_user_multiplier_stats ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '用户身份键', + hit_type VARCHAR(32) NOT NULL COMMENT '普通奖和两类大奖独立分类', + multiplier_ppm BIGINT NOT NULL COMMENT '倍率', + draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '命中次数', + wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '消耗金币', + payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '返奖金币', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间', + PRIMARY KEY (app_code, pool_id, strategy_version, identity_type, identity_key, hit_type, multiplier_ppm) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像倍率分布'; + +CREATE TABLE IF NOT EXISTS lucky_gift_user_jackpot_hits ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + source_type VARCHAR(24) NOT NULL COMMENT 'internal/external_item', + source_draw_id VARCHAR(128) NOT NULL COMMENT '来源抽奖事实 ID', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '用户身份键', + request_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '内部 command_id 或外部 request_id', + rule_version BIGINT NOT NULL DEFAULT 0 COMMENT '规则版本', + stage VARCHAR(32) NOT NULL DEFAULT '' COMMENT '命中时阶段', + mechanism VARCHAR(32) NOT NULL COMMENT '大奖机制', + reason_code VARCHAR(96) NOT NULL DEFAULT '' COMMENT '最终原因码', + tier_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '大奖 tier ID', + multiplier_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '大奖倍率', + wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '本抽消耗', + payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '本抽返奖', + conditions_json JSON NOT NULL COMMENT '大奖条件快照', + occurred_at_ms BIGINT NOT NULL COMMENT '支付事实时间', + created_at_ms BIGINT NOT NULL COMMENT '读模型写入时间', + PRIMARY KEY (app_code, source_type, source_draw_id), + KEY idx_lucky_user_jackpot_profile (app_code, pool_id, strategy_version, identity_type, identity_key, occurred_at_ms DESC, source_type DESC, source_draw_id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像大奖解释'; + CREATE TABLE IF NOT EXISTS external_lucky_gift_request_locks ( app_code VARCHAR(32) NOT NULL COMMENT '外部 App 应用编码', request_id VARCHAR(128) NOT NULL COMMENT '外部业务幂等 ID', @@ -433,7 +545,8 @@ CREATE TABLE IF NOT EXISTS external_lucky_gift_draws ( UNIQUE KEY uk_external_lucky_draw_id (app_code, draw_id), KEY idx_external_lucky_user (app_code, external_user_id, created_at_ms), KEY idx_external_lucky_pool (app_code, pool_id, created_at_ms), - KEY idx_external_lucky_status (app_code, reward_status, created_at_ms) + KEY idx_external_lucky_status (app_code, reward_status, created_at_ms), + KEY idx_external_lucky_profile_cursor (app_code, created_at_ms, draw_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='外部 App 幸运礼物抽奖事实表'; CREATE TABLE IF NOT EXISTS external_lucky_gift_draw_items ( @@ -458,5 +571,6 @@ CREATE TABLE IF NOT EXISTS external_lucky_gift_draw_items ( created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', PRIMARY KEY (app_code, request_id, item_index), UNIQUE KEY uk_external_lucky_item_draw (app_code, draw_id), - KEY idx_external_lucky_item_user (app_code, external_user_id, created_at_ms) + KEY idx_external_lucky_item_user (app_code, external_user_id, created_at_ms), + KEY idx_external_lucky_item_profile_cursor (app_code, created_at_ms, draw_id) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='外部dynamic_v3顺序子抽审计'; diff --git a/services/lucky-gift-service/deploy/mysql/migrations/009_lucky_gift_user_profiles.sql b/services/lucky-gift-service/deploy/mysql/migrations/009_lucky_gift_user_profiles.sql new file mode 100644 index 00000000..363e718e --- /dev/null +++ b/services/lucky-gift-service/deploy/mysql/migrations/009_lucky_gift_user_profiles.sql @@ -0,0 +1,213 @@ +SET NAMES utf8mb4 COLLATE utf8mb4_unicode_ci; + +USE hyapp_lucky_gift; + +-- 用户画像是幸运礼物事实的专用读模型。列表、窗口指标和大奖解释都只能读取这些表, +-- 不能在运营查询时临时 GROUP BY 全量内部/外部抽奖事实。 +CREATE TABLE IF NOT EXISTS lucky_gift_user_profiles ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3;两套奖池画像严格隔离', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '内部 user_id 文本或外部 external_user_id;永不存展示稳定哈希', + strategy_user_id BIGINT NOT NULL COMMENT '策略状态用户键;外部值仅供本服务关联状态,绝不下发', + internal_user_id BIGINT NOT NULL DEFAULT 0 COMMENT '内部真实 user_id;外部固定为 0', + external_user_id VARCHAR(128) COLLATE utf8mb4_bin NOT NULL DEFAULT '' COMMENT '外部原始用户 ID;内部固定为空', + rule_version BIGINT NOT NULL DEFAULT 0 COMMENT '最近一次不可变规则版本', + current_stage VARCHAR(32) NOT NULL DEFAULT '' COMMENT '最近一次抽奖阶段', + total_draws BIGINT NOT NULL DEFAULT 0 COMMENT '累计子抽次数', + total_wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '累计消耗金币', + total_payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '累计用户可见返奖金币', + lifetime_rtp_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '累计 RTP,物化后用于无 filesort 游标排序', + total_base_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '累计基础返奖金币', + ordinary_win_count BIGINT NOT NULL DEFAULT 0 COMMENT '普通非零奖命中次数', + high_multiplier_ordinary_win_count BIGINT NOT NULL DEFAULT 0 COMMENT '高倍率普通奖命中次数;与大奖独立', + rtp_compensation_jackpot_count BIGINT NOT NULL DEFAULT 0 COMMENT 'RTP 补偿大奖命中次数', + cumulative_spend_jackpot_count BIGINT NOT NULL DEFAULT 0 COMMENT '累计消费大奖命中次数', + jackpot_count BIGINT NOT NULL DEFAULT 0 COMMENT '两类独立大奖累计次数,物化后用于排序和筛选', + guarantee_hit_count BIGINT NOT NULL DEFAULT 0 COMMENT '连续未中奖保护命中次数', + zero_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '0 倍未中奖次数', + granted_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '已发放次数', + pending_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '待发放次数', + failed_draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '发放失败次数', + max_multiplier_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '历史最高倍率', + max_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '历史单抽最高返奖', + last_downweight_active TINYINT(1) NOT NULL DEFAULT 0 COMMENT '最近一次抽奖是否触发 24h RTP 降权', + current_equivalent_draws BIGINT NOT NULL DEFAULT 0 COMMENT '当前运行状态的等价抽数快照', + current_loss_streak BIGINT NOT NULL DEFAULT 0 COMMENT '当前连续未中奖次数快照', + current_pending_spend_jackpot_tokens BIGINT NOT NULL DEFAULT 0 COMMENT '当前累计消费大奖资格快照', + first_draw_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '首次抽奖事实时间', + last_draw_at_ms BIGINT NOT NULL DEFAULT 0 COMMENT '最近抽奖事实时间', + aggregated_at_ms BIGINT NOT NULL COMMENT '最近聚合时间,UTC epoch ms', + created_at_ms BIGINT NOT NULL COMMENT '创建时间,UTC epoch ms', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (app_code, pool_id, strategy_version, identity_type, identity_key), + KEY idx_lucky_user_profile_sort_last (app_code, pool_id, strategy_version, last_draw_at_ms DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_rtp (app_code, pool_id, strategy_version, lifetime_rtp_ppm DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_wager (app_code, pool_id, strategy_version, total_wager_coins DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_payout (app_code, pool_id, strategy_version, total_payout_coins DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_jackpot (app_code, pool_id, strategy_version, jackpot_count DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_sort_loss (app_code, pool_id, strategy_version, current_loss_streak DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_stage (app_code, pool_id, strategy_version, current_stage, identity_type, identity_key), + KEY idx_lucky_user_profile_pending (app_code, pool_id, strategy_version, current_pending_spend_jackpot_tokens DESC, identity_type DESC, identity_key DESC), + KEY idx_lucky_user_profile_internal (app_code, pool_id, strategy_version, internal_user_id), + KEY idx_lucky_user_profile_external (app_code, pool_id, strategy_version, external_user_id) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像累计快照'; + +-- 画像事件只保留统计所需的窄字段。永久唯一键既支持严格滚动窗口,也让历史回灌与实时写入幂等共存。 +CREATE TABLE IF NOT EXISTS lucky_gift_user_profile_events ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + source_type VARCHAR(24) NOT NULL COMMENT 'internal/external_item/external_legacy', + source_draw_id VARCHAR(128) NOT NULL COMMENT '来源抽奖事实 ID', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '可搜索用户身份键', + draw_count BIGINT NOT NULL DEFAULT 1 COMMENT '该事件代表的子抽次数;fixed_v2 外部整单固定为 1,旧 dynamic 聚合事实可大于 1', + wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '消耗金币', + payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '用户可见返奖金币', + base_reward_coins BIGINT NOT NULL DEFAULT 0 COMMENT '基础返奖金币', + multiplier_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '命中倍率', + hit_type VARCHAR(32) NOT NULL COMMENT 'zero/ordinary/rtp_compensation/spend_milestone', + reward_status VARCHAR(32) NOT NULL COMMENT 'pending/granted/failed', + occurred_at_ms BIGINT NOT NULL COMMENT '支付事实时间,UTC epoch ms', + aggregation_token VARCHAR(96) NOT NULL DEFAULT '' COMMENT '首次插入者的聚合批次令牌;用于在线写入与回灌并发去重', + created_at_ms BIGINT NOT NULL COMMENT '读模型写入时间,UTC epoch ms', + PRIMARY KEY (app_code, source_type, source_draw_id), + KEY idx_lucky_user_profile_event_window (app_code, pool_id, strategy_version, identity_type, identity_key, occurred_at_ms), + KEY idx_lucky_user_profile_event_token (app_code, source_type, aggregation_token) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像窗口事件'; + +CREATE TABLE IF NOT EXISTS lucky_gift_user_multiplier_stats ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '可搜索用户身份键', + hit_type VARCHAR(32) NOT NULL COMMENT '普通奖和两类大奖分别聚合,允许同倍率共存', + multiplier_ppm BIGINT NOT NULL COMMENT '倍率,1x=1000000', + draw_count BIGINT NOT NULL DEFAULT 0 COMMENT '命中次数', + wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '对应抽奖消耗', + payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '对应返奖', + updated_at_ms BIGINT NOT NULL COMMENT '更新时间,UTC epoch ms', + PRIMARY KEY (app_code, pool_id, strategy_version, identity_type, identity_key, hit_type, multiplier_ppm) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像倍率分布'; + +CREATE TABLE IF NOT EXISTS lucky_gift_user_jackpot_hits ( + app_code VARCHAR(32) NOT NULL COMMENT '应用编码', + source_type VARCHAR(24) NOT NULL COMMENT 'internal/external_item', + source_draw_id VARCHAR(128) NOT NULL COMMENT '来源抽奖事实 ID', + pool_id VARCHAR(96) NOT NULL COMMENT '幸运礼物奖池 ID', + strategy_version VARCHAR(32) NOT NULL COMMENT 'fixed_v2/dynamic_v3', + identity_type VARCHAR(16) NOT NULL COMMENT 'internal/external', + identity_key VARCHAR(128) COLLATE utf8mb4_bin NOT NULL COMMENT '可搜索用户身份键', + request_id VARCHAR(128) NOT NULL DEFAULT '' COMMENT '内部 command_id 或外部 request_id', + rule_version BIGINT NOT NULL DEFAULT 0 COMMENT '不可变规则版本', + stage VARCHAR(32) NOT NULL DEFAULT '' COMMENT '命中时阶段', + mechanism VARCHAR(32) NOT NULL COMMENT 'rtp_compensation/spend_milestone', + reason_code VARCHAR(96) NOT NULL DEFAULT '' COMMENT '内核最终原因码', + tier_id VARCHAR(96) NOT NULL DEFAULT '' COMMENT '独立大奖 tier ID', + multiplier_ppm BIGINT NOT NULL DEFAULT 0 COMMENT '大奖倍率', + wager_coins BIGINT NOT NULL DEFAULT 0 COMMENT '本抽消耗', + payout_coins BIGINT NOT NULL DEFAULT 0 COMMENT '本抽返奖', + conditions_json JSON NOT NULL COMMENT '命中当刻条件快照', + occurred_at_ms BIGINT NOT NULL COMMENT '支付事实时间,UTC epoch ms', + created_at_ms BIGINT NOT NULL COMMENT '读模型写入时间,UTC epoch ms', + PRIMARY KEY (app_code, source_type, source_draw_id), + KEY idx_lucky_user_jackpot_profile (app_code, pool_id, strategy_version, identity_type, identity_key, occurred_at_ms DESC, source_type DESC, source_draw_id DESC) +) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_unicode_ci COMMENT='幸运礼物用户画像大奖解释'; + +-- migration 可在建表完成、进程尚未回灌时安全重跑。列使用 INSTANT;画像表在首次上线时为空, +-- 排序索引使用 INPLACE+LOCK=NONE,明确禁止意外 COPY 大表。 +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND COLUMN_NAME='lifetime_rtp_ppm')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD COLUMN lifetime_rtp_ppm BIGINT NOT NULL DEFAULT 0 COMMENT ''累计 RTP,物化后用于无 filesort 游标排序'' AFTER total_payout_coins, ALGORITHM=INSTANT', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND COLUMN_NAME='jackpot_count')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD COLUMN jackpot_count BIGINT NOT NULL DEFAULT 0 COMMENT ''两类独立大奖累计次数'' AFTER cumulative_spend_jackpot_count, ALGORITHM=INSTANT', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND COLUMN_NAME='current_equivalent_draws')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD COLUMN current_equivalent_draws BIGINT NOT NULL DEFAULT 0 COMMENT ''当前运行状态的等价抽数快照'' AFTER last_downweight_active, ALGORITHM=INSTANT', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND COLUMN_NAME='current_loss_streak')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD COLUMN current_loss_streak BIGINT NOT NULL DEFAULT 0 COMMENT ''当前连续未中奖次数快照'' AFTER current_equivalent_draws, ALGORITHM=INSTANT', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.COLUMNS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND COLUMN_NAME='current_pending_spend_jackpot_tokens')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD COLUMN current_pending_spend_jackpot_tokens BIGINT NOT NULL DEFAULT 0 COMMENT ''当前累计消费大奖资格快照'' AFTER current_loss_streak, ALGORITHM=INSTANT', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_sort_last')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_sort_last (app_code, pool_id, strategy_version, last_draw_at_ms DESC, identity_type DESC, identity_key DESC), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_sort_rtp')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_sort_rtp (app_code, pool_id, strategy_version, lifetime_rtp_ppm DESC, identity_type DESC, identity_key DESC), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_sort_wager')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_sort_wager (app_code, pool_id, strategy_version, total_wager_coins DESC, identity_type DESC, identity_key DESC), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_sort_payout')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_sort_payout (app_code, pool_id, strategy_version, total_payout_coins DESC, identity_type DESC, identity_key DESC), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_sort_jackpot')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_sort_jackpot (app_code, pool_id, strategy_version, jackpot_count DESC, identity_type DESC, identity_key DESC), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_sort_loss')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_sort_loss (app_code, pool_id, strategy_version, current_loss_streak DESC, identity_type DESC, identity_key DESC), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_stage')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_stage (app_code, pool_id, strategy_version, current_stage, identity_type, identity_key), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_pending')=0, + 'ALTER TABLE lucky_gift_user_profiles ADD KEY idx_lucky_user_profile_pending (app_code, pool_id, strategy_version, current_pending_spend_jackpot_tokens DESC, identity_type DESC, identity_key DESC), ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 早期本地 009 使用全 ASC 的 list 索引;完整 sort_last 已覆盖其正向和反向扫描,删除重复索引避免每抽双写。 +SET @ddl := IF((SELECT COUNT(*) FROM information_schema.STATISTICS WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_profiles' AND INDEX_NAME='idx_lucky_user_profile_list')>0, + 'ALTER TABLE lucky_gift_user_profiles DROP INDEX idx_lucky_user_profile_list, ALGORITHM=INPLACE, LOCK=NONE', 'SELECT 1'); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 正常首次上线时画像表尚为空;该更新只用于半完成 migration 后已有画像的幂等修复。 +-- 只扫描专用小型读模型并按主键关联状态,不触碰 8 万级开奖事实表。 +UPDATE lucky_gift_user_profiles p +LEFT JOIN lucky_user_states s + ON s.app_code = p.app_code AND s.user_id = p.strategy_user_id AND s.gift_id = p.pool_id +SET p.lifetime_rtp_ppm = CASE + WHEN p.total_wager_coins <= 0 THEN 0 + ELSE CAST(LEAST(CAST(9223372036854775807 AS DECIMAL(65,0)), + FLOOR(CAST(p.total_payout_coins AS DECIMAL(65,0)) * 1000000 / p.total_wager_coins)) AS SIGNED) + END, + p.jackpot_count = p.rtp_compensation_jackpot_count + p.cumulative_spend_jackpot_count, + p.current_equivalent_draws = COALESCE(s.equivalent_draws, 0), + p.current_loss_streak = COALESCE(s.loss_streak, 0), + p.current_pending_spend_jackpot_tokens = COALESCE(s.pending_spend_jackpot_tokens, 0); + +-- 早期半完成的 009 只有 occurred_at_ms;完整大奖游标还必须把 source_type/source_draw_id 纳入索引顺序。 +SET @jackpot_cursor_columns := (SELECT GROUP_CONCAT(COLUMN_NAME ORDER BY SEQ_IN_INDEX SEPARATOR ',') FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA=DATABASE() AND TABLE_NAME='lucky_gift_user_jackpot_hits' AND INDEX_NAME='idx_lucky_user_jackpot_profile'); +SET @ddl := CASE + WHEN COALESCE(@jackpot_cursor_columns, '') = '' THEN + 'ALTER TABLE lucky_gift_user_jackpot_hits ADD KEY idx_lucky_user_jackpot_profile (app_code, pool_id, strategy_version, identity_type, identity_key, occurred_at_ms DESC, source_type DESC, source_draw_id DESC), ALGORITHM=INPLACE, LOCK=NONE' + WHEN @jackpot_cursor_columns <> 'app_code,pool_id,strategy_version,identity_type,identity_key,occurred_at_ms,source_type,source_draw_id' THEN + 'ALTER TABLE lucky_gift_user_jackpot_hits DROP INDEX idx_lucky_user_jackpot_profile, ADD KEY idx_lucky_user_jackpot_profile (app_code, pool_id, strategy_version, identity_type, identity_key, occurred_at_ms DESC, source_type DESC, source_draw_id DESC), ALGORITHM=INPLACE, LOCK=NONE' + ELSE 'SELECT 1' +END; +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +-- 两个来源索引按 information_schema 守卫,允许 migration 在网络中断或半完成后安全重跑。 +-- INPLACE+LOCK=NONE 禁止退化成 COPY;上线前仍必须检查表大小和长事务,避免 MDL 等待。 +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'external_lucky_gift_draws' + AND INDEX_NAME = 'idx_external_lucky_profile_cursor') = 0, + 'ALTER TABLE external_lucky_gift_draws ADD KEY idx_external_lucky_profile_cursor (app_code, created_at_ms, draw_id), ALGORITHM=INPLACE, LOCK=NONE', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; + +SET @ddl := IF( + (SELECT COUNT(*) FROM information_schema.STATISTICS + WHERE TABLE_SCHEMA = DATABASE() AND TABLE_NAME = 'external_lucky_gift_draw_items' + AND INDEX_NAME = 'idx_external_lucky_item_profile_cursor') = 0, + 'ALTER TABLE external_lucky_gift_draw_items ADD KEY idx_external_lucky_item_profile_cursor (app_code, created_at_ms, draw_id), ALGORITHM=INPLACE, LOCK=NONE', + 'SELECT 1' +); +PREPARE stmt FROM @ddl; EXECUTE stmt; DEALLOCATE PREPARE stmt; diff --git a/services/lucky-gift-service/internal/domain/luckygift/lucky_gift.go b/services/lucky-gift-service/internal/domain/luckygift/lucky_gift.go index 507f900c..279d5a13 100644 --- a/services/lucky-gift-service/internal/domain/luckygift/lucky_gift.go +++ b/services/lucky-gift-service/internal/domain/luckygift/lucky_gift.go @@ -347,6 +347,138 @@ type DrawQuery struct { ExternalOnly bool } +const ( + UserProfileIdentityInternal = "internal" + UserProfileIdentityExternal = "external" + + UserProfileHitZero = "zero" + UserProfileHitOrdinary = "ordinary" + UserProfileHitRTPCompensation = "rtp_compensation" + UserProfileHitSpendMilestone = "spend_milestone" +) + +// UserProfileQuery 只允许按已经物化的身份键分页。内部短号、长号和靓号由 admin-server +// 通过 user-service 解析成 InternalUserID;外部用户只用原始 ExternalUserID 搜索,稳定哈希绝不跨服务暴露。 +type UserProfileQuery struct { + AppCode string + PoolID string + IdentityType string + InternalUserID int64 + ExternalUserID string + Stage string + Status string + SortBy string + SortDirection string + Cursor string + Page int32 + PageSize int32 + JackpotPage int32 + JackpotPageSize int32 + JackpotCursor string +} + +// UserProfileWindow 是严格时间窗内的用户资金结果。NetCoins 从用户视角计算为返奖减消耗; +// RTP 分母为 0 时保持 0,并由 HasRTP 区分“真实 0%”和“没有样本”。 +type UserProfileWindow struct { + Draws int64 + WagerCoins int64 + PayoutCoins int64 + NetCoins int64 + RTPPPM int64 + HasRTP bool +} + +// UserProfile 是 app+pool+用户的运营画像,不携带外部 strategy_user_id。 +// StrategyUserID 只在 lucky-gift-service 仓储层关联 current state,transport 映射时必须丢弃。 +type UserProfile struct { + AppCode string + PoolID string + IdentityType string + IdentityKey string + StrategyUserID int64 + InternalUserID int64 + ExternalUserID string + RuleVersion int64 + StrategyVersion string + Stage string + PaidDraws int64 + EquivalentDraws int64 + LossStreak int64 + PendingSpendJackpotTokens int64 + GuaranteeDrawsRemaining int64 + DownweightActive bool + DownweightFactorPPM int64 + User24HourRTPThresholdPPM int64 + OneHour UserProfileWindow + TwentyFourHour UserProfileWindow + FortyEightHour UserProfileWindow + Lifetime UserProfileWindow + BaseRewardCoins int64 + OrdinaryWinCount int64 + HighMultiplierOrdinaryWinCount int64 + RTPCompensationJackpotCount int64 + CumulativeSpendJackpotCount int64 + GuaranteeHitCount int64 + ZeroDrawCount int64 + GrantedDrawCount int64 + PendingDrawCount int64 + FailedDrawCount int64 + MaxMultiplierPPM int64 + MaxRewardCoins int64 + FirstDrawAtMS int64 + LastDrawAtMS int64 + AggregatedAtMS int64 + DataLagMS int64 +} + +type UserProfileMultiplierStat struct { + HitType string + MultiplierPPM int64 + DrawCount int64 + WagerCoins int64 + PayoutCoins int64 +} + +type UserProfileJackpotCondition struct { + Name string + Numerator int64 + Denominator int64 + RatioPPM int64 + LimitPPM int64 + FactorPPM int64 + Passed bool + Reason string +} + +type UserProfileJackpotHit struct { + // SourceType 只参与 owner service 的稳定 keyset 游标,不通过 protobuf/HTTP 对外展示。 + SourceType string + DrawID string + RequestID string + RuleVersion int64 + Stage string + Mechanism string + ReasonCode string + ReasonSummary string + TierID string + MultiplierPPM int64 + WagerCoins int64 + PayoutCoins int64 + OccurredAtMS int64 + Conditions []UserProfileJackpotCondition +} + +type UserProfileDetail struct { + Profile UserProfile + MultiplierDistribution []UserProfileMultiplierStat + JackpotHits []UserProfileJackpotHit + JackpotHitTotal int64 + JackpotPage int32 + JackpotPageSize int32 + NextJackpotCursor string + JackpotHasMore bool +} + // ExternalDrawCommand 是外部 App “已扣费后请求抽奖”的业务事实。 // HyApp 不接管外部 App 余额,因此这里没有 target_user_id、room_id 或 wallet command_id;幂等边界固定为 // app_code + request_id,任何重试都必须返回同一条抽奖结果。 diff --git a/services/lucky-gift-service/internal/service/luckygift/service.go b/services/lucky-gift-service/internal/service/luckygift/service.go index e4c7e3be..24f66631 100644 --- a/services/lucky-gift-service/internal/service/luckygift/service.go +++ b/services/lucky-gift-service/internal/service/luckygift/service.go @@ -37,6 +37,8 @@ type Repository interface { ExecuteExternalGiftDraw(ctx context.Context, cmd domain.ExternalDrawCommand, nowMS int64) (domain.ExternalDrawResult, error) ListLuckyGiftDraws(ctx context.Context, query domain.DrawQuery) ([]domain.DrawResult, int64, error) GetLuckyGiftDrawSummary(ctx context.Context, query domain.DrawQuery) (domain.DrawSummary, error) + ListLuckyGiftUserProfiles(ctx context.Context, query domain.UserProfileQuery, nowMS int64) ([]domain.UserProfile, int64, string, bool, error) + GetLuckyGiftUserProfile(ctx context.Context, query domain.UserProfileQuery, nowMS int64) (domain.UserProfileDetail, bool, error) ListLuckyGiftPoolBalances(ctx context.Context, query domain.PoolBalanceQuery) ([]domain.PoolBalance, error) AdjustLuckyGiftPoolBalance(ctx context.Context, command domain.PoolAdjustmentCommand, nowMS int64) (domain.PoolAdjustmentResult, error) GetLuckyGiftDrawRewardState(ctx context.Context, appCode string, drawIDs []string) (domain.DrawRewardState, error) @@ -54,6 +56,12 @@ type Repository interface { MarkLuckyGiftDrawsFailed(ctx context.Context, appCode string, drawIDs []string, failureReason string, nowMS int64) error } +// luckyGiftUserProfileRefresher 是 MySQL 读模型的可选历史回灌能力。测试仓储和其他最小实现 +// 不需要为了后台异步任务扩宽业务接口;生产仓储实现后由 worker 自动发现并运行。 +type luckyGiftUserProfileRefresher interface { + RefreshLuckyGiftUserProfiles(ctx context.Context, nowMS int64, batchSize int) (int, error) +} + // WalletClient 是幸运礼物返奖唯一账务依赖;wallet-service 仍负责持久 outbox,activity 只做成功落库后的低延迟余额 IM。 type WalletClient interface { CreditLuckyGiftReward(ctx context.Context, req *walletv1.CreditLuckyGiftRewardRequest, opts ...grpc.CallOption) (*walletv1.CreditLuckyGiftRewardResponse, error) @@ -278,6 +286,7 @@ func (s *Service) RunWorker(ctx context.Context, options WorkerOptions) { ticker := time.NewTicker(options.PollInterval) defer ticker.Stop() nextStatsRefresh := time.Time{} + nextUserProfileRefresh := time.Time{} for { if _, err := s.ProcessPendingDrawOutbox(ctx, options); err != nil && ctx.Err() == nil { // 批处理失败不删除 outbox;锁过期后下一轮或其他实例会继续补偿。 @@ -293,6 +302,24 @@ func (s *Service) RunWorker(ctx context.Context, options WorkerOptions) { } nextStatsRefresh = now.Add(options.StatsInterval) } + if refresher, ok := s.repository.(luckyGiftUserProfileRefresher); ok && + options.StatsInterval > 0 && (nextUserProfileRefresh.IsZero() || !now.Before(nextUserProfileRefresh)) { + // 画像历史回灌独立于 admin/databi 的十分钟快照:每轮只串行处理 5000 条,保持事务和内存有界; + // 有进度就在下一个 worker poll 继续,直到全部游标返回 0 才进入低频等待。 + const profileBatchSize = 5000 + processed, refreshErr := refresher.RefreshLuckyGiftUserProfiles(ctx, now.UnixMilli(), profileBatchSize) + switch { + case refreshErr != nil: + if ctx.Err() == nil { + logx.Error(ctx, "lucky_gift_user_profile_refresh_failed", refreshErr, slog.String("worker_id", options.WorkerID)) + } + nextUserProfileRefresh = now.Add(options.StatsInterval) + case processed > 0: + nextUserProfileRefresh = now.Add(options.PollInterval) + default: + nextUserProfileRefresh = now.Add(options.StatsInterval) + } + } select { case <-ctx.Done(): return @@ -774,6 +801,52 @@ func (s *Service) GetDrawSummary(ctx context.Context, query domain.DrawQuery) (d return s.repository.GetLuckyGiftDrawSummary(ctx, normalizeDrawQuery(query)) } +func (s *Service) ListUserProfiles(ctx context.Context, query domain.UserProfileQuery) ([]domain.UserProfile, int64, string, bool, int64, error) { + if err := s.requireRepository(); err != nil { + return nil, 0, "", false, 0, err + } + query, err := normalizeUserProfileQuery(query) + if err != nil { + return nil, 0, "", false, 0, err + } + snapshotAtMS := s.now().UTC().UnixMilli() + profiles, total, nextCursor, hasMore, err := s.repository.ListLuckyGiftUserProfiles(ctx, query, snapshotAtMS) + return profiles, total, nextCursor, hasMore, snapshotAtMS, err +} + +func (s *Service) GetUserProfile(ctx context.Context, query domain.UserProfileQuery) (domain.UserProfileDetail, int64, error) { + if err := s.requireRepository(); err != nil { + return domain.UserProfileDetail{}, 0, err + } + query, err := normalizeUserProfileQuery(query) + if err != nil { + return domain.UserProfileDetail{}, 0, err + } + // 详情入口必须选择且只选择一种真实身份键;否则空 identity_type 会退化成“取列表第一行”, + // 错配的另一种 key 还可能让运营打开与请求不一致的用户画像。 + switch query.IdentityType { + case domain.UserProfileIdentityInternal: + if query.InternalUserID <= 0 || query.ExternalUserID != "" { + return domain.UserProfileDetail{}, 0, xerr.New(xerr.InvalidArgument, "internal lucky gift profile requires only user_id") + } + case domain.UserProfileIdentityExternal: + if query.ExternalUserID == "" || query.InternalUserID != 0 { + return domain.UserProfileDetail{}, 0, xerr.New(xerr.InvalidArgument, "external lucky gift profile requires only external_user_id") + } + default: + return domain.UserProfileDetail{}, 0, xerr.New(xerr.InvalidArgument, "lucky gift profile identity_type must be internal or external") + } + snapshotAtMS := s.now().UTC().UnixMilli() + detail, found, err := s.repository.GetLuckyGiftUserProfile(ctx, query, snapshotAtMS) + if err != nil { + return domain.UserProfileDetail{}, 0, err + } + if !found { + return domain.UserProfileDetail{}, 0, xerr.New(xerr.NotFound, "lucky gift user profile not found") + } + return detail, snapshotAtMS, nil +} + func (s *Service) ListPoolBalances(ctx context.Context, query domain.PoolBalanceQuery) ([]domain.PoolBalance, error) { if err := s.requireRepository(); err != nil { return nil, err @@ -850,6 +923,53 @@ func normalizeDrawQuery(query domain.DrawQuery) domain.DrawQuery { return query } +func normalizeUserProfileQuery(query domain.UserProfileQuery) (domain.UserProfileQuery, error) { + query.AppCode = strings.ToLower(strings.TrimSpace(query.AppCode)) + if query.AppCode == "" { + return domain.UserProfileQuery{}, xerr.New(xerr.InvalidArgument, "lucky gift user profile app_code is required") + } + if strings.TrimSpace(query.PoolID) == "" { + return domain.UserProfileQuery{}, xerr.New(xerr.InvalidArgument, "lucky gift user profile pool_id is required") + } + query.PoolID = normalizePoolID(query.PoolID) + query.IdentityType = strings.ToLower(strings.TrimSpace(query.IdentityType)) + if query.IdentityType != "" && query.IdentityType != domain.UserProfileIdentityInternal && query.IdentityType != domain.UserProfileIdentityExternal { + return domain.UserProfileQuery{}, xerr.New(xerr.InvalidArgument, "unsupported lucky gift profile identity_type") + } + query.ExternalUserID = strings.TrimSpace(query.ExternalUserID) + if utf8.RuneCountInString(query.ExternalUserID) > 128 { + return domain.UserProfileQuery{}, xerr.New(xerr.InvalidArgument, "external_user_id is too long") + } + query.Stage = strings.ToLower(strings.TrimSpace(query.Stage)) + query.Status = strings.ToLower(strings.TrimSpace(query.Status)) + query.SortBy = strings.ToLower(strings.TrimSpace(query.SortBy)) + query.SortDirection = strings.ToLower(strings.TrimSpace(query.SortDirection)) + query.Cursor = strings.TrimSpace(query.Cursor) + query.JackpotCursor = strings.TrimSpace(query.JackpotCursor) + if len(query.Cursor) > 2048 || len(query.JackpotCursor) > 2048 { + return domain.UserProfileQuery{}, xerr.New(xerr.InvalidArgument, "lucky gift profile cursor is too long") + } + if query.Page <= 0 { + query.Page = 1 + } + if query.PageSize <= 0 { + query.PageSize = 20 + } + if query.PageSize > 100 { + query.PageSize = 100 + } + if query.JackpotPage <= 0 { + query.JackpotPage = 1 + } + if query.JackpotPageSize <= 0 { + query.JackpotPageSize = 20 + } + if query.JackpotPageSize > 50 { + query.JackpotPageSize = 50 + } + return query, nil +} + type luckyGiftDrawnPayload struct { EventType string `json:"event_type"` EventID string `json:"event_id"` diff --git a/services/lucky-gift-service/internal/storage/mysql/external_repository.go b/services/lucky-gift-service/internal/storage/mysql/external_repository.go index 8f9467d4..841a4e47 100644 --- a/services/lucky-gift-service/internal/storage/mysql/external_repository.go +++ b/services/lucky-gift-service/internal/storage/mysql/external_repository.go @@ -154,6 +154,29 @@ func (r *Repository) ExecuteExternalGiftDraw(ctx context.Context, cmd domain.Ext ); err != nil { return domain.ExternalDrawResult{}, err } + // dynamic_v3 已由逐抽 item 同事务写画像;fixed_v2 和停用规则没有 item,必须用聚合事实补一条,二者不能重复累计。 + if !(exists && rule.Enabled && rule.StrategyVersion == domain.StrategyDynamicV3) { + strategyVersion := strings.TrimSpace(rule.StrategyVersion) + if strategyVersion == "" { + strategyVersion = domain.StrategyFixedV2 + } + fact := luckyUserProfileFact{ + AppCode: cmd.AppCode, SourceType: luckyUserProfileSourceExternalLegacy, SourceDrawID: drawID, + RequestID: cmd.RequestID, PoolID: poolID, IdentityType: domain.UserProfileIdentityExternal, + IdentityKey: cmd.ExternalUserID, StrategyUserID: externalLuckyUserID(cmd.AppCode, cmd.ExternalUserID), + ExternalUserID: cmd.ExternalUserID, RuleVersion: ruleVersion, StrategyVersion: strategyVersion, + // fixed_v2 的外部请求把 gift_count 合并成一次随机:流水是整单 total_amount, + // 但 paid_draws/loss_streak 只推进一次。画像必须沿用同一口径,不能把一条结果放大成 N 次命中。 + Stage: selected.Stage, DrawCount: 1, WagerCoins: cmd.TotalAmount, + PayoutCoins: rewardAmount, BaseRewardCoins: rewardAmount, MultiplierPPM: selected.MultiplierPPM, + RewardStatus: row.RewardStatus, TierID: selected.TierID, OccurredAtMS: cmd.PaidAtMS, CreatedAtMS: nowMS, + } + fact.HitType = luckyUserProfileHitType(fact.PayoutCoins, "", nil, nil) + fact.HighOrdinary = selected.HighMultiplier && fact.HitType == domain.UserProfileHitOrdinary + if err := r.persistLuckyGiftUserProfileFacts(ctx, tx, []luckyUserProfileFact{fact}, nowMS); err != nil { + return domain.ExternalDrawResult{}, err + } + } payload, err := json.Marshal(map[string]any{ "event_type": domain.EventTypeExternalLuckyGiftDrawn, "event_id": domain.EventTypeExternalLuckyGiftDrawn + ":" + drawID, @@ -259,7 +282,10 @@ func (r *Repository) executeExternalGiftEconomy(ctx context.Context, tx *sql.Tx, if err := r.applyExternalLuckyEconomy(ctx, tx, cmd.AppCode, config, drawCommand, candidate, globalWindow, giftWindow, basePool, nextCumulativeWager, nextEquivalentDraws, nowMS); err != nil { return externalDrawCandidate{}, 0, err } - return externalDrawCandidate{TierID: candidate.TierID, MultiplierPPM: candidate.MultiplierPPM}, candidate.effectiveReward(), nil + return externalDrawCandidate{ + TierID: candidate.TierID, MultiplierPPM: candidate.MultiplierPPM, + Stage: experiencePool, HighMultiplier: candidate.HighMultiplier, + }, candidate.effectiveReward(), nil } // executeExternalDynamicGiftEconomy 让外部 App 复用与房间送礼完全相同的 dynamic_v3 内核。 @@ -313,6 +339,9 @@ func (r *Repository) executeExternalDynamicGiftEconomy(ctx context.Context, tx * reward += result.EffectiveRewardCoins } candidate := externalDrawCandidate{TierID: "dynamic_v3_batch"} + if len(results) > 0 { + candidate.Stage = results[len(results)-1].ExperiencePool + } if len(results) == 1 { candidate.TierID = results[0].SelectedTierID candidate.MultiplierPPM = results[0].MultiplierPPM @@ -396,8 +425,10 @@ func (r *Repository) getExternalGiftDrawForUpdate(ctx context.Context, tx *sql.T } type externalDrawCandidate struct { - TierID string - MultiplierPPM int64 + TierID string + MultiplierPPM int64 + Stage string + HighMultiplier bool } func selectExternalDrawCandidate(rule domain.RuleConfig) (externalDrawCandidate, error) { diff --git a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_dynamic_repository.go b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_dynamic_repository.go index 092fbaa6..8b3d45cc 100644 --- a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_dynamic_repository.go +++ b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_dynamic_repository.go @@ -643,5 +643,11 @@ func (r *Repository) insertExternalLuckyGiftDrawItems(ctx context.Context, tx *s return err } } - return nil + facts := make([]luckyUserProfileFact, 0, len(records)) + strategyUserID := externalLuckyUserID(appCode, persistence.ExternalUserID) + for _, record := range records { + facts = append(facts, luckyUserProfileFactFromRecord(record, luckyUserProfileSourceExternalItem, + domain.UserProfileIdentityExternal, persistence.ExternalUserID, persistence.ExternalUserID, strategyUserID)) + } + return r.persistLuckyGiftUserProfileFacts(ctx, tx, facts, nowMS) } diff --git a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_repository.go b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_repository.go index b28ede6c..34ef75ce 100644 --- a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_repository.go +++ b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_repository.go @@ -569,6 +569,9 @@ func (r *Repository) updateLuckyPoolNet(ctx context.Context, tx *sql.Tx, appCode // insertLuckyDrawRecords 批量写入审计明细。 // 仍然保留每一抽一条记录,因为后台统计“抽奖次数”和问题复盘都需要单抽粒度;优化点是批量 INSERT 而不是 N 次 INSERT。 func (r *Repository) insertLuckyDrawRecords(ctx context.Context, tx *sql.Tx, records []luckyDrawRecordInput) error { + if len(records) == 0 { + return nil + } const chunkSize = 200 for start := 0; start < len(records); start += chunkSize { end := start + chunkSize @@ -598,7 +601,14 @@ func (r *Repository) insertLuckyDrawRecords(ctx context.Context, tx *sql.Tx, rec return err } } - return nil + // 画像事件与抽奖事实同事务提交;后台页面因此不依赖下一轮历史回灌,同时事件唯一键仍允许回灌安全补旧数据。 + facts := make([]luckyUserProfileFact, 0, len(records)) + for _, record := range records { + userID := record.Command.UserID + facts = append(facts, luckyUserProfileFactFromRecord(record, luckyUserProfileSourceInternal, + domain.UserProfileIdentityInternal, fmt.Sprintf("%d", userID), "", userID)) + } + return r.persistLuckyGiftUserProfileFacts(ctx, tx, facts, records[len(records)-1].NowMS) } type luckyDrawPoolStatKey struct { @@ -1425,6 +1435,18 @@ func (r *Repository) applyLuckyDraw(ctx context.Context, tx *sql.Tx, input lucky ); err != nil { return err } + profileRecord := luckyDrawRecordInput{ + AppCode: input.AppCode, DrawID: input.DrawID, Command: input.Command, Config: input.Config, + ExperiencePool: input.ExperiencePool, Candidate: input.Candidate, Limited: input.Limited, + StageFeedback: input.StageFeedback, GlobalWindow: input.GlobalWindow, GiftWindow: input.GiftWindow, + BasePool: input.BasePool, RewardStatus: input.RewardStatus, NowMS: input.NowMS, + } + if err := r.persistLuckyGiftUserProfileFacts(ctx, tx, []luckyUserProfileFact{ + luckyUserProfileFactFromRecord(profileRecord, luckyUserProfileSourceInternal, + domain.UserProfileIdentityInternal, fmt.Sprintf("%d", input.Command.UserID), "", input.Command.UserID), + }, input.NowMS); err != nil { + return err + } if luckyDrawNeedsRewardSettlementOutbox(input.Candidate) { // 同一 owner outbox 先补偿钱包返奖,再在 granted 收敛后发布中奖事实;room/activity 的展示副作用 // 由各自本地 outbox 独立补偿,不能在抽奖事务里直接调用腾讯云 IM。 @@ -1869,6 +1891,10 @@ func (r *Repository) markLuckyGiftDraws(ctx context.Context, appCode string, dra _ = tx.Rollback() return err } + if err := r.migrateLuckyGiftUserProfileRewardStatuses(ctx, tx, appCode, chunk, status, onlyPending, nowMS); err != nil { + _ = tx.Rollback() + return err + } if err := tx.Commit(); err != nil { return err } diff --git a/services/lucky-gift-service/internal/storage/mysql/lucky_gift_user_profile_repository.go b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_user_profile_repository.go new file mode 100644 index 00000000..9ec3ef94 --- /dev/null +++ b/services/lucky-gift-service/internal/storage/mysql/lucky_gift_user_profile_repository.go @@ -0,0 +1,1471 @@ +package mysql + +import ( + "context" + "crypto/sha256" + "database/sql" + "encoding/base64" + "encoding/json" + "fmt" + "sort" + "strconv" + "strings" + "time" + + "hyapp/pkg/appcode" + "hyapp/pkg/idgen" + "hyapp/pkg/xerr" + domain "hyapp/services/lucky-gift-service/internal/domain/luckygift" +) + +const ( + luckyUserProfileSourceInternal = "internal" + luckyUserProfileSourceExternalItem = "external_item" + luckyUserProfileSourceExternalLegacy = "external_legacy" + + luckyUserProfileCursorInternal = "user_profile_internal" + luckyUserProfileCursorExternalItem = "user_profile_external_item" + luckyUserProfileCursorExternalLegacy = "user_profile_external_legacy" +) + +// luckyUserProfileFact 是内部、外部逐抽和外部旧聚合事实的统一窄模型。大奖类型只能来自 +// dynamic_v3 trace.JackpotMechanism;倍率再高也不能把普通 tier 误算成大奖。 +type luckyUserProfileFact struct { + AppCode string + SourceType string + SourceDrawID string + RequestID string + PoolID string + IdentityType string + IdentityKey string + StrategyUserID int64 + InternalUserID int64 + ExternalUserID string + RuleVersion int64 + StrategyVersion string + Stage string + DrawCount int64 + WagerCoins int64 + PayoutCoins int64 + BaseRewardCoins int64 + MultiplierPPM int64 + HitType string + RewardStatus string + HighOrdinary bool + GuaranteeHit bool + Downweight bool + TierID string + ReasonCode string + Conditions []domain.StrategyConditionTrace + OccurredAtMS int64 + CreatedAtMS int64 +} + +type luckyUserProfileCandidateSnapshot struct { + DynamicV3 *domain.DecisionTrace `json:"dynamic_v3"` +} + +type luckyUserProfileKey struct { + AppCode string + PoolID string + StrategyVersion string + IdentityType string + IdentityKey string +} + +// luckyUserProfileListCursor 固化“排序值 + 完整身份主键”。客户端只把 base64url token +// 原样回传;ScopeHash 阻止把某个筛选/排序下的游标误用于另一个查询,避免静默漏行。 +type luckyUserProfileListCursor struct { + Version int `json:"v"` + ScopeHash string `json:"s"` + SortValue int64 `json:"n"` + IdentityType string `json:"t"` + IdentityKey string `json:"k"` +} + +// luckyUserJackpotCursor 使用大奖索引的完整 DESC 唯一顺序;source_type 是主键的一部分, +// 同一毫秒且两个来源 draw_id 相同时仍不会重读或跳过。 +type luckyUserJackpotCursor struct { + Version int `json:"v"` + ScopeHash string `json:"s"` + OccurredAtMS int64 `json:"o"` + SourceType string `json:"t"` + DrawID string `json:"d"` +} + +type luckyUserProfileDelta struct { + Latest luckyUserProfileFact + TotalDraws int64 + TotalWagerCoins int64 + TotalPayoutCoins int64 + TotalBaseRewardCoins int64 + OrdinaryWinCount int64 + HighMultiplierOrdinaryWinCount int64 + RTPCompensationJackpotCount int64 + CumulativeSpendJackpotCount int64 + GuaranteeHitCount int64 + ZeroDrawCount int64 + GrantedDrawCount int64 + PendingDrawCount int64 + FailedDrawCount int64 + MaxMultiplierPPM int64 + MaxRewardCoins int64 + FirstDrawAtMS int64 + LastDrawAtMS int64 +} + +type luckyUserMultiplierKey struct { + ProfileKey luckyUserProfileKey + HitType string + MultiplierPPM int64 +} + +type luckyUserMultiplierDelta struct { + DrawCount int64 + WagerCoins int64 + PayoutCoins int64 +} + +func luckyUserProfileFactFromRecord(record luckyDrawRecordInput, sourceType, identityType, identityKey, externalUserID string, strategyUserID int64) luckyUserProfileFact { + fact := luckyUserProfileFact{ + AppCode: record.AppCode, SourceType: sourceType, SourceDrawID: record.DrawID, + RequestID: record.Command.CommandID, PoolID: record.Config.PoolID, + IdentityType: identityType, IdentityKey: identityKey, StrategyUserID: strategyUserID, + ExternalUserID: externalUserID, RuleVersion: record.Config.RuleVersion, + StrategyVersion: record.Config.StrategyVersion, Stage: record.ExperiencePool, DrawCount: 1, + WagerCoins: record.Command.CoinSpent, PayoutCoins: record.Candidate.effectiveReward(), + BaseRewardCoins: record.Candidate.BaseReward, MultiplierPPM: record.Candidate.MultiplierPPM, + RewardStatus: record.RewardStatus, HighOrdinary: record.Candidate.HighMultiplier, + TierID: record.Candidate.TierID, OccurredAtMS: luckyPaidTime(record.Command, record.NowMS).UnixMilli(), + CreatedAtMS: record.NowMS, + } + if identityType == domain.UserProfileIdentityInternal { + fact.InternalUserID = strategyUserID + } + if record.StrategyDecision != nil { + applyLuckyUserProfileTrace(&fact, &record.StrategyDecision.Trace) + } + fact.HitType = luckyUserProfileHitType(fact.PayoutCoins, fact.ReasonCode, fact.Conditions, record.StrategyDecision) + fact.HighOrdinary = fact.HighOrdinary && fact.HitType == domain.UserProfileHitOrdinary + return fact +} + +func luckyUserProfileHitType(payout int64, _ string, _ []domain.StrategyConditionTrace, decision *domain.StrategyDecision) string { + if payout <= 0 { + return domain.UserProfileHitZero + } + if decision != nil { + // 只认固化在审计 trace 中的独立大奖机制。普通概率允许配置同倍率,绝不能按倍率或 tier.Jackpot 猜大奖。 + switch decision.Trace.JackpotMechanism { + case domain.StrategyJackpotMechanismRTPCompensation: + return domain.UserProfileHitRTPCompensation + case domain.StrategyJackpotMechanismMilestone: + return domain.UserProfileHitSpendMilestone + } + } + return domain.UserProfileHitOrdinary +} + +func applyLuckyUserProfileTrace(fact *luckyUserProfileFact, trace *domain.DecisionTrace) { + if fact == nil || trace == nil { + return + } + fact.ReasonCode = strings.TrimSpace(trace.FinalReason) + fact.Conditions = append([]domain.StrategyConditionTrace(nil), trace.Conditions...) + for _, condition := range trace.Conditions { + if condition.Name == domain.StrategyConditionUser24HourRTPDownweight { + fact.Downweight = condition.Passed + } + } + for _, removed := range trace.Removed { + if removed.Reason == domain.StrategyRemovalMissProtection { + fact.GuaranteeHit = true + break + } + } + if strings.TrimSpace(trace.Stage) != "" { + fact.Stage = trace.Stage + } +} + +func luckyUserProfileFactFromSnapshot(fact luckyUserProfileFact, snapshot []byte) luckyUserProfileFact { + var decoded luckyUserProfileCandidateSnapshot + if len(snapshot) == 0 || json.Unmarshal(snapshot, &decoded) != nil || decoded.DynamicV3 == nil { + fact.StrategyVersion = domain.StrategyFixedV2 + fact.HitType = luckyUserProfileHitType(fact.PayoutCoins, "", nil, nil) + fact.HighOrdinary = fact.HighOrdinary && fact.HitType == domain.UserProfileHitOrdinary + return fact + } + fact.StrategyVersion = domain.StrategyDynamicV3 + applyLuckyUserProfileTrace(&fact, decoded.DynamicV3) + if fact.PayoutCoins <= 0 { + fact.HitType = domain.UserProfileHitZero + } else { + // 历史回灌直接读取持久 trace 的机制字段;不从倍率、tier_id 或旧 high_multiplier 标记推断大奖。 + switch decoded.DynamicV3.JackpotMechanism { + case domain.StrategyJackpotMechanismRTPCompensation: + fact.HitType = domain.UserProfileHitRTPCompensation + case domain.StrategyJackpotMechanismMilestone: + fact.HitType = domain.UserProfileHitSpendMilestone + default: + fact.HitType = domain.UserProfileHitOrdinary + } + } + fact.HighOrdinary = fact.HighOrdinary && fact.HitType == domain.UserProfileHitOrdinary + return fact +} + +// persistLuckyGiftUserProfileFacts 将事实唯一键和累计画像放在调用方事务内提交。aggregation_token +// 只会落在首次 INSERT IGNORE 成功的事件上,在线写与历史回灌并发时也不会双算累计值。 +func (r *Repository) persistLuckyGiftUserProfileFacts(ctx context.Context, tx *sql.Tx, facts []luckyUserProfileFact, nowMS int64) error { + if len(facts) == 0 { + return nil + } + grouped := make(map[string][]luckyUserProfileFact) + for _, fact := range facts { + if fact.DrawCount <= 0 { + fact.DrawCount = 1 + } + if fact.OccurredAtMS <= 0 { + fact.OccurredAtMS = nowMS + } + if fact.CreatedAtMS <= 0 { + fact.CreatedAtMS = nowMS + } + key := fact.AppCode + "\x00" + fact.SourceType + grouped[key] = append(grouped[key], fact) + } + for _, sourceFacts := range grouped { + if err := r.persistLuckyGiftUserProfileSourceFacts(ctx, tx, sourceFacts, nowMS); err != nil { + return err + } + } + return nil +} + +func (r *Repository) persistLuckyGiftUserProfileSourceFacts(ctx context.Context, tx *sql.Tx, facts []luckyUserProfileFact, nowMS int64) error { + if len(facts) == 0 { + return nil + } + token := idgen.New("lucky_profile_batch") + const chunkSize = 200 + for start := 0; start < len(facts); start += chunkSize { + end := start + chunkSize + if end > len(facts) { + end = len(facts) + } + var query strings.Builder + query.WriteString(`INSERT IGNORE INTO lucky_gift_user_profile_events ( + app_code, source_type, source_draw_id, pool_id, strategy_version, identity_type, identity_key, + draw_count, wager_coins, payout_coins, base_reward_coins, multiplier_ppm, + hit_type, reward_status, occurred_at_ms, aggregation_token, created_at_ms + ) VALUES `) + args := make([]any, 0, (end-start)*17) + for index, fact := range facts[start:end] { + if index > 0 { + query.WriteByte(',') + } + query.WriteString("(" + luckySQLPlaceholders(17) + ")") + args = append(args, fact.AppCode, fact.SourceType, fact.SourceDrawID, fact.PoolID, fact.StrategyVersion, fact.IdentityType, fact.IdentityKey, + fact.DrawCount, fact.WagerCoins, fact.PayoutCoins, fact.BaseRewardCoins, fact.MultiplierPPM, + fact.HitType, fact.RewardStatus, fact.OccurredAtMS, token, nowMS) + } + if _, err := tx.ExecContext(ctx, query.String(), args...); err != nil { + return err + } + } + + insertedRows, err := tx.QueryContext(ctx, ` + SELECT source_draw_id + FROM lucky_gift_user_profile_events FORCE INDEX (idx_lucky_user_profile_event_token) + WHERE app_code = ? AND source_type = ? AND aggregation_token = ?`, facts[0].AppCode, facts[0].SourceType, token) + if err != nil { + return err + } + inserted := make(map[string]struct{}, len(facts)) + for insertedRows.Next() { + var drawID string + if err := insertedRows.Scan(&drawID); err != nil { + _ = insertedRows.Close() + return err + } + inserted[drawID] = struct{}{} + } + if err := insertedRows.Err(); err != nil { + _ = insertedRows.Close() + return err + } + if err := insertedRows.Close(); err != nil { + return err + } + if len(inserted) == 0 { + return nil + } + newFacts := make([]luckyUserProfileFact, 0, len(inserted)) + for _, fact := range facts { + if _, ok := inserted[fact.SourceDrawID]; ok { + newFacts = append(newFacts, fact) + // 输入即使被上游意外重复,也只能由事件唯一键对应的一份事实推进累计值。 + delete(inserted, fact.SourceDrawID) + } + } + return r.applyLuckyGiftUserProfileDeltas(ctx, tx, newFacts, nowMS) +} + +func (r *Repository) applyLuckyGiftUserProfileDeltas(ctx context.Context, tx *sql.Tx, facts []luckyUserProfileFact, nowMS int64) error { + profiles := make(map[luckyUserProfileKey]*luckyUserProfileDelta) + multipliers := make(map[luckyUserMultiplierKey]*luckyUserMultiplierDelta) + jackpots := make([]luckyUserProfileFact, 0) + for _, fact := range facts { + key := luckyUserProfileKey{AppCode: fact.AppCode, PoolID: fact.PoolID, StrategyVersion: fact.StrategyVersion, IdentityType: fact.IdentityType, IdentityKey: fact.IdentityKey} + delta := profiles[key] + if delta == nil { + delta = &luckyUserProfileDelta{Latest: fact, FirstDrawAtMS: fact.OccurredAtMS, LastDrawAtMS: fact.OccurredAtMS} + profiles[key] = delta + } + if fact.OccurredAtMS > delta.LastDrawAtMS || (fact.OccurredAtMS == delta.LastDrawAtMS && fact.SourceDrawID > delta.Latest.SourceDrawID) { + delta.Latest = fact + delta.LastDrawAtMS = fact.OccurredAtMS + } + if fact.OccurredAtMS < delta.FirstDrawAtMS { + delta.FirstDrawAtMS = fact.OccurredAtMS + } + delta.TotalDraws += fact.DrawCount + delta.TotalWagerCoins += fact.WagerCoins + delta.TotalPayoutCoins += fact.PayoutCoins + delta.TotalBaseRewardCoins += fact.BaseRewardCoins + switch fact.HitType { + case domain.UserProfileHitRTPCompensation: + delta.RTPCompensationJackpotCount += fact.DrawCount + jackpots = append(jackpots, fact) + case domain.UserProfileHitSpendMilestone: + delta.CumulativeSpendJackpotCount += fact.DrawCount + jackpots = append(jackpots, fact) + case domain.UserProfileHitOrdinary: + if fact.PayoutCoins > 0 { + delta.OrdinaryWinCount += fact.DrawCount + if fact.HighOrdinary { + delta.HighMultiplierOrdinaryWinCount += fact.DrawCount + } + } + default: + delta.ZeroDrawCount += fact.DrawCount + } + if fact.GuaranteeHit && fact.PayoutCoins > 0 { + delta.GuaranteeHitCount += fact.DrawCount + } + switch fact.RewardStatus { + case domain.StatusPending: + delta.PendingDrawCount += fact.DrawCount + case domain.StatusFailed: + delta.FailedDrawCount += fact.DrawCount + default: + delta.GrantedDrawCount += fact.DrawCount + } + if fact.MultiplierPPM > delta.MaxMultiplierPPM { + delta.MaxMultiplierPPM = fact.MultiplierPPM + } + if fact.PayoutCoins > delta.MaxRewardCoins { + delta.MaxRewardCoins = fact.PayoutCoins + } + multiplierKey := luckyUserMultiplierKey{ProfileKey: key, HitType: fact.HitType, MultiplierPPM: fact.MultiplierPPM} + multiplier := multipliers[multiplierKey] + if multiplier == nil { + multiplier = &luckyUserMultiplierDelta{} + multipliers[multiplierKey] = multiplier + } + multiplier.DrawCount += fact.DrawCount + multiplier.WagerCoins += fact.WagerCoins + multiplier.PayoutCoins += fact.PayoutCoins + } + for key, delta := range profiles { + if err := r.upsertLuckyGiftUserProfileDelta(ctx, tx, key, *delta, nowMS); err != nil { + return err + } + } + for key, delta := range multipliers { + if _, err := tx.ExecContext(ctx, ` + INSERT INTO lucky_gift_user_multiplier_stats ( + app_code, pool_id, strategy_version, identity_type, identity_key, hit_type, multiplier_ppm, + draw_count, wager_coins, payout_coins, updated_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?) + ON DUPLICATE KEY UPDATE + draw_count = draw_count + VALUES(draw_count), + wager_coins = wager_coins + VALUES(wager_coins), + payout_coins = payout_coins + VALUES(payout_coins), + updated_at_ms = VALUES(updated_at_ms)`, + key.ProfileKey.AppCode, key.ProfileKey.PoolID, key.ProfileKey.StrategyVersion, key.ProfileKey.IdentityType, key.ProfileKey.IdentityKey, + key.HitType, key.MultiplierPPM, delta.DrawCount, delta.WagerCoins, delta.PayoutCoins, nowMS); err != nil { + return err + } + } + for _, fact := range jackpots { + conditionsJSON, err := json.Marshal(fact.Conditions) + if err != nil { + return err + } + if _, err := tx.ExecContext(ctx, ` + INSERT IGNORE INTO lucky_gift_user_jackpot_hits ( + app_code, source_type, source_draw_id, pool_id, strategy_version, identity_type, identity_key, + request_id, rule_version, stage, mechanism, reason_code, tier_id, + multiplier_ppm, wager_coins, payout_coins, conditions_json, occurred_at_ms, created_at_ms + ) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)`, + fact.AppCode, fact.SourceType, fact.SourceDrawID, fact.PoolID, fact.StrategyVersion, fact.IdentityType, fact.IdentityKey, + fact.RequestID, fact.RuleVersion, fact.Stage, fact.HitType, fact.ReasonCode, fact.TierID, + fact.MultiplierPPM, fact.WagerCoins, fact.PayoutCoins, string(conditionsJSON), fact.OccurredAtMS, nowMS); err != nil { + return err + } + } + return nil +} + +func (r *Repository) upsertLuckyGiftUserProfileDelta(ctx context.Context, tx *sql.Tx, key luckyUserProfileKey, delta luckyUserProfileDelta, nowMS int64) error { + latest := delta.Latest + if _, err := tx.ExecContext(ctx, ` + INSERT INTO lucky_gift_user_profiles ( + app_code, pool_id, strategy_version, identity_type, identity_key, strategy_user_id, internal_user_id, external_user_id, + rule_version, current_stage, total_draws, total_wager_coins, total_payout_coins, + total_base_reward_coins, ordinary_win_count, high_multiplier_ordinary_win_count, + rtp_compensation_jackpot_count, cumulative_spend_jackpot_count, guarantee_hit_count, zero_draw_count, + granted_draw_count, pending_draw_count, failed_draw_count, max_multiplier_ppm, max_reward_coins, + last_downweight_active, first_draw_at_ms, last_draw_at_ms, aggregated_at_ms, created_at_ms, updated_at_ms + ) VALUES (`+luckySQLPlaceholders(31)+`) + ON DUPLICATE KEY UPDATE + strategy_user_id = VALUES(strategy_user_id), + internal_user_id = VALUES(internal_user_id), + external_user_id = VALUES(external_user_id), + rule_version = IF(VALUES(last_draw_at_ms) >= last_draw_at_ms, VALUES(rule_version), rule_version), + current_stage = IF(VALUES(last_draw_at_ms) >= last_draw_at_ms, VALUES(current_stage), current_stage), + total_draws = total_draws + VALUES(total_draws), + total_wager_coins = total_wager_coins + VALUES(total_wager_coins), + total_payout_coins = total_payout_coins + VALUES(total_payout_coins), + total_base_reward_coins = total_base_reward_coins + VALUES(total_base_reward_coins), + ordinary_win_count = ordinary_win_count + VALUES(ordinary_win_count), + high_multiplier_ordinary_win_count = high_multiplier_ordinary_win_count + VALUES(high_multiplier_ordinary_win_count), + rtp_compensation_jackpot_count = rtp_compensation_jackpot_count + VALUES(rtp_compensation_jackpot_count), + cumulative_spend_jackpot_count = cumulative_spend_jackpot_count + VALUES(cumulative_spend_jackpot_count), + guarantee_hit_count = guarantee_hit_count + VALUES(guarantee_hit_count), + zero_draw_count = zero_draw_count + VALUES(zero_draw_count), + granted_draw_count = granted_draw_count + VALUES(granted_draw_count), + pending_draw_count = pending_draw_count + VALUES(pending_draw_count), + failed_draw_count = failed_draw_count + VALUES(failed_draw_count), + max_multiplier_ppm = GREATEST(max_multiplier_ppm, VALUES(max_multiplier_ppm)), + max_reward_coins = GREATEST(max_reward_coins, VALUES(max_reward_coins)), + last_downweight_active = IF(VALUES(last_draw_at_ms) >= last_draw_at_ms, VALUES(last_downweight_active), last_downweight_active), + first_draw_at_ms = IF(first_draw_at_ms = 0, VALUES(first_draw_at_ms), LEAST(first_draw_at_ms, VALUES(first_draw_at_ms))), + last_draw_at_ms = GREATEST(last_draw_at_ms, VALUES(last_draw_at_ms)), + aggregated_at_ms = GREATEST(aggregated_at_ms, VALUES(aggregated_at_ms)), + updated_at_ms = VALUES(updated_at_ms)`, + key.AppCode, key.PoolID, key.StrategyVersion, key.IdentityType, key.IdentityKey, latest.StrategyUserID, latest.InternalUserID, latest.ExternalUserID, + latest.RuleVersion, latest.Stage, delta.TotalDraws, delta.TotalWagerCoins, delta.TotalPayoutCoins, + delta.TotalBaseRewardCoins, delta.OrdinaryWinCount, delta.HighMultiplierOrdinaryWinCount, + delta.RTPCompensationJackpotCount, delta.CumulativeSpendJackpotCount, delta.GuaranteeHitCount, delta.ZeroDrawCount, + delta.GrantedDrawCount, delta.PendingDrawCount, delta.FailedDrawCount, delta.MaxMultiplierPPM, delta.MaxRewardCoins, + latest.Downweight, delta.FirstDrawAtMS, delta.LastDrawAtMS, nowMS, nowMS, nowMS); err != nil { + return err + } + return r.syncLuckyGiftUserProfileSortState(ctx, tx, key) +} + +// syncLuckyGiftUserProfileSortState 在同一开奖/回灌事务内固化所有列表排序字段。列表因此不再 +// JOIN lucky_user_states,也不需要对累计 RTP 或大奖数做表达式 filesort;状态行缺失时明确回落 0。 +func (r *Repository) syncLuckyGiftUserProfileSortState(ctx context.Context, tx *sql.Tx, key luckyUserProfileKey) error { + _, err := tx.ExecContext(ctx, ` + UPDATE lucky_gift_user_profiles p + LEFT JOIN lucky_user_states s + ON s.app_code = p.app_code AND s.user_id = p.strategy_user_id AND s.gift_id = p.pool_id + SET p.lifetime_rtp_ppm = CASE + WHEN p.total_wager_coins <= 0 THEN 0 + ELSE CAST(LEAST(CAST(9223372036854775807 AS DECIMAL(65,0)), + FLOOR(CAST(p.total_payout_coins AS DECIMAL(65,0)) * 1000000 / p.total_wager_coins)) AS SIGNED) + END, + p.jackpot_count = p.rtp_compensation_jackpot_count + p.cumulative_spend_jackpot_count, + p.current_equivalent_draws = COALESCE(s.equivalent_draws, 0), + p.current_loss_streak = COALESCE(s.loss_streak, 0), + p.current_pending_spend_jackpot_tokens = COALESCE(s.pending_spend_jackpot_tokens, 0) + WHERE p.app_code = ? AND p.pool_id = ? AND p.strategy_version = ? + AND p.identity_type = ? AND p.identity_key = ?`, + key.AppCode, key.PoolID, key.StrategyVersion, key.IdentityType, key.IdentityKey) + return err +} + +func (r *Repository) ListLuckyGiftUserProfiles(ctx context.Context, query domain.UserProfileQuery, nowMS int64) ([]domain.UserProfile, int64, string, bool, error) { + if r == nil || r.db == nil { + return nil, 0, "", false, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + // COUNT、当前页、窗口指标必须来自同一 InnoDB 一致性快照;否则并发开奖可能让 + // total/累计值仍是旧提交,而 24h 窗口已经包含新提交,页面同一行会自相矛盾。 + tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: true}) + if err != nil { + return nil, 0, "", false, err + } + defer func() { _ = tx.Rollback() }() + profiles, total, nextCursor, hasMore, err := r.listLuckyGiftUserProfiles(ctx, tx, query, nowMS) + if err != nil { + return nil, 0, "", false, err + } + if err := tx.Commit(); err != nil { + return nil, 0, "", false, err + } + return profiles, total, nextCursor, hasMore, nil +} + +func (r *Repository) listLuckyGiftUserProfiles(ctx context.Context, q luckyRuleQueryer, query domain.UserProfileQuery, nowMS int64) ([]domain.UserProfile, int64, string, bool, error) { + rule, found, err := r.getLuckyGiftRuleConfig(ctx, q, query.AppCode, query.PoolID, false) + if err != nil { + return nil, 0, "", false, err + } + if !found { + return []domain.UserProfile{}, 0, "", false, nil + } + whereSQL, args, err := luckyUserProfileWhereClause(query, rule.StrategyVersion) + if err != nil { + return nil, 0, "", false, err + } + order, err := luckyUserProfileOrderClause(query.SortBy, query.SortDirection) + if err != nil { + return nil, 0, "", false, err + } + var total int64 + if err := q.QueryRowContext(ctx, ` + SELECT COUNT(*) + FROM lucky_gift_user_profiles p FORCE INDEX (`+luckyUserProfileCountIndex(query, order)+`) + WHERE `+whereSQL, args...).Scan(&total); err != nil { + return nil, 0, "", false, err + } + listWhereSQL := whereSQL + listArgs := append([]any(nil), args...) + if query.Cursor != "" { + cursor, cursorErr := decodeLuckyUserProfileListCursor(query.Cursor, query, rule.StrategyVersion, order) + if cursorErr != nil { + return nil, 0, "", false, cursorErr + } + comparison := "<" + if order.Direction == "ASC" { + comparison = ">" + } + // 三列统一方向,和 FORCE 的 DESC 复合索引完全一致;ASC 由 InnoDB 反向扫描同一索引。 + listWhereSQL += " AND (" + order.Column + " " + comparison + " ? OR (" + order.Column + " = ? AND " + + "(p.identity_type " + comparison + " ? OR (p.identity_type = ? AND p.identity_key " + comparison + " ?))))" + listArgs = append(listArgs, cursor.SortValue, cursor.SortValue, cursor.IdentityType, cursor.IdentityType, cursor.IdentityKey) + } + listArgs = append(listArgs, int(query.PageSize)+1) + rows, err := q.QueryContext(ctx, ` + SELECT p.app_code, p.pool_id, p.identity_type, p.identity_key, p.strategy_user_id, + p.internal_user_id, p.external_user_id, p.rule_version, p.strategy_version, p.current_stage, + p.total_draws, p.current_equivalent_draws, p.current_loss_streak, + p.current_pending_spend_jackpot_tokens, p.total_draws, p.total_wager_coins, + p.total_payout_coins, p.lifetime_rtp_ppm, p.total_base_reward_coins, p.ordinary_win_count, + p.high_multiplier_ordinary_win_count, p.rtp_compensation_jackpot_count, + p.cumulative_spend_jackpot_count, p.guarantee_hit_count, p.zero_draw_count, + p.granted_draw_count, p.pending_draw_count, p.failed_draw_count, p.max_multiplier_ppm, + p.max_reward_coins, p.first_draw_at_ms, p.last_draw_at_ms, p.aggregated_at_ms + FROM lucky_gift_user_profiles p FORCE INDEX (`+order.Index+`) + WHERE `+listWhereSQL+` + ORDER BY `+order.Column+` `+order.Direction+`, p.identity_type `+order.Direction+`, p.identity_key `+order.Direction+` + LIMIT ?`, listArgs...) + if err != nil { + return nil, 0, "", false, err + } + defer rows.Close() + profiles := make([]domain.UserProfile, 0, query.PageSize+1) + for rows.Next() { + var profile domain.UserProfile + var totalDraws, totalWager, totalPayout, lifetimeRTPPPM int64 + if err := rows.Scan( + &profile.AppCode, &profile.PoolID, &profile.IdentityType, &profile.IdentityKey, &profile.StrategyUserID, + &profile.InternalUserID, &profile.ExternalUserID, &profile.RuleVersion, &profile.StrategyVersion, &profile.Stage, + &profile.PaidDraws, &profile.EquivalentDraws, &profile.LossStreak, &profile.PendingSpendJackpotTokens, + &totalDraws, &totalWager, &totalPayout, &lifetimeRTPPPM, &profile.BaseRewardCoins, &profile.OrdinaryWinCount, + &profile.HighMultiplierOrdinaryWinCount, &profile.RTPCompensationJackpotCount, + &profile.CumulativeSpendJackpotCount, &profile.GuaranteeHitCount, &profile.ZeroDrawCount, + &profile.GrantedDrawCount, &profile.PendingDrawCount, &profile.FailedDrawCount, &profile.MaxMultiplierPPM, + &profile.MaxRewardCoins, &profile.FirstDrawAtMS, &profile.LastDrawAtMS, &profile.AggregatedAtMS, + ); err != nil { + return nil, 0, "", false, err + } + profile.Lifetime = luckyUserProfileWindow(totalDraws, totalWager, totalPayout) + if totalWager > 0 { + profile.Lifetime.RTPPPM = lifetimeRTPPPM + profile.Lifetime.HasRTP = true + } + if nowMS > profile.AggregatedAtMS { + // data_lag 表达后台读到的画像距当前快照有多久;抽奖时间到聚合时间只是处理耗时, + // 会把历史回灌误显示成实时画像延迟,无法回答运营真正关心的“这行数据有多旧”。 + profile.DataLagMS = nowMS - profile.AggregatedAtMS + } + profiles = append(profiles, profile) + } + if err := rows.Err(); err != nil { + return nil, 0, "", false, err + } + hasMore := len(profiles) > int(query.PageSize) + if hasMore { + profiles = profiles[:query.PageSize] + } + if err := r.loadLuckyGiftUserProfileWindows(ctx, q, query.AppCode, query.PoolID, rule.StrategyVersion, profiles, nowMS); err != nil { + return nil, 0, "", false, err + } + applyLuckyGiftUserProfileCurrentControls(rule, profiles) + nextCursor := "" + if hasMore && len(profiles) > 0 { + nextCursor, err = encodeLuckyUserProfileListCursor(query, rule.StrategyVersion, order, profiles[len(profiles)-1]) + if err != nil { + return nil, 0, "", false, err + } + } + return profiles, total, nextCursor, hasMore, nil +} + +func luckyUserProfileCountIndex(query domain.UserProfileQuery, order luckyUserProfileOrder) string { + // 精确身份搜索优先使用专用索引;其余状态 COUNT 选择同前缀物化索引,永不回表 JOIN current state。 + if query.InternalUserID > 0 && query.ExternalUserID == "" { + return "idx_lucky_user_profile_internal" + } + if query.ExternalUserID != "" && query.InternalUserID <= 0 { + return "idx_lucky_user_profile_external" + } + if query.Stage != "" { + return "idx_lucky_user_profile_stage" + } + switch query.Status { + case "jackpot": + return "idx_lucky_user_profile_sort_jackpot" + case "loss_streak": + return "idx_lucky_user_profile_sort_loss" + case "pending_token": + return "idx_lucky_user_profile_pending" + default: + return order.Index + } +} + +func luckyUserProfileWhereClause(query domain.UserProfileQuery, strategyVersion string) (string, []any, error) { + where := []string{"p.app_code = ?", "p.pool_id = ?", "p.strategy_version = ?"} + args := []any{query.AppCode, query.PoolID, strategyVersion} + identityType := strings.TrimSpace(query.IdentityType) + if identityType != "" { + where = append(where, "p.identity_type = ?") + args = append(args, identityType) + } + // identity_type=all 时内部和外部搜索条件必须是 OR;AND 会让任何一行都不可能同时满足两种身份。 + if identityType == "" && query.InternalUserID > 0 && query.ExternalUserID != "" { + where = append(where, "((p.identity_type = 'internal' AND p.internal_user_id = ?) OR (p.identity_type = 'external' AND p.external_user_id = ?))") + args = append(args, query.InternalUserID, query.ExternalUserID) + } else { + if query.InternalUserID > 0 { + where = append(where, "p.internal_user_id = ?") + args = append(args, query.InternalUserID) + } + if query.ExternalUserID != "" { + where = append(where, "p.external_user_id = ?") + args = append(args, query.ExternalUserID) + } + } + if query.Stage != "" { + where = append(where, "p.current_stage = ?") + args = append(args, query.Stage) + } + switch query.Status { + case "": + case "jackpot": + where = append(where, "p.jackpot_count > 0") + case "loss_streak": + where = append(where, "p.current_loss_streak > 0") + case "pending_token": + where = append(where, "p.current_pending_spend_jackpot_tokens > 0") + default: + return "", nil, xerr.New(xerr.InvalidArgument, "unsupported lucky gift user profile status") + } + return strings.Join(where, " AND "), args, nil +} + +type luckyUserProfileOrder struct { + Name string + Column string + Index string + Direction string +} + +func luckyUserProfileOrderClause(sortBy, direction string) (luckyUserProfileOrder, error) { + order := luckyUserProfileOrder{Name: "last_draw_at", Column: "p.last_draw_at_ms", Index: "idx_lucky_user_profile_sort_last"} + switch strings.TrimSpace(sortBy) { + case "", "last_draw_at": + case "lifetime_rtp": + order.Name, order.Column, order.Index = "lifetime_rtp", "p.lifetime_rtp_ppm", "idx_lucky_user_profile_sort_rtp" + case "lifetime_wager": + order.Name, order.Column, order.Index = "lifetime_wager", "p.total_wager_coins", "idx_lucky_user_profile_sort_wager" + case "lifetime_payout": + order.Name, order.Column, order.Index = "lifetime_payout", "p.total_payout_coins", "idx_lucky_user_profile_sort_payout" + case "jackpot_count": + order.Name, order.Column, order.Index = "jackpot_count", "p.jackpot_count", "idx_lucky_user_profile_sort_jackpot" + case "loss_streak": + order.Name, order.Column, order.Index = "loss_streak", "p.current_loss_streak", "idx_lucky_user_profile_sort_loss" + default: + return luckyUserProfileOrder{}, xerr.New(xerr.InvalidArgument, "unsupported lucky gift user profile sort_by") + } + direction = strings.ToUpper(strings.TrimSpace(direction)) + if direction == "" { + direction = "DESC" + } + if direction != "ASC" && direction != "DESC" { + return luckyUserProfileOrder{}, xerr.New(xerr.InvalidArgument, "unsupported lucky gift user profile sort_direction") + } + order.Direction = direction + return order, nil +} + +func luckyUserProfileCursorScope(query domain.UserProfileQuery, strategyVersion string, order luckyUserProfileOrder) string { + canonical := strings.Join([]string{ + query.AppCode, query.PoolID, strategyVersion, query.IdentityType, + strconv.FormatInt(query.InternalUserID, 10), query.ExternalUserID, query.Stage, query.Status, + order.Name, order.Direction, + }, "\x00") + sum := sha256.Sum256([]byte(canonical)) + return fmt.Sprintf("%x", sum[:]) +} + +func encodeLuckyUserProfileListCursor(query domain.UserProfileQuery, strategyVersion string, order luckyUserProfileOrder, profile domain.UserProfile) (string, error) { + cursor := luckyUserProfileListCursor{ + Version: 1, ScopeHash: luckyUserProfileCursorScope(query, strategyVersion, order), + SortValue: luckyUserProfileSortValue(profile, order.Name), + IdentityType: profile.IdentityType, IdentityKey: profile.IdentityKey, + } + payload, err := json.Marshal(cursor) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(payload), nil +} + +func decodeLuckyUserProfileListCursor(raw string, query domain.UserProfileQuery, strategyVersion string, order luckyUserProfileOrder) (luckyUserProfileListCursor, error) { + payload, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(raw)) + if err != nil { + return luckyUserProfileListCursor{}, xerr.New(xerr.InvalidArgument, "invalid lucky gift user profile cursor") + } + var cursor luckyUserProfileListCursor + if json.Unmarshal(payload, &cursor) != nil || cursor.Version != 1 || + cursor.ScopeHash != luckyUserProfileCursorScope(query, strategyVersion, order) || + (cursor.IdentityType != domain.UserProfileIdentityInternal && cursor.IdentityType != domain.UserProfileIdentityExternal) || + cursor.IdentityKey == "" || len(cursor.IdentityKey) > 128 { + return luckyUserProfileListCursor{}, xerr.New(xerr.InvalidArgument, "invalid or mismatched lucky gift user profile cursor") + } + return cursor, nil +} + +func luckyUserProfileSortValue(profile domain.UserProfile, sortName string) int64 { + switch sortName { + case "lifetime_rtp": + return profile.Lifetime.RTPPPM + case "lifetime_wager": + return profile.Lifetime.WagerCoins + case "lifetime_payout": + return profile.Lifetime.PayoutCoins + case "jackpot_count": + return profile.RTPCompensationJackpotCount + profile.CumulativeSpendJackpotCount + case "loss_streak": + return profile.LossStreak + default: + return profile.LastDrawAtMS + } +} + +func luckyUserProfileWindow(draws, wager, payout int64) domain.UserProfileWindow { + window := domain.UserProfileWindow{Draws: draws, WagerCoins: wager, PayoutCoins: payout, NetCoins: payout - wager} + if ratio, ok := (domain.StrategyRTP{WagerCoins: wager, PayoutCoins: payout}).RatioPPM(); ok { + window.RTPPPM = ratio + window.HasRTP = true + } + return window +} + +type luckyUserProfileWindowAggregate struct { + OneHourDraws, OneHourWager, OneHourPayout int64 + TwentyFourDraws, TwentyFourWager, TwentyFourPayout int64 + FortyEightDraws, FortyEightWager, FortyEightPayout int64 +} + +func (r *Repository) loadLuckyGiftUserProfileWindows(ctx context.Context, q luckyRuleQueryer, appCode, poolID, strategyVersion string, profiles []domain.UserProfile, nowMS int64) error { + if len(profiles) == 0 { + return nil + } + var identitySQL strings.Builder + args := []any{nowMS - 48*time.Hour.Milliseconds(), nowMS, appCode, poolID, strategyVersion} + for index, profile := range profiles { + if index > 0 { + identitySQL.WriteString(" OR ") + } + identitySQL.WriteString("(identity_type = ? AND identity_key = ?)") + args = append(args, profile.IdentityType, profile.IdentityKey) + } + args = append(args, + nowMS-time.Hour.Milliseconds(), nowMS-time.Hour.Milliseconds(), nowMS-time.Hour.Milliseconds(), + nowMS-24*time.Hour.Milliseconds(), nowMS-24*time.Hour.Milliseconds(), nowMS-24*time.Hour.Milliseconds(), + ) + rows, err := q.QueryContext(ctx, ` + SELECT identity_type, identity_key, + COALESCE(SUM(CASE WHEN occurred_at_ms >= ? THEN draw_count ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN occurred_at_ms >= ? THEN wager_coins ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN occurred_at_ms >= ? THEN payout_coins ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN occurred_at_ms >= ? THEN draw_count ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN occurred_at_ms >= ? THEN wager_coins ELSE 0 END), 0), + COALESCE(SUM(CASE WHEN occurred_at_ms >= ? THEN payout_coins ELSE 0 END), 0), + COALESCE(SUM(draw_count), 0), COALESCE(SUM(wager_coins), 0), COALESCE(SUM(payout_coins), 0) + FROM lucky_gift_user_profile_events FORCE INDEX (idx_lucky_user_profile_event_window) + WHERE occurred_at_ms >= ? AND occurred_at_ms < ? AND app_code = ? AND pool_id = ? AND strategy_version = ? + AND (`+identitySQL.String()+`) + GROUP BY identity_type, identity_key`, rotateLuckyUserWindowArgs(args, len(profiles))...) + if err != nil { + return err + } + defer rows.Close() + byIdentity := make(map[string]luckyUserProfileWindowAggregate, len(profiles)) + for rows.Next() { + var identityType, identityKey string + var aggregate luckyUserProfileWindowAggregate + if err := rows.Scan(&identityType, &identityKey, + &aggregate.OneHourDraws, &aggregate.OneHourWager, &aggregate.OneHourPayout, + &aggregate.TwentyFourDraws, &aggregate.TwentyFourWager, &aggregate.TwentyFourPayout, + &aggregate.FortyEightDraws, &aggregate.FortyEightWager, &aggregate.FortyEightPayout); err != nil { + return err + } + byIdentity[identityType+"\x00"+identityKey] = aggregate + } + if err := rows.Err(); err != nil { + return err + } + for index := range profiles { + aggregate := byIdentity[profiles[index].IdentityType+"\x00"+profiles[index].IdentityKey] + profiles[index].OneHour = luckyUserProfileWindow(aggregate.OneHourDraws, aggregate.OneHourWager, aggregate.OneHourPayout) + profiles[index].TwentyFourHour = luckyUserProfileWindow(aggregate.TwentyFourDraws, aggregate.TwentyFourWager, aggregate.TwentyFourPayout) + profiles[index].FortyEightHour = luckyUserProfileWindow(aggregate.FortyEightDraws, aggregate.FortyEightWager, aggregate.FortyEightPayout) + } + return nil +} + +// rotateLuckyUserWindowArgs 将固定窗口参数放到 SELECT 占位符前,再接 WHERE 和当前页身份键。 +// 调用方按可读顺序构造参数,此处只做一次机械重排,避免动态 SQL 拼接错位。 +func rotateLuckyUserWindowArgs(args []any, profileCount int) []any { + whereFixed := args[:5] + identities := args[5 : 5+profileCount*2] + windowCutoffs := args[5+profileCount*2:] + out := make([]any, 0, len(args)) + out = append(out, windowCutoffs...) + out = append(out, whereFixed...) + out = append(out, identities...) + return out +} + +func applyLuckyGiftUserProfileCurrentControls(rule domain.RuleConfig, profiles []domain.UserProfile) { + if len(profiles) == 0 { + return + } + for index := range profiles { + profile := &profiles[index] + profile.DownweightFactorPPM = luckyPPMScale + profile.User24HourRTPThresholdPPM = rule.User24HourRTPThresholdPPM + if rule.LossStreakGuarantee > profile.LossStreak { + profile.GuaranteeDrawsRemaining = rule.LossStreakGuarantee - profile.LossStreak + } + if profile.StrategyVersion != domain.StrategyDynamicV3 { + if config, configErr := luckyRuntimeConfigFromRuleConfig(rule); configErr == nil { + profile.Stage = luckyExperiencePool(profile.EquivalentDraws, config) + } + } + factor := rule.User24HourOrdinaryWinFactorPPM + if factor <= 0 || factor > luckyPPMScale { + factor = luckyPPMScale + } + profile.DownweightFactorPPM = factor + profile.DownweightActive = rule.StrategyVersion == domain.StrategyDynamicV3 && + rule.User24HourRTPThresholdPPM > 0 && factor < luckyPPMScale && + profile.TwentyFourHour.HasRTP && profile.TwentyFourHour.RTPPPM > rule.User24HourRTPThresholdPPM + } +} + +func (r *Repository) GetLuckyGiftUserProfile(ctx context.Context, query domain.UserProfileQuery, nowMS int64) (domain.UserProfileDetail, bool, error) { + query.Page = 1 + query.PageSize = 1 + query.Stage = "" + query.Status = "" + query.SortBy = "last_draw_at" + // 展开详情仍必须和列表头部画像、倍率分布、大奖总数处于同一快照;加载更多页之间 + // 可以看到新开奖,但单次响应内部不能出现“总数 10、当前页第 11 条”的撕裂数据。 + tx, err := r.db.BeginTx(ctx, &sql.TxOptions{Isolation: sql.LevelRepeatableRead, ReadOnly: true}) + if err != nil { + return domain.UserProfileDetail{}, false, err + } + defer func() { _ = tx.Rollback() }() + profiles, _, _, _, err := r.listLuckyGiftUserProfiles(ctx, tx, query, nowMS) + if err != nil { + return domain.UserProfileDetail{}, false, err + } + if len(profiles) == 0 { + return domain.UserProfileDetail{}, false, tx.Commit() + } + profile := profiles[0] + detail := domain.UserProfileDetail{ + Profile: profile, JackpotPage: query.JackpotPage, JackpotPageSize: query.JackpotPageSize, + } + rows, err := tx.QueryContext(ctx, ` + SELECT hit_type, multiplier_ppm, draw_count, wager_coins, payout_coins + FROM lucky_gift_user_multiplier_stats + WHERE app_code = ? AND pool_id = ? AND strategy_version = ? AND identity_type = ? AND identity_key = ? + ORDER BY hit_type ASC, multiplier_ppm ASC`, profile.AppCode, profile.PoolID, profile.StrategyVersion, profile.IdentityType, profile.IdentityKey) + if err != nil { + return domain.UserProfileDetail{}, false, err + } + for rows.Next() { + var stat domain.UserProfileMultiplierStat + if err := rows.Scan(&stat.HitType, &stat.MultiplierPPM, &stat.DrawCount, &stat.WagerCoins, &stat.PayoutCoins); err != nil { + _ = rows.Close() + return domain.UserProfileDetail{}, false, err + } + detail.MultiplierDistribution = append(detail.MultiplierDistribution, stat) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return domain.UserProfileDetail{}, false, err + } + if err := rows.Close(); err != nil { + return domain.UserProfileDetail{}, false, err + } + if err := tx.QueryRowContext(ctx, ` + SELECT COUNT(*) FROM lucky_gift_user_jackpot_hits + WHERE app_code = ? AND pool_id = ? AND strategy_version = ? AND identity_type = ? AND identity_key = ?`, + profile.AppCode, profile.PoolID, profile.StrategyVersion, profile.IdentityType, profile.IdentityKey).Scan(&detail.JackpotHitTotal); err != nil { + return domain.UserProfileDetail{}, false, err + } + jackpotWhere := `app_code = ? AND pool_id = ? AND strategy_version = ? AND identity_type = ? AND identity_key = ?` + jackpotArgs := []any{profile.AppCode, profile.PoolID, profile.StrategyVersion, profile.IdentityType, profile.IdentityKey} + if query.JackpotCursor != "" { + cursor, cursorErr := decodeLuckyUserJackpotCursor(query.JackpotCursor, profile) + if cursorErr != nil { + return domain.UserProfileDetail{}, false, cursorErr + } + jackpotWhere += ` AND (occurred_at_ms < ? OR (occurred_at_ms = ? AND + (source_type < ? OR (source_type = ? AND source_draw_id < ?))))` + jackpotArgs = append(jackpotArgs, cursor.OccurredAtMS, cursor.OccurredAtMS, cursor.SourceType, cursor.SourceType, cursor.DrawID) + } + jackpotArgs = append(jackpotArgs, query.JackpotPageSize+1) + jackpotRows, err := tx.QueryContext(ctx, ` + SELECT source_type, source_draw_id, request_id, rule_version, stage, mechanism, reason_code, tier_id, + multiplier_ppm, wager_coins, payout_coins, occurred_at_ms, conditions_json + FROM lucky_gift_user_jackpot_hits FORCE INDEX (idx_lucky_user_jackpot_profile) + WHERE `+jackpotWhere+` + ORDER BY occurred_at_ms DESC, source_type DESC, source_draw_id DESC + LIMIT ?`, jackpotArgs...) + if err != nil { + return domain.UserProfileDetail{}, false, err + } + defer jackpotRows.Close() + for jackpotRows.Next() { + var hit domain.UserProfileJackpotHit + var conditionsJSON []byte + if err := jackpotRows.Scan(&hit.SourceType, &hit.DrawID, &hit.RequestID, &hit.RuleVersion, &hit.Stage, &hit.Mechanism, + &hit.ReasonCode, &hit.TierID, &hit.MultiplierPPM, &hit.WagerCoins, &hit.PayoutCoins, + &hit.OccurredAtMS, &conditionsJSON); err != nil { + return domain.UserProfileDetail{}, false, err + } + var conditions []domain.StrategyConditionTrace + if err := json.Unmarshal(conditionsJSON, &conditions); err != nil { + return domain.UserProfileDetail{}, false, err + } + for _, condition := range conditions { + hit.Conditions = append(hit.Conditions, domain.UserProfileJackpotCondition{ + Name: condition.Name, Numerator: condition.Numerator, Denominator: condition.Denominator, + RatioPPM: condition.RatioPPM, LimitPPM: condition.LimitPPM, FactorPPM: condition.FactorPPM, + Passed: condition.Passed, Reason: condition.Reason, + }) + } + hit.ReasonSummary = luckyUserProfileJackpotReasonSummary(hit.Mechanism) + detail.JackpotHits = append(detail.JackpotHits, hit) + } + if err := jackpotRows.Err(); err != nil { + return domain.UserProfileDetail{}, false, err + } + if err := jackpotRows.Close(); err != nil { + return domain.UserProfileDetail{}, false, err + } + detail.JackpotHasMore = len(detail.JackpotHits) > int(query.JackpotPageSize) + if detail.JackpotHasMore { + detail.JackpotHits = detail.JackpotHits[:query.JackpotPageSize] + last := detail.JackpotHits[len(detail.JackpotHits)-1] + detail.NextJackpotCursor, err = encodeLuckyUserJackpotCursor(profile, last) + if err != nil { + return domain.UserProfileDetail{}, false, err + } + } + if err := tx.Commit(); err != nil { + return domain.UserProfileDetail{}, false, err + } + return detail, true, nil +} + +func luckyUserJackpotCursorScope(profile domain.UserProfile) string { + canonical := strings.Join([]string{ + profile.AppCode, profile.PoolID, profile.StrategyVersion, profile.IdentityType, profile.IdentityKey, + }, "\x00") + sum := sha256.Sum256([]byte(canonical)) + return fmt.Sprintf("%x", sum[:]) +} + +func encodeLuckyUserJackpotCursor(profile domain.UserProfile, hit domain.UserProfileJackpotHit) (string, error) { + payload, err := json.Marshal(luckyUserJackpotCursor{ + Version: 1, ScopeHash: luckyUserJackpotCursorScope(profile), OccurredAtMS: hit.OccurredAtMS, + SourceType: hit.SourceType, DrawID: hit.DrawID, + }) + if err != nil { + return "", err + } + return base64.RawURLEncoding.EncodeToString(payload), nil +} + +func decodeLuckyUserJackpotCursor(raw string, profile domain.UserProfile) (luckyUserJackpotCursor, error) { + payload, err := base64.RawURLEncoding.DecodeString(strings.TrimSpace(raw)) + if err != nil { + return luckyUserJackpotCursor{}, xerr.New(xerr.InvalidArgument, "invalid lucky gift jackpot cursor") + } + var cursor luckyUserJackpotCursor + if json.Unmarshal(payload, &cursor) != nil || cursor.Version != 1 || + cursor.ScopeHash != luckyUserJackpotCursorScope(profile) || cursor.OccurredAtMS < 0 || + cursor.SourceType == "" || len(cursor.SourceType) > 24 || cursor.DrawID == "" || len(cursor.DrawID) > 128 { + return luckyUserJackpotCursor{}, xerr.New(xerr.InvalidArgument, "invalid or mismatched lucky gift jackpot cursor") + } + return cursor, nil +} + +func luckyUserProfileJackpotReasonSummary(mechanism string) string { + switch mechanism { + case domain.UserProfileHitRTPCompensation: + return "用户满足补偿大奖的 RTP、成熟度、每日次数和奖池支付条件,随后在独立大奖奖档中命中。" + case domain.UserProfileHitSpendMilestone: + return "用户已有累计消费大奖资格,RTP、每日次数和奖池支付条件通过,本次命中并消耗一个资格。" + default: + return "本次由独立大奖路径命中;下方条件保留了开奖当刻的真实判断结果。" + } +} + +// RefreshLuckyGiftUserProfiles 用三个独立游标回灌内部逐抽、外部逐抽和外部旧聚合事实。 +// 游标从 0 开始且每批有上限;运营查询永远只读画像表,不会因为历史回灌扫描事实表。 +func (r *Repository) RefreshLuckyGiftUserProfiles(ctx context.Context, nowMS int64, batchSize int) (int, error) { + if r == nil || r.db == nil { + return 0, xerr.New(xerr.Unavailable, "mysql repository is not configured") + } + if batchSize <= 0 { + batchSize = 5000 + } + if batchSize > 50000 { + batchSize = 50000 + } + apps, err := r.listLuckyGiftUserProfileSourceApps(ctx) + if err != nil { + return 0, err + } + if app := strings.TrimSpace(appcode.FromContext(ctx)); app != "" { + apps[appcode.Normalize(app)] = struct{}{} + } + appCodes := make([]string, 0, len(apps)) + for app := range apps { + appCodes = append(appCodes, app) + } + sort.Strings(appCodes) + processed := 0 + for _, app := range appCodes { + count, err := r.refreshLuckyGiftUserProfileInternal(ctx, app, nowMS, batchSize) + if err != nil { + return processed, err + } + processed += count + count, err = r.refreshLuckyGiftUserProfileExternalItems(ctx, app, nowMS, batchSize) + if err != nil { + return processed, err + } + processed += count + count, err = r.refreshLuckyGiftUserProfileExternalLegacy(ctx, app, nowMS, batchSize) + if err != nil { + return processed, err + } + processed += count + } + return processed, nil +} + +func (r *Repository) listLuckyGiftUserProfileSourceApps(ctx context.Context) (map[string]struct{}, error) { + rows, err := r.db.QueryContext(ctx, ` + SELECT app_code FROM lucky_gift_rule_versions GROUP BY app_code + UNION + SELECT app_code FROM lucky_gift_user_profiles GROUP BY app_code + UNION + SELECT app_code FROM lucky_draw_pool_stat_cursors + WHERE cursor_name IN (?, ?, ?) + GROUP BY app_code`, luckyUserProfileCursorInternal, luckyUserProfileCursorExternalItem, luckyUserProfileCursorExternalLegacy) + if err != nil { + return nil, err + } + defer rows.Close() + apps := make(map[string]struct{}) + for rows.Next() { + var app string + if err := rows.Scan(&app); err != nil { + return nil, err + } + if app = appcode.Normalize(app); app != "" { + apps[app] = struct{}{} + } + } + return apps, rows.Err() +} + +func (r *Repository) lockLuckyGiftUserProfileCursor(ctx context.Context, tx *sql.Tx, appCode, cursorName string, nowMS int64) (luckyDrawStatsCursor, error) { + if _, err := tx.ExecContext(ctx, ` + INSERT IGNORE INTO lucky_draw_pool_stat_cursors ( + app_code, cursor_name, last_draw_created_at_ms, last_draw_id, created_at_ms, updated_at_ms + ) VALUES (?, ?, 0, '', ?, ?)`, appCode, cursorName, nowMS, nowMS); err != nil { + return luckyDrawStatsCursor{}, err + } + var cursor luckyDrawStatsCursor + if err := tx.QueryRowContext(ctx, ` + SELECT last_draw_created_at_ms, last_draw_id + FROM lucky_draw_pool_stat_cursors + WHERE app_code = ? AND cursor_name = ? + FOR UPDATE`, appCode, cursorName).Scan(&cursor.LastDrawCreatedAtMS, &cursor.LastDrawID); err != nil { + return luckyDrawStatsCursor{}, err + } + return cursor, nil +} + +func (r *Repository) advanceLuckyGiftUserProfileCursor(ctx context.Context, tx *sql.Tx, appCode, cursorName string, cursor luckyDrawStatsCursor, nowMS int64) error { + _, err := tx.ExecContext(ctx, ` + UPDATE lucky_draw_pool_stat_cursors + SET last_draw_created_at_ms = ?, last_draw_id = ?, updated_at_ms = ? + WHERE app_code = ? AND cursor_name = ?`, cursor.LastDrawCreatedAtMS, cursor.LastDrawID, nowMS, appCode, cursorName) + return err +} + +func (r *Repository) refreshLuckyGiftUserProfileInternal(ctx context.Context, appCode string, nowMS int64, batchSize int) (int, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return 0, err + } + defer func() { _ = tx.Rollback() }() + cursor, err := r.lockLuckyGiftUserProfileCursor(ctx, tx, appCode, luckyUserProfileCursorInternal, nowMS) + if err != nil { + return 0, err + } + rows, err := tx.QueryContext(ctx, ` + SELECT draw_id, command_id, user_id, pool_id, rule_version, experience_pool, selected_tier_id, + coin_spent, base_reward_coins, effective_reward_coins, high_multiplier, + candidate_tiers_json, reward_status, paid_at_ms, created_at_ms + FROM lucky_draw_records FORCE INDEX (idx_lucky_draw_created) + WHERE app_code = ? AND (created_at_ms > ? OR (created_at_ms = ? AND draw_id > ?)) + ORDER BY created_at_ms ASC, draw_id ASC + LIMIT ?`, appCode, cursor.LastDrawCreatedAtMS, cursor.LastDrawCreatedAtMS, cursor.LastDrawID, batchSize) + if err != nil { + return 0, err + } + facts := make([]luckyUserProfileFact, 0, batchSize) + last := cursor + for rows.Next() { + var fact luckyUserProfileFact + var userID int64 + var highMultiplier bool + var snapshot []byte + if err := rows.Scan(&fact.SourceDrawID, &fact.RequestID, &userID, &fact.PoolID, &fact.RuleVersion, + &fact.Stage, &fact.TierID, &fact.WagerCoins, &fact.BaseRewardCoins, &fact.PayoutCoins, + &highMultiplier, &snapshot, &fact.RewardStatus, &fact.OccurredAtMS, &fact.CreatedAtMS); err != nil { + _ = rows.Close() + return 0, err + } + fact.AppCode = appCode + fact.SourceType = luckyUserProfileSourceInternal + fact.IdentityType = domain.UserProfileIdentityInternal + fact.IdentityKey = strconv.FormatInt(userID, 10) + fact.StrategyUserID = userID + fact.InternalUserID = userID + fact.DrawCount = 1 + fact.HighOrdinary = highMultiplier + fact.MultiplierPPM = luckyUserProfileMultiplierFromSnapshot(snapshot) + fact = luckyUserProfileFactFromSnapshot(fact, snapshot) + facts = append(facts, fact) + last = luckyDrawStatsCursor{LastDrawCreatedAtMS: fact.CreatedAtMS, LastDrawID: fact.SourceDrawID} + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return 0, err + } + if err := rows.Close(); err != nil { + return 0, err + } + if err := r.persistLuckyGiftUserProfileFacts(ctx, tx, facts, nowMS); err != nil { + return 0, err + } + if len(facts) > 0 { + if err := r.advanceLuckyGiftUserProfileCursor(ctx, tx, appCode, luckyUserProfileCursorInternal, last, nowMS); err != nil { + return 0, err + } + } + if err := tx.Commit(); err != nil { + return 0, err + } + return len(facts), nil +} + +func luckyUserProfileMultiplierFromSnapshot(snapshot []byte) int64 { + var data struct { + MultiplierPPM int64 `json:"multiplier_ppm"` + } + _ = json.Unmarshal(snapshot, &data) + return data.MultiplierPPM +} + +func (r *Repository) refreshLuckyGiftUserProfileExternalItems(ctx context.Context, appCode string, nowMS int64, batchSize int) (int, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return 0, err + } + defer func() { _ = tx.Rollback() }() + cursor, err := r.lockLuckyGiftUserProfileCursor(ctx, tx, appCode, luckyUserProfileCursorExternalItem, nowMS) + if err != nil { + return 0, err + } + rows, err := tx.QueryContext(ctx, ` + SELECT draw_id, request_id, external_user_id, pool_id, rule_version, stage, selected_tier_id, + unit_amount, reward_amount, multiplier_ppm, candidate_snapshot_json, reward_status, + paid_at_ms, created_at_ms + FROM external_lucky_gift_draw_items FORCE INDEX (idx_external_lucky_item_profile_cursor) + WHERE app_code = ? AND (created_at_ms > ? OR (created_at_ms = ? AND draw_id > ?)) + ORDER BY created_at_ms ASC, draw_id ASC + LIMIT ?`, appCode, cursor.LastDrawCreatedAtMS, cursor.LastDrawCreatedAtMS, cursor.LastDrawID, batchSize) + if err != nil { + return 0, err + } + facts := make([]luckyUserProfileFact, 0, batchSize) + last := cursor + for rows.Next() { + var fact luckyUserProfileFact + var snapshot []byte + if err := rows.Scan(&fact.SourceDrawID, &fact.RequestID, &fact.ExternalUserID, &fact.PoolID, + &fact.RuleVersion, &fact.Stage, &fact.TierID, &fact.WagerCoins, &fact.PayoutCoins, + &fact.MultiplierPPM, &snapshot, &fact.RewardStatus, &fact.OccurredAtMS, &fact.CreatedAtMS); err != nil { + _ = rows.Close() + return 0, err + } + fact.AppCode = appCode + fact.SourceType = luckyUserProfileSourceExternalItem + fact.IdentityType = domain.UserProfileIdentityExternal + fact.IdentityKey = fact.ExternalUserID + fact.StrategyUserID = externalLuckyUserID(appCode, fact.ExternalUserID) + fact.DrawCount = 1 + fact.BaseRewardCoins = fact.PayoutCoins + fact = luckyUserProfileFactFromSnapshot(fact, snapshot) + facts = append(facts, fact) + last = luckyDrawStatsCursor{LastDrawCreatedAtMS: fact.CreatedAtMS, LastDrawID: fact.SourceDrawID} + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return 0, err + } + if err := rows.Close(); err != nil { + return 0, err + } + if err := r.persistLuckyGiftUserProfileFacts(ctx, tx, facts, nowMS); err != nil { + return 0, err + } + if len(facts) > 0 { + if err := r.advanceLuckyGiftUserProfileCursor(ctx, tx, appCode, luckyUserProfileCursorExternalItem, last, nowMS); err != nil { + return 0, err + } + } + if err := tx.Commit(); err != nil { + return 0, err + } + return len(facts), nil +} + +func (r *Repository) refreshLuckyGiftUserProfileExternalLegacy(ctx context.Context, appCode string, nowMS int64, batchSize int) (int, error) { + tx, err := r.db.BeginTx(ctx, nil) + if err != nil { + return 0, err + } + defer func() { _ = tx.Rollback() }() + cursor, err := r.lockLuckyGiftUserProfileCursor(ctx, tx, appCode, luckyUserProfileCursorExternalLegacy, nowMS) + if err != nil { + return 0, err + } + rows, err := tx.QueryContext(ctx, ` + SELECT d.draw_id, d.request_id, d.external_user_id, d.pool_id, d.gift_count, d.total_amount, + d.reward_amount, d.multiplier_ppm, d.reward_status, d.rule_version, d.selected_tier_id, + d.paid_at_ms, d.created_at_ms, COALESCE(rv.strategy_version, 'fixed_v2'), + EXISTS(SELECT 1 FROM external_lucky_gift_draw_items i + WHERE i.app_code = d.app_code AND i.request_id = d.request_id LIMIT 1) + FROM external_lucky_gift_draws d FORCE INDEX (idx_external_lucky_profile_cursor) + LEFT JOIN lucky_gift_rule_versions rv + ON rv.app_code = d.app_code AND rv.pool_id = d.pool_id AND rv.rule_version = d.rule_version + WHERE d.app_code = ? AND (d.created_at_ms > ? OR (d.created_at_ms = ? AND d.draw_id > ?)) + ORDER BY d.created_at_ms ASC, d.draw_id ASC + LIMIT ?`, appCode, cursor.LastDrawCreatedAtMS, cursor.LastDrawCreatedAtMS, cursor.LastDrawID, batchSize) + if err != nil { + return 0, err + } + facts := make([]luckyUserProfileFact, 0, batchSize) + last := cursor + rowCount := 0 + for rows.Next() { + var fact luckyUserProfileFact + var hasItems bool + if err := rows.Scan(&fact.SourceDrawID, &fact.RequestID, &fact.ExternalUserID, &fact.PoolID, + &fact.DrawCount, &fact.WagerCoins, &fact.PayoutCoins, &fact.MultiplierPPM, &fact.RewardStatus, + &fact.RuleVersion, &fact.TierID, &fact.OccurredAtMS, &fact.CreatedAtMS, &fact.StrategyVersion, &hasItems); err != nil { + _ = rows.Close() + return 0, err + } + rowCount++ + last = luckyDrawStatsCursor{LastDrawCreatedAtMS: fact.CreatedAtMS, LastDrawID: fact.SourceDrawID} + if hasItems { + continue + } + fact.AppCode = appCode + fact.SourceType = luckyUserProfileSourceExternalLegacy + fact.IdentityType = domain.UserProfileIdentityExternal + fact.IdentityKey = fact.ExternalUserID + fact.StrategyUserID = externalLuckyUserID(appCode, fact.ExternalUserID) + if fact.StrategyVersion == domain.StrategyFixedV2 { + // fixed_v2 的外部聚合行无论 gift_count 多大都只随机一次;dynamic_v3 才是逐份开奖。 + // dynamic_v3 正常会被 item 游标消费,这个分支只为兼容极早期缺 item 的聚合事实。 + fact.DrawCount = 1 + } + fact.BaseRewardCoins = fact.PayoutCoins + fact.HitType = luckyUserProfileHitType(fact.PayoutCoins, "", nil, nil) + // 旧外部聚合事实没有 high_multiplier 审计列,保持 false;不能自行发明倍率阈值补猜。 + fact.HighOrdinary = false + facts = append(facts, fact) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return 0, err + } + if err := rows.Close(); err != nil { + return 0, err + } + if err := r.persistLuckyGiftUserProfileFacts(ctx, tx, facts, nowMS); err != nil { + return 0, err + } + if rowCount > 0 { + if err := r.advanceLuckyGiftUserProfileCursor(ctx, tx, appCode, luckyUserProfileCursorExternalLegacy, last, nowMS); err != nil { + return 0, err + } + } + if err := tx.Commit(); err != nil { + return 0, err + } + return rowCount, nil +} + +type luckyUserProfileRewardStatusDelta struct { + Pending int64 + Granted int64 + Failed int64 +} + +// migrateLuckyGiftUserProfileRewardStatuses 与 lucky_draw_records 的终态更新共用事务。 +// 它先锁画像事件并记住旧状态,再原子迁移三个计数,避免初写 pending 的中奖在画像中永久停留。 +func (r *Repository) migrateLuckyGiftUserProfileRewardStatuses(ctx context.Context, tx *sql.Tx, appCode string, drawIDs []string, status string, onlyPending bool, nowMS int64) error { + if len(drawIDs) == 0 { + return nil + } + args := []any{appCode} + for _, drawID := range drawIDs { + args = append(args, drawID) + } + query := ` + SELECT pool_id, strategy_version, identity_type, identity_key, reward_status, draw_count + FROM lucky_gift_user_profile_events + WHERE app_code = ? AND source_type = 'internal' + AND source_draw_id IN (` + luckySQLPlaceholders(len(drawIDs)) + `) + AND reward_status <> ?` + args = append(args, status) + if onlyPending { + query += ` AND reward_status = ?` + args = append(args, domain.StatusPending) + } + query += ` FOR UPDATE` + rows, err := tx.QueryContext(ctx, query, args...) + if err != nil { + return err + } + deltas := make(map[luckyUserProfileKey]*luckyUserProfileRewardStatusDelta) + for rows.Next() { + var key luckyUserProfileKey + var oldStatus string + var drawCount int64 + key.AppCode = appCode + if err := rows.Scan(&key.PoolID, &key.StrategyVersion, &key.IdentityType, &key.IdentityKey, &oldStatus, &drawCount); err != nil { + _ = rows.Close() + return err + } + delta := deltas[key] + if delta == nil { + delta = &luckyUserProfileRewardStatusDelta{} + deltas[key] = delta + } + luckyUserProfileAddRewardStatusDelta(delta, oldStatus, -drawCount) + luckyUserProfileAddRewardStatusDelta(delta, status, drawCount) + } + if err := rows.Err(); err != nil { + _ = rows.Close() + return err + } + if err := rows.Close(); err != nil { + return err + } + if len(deltas) == 0 { + return nil + } + updateArgs := []any{status, appCode} + for _, drawID := range drawIDs { + updateArgs = append(updateArgs, drawID) + } + updateQuery := ` + UPDATE lucky_gift_user_profile_events + SET reward_status = ? + WHERE app_code = ? AND source_type = 'internal' + AND source_draw_id IN (` + luckySQLPlaceholders(len(drawIDs)) + `) + AND reward_status <> ?` + updateArgs = append(updateArgs, status) + if onlyPending { + updateQuery += ` AND reward_status = ?` + updateArgs = append(updateArgs, domain.StatusPending) + } + if _, err := tx.ExecContext(ctx, updateQuery, updateArgs...); err != nil { + return err + } + for key, delta := range deltas { + if _, err := tx.ExecContext(ctx, ` + UPDATE lucky_gift_user_profiles + SET pending_draw_count = GREATEST(pending_draw_count + ?, 0), + granted_draw_count = GREATEST(granted_draw_count + ?, 0), + failed_draw_count = GREATEST(failed_draw_count + ?, 0), + aggregated_at_ms = GREATEST(aggregated_at_ms, ?), updated_at_ms = ? + WHERE app_code = ? AND pool_id = ? AND strategy_version = ? AND identity_type = ? AND identity_key = ?`, + delta.Pending, delta.Granted, delta.Failed, nowMS, nowMS, + key.AppCode, key.PoolID, key.StrategyVersion, key.IdentityType, key.IdentityKey); err != nil { + return err + } + } + return nil +} + +func luckyUserProfileAddRewardStatusDelta(delta *luckyUserProfileRewardStatusDelta, status string, value int64) { + if delta == nil { + return + } + switch status { + case domain.StatusPending: + delta.Pending += value + case domain.StatusFailed: + delta.Failed += value + default: + delta.Granted += value + } +} diff --git a/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go b/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go index 6b5d6523..b86a3b2a 100644 --- a/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go +++ b/services/lucky-gift-service/internal/transport/grpc/lucky_gift_server.go @@ -224,6 +224,93 @@ func (s *AdminLuckyGiftServer) GetLuckyGiftDrawSummary(ctx context.Context, req return &luckygiftv1.GetLuckyGiftDrawSummaryResponse{Summary: luckyDrawSummaryToProto(summary)}, nil } +func (s *AdminLuckyGiftServer) ListLuckyGiftUserProfiles(ctx context.Context, req *luckygiftv1.ListLuckyGiftUserProfilesRequest) (*luckygiftv1.ListLuckyGiftUserProfilesResponse, error) { + appCode := strings.TrimSpace(req.GetMeta().GetAppCode()) + ctx = appcode.WithContext(ctx, appCode) + profiles, total, nextCursor, hasMore, snapshotAtMS, err := s.svc.ListUserProfiles(ctx, domain.UserProfileQuery{ + AppCode: appCode, + PoolID: req.GetPoolId(), + IdentityType: req.GetIdentityType(), + InternalUserID: req.GetInternalUserId(), + ExternalUserID: req.GetExternalUserId(), + Stage: req.GetStage(), + Status: req.GetStatus(), + SortBy: req.GetSortBy(), + SortDirection: req.GetSortDirection(), + Cursor: req.GetCursor(), + Page: req.GetPage(), + PageSize: req.GetPageSize(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + resp := &luckygiftv1.ListLuckyGiftUserProfilesResponse{ + Profiles: make([]*luckygiftv1.LuckyGiftUserProfile, 0, len(profiles)), + Total: total, + SnapshotAtMs: snapshotAtMS, + NextCursor: nextCursor, + HasMore: hasMore, + } + for _, profile := range profiles { + resp.Profiles = append(resp.Profiles, luckyUserProfileToProto(profile)) + } + return resp, nil +} + +func (s *AdminLuckyGiftServer) GetLuckyGiftUserProfile(ctx context.Context, req *luckygiftv1.GetLuckyGiftUserProfileRequest) (*luckygiftv1.GetLuckyGiftUserProfileResponse, error) { + appCode := strings.TrimSpace(req.GetMeta().GetAppCode()) + ctx = appcode.WithContext(ctx, appCode) + detail, snapshotAtMS, err := s.svc.GetUserProfile(ctx, domain.UserProfileQuery{ + AppCode: appCode, + PoolID: req.GetPoolId(), + IdentityType: req.GetIdentityType(), + InternalUserID: req.GetInternalUserId(), + ExternalUserID: req.GetExternalUserId(), + JackpotPage: req.GetJackpotPage(), + JackpotPageSize: req.GetJackpotPageSize(), + JackpotCursor: req.GetJackpotCursor(), + }) + if err != nil { + return nil, xerr.ToGRPCError(err) + } + resp := &luckygiftv1.GetLuckyGiftUserProfileResponse{ + Profile: luckyUserProfileToProto(detail.Profile), + MultiplierDistribution: make([]*luckygiftv1.LuckyGiftUserMultiplierStat, 0, len(detail.MultiplierDistribution)), + JackpotHits: make([]*luckygiftv1.LuckyGiftUserJackpotHit, 0, len(detail.JackpotHits)), + JackpotHitTotal: detail.JackpotHitTotal, + SnapshotAtMs: snapshotAtMS, + JackpotPage: detail.JackpotPage, + JackpotPageSize: detail.JackpotPageSize, + NextJackpotCursor: detail.NextJackpotCursor, + JackpotHasMore: detail.JackpotHasMore, + } + for _, stat := range detail.MultiplierDistribution { + resp.MultiplierDistribution = append(resp.MultiplierDistribution, &luckygiftv1.LuckyGiftUserMultiplierStat{ + HitType: stat.HitType, MultiplierPpm: stat.MultiplierPPM, DrawCount: stat.DrawCount, + WagerCoins: stat.WagerCoins, PayoutCoins: stat.PayoutCoins, + }) + } + for _, hit := range detail.JackpotHits { + item := &luckygiftv1.LuckyGiftUserJackpotHit{ + DrawId: hit.DrawID, RequestId: hit.RequestID, RuleVersion: hit.RuleVersion, Stage: hit.Stage, + EventKey: hit.SourceType + ":" + hit.DrawID, + Mechanism: hit.Mechanism, ReasonCode: hit.ReasonCode, ReasonSummary: hit.ReasonSummary, + TierId: hit.TierID, MultiplierPpm: hit.MultiplierPPM, WagerCoins: hit.WagerCoins, + PayoutCoins: hit.PayoutCoins, OccurredAtMs: hit.OccurredAtMS, + Conditions: make([]*luckygiftv1.LuckyGiftUserJackpotCondition, 0, len(hit.Conditions)), + } + for _, condition := range hit.Conditions { + item.Conditions = append(item.Conditions, &luckygiftv1.LuckyGiftUserJackpotCondition{ + Name: condition.Name, Numerator: condition.Numerator, Denominator: condition.Denominator, + RatioPpm: condition.RatioPPM, LimitPpm: condition.LimitPPM, FactorPpm: condition.FactorPPM, + Passed: condition.Passed, Reason: condition.Reason, + }) + } + resp.JackpotHits = append(resp.JackpotHits, item) + } + return resp, nil +} + func (s *AdminLuckyGiftServer) ListLuckyGiftPoolBalances(ctx context.Context, req *luckygiftv1.ListLuckyGiftPoolBalancesRequest) (*luckygiftv1.ListLuckyGiftPoolBalancesResponse, error) { appCode := req.GetMeta().GetAppCode() if appCode != "" { @@ -496,6 +583,41 @@ func luckyPoolBalanceToProto(pool domain.PoolBalance) *luckygiftv1.LuckyGiftPool } } +func luckyUserProfileWindowToProto(window domain.UserProfileWindow) *luckygiftv1.LuckyGiftUserProfileWindow { + return &luckygiftv1.LuckyGiftUserProfileWindow{ + Draws: window.Draws, WagerCoins: window.WagerCoins, PayoutCoins: window.PayoutCoins, + NetCoins: window.NetCoins, RtpPpm: window.RTPPPM, HasRtp: window.HasRTP, + } +} + +func luckyUserProfileToProto(profile domain.UserProfile) *luckygiftv1.LuckyGiftUserProfile { + // StrategyUserID 可能是 external_user_id 的稳定哈希,只允许仓储层关联 lucky_user_states; + // transport 显式逐字段映射且没有该字段,避免未来通过反射或 JSON 误下发。 + return &luckygiftv1.LuckyGiftUserProfile{ + AppCode: profile.AppCode, PoolId: profile.PoolID, IdentityType: profile.IdentityType, + InternalUserId: profile.InternalUserID, ExternalUserId: profile.ExternalUserID, + RuleVersion: profile.RuleVersion, StrategyVersion: profile.StrategyVersion, Stage: profile.Stage, + PaidDraws: profile.PaidDraws, EquivalentDraws: profile.EquivalentDraws, LossStreak: profile.LossStreak, + PendingSpendJackpotTokens: profile.PendingSpendJackpotTokens, + GuaranteeDrawsRemaining: profile.GuaranteeDrawsRemaining, + DownweightActive: profile.DownweightActive, DownweightFactorPpm: profile.DownweightFactorPPM, + User_24HRtpThresholdPpm: profile.User24HourRTPThresholdPPM, + OneHour: luckyUserProfileWindowToProto(profile.OneHour), + TwentyFourHours: luckyUserProfileWindowToProto(profile.TwentyFourHour), + FortyEightHours: luckyUserProfileWindowToProto(profile.FortyEightHour), + Lifetime: luckyUserProfileWindowToProto(profile.Lifetime), + BaseRewardCoins: profile.BaseRewardCoins, OrdinaryWinCount: profile.OrdinaryWinCount, + HighMultiplierOrdinaryWinCount: profile.HighMultiplierOrdinaryWinCount, + RtpCompensationJackpotCount: profile.RTPCompensationJackpotCount, + CumulativeSpendJackpotCount: profile.CumulativeSpendJackpotCount, + GuaranteeHitCount: profile.GuaranteeHitCount, ZeroDrawCount: profile.ZeroDrawCount, + GrantedDrawCount: profile.GrantedDrawCount, PendingDrawCount: profile.PendingDrawCount, + FailedDrawCount: profile.FailedDrawCount, MaxMultiplierPpm: profile.MaxMultiplierPPM, + MaxRewardCoins: profile.MaxRewardCoins, FirstDrawAtMs: profile.FirstDrawAtMS, + LastDrawAtMs: profile.LastDrawAtMS, AggregatedAtMs: profile.AggregatedAtMS, DataLagMs: profile.DataLagMS, + } +} + func luckyPoolAdjustmentToProto(adjustment domain.PoolAdjustment) *luckygiftv1.LuckyGiftPoolAdjustment { return &luckygiftv1.LuckyGiftPoolAdjustment{ AdjustmentId: adjustment.AdjustmentID,