zhx dc8435f869 增加幸运礼物 AB 实验:按用户哈希分流灰度发布配置版本
- lucky_gift_experiments/overrides 表(006 迁移,含分组统计索引与 binary user_key)
- 创建实验=同事务发布 treatment+登记;两组强制同策略;实验期常规发布拒绝
- Check/单抽/批量/外部抽按用户钉住组内版本;结束实验重发布获胜快照全量收敛
- AdminLuckyGiftService 新增 5 个实验 RPC(放量字段 proto3 optional)
- admin 实验管理 HTTP API + lucky-gift:experiment 权限(迁移 103)
- 设计文档 docs/幸运礼物AB实验.md;域/真库/admin 回归测试

Co-Authored-By: Claude Fable 5 <noreply@anthropic.com>
2026-07-17 00:22:12 +08:00

650 lines
28 KiB
Go
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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(),
})
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(),
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) 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(),
RechargeBoostWindowMS: config.GetRechargeBoostWindowMs(),
RechargeBoostFactorPPM: config.GetRechargeBoostFactorPpm(),
JackpotMultiplierPPMs: append([]int64(nil), config.GetJackpotMultiplierPpms()...),
JackpotGlobalRTPMaxPPM: config.GetJackpotGlobalRtpMaxPpm(),
JackpotUserDayRTPMaxPPM: config.GetJackpotUserDayRtpMaxPpm(),
JackpotUser72hRTPMaxPPM: config.GetJackpotUser_72HRtpMaxPpm(),
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 {
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,
RechargeBoostWindowMs: config.RechargeBoostWindowMS,
RechargeBoostFactorPpm: config.RechargeBoostFactorPPM,
JackpotMultiplierPpms: append([]int64(nil), config.JackpotMultiplierPPMs...),
JackpotGlobalRtpMaxPpm: config.JackpotGlobalRTPMaxPPM,
JackpotUserDayRtpMaxPpm: config.JackpotUserDayRTPMaxPPM,
JackpotUser_72HRtpMaxPpm: config.JackpotUser72hRTPMaxPPM,
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 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,
}
}