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, 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(), RequestID: req.GetRequestId(), GiftCount: req.GetGiftCount(), UnitAmount: req.GetUnitAmount(), TotalAmount: req.GetTotalAmount(), Currency: req.GetCurrency(), PaidAtMS: req.GetPaidAtMs(), MetadataJSON: req.GetMetadataJson(), PoolID: req.GetPoolId(), }) 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(), VisibleRegionID: meta.GetVisibleRegionId(), CountryID: meta.GetCountryId(), } } // 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) 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(), }) 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 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(), 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(), Stages: stages, } } func luckyRuleConfigToProto(config domain.RuleConfig) *luckygiftv1.LuckyGiftRuleConfig { 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, 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, Stages: stages, } } func luckyDrawResultToProto(result domain.DrawResult) *luckygiftv1.LuckyGiftDrawResult { return &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, } } 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, Balance: pool.Balance, ReserveFloor: pool.ReserveFloor, AvailableBalance: pool.AvailableBalance, TotalIn: pool.TotalIn, TotalOut: pool.TotalOut, Materialized: pool.Materialized, UpdatedAtMs: pool.UpdatedAtMS, } }