636 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(),
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 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(),
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,
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,
}
}