package grpc import ( "context" "strings" luckygiftv1 "hyapp.local/api/proto/luckygift/v1" "hyapp/pkg/appcode" "hyapp/pkg/xerr" domain "hyapp/services/lucky-gift-service/internal/domain/luckygift" service "hyapp/services/lucky-gift-service/internal/service/luckygift" ) // LuckyGiftServer 暴露房间送礼主链路调用的幸运礼物查询和抽奖入口。 type LuckyGiftServer struct { luckygiftv1.UnimplementedLuckyGiftServiceServer svc *service.Service } func NewLuckyGiftServer(svc *service.Service) *LuckyGiftServer { return &LuckyGiftServer{svc: svc} } func (s *LuckyGiftServer) CheckLuckyGift(ctx context.Context, req *luckygiftv1.CheckLuckyGiftRequest) (*luckygiftv1.CheckLuckyGiftResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) result, err := s.svc.Check(ctx, domain.CheckCommand{ PoolID: req.GetPoolId(), GiftID: req.GetGiftId(), UserID: req.GetUserId(), RoomID: req.GetRoomId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.CheckLuckyGiftResponse{ Enabled: result.Enabled, Reason: result.Reason, PoolId: result.PoolID, GiftId: result.GiftID, GiftPrice: result.GiftPrice, RuleVersion: result.RuleVersion, StrategyVersion: result.StrategyVersion, TargetRtpPpm: result.TargetRTPPPM, ExperiencePool: result.ExperiencePool, }, nil } func (s *LuckyGiftServer) ExecuteLuckyGiftDraw(ctx context.Context, req *luckygiftv1.ExecuteLuckyGiftDrawRequest) (*luckygiftv1.ExecuteLuckyGiftDrawResponse, error) { meta := req.GetLuckyGift() ctx = appcode.WithContext(ctx, meta.GetMeta().GetAppCode()) result, err := s.svc.Draw(ctx, luckyDrawCommandFromProto(meta)) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.ExecuteLuckyGiftDrawResponse{Result: luckyDrawResultToProto(result)}, nil } func (s *LuckyGiftServer) BatchExecuteLuckyGiftDraw(ctx context.Context, req *luckygiftv1.BatchExecuteLuckyGiftDrawRequest) (*luckygiftv1.BatchExecuteLuckyGiftDrawResponse, error) { if len(req.GetLuckyGifts()) == 0 { return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift batch draw commands are required")) } appCode := strings.TrimSpace(req.GetLuckyGifts()[0].GetMeta().GetAppCode()) if appCode == "" { return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift batch app_code is required")) } ctx = appcode.WithContext(ctx, appCode) cmds := make([]domain.DrawCommand, 0, len(req.GetLuckyGifts())) for _, meta := range req.GetLuckyGifts() { // 一个批次共享同一条房间送礼命令和同一个 app 账本;混入其他 app_code 会污染第一条 app 的 RTP 和单池账本。 if strings.TrimSpace(meta.GetMeta().GetAppCode()) != appCode { return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift batch app_code mismatch")) } cmds = append(cmds, luckyDrawCommandFromProto(meta)) } results, err := s.svc.DrawBatch(ctx, cmds) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &luckygiftv1.BatchExecuteLuckyGiftDrawResponse{Results: make([]*luckygiftv1.LuckyGiftDrawResult, 0, len(results))} for _, result := range results { resp.Results = append(resp.Results, luckyDrawResultToProto(result)) } return resp, nil } func (s *LuckyGiftServer) ExecuteExternalGiftDraw(ctx context.Context, req *luckygiftv1.ExternalGiftDrawRequest) (*luckygiftv1.ExternalGiftDrawResponse, error) { ctx = appcode.WithContext(ctx, req.GetAppCode()) result, err := s.svc.ExecuteExternalDraw(ctx, domain.ExternalDrawCommand{ AppCode: req.GetAppCode(), ExternalUserID: req.GetExternalUserId(), DeviceID: req.GetDeviceId(), RequestID: req.GetRequestId(), GiftCount: req.GetGiftCount(), UnitAmount: req.GetUnitAmount(), TotalAmount: req.GetTotalAmount(), Currency: req.GetCurrency(), PaidAtMS: req.GetPaidAtMs(), MetadataJSON: req.GetMetadataJson(), PoolID: req.GetPoolId(), UserRegisteredAtMS: req.GetUserRegisteredAtMs(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return externalDrawResultToProto(result), nil } func luckyDrawCommandFromProto(meta *luckygiftv1.LuckyGiftMeta) domain.DrawCommand { return domain.DrawCommand{ CommandID: meta.GetCommandId(), PoolID: meta.GetPoolId(), UserID: meta.GetUserId(), TargetUserID: meta.GetTargetUserId(), DeviceID: meta.GetDeviceId(), RoomID: meta.GetRoomId(), AnchorID: meta.GetAnchorId(), GiftID: meta.GetGiftId(), GiftCount: meta.GetGiftCount(), CoinSpent: meta.GetCoinSpent(), PaidAtMS: meta.GetPaidAtMs(), UserRegisteredAtMS: meta.GetUserRegisteredAtMs(), VisibleRegionID: meta.GetVisibleRegionId(), CountryID: meta.GetCountryId(), SenderName: meta.GetSenderName(), SenderAvatar: meta.GetSenderAvatar(), SenderDisplayUserID: meta.GetSenderDisplayUserId(), SenderPrettyDisplayUserID: meta.GetSenderPrettyDisplayUserId(), // 充值分层、短时加权与主播风控只消费 wallet-service 已落账的事实, // lucky-gift-service 不跨 owner 数据库反查并重新解释同一笔交易。 Recharge7DCoins: meta.GetRecharge_7DCoins(), Recharge30DCoins: meta.GetRecharge_30DCoins(), LastRechargedAtMS: meta.GetLastRechargedAtMs(), GiftIncomeCoins: meta.GetGiftIncomeCoins(), } } // AdminLuckyGiftServer 暴露后台幸运礼物规则配置和抽奖审计入口。 type AdminLuckyGiftServer struct { luckygiftv1.UnimplementedAdminLuckyGiftServiceServer svc *service.Service } func NewAdminLuckyGiftServer(svc *service.Service) *AdminLuckyGiftServer { return &AdminLuckyGiftServer{svc: svc} } func (s *AdminLuckyGiftServer) GetLuckyGiftConfig(ctx context.Context, req *luckygiftv1.GetLuckyGiftConfigRequest) (*luckygiftv1.GetLuckyGiftConfigResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) config, err := s.svc.GetConfig(ctx, req.GetPoolId()) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.GetLuckyGiftConfigResponse{Config: luckyRuleConfigToProto(config)}, nil } func (s *AdminLuckyGiftServer) UpsertLuckyGiftConfig(ctx context.Context, req *luckygiftv1.UpsertLuckyGiftConfigRequest) (*luckygiftv1.UpsertLuckyGiftConfigResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) config := luckyRuleConfigFromProto(req.GetConfig()) config.CreatedByAdminID = req.GetOperatorAdminId() updated, err := s.svc.UpsertConfig(ctx, config) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.UpsertLuckyGiftConfigResponse{Config: luckyRuleConfigToProto(updated)}, nil } func (s *AdminLuckyGiftServer) ListLuckyGiftConfigs(ctx context.Context, req *luckygiftv1.ListLuckyGiftConfigsRequest) (*luckygiftv1.ListLuckyGiftConfigsResponse, error) { rawAppCode := req.GetMeta().GetAppCode() if rawAppCode != "" { ctx = appcode.WithContext(ctx, rawAppCode) } configs, err := s.svc.ListConfigs(ctx, rawAppCode) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &luckygiftv1.ListLuckyGiftConfigsResponse{Configs: make([]*luckygiftv1.LuckyGiftRuleConfig, 0, len(configs))} for _, config := range configs { resp.Configs = append(resp.Configs, luckyRuleConfigToProto(config)) } return resp, nil } func (s *AdminLuckyGiftServer) ListLuckyGiftDraws(ctx context.Context, req *luckygiftv1.ListLuckyGiftDrawsRequest) (*luckygiftv1.ListLuckyGiftDrawsResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) draws, total, err := s.svc.ListDraws(ctx, domain.DrawQuery{ AppCode: req.GetMeta().GetAppCode(), GiftID: req.GetGiftId(), PoolID: req.GetPoolId(), UserID: req.GetUserId(), RoomID: req.GetRoomId(), Status: req.GetStatus(), Page: req.GetPage(), PageSize: req.GetPageSize(), ExternalUserID: req.GetExternalUserId(), ExternalOnly: req.GetExternalOnly(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &luckygiftv1.ListLuckyGiftDrawsResponse{Draws: make([]*luckygiftv1.LuckyGiftDrawResult, 0, len(draws)), Total: total} for _, draw := range draws { resp.Draws = append(resp.Draws, luckyDrawResultToProto(draw)) } return resp, nil } func (s *AdminLuckyGiftServer) GetLuckyGiftDrawSummary(ctx context.Context, req *luckygiftv1.GetLuckyGiftDrawSummaryRequest) (*luckygiftv1.GetLuckyGiftDrawSummaryResponse, error) { ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) summary, err := s.svc.GetDrawSummary(ctx, domain.DrawQuery{ AppCode: req.GetMeta().GetAppCode(), GiftID: req.GetGiftId(), PoolID: req.GetPoolId(), UserID: req.GetUserId(), RoomID: req.GetRoomId(), Status: req.GetStatus(), ExternalUserID: req.GetExternalUserId(), ExternalOnly: req.GetExternalOnly(), }) if err != nil { return nil, xerr.ToGRPCError(err) } 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 != "" { ctx = appcode.WithContext(ctx, appCode) } pools, err := s.svc.ListPoolBalances(ctx, domain.PoolBalanceQuery{ AppCode: appCode, PoolID: req.GetPoolId(), StrategyVersion: req.GetStrategyVersion(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &luckygiftv1.ListLuckyGiftPoolBalancesResponse{Pools: make([]*luckygiftv1.LuckyGiftPoolBalance, 0, len(pools))} for _, pool := range pools { resp.Pools = append(resp.Pools, luckyPoolBalanceToProto(pool)) } return resp, nil } func (s *AdminLuckyGiftServer) AdjustLuckyGiftPoolBalance(ctx context.Context, req *luckygiftv1.AdjustLuckyGiftPoolBalanceRequest) (*luckygiftv1.AdjustLuckyGiftPoolBalanceResponse, error) { if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" { // 资金 RPC 必须 fail-closed;缺失 meta 不能像普通兼容查询一样静默落到默认 lalu 租户。 return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift pool adjustment app_code is required")) } ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) result, err := s.svc.AdjustPoolBalance(ctx, domain.PoolAdjustmentCommand{ PoolID: req.GetPoolId(), StrategyVersion: req.GetStrategyVersion(), AdjustmentID: req.GetAdjustmentId(), Direction: req.GetDirection(), AmountCoins: req.GetAmountCoins(), Reason: req.GetReason(), OperatorAdminID: req.GetOperatorAdminId(), RequestID: req.GetMeta().GetRequestId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.AdjustLuckyGiftPoolBalanceResponse{ Adjustment: luckyPoolAdjustmentToProto(result.Adjustment), Pool: luckyPoolBalanceToProto(result.Pool), IdempotentReplay: result.IdempotentReplay, }, nil } func (s *AdminLuckyGiftServer) CreateLuckyGiftExperiment(ctx context.Context, req *luckygiftv1.CreateLuckyGiftExperimentRequest) (*luckygiftv1.CreateLuckyGiftExperimentResponse, error) { if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" { // 实验创建会发布新配置版本并改变线上分流;缺 meta 不能静默落到默认租户。 return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required")) } ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) experiment, treatment, err := s.svc.CreateExperiment(ctx, domain.CreateExperimentCommand{ Name: req.GetName(), TreatmentConfig: luckyRuleConfigFromProto(req.GetTreatmentConfig()), TreatmentTrafficPPM: req.GetTreatmentTrafficPpm(), OperatorAdminID: req.GetOperatorAdminId(), Overrides: luckyExperimentOverridesFromProto(req.GetOverrides()), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.CreateLuckyGiftExperimentResponse{ Experiment: luckyExperimentToProto(experiment), TreatmentConfig: luckyRuleConfigToProto(treatment), }, nil } func (s *AdminLuckyGiftServer) ListLuckyGiftExperiments(ctx context.Context, req *luckygiftv1.ListLuckyGiftExperimentsRequest) (*luckygiftv1.ListLuckyGiftExperimentsResponse, error) { if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" { // 实验查询按单 app 组织;空 app_code 不能像兼容查询一样静默落到默认租户。 return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required")) } ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) experiments, err := s.svc.ListExperiments(ctx, domain.ExperimentQuery{ AppCode: req.GetMeta().GetAppCode(), PoolID: req.GetPoolId(), Status: req.GetStatus(), WithStats: req.GetWithStats(), }) if err != nil { return nil, xerr.ToGRPCError(err) } resp := &luckygiftv1.ListLuckyGiftExperimentsResponse{Experiments: make([]*luckygiftv1.LuckyGiftExperiment, 0, len(experiments))} for _, experiment := range experiments { resp.Experiments = append(resp.Experiments, luckyExperimentToProto(experiment)) } return resp, nil } func (s *AdminLuckyGiftServer) UpdateLuckyGiftExperiment(ctx context.Context, req *luckygiftv1.UpdateLuckyGiftExperimentRequest) (*luckygiftv1.UpdateLuckyGiftExperimentResponse, error) { if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" { return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required")) } ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) // 流量字段用 proto3 optional 的存在性表达"不修改";缺省 0 绝不能被解释成放量清零。 trafficPPM := int64(-1) if req.TreatmentTrafficPpm != nil { trafficPPM = req.GetTreatmentTrafficPpm() } experiment, err := s.svc.UpdateExperiment(ctx, domain.UpdateExperimentCommand{ ExperimentID: req.GetExperimentId(), TreatmentTrafficPPM: trafficPPM, Status: req.GetStatus(), OperatorAdminID: req.GetOperatorAdminId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.UpdateLuckyGiftExperimentResponse{Experiment: luckyExperimentToProto(experiment)}, nil } func (s *AdminLuckyGiftServer) SetLuckyGiftExperimentOverrides(ctx context.Context, req *luckygiftv1.SetLuckyGiftExperimentOverridesRequest) (*luckygiftv1.SetLuckyGiftExperimentOverridesResponse, error) { if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" { return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required")) } ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) experiment, err := s.svc.SetExperimentOverrides(ctx, domain.SetExperimentOverridesCommand{ ExperimentID: req.GetExperimentId(), Upserts: luckyExperimentOverridesFromProto(req.GetUpserts()), Removes: req.GetRemoves(), OperatorAdminID: req.GetOperatorAdminId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.SetLuckyGiftExperimentOverridesResponse{Experiment: luckyExperimentToProto(experiment)}, nil } func (s *AdminLuckyGiftServer) EndLuckyGiftExperiment(ctx context.Context, req *luckygiftv1.EndLuckyGiftExperimentRequest) (*luckygiftv1.EndLuckyGiftExperimentResponse, error) { if req == nil || strings.TrimSpace(req.GetMeta().GetAppCode()) == "" { return nil, xerr.ToGRPCError(xerr.New(xerr.InvalidArgument, "lucky gift experiment app_code is required")) } ctx = appcode.WithContext(ctx, req.GetMeta().GetAppCode()) experiment, finalConfig, err := s.svc.EndExperiment(ctx, domain.EndExperimentCommand{ ExperimentID: req.GetExperimentId(), WinnerArm: req.GetWinnerArm(), OperatorAdminID: req.GetOperatorAdminId(), }) if err != nil { return nil, xerr.ToGRPCError(err) } return &luckygiftv1.EndLuckyGiftExperimentResponse{ Experiment: luckyExperimentToProto(experiment), FinalConfig: luckyRuleConfigToProto(finalConfig), }, nil } func luckyExperimentOverridesFromProto(overrides []*luckygiftv1.LuckyGiftExperimentOverride) []domain.ExperimentOverride { out := make([]domain.ExperimentOverride, 0, len(overrides)) for _, override := range overrides { if override == nil { continue } out = append(out, domain.ExperimentOverride{UserKey: override.GetUserKey(), Arm: override.GetArm()}) } return out } func luckyExperimentToProto(experiment domain.Experiment) *luckygiftv1.LuckyGiftExperiment { out := &luckygiftv1.LuckyGiftExperiment{ AppCode: experiment.AppCode, PoolId: experiment.PoolID, ExperimentId: experiment.ExperimentID, Name: experiment.Name, ControlRuleVersion: experiment.ControlRuleVersion, TreatmentRuleVersion: experiment.TreatmentRuleVersion, TreatmentTrafficPpm: experiment.TreatmentTrafficPPM, BucketSalt: experiment.BucketSalt, Status: experiment.Status, WinnerArm: experiment.WinnerArm, WinnerRuleVersion: experiment.WinnerRuleVersion, FinalRuleVersion: experiment.FinalRuleVersion, CreatedByAdminId: experiment.CreatedByAdminID, StartedAtMs: experiment.StartedAtMS, EndedAtMs: experiment.EndedAtMS, UpdatedAtMs: experiment.UpdatedAtMS, } for _, override := range experiment.Overrides { out.Overrides = append(out.Overrides, &luckygiftv1.LuckyGiftExperimentOverride{UserKey: override.UserKey, Arm: override.Arm}) } for _, stats := range experiment.ArmStats { out.ArmStats = append(out.ArmStats, &luckygiftv1.LuckyGiftExperimentArmStats{ Arm: stats.Arm, RuleVersion: stats.RuleVersion, StrategyVersion: stats.StrategyVersion, TotalDraws: stats.TotalDraws, UniqueUsers: stats.UniqueUsers, TotalSpentCoins: stats.TotalSpentCoins, TotalRewardCoins: stats.TotalRewardCoins, ActualRtpPpm: stats.ActualRTPPPM, }) } return out } func luckyRuleConfigFromProto(config *luckygiftv1.LuckyGiftRuleConfig) domain.RuleConfig { if config == nil { return domain.RuleConfig{} } stages := make([]domain.RuleStage, 0, len(config.GetStages())) for _, stage := range config.GetStages() { tiers := make([]domain.RuleTier, 0, len(stage.GetTiers())) for _, tier := range stage.GetTiers() { tiers = append(tiers, domain.RuleTier{ Stage: tier.GetStage(), TierID: tier.GetTierId(), MultiplierPPM: tier.GetMultiplierPpm(), BaseWeightPPM: tier.GetBaseWeightPpm(), HighWaterOnly: tier.GetHighWaterOnly(), Enabled: tier.GetEnabled(), }) } stages = append(stages, domain.RuleStage{ Stage: stage.GetStage(), MinRecharge7DCoins: stage.GetMinRecharge_7DCoins(), MinRecharge30DCoins: stage.GetMinRecharge_30DCoins(), Tiers: tiers, }) } return domain.RuleConfig{ AppCode: config.GetAppCode(), PoolID: config.GetPoolId(), RuleVersion: config.GetRuleVersion(), Enabled: config.GetEnabled(), TargetRTPPPM: config.GetTargetRtpPpm(), PoolRatePPM: config.GetPoolRatePpm(), SettlementWindowWager: config.GetSettlementWindowWager(), ControlBandPPM: config.GetControlBandPpm(), GiftPriceReference: config.GetGiftPriceReference(), NoviceMaxEquivalentDraws: config.GetNoviceMaxEquivalentDraws(), NormalMaxEquivalentDraws: config.GetNormalMaxEquivalentDraws(), EffectiveFromMS: config.GetEffectiveFromMs(), CreatedByAdminID: config.GetCreatedByAdminId(), CreatedAtMS: config.GetCreatedAtMs(), StrategyVersion: config.GetStrategyVersion(), ProfitRatePPM: config.GetProfitRatePpm(), AnchorRatePPM: config.GetAnchorRatePpm(), InitialPoolCoins: config.GetInitialPoolCoins(), LossStreakGuarantee: config.GetLossStreakGuarantee(), LowWatermarkCoins: config.GetLowWatermarkCoins(), LowWaterNonzeroFactorPPM: config.GetLowWaterNonzeroFactorPpm(), HighWatermarkCoins: config.GetHighWatermarkCoins(), HighWaterNonzeroFactorPPM: config.GetHighWaterNonzeroFactorPpm(), V2HighWaterBoostThresholdCoins: config.GetV2HighWaterBoostThresholdCoins(), V2HighWaterBoostFactorPPM: config.GetV2HighWaterBoostFactorPpm(), V2HighWaterBoostRecoverCoins: config.GetV2HighWaterBoostRecoverCoins(), RechargeBoostWindowMS: config.GetRechargeBoostWindowMs(), RechargeBoostFactorPPM: config.GetRechargeBoostFactorPpm(), User24HourRTPThresholdPPM: config.GetUser_24HRtpThresholdPpm(), User24HourOrdinaryWinFactorPPM: config.GetUser_24HOrdinaryWinFactorPpm(), JackpotMultiplierPPMs: append([]int64(nil), config.GetJackpotMultiplierPpms()...), JackpotGlobalRTPMaxPPM: config.GetJackpotGlobalRtpMaxPpm(), JackpotUserRoundRTPMaxPPM: config.GetJackpotUserRoundRtpMaxPpm(), JackpotUser48hRTPMaxPPM: config.GetJackpotUser_48HRtpMaxPpm(), JackpotSpendThresholdCoins: config.GetJackpotSpendThresholdCoins(), MaxJackpotHitsPerUserDay: config.GetMaxJackpotHitsPerUserDay(), MaxSinglePayout: config.GetMaxSinglePayout(), UserHourlyPayoutCap: config.GetUserHourlyPayoutCap(), UserDailyPayoutCap: config.GetUserDailyPayoutCap(), DeviceDailyPayoutCap: config.GetDeviceDailyPayoutCap(), RoomHourlyPayoutCap: config.GetRoomHourlyPayoutCap(), AnchorDailyPayoutCap: config.GetAnchorDailyPayoutCap(), Stages: stages, } } func luckyRuleConfigToProto(config domain.RuleConfig) *luckygiftv1.LuckyGiftRuleConfig { roundRTPMaxPPM := config.JackpotUserRoundRTPMaxPPM if roundRTPMaxPPM == 0 { roundRTPMaxPPM = config.JackpotUserDayRTPMaxPPM } rolling48hRTPMaxPPM := config.JackpotUser48hRTPMaxPPM if rolling48hRTPMaxPPM == 0 { rolling48hRTPMaxPPM = config.JackpotUser72hRTPMaxPPM } stages := make([]*luckygiftv1.LuckyGiftRuleStage, 0, len(config.Stages)) for _, stage := range config.Stages { tiers := make([]*luckygiftv1.LuckyGiftRuleTier, 0, len(stage.Tiers)) for _, tier := range stage.Tiers { tiers = append(tiers, &luckygiftv1.LuckyGiftRuleTier{ Stage: tier.Stage, TierId: tier.TierID, MultiplierPpm: tier.MultiplierPPM, BaseWeightPpm: tier.BaseWeightPPM, HighWaterOnly: tier.HighWaterOnly, Enabled: tier.Enabled, }) } stages = append(stages, &luckygiftv1.LuckyGiftRuleStage{ Stage: stage.Stage, MinRecharge_7DCoins: stage.MinRecharge7DCoins, MinRecharge_30DCoins: stage.MinRecharge30DCoins, Tiers: tiers, }) } return &luckygiftv1.LuckyGiftRuleConfig{ AppCode: config.AppCode, PoolId: config.PoolID, RuleVersion: config.RuleVersion, Enabled: config.Enabled, TargetRtpPpm: config.TargetRTPPPM, PoolRatePpm: config.PoolRatePPM, SettlementWindowWager: config.SettlementWindowWager, ControlBandPpm: config.ControlBandPPM, GiftPriceReference: config.GiftPriceReference, NoviceMaxEquivalentDraws: config.NoviceMaxEquivalentDraws, NormalMaxEquivalentDraws: config.NormalMaxEquivalentDraws, EffectiveFromMs: config.EffectiveFromMS, CreatedByAdminId: config.CreatedByAdminID, CreatedAtMs: config.CreatedAtMS, StrategyVersion: config.StrategyVersion, ProfitRatePpm: config.ProfitRatePPM, AnchorRatePpm: config.AnchorRatePPM, InitialPoolCoins: config.InitialPoolCoins, LossStreakGuarantee: config.LossStreakGuarantee, LowWatermarkCoins: config.LowWatermarkCoins, LowWaterNonzeroFactorPpm: config.LowWaterNonzeroFactorPPM, HighWatermarkCoins: config.HighWatermarkCoins, HighWaterNonzeroFactorPpm: config.HighWaterNonzeroFactorPPM, V2HighWaterBoostThresholdCoins: config.V2HighWaterBoostThresholdCoins, V2HighWaterBoostFactorPpm: config.V2HighWaterBoostFactorPPM, V2HighWaterBoostRecoverCoins: config.V2HighWaterBoostRecoverCoins, RechargeBoostWindowMs: config.RechargeBoostWindowMS, RechargeBoostFactorPpm: config.RechargeBoostFactorPPM, User_24HRtpThresholdPpm: config.User24HourRTPThresholdPPM, User_24HOrdinaryWinFactorPpm: config.User24HourOrdinaryWinFactorPPM, JackpotMultiplierPpms: append([]int64(nil), config.JackpotMultiplierPPMs...), JackpotGlobalRtpMaxPpm: config.JackpotGlobalRTPMaxPPM, JackpotUserRoundRtpMaxPpm: roundRTPMaxPPM, JackpotUser_48HRtpMaxPpm: rolling48hRTPMaxPPM, JackpotSpendThresholdCoins: config.JackpotSpendThresholdCoins, MaxJackpotHitsPerUserDay: config.MaxJackpotHitsPerUserDay, MaxSinglePayout: config.MaxSinglePayout, UserHourlyPayoutCap: config.UserHourlyPayoutCap, UserDailyPayoutCap: config.UserDailyPayoutCap, DeviceDailyPayoutCap: config.DeviceDailyPayoutCap, RoomHourlyPayoutCap: config.RoomHourlyPayoutCap, AnchorDailyPayoutCap: config.AnchorDailyPayoutCap, Stages: stages, } } func luckyDrawResultToProto(result domain.DrawResult) *luckygiftv1.LuckyGiftDrawResult { response := &luckygiftv1.LuckyGiftDrawResult{ DrawId: result.DrawID, CommandId: result.CommandID, AppCode: result.AppCode, UserId: result.UserID, ExternalUserId: result.ExternalUserID, PoolId: result.PoolID, GiftId: result.GiftID, RuleVersion: result.RuleVersion, ExperiencePool: result.ExperiencePool, SelectedTierId: result.SelectedTierID, MultiplierPpm: result.MultiplierPPM, BaseRewardCoins: result.BaseRewardCoins, EffectiveRewardCoins: result.EffectiveRewardCoins, RewardStatus: result.RewardStatus, RtpWindowIndex: result.RTPWindowIndex, GiftRtpWindowIndex: result.GiftRTPWindowIndex, GlobalBaseRtpPpm: result.GlobalBaseRTPPPM, GiftBaseRtpPpm: result.GiftBaseRTPPPM, StageFeedback: result.StageFeedback, HighMultiplier: result.HighMultiplier, CreatedAtMs: result.CreatedAtMS, WalletTransactionId: result.WalletTransactionID, CoinBalanceAfter: result.CoinBalanceAfter, } for _, hit := range result.Hits { response.Hits = append(response.Hits, &luckygiftv1.LuckyGiftHit{ GiftIndex: hit.GiftIndex, DrawId: hit.DrawID, SelectedTierId: hit.SelectedTierID, MultiplierPpm: hit.MultiplierPPM, RewardCoins: hit.RewardCoins, }) } return response } func externalDrawResultToProto(result domain.ExternalDrawResult) *luckygiftv1.ExternalGiftDrawResponse { return &luckygiftv1.ExternalGiftDrawResponse{ DrawId: result.DrawID, RequestId: result.RequestID, AppCode: result.AppCode, ExternalUserId: result.ExternalUserID, GiftCount: result.GiftCount, UnitAmount: result.UnitAmount, TotalAmount: result.TotalAmount, RewardAmount: result.RewardAmount, MultiplierPpm: result.MultiplierPPM, RewardStatus: result.RewardStatus, RuleVersion: result.RuleVersion, CreatedAtMs: result.CreatedAtMS, } } func luckyDrawSummaryToProto(summary domain.DrawSummary) *luckygiftv1.LuckyGiftDrawSummary { return &luckygiftv1.LuckyGiftDrawSummary{ PoolId: summary.PoolID, TotalDraws: summary.TotalDraws, UniqueUsers: summary.UniqueUsers, UniqueRooms: summary.UniqueRooms, TotalSpentCoins: summary.TotalSpentCoins, TotalRewardCoins: summary.TotalRewardCoins, BaseRewardCoins: summary.BaseRewardCoins, ActualRtpPpm: summary.ActualRTPPPM, PendingDraws: summary.PendingDraws, GrantedDraws: summary.GrantedDraws, FailedDraws: summary.FailedDraws, } } func luckyPoolBalanceToProto(pool domain.PoolBalance) *luckygiftv1.LuckyGiftPoolBalance { return &luckygiftv1.LuckyGiftPoolBalance{ AppCode: pool.AppCode, PoolId: pool.PoolID, StrategyVersion: pool.StrategyVersion, Balance: pool.Balance, ReserveFloor: pool.ReserveFloor, AvailableBalance: pool.AvailableBalance, TotalIn: pool.TotalIn, TotalOut: pool.TotalOut, ManualCreditTotal: pool.ManualCreditTotal, ManualDebitTotal: pool.ManualDebitTotal, Materialized: pool.Materialized, UpdatedAtMs: pool.UpdatedAtMS, } } 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, AppCode: adjustment.AppCode, PoolId: adjustment.PoolID, StrategyVersion: adjustment.StrategyVersion, Direction: adjustment.Direction, AmountCoins: adjustment.AmountCoins, Reason: adjustment.Reason, OperatorAdminId: adjustment.OperatorAdminID, BalanceBefore: adjustment.BalanceBefore, BalanceAfter: adjustment.BalanceAfter, CreatedAtMs: adjustment.CreatedAtMS, } }