622 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 luckygift
import (
"context"
"fmt"
"strconv"
"strings"
"hyapp-admin-server/internal/appctx"
"hyapp-admin-server/internal/integration/userclient"
"hyapp-admin-server/internal/middleware"
"hyapp-admin-server/internal/modules/shared"
"hyapp-admin-server/internal/response"
luckygiftv1 "hyapp.local/api/proto/luckygift/v1"
"github.com/gin-gonic/gin"
"google.golang.org/grpc/codes"
grpcstatus "google.golang.org/grpc/status"
)
type luckyGiftInternalUserDTO struct {
// UserID 使用字符串返回,避免浏览器把 64 位长号转成 JavaScript number 后丢失末位精度。
UserID string `json:"user_id"`
DisplayUserID string `json:"display_user_id"`
DefaultDisplayID string `json:"default_display_user_id"`
PrettyID string `json:"pretty_id"`
PrettyDisplayUserID string `json:"pretty_display_user_id"`
Name string `json:"name"`
Avatar string `json:"avatar"`
Status string `json:"status"`
}
type luckyGiftUserProfileWindowDTO struct {
Draws int64 `json:"draws"`
WagerCoins int64 `json:"wager_coins"`
PayoutCoins int64 `json:"payout_coins"`
NetCoins int64 `json:"net_coins"`
RTPPPM int64 `json:"rtp_ppm"`
HasRTP bool `json:"has_rtp"`
}
type luckyGiftUserProfileDTO struct {
AppCode string `json:"app_code"`
PoolID string `json:"pool_id"`
IdentityType string `json:"identity_type"`
InternalUserID string `json:"internal_user_id,omitempty"`
ExternalUserID string `json:"external_user_id,omitempty"`
InternalUser *luckyGiftInternalUserDTO `json:"internal_user,omitempty"`
RuleVersion int64 `json:"rule_version"`
StrategyVersion string `json:"strategy_version"`
Stage string `json:"stage"`
PaidDraws int64 `json:"paid_draws"`
EquivalentDraws int64 `json:"equivalent_draws"`
LossStreak int64 `json:"loss_streak"`
PendingSpendJackpotTokens int64 `json:"pending_spend_jackpot_tokens"`
GuaranteeDrawsRemaining int64 `json:"guarantee_draws_remaining"`
DownweightActive bool `json:"downweight_active"`
DownweightFactorPPM int64 `json:"downweight_factor_ppm"`
User24HourRTPThresholdPPM int64 `json:"user_24h_rtp_threshold_ppm"`
CurrentRTPPPM int64 `json:"current_rtp_ppm"`
HasCurrentRTP bool `json:"has_current_rtp"`
OneHour luckyGiftUserProfileWindowDTO `json:"one_hour"`
TwentyFourHours luckyGiftUserProfileWindowDTO `json:"twenty_four_hours"`
FortyEightHours luckyGiftUserProfileWindowDTO `json:"forty_eight_hours"`
Lifetime luckyGiftUserProfileWindowDTO `json:"lifetime"`
BaseRewardCoins int64 `json:"base_reward_coins"`
OrdinaryWinCount int64 `json:"ordinary_win_count"`
HighMultiplierOrdinaryWinCount int64 `json:"high_multiplier_ordinary_win_count"`
RTPCompensationJackpotCount int64 `json:"rtp_compensation_jackpot_count"`
CumulativeSpendJackpotCount int64 `json:"cumulative_spend_jackpot_count"`
JackpotWinCount int64 `json:"jackpot_win_count"`
GuaranteeHitCount int64 `json:"guarantee_hit_count"`
ZeroDrawCount int64 `json:"zero_draw_count"`
GrantedDrawCount int64 `json:"granted_draw_count"`
PendingDrawCount int64 `json:"pending_draw_count"`
FailedDrawCount int64 `json:"failed_draw_count"`
MaxMultiplierPPM int64 `json:"max_multiplier_ppm"`
MaxRewardCoins int64 `json:"max_reward_coins"`
FirstDrawAtMS int64 `json:"first_draw_at_ms"`
LastDrawAtMS int64 `json:"last_draw_at_ms"`
AggregatedAtMS int64 `json:"aggregated_at_ms"`
DataLagMS int64 `json:"data_lag_ms"`
}
type luckyGiftUserProfilePageDTO struct {
Items []luckyGiftUserProfileDTO `json:"items"`
Page int `json:"page"`
PageSize int `json:"page_size"`
Total int64 `json:"total"`
SnapshotAtMS int64 `json:"snapshot_at_ms"`
NextCursor string `json:"next_cursor,omitempty"`
HasMore bool `json:"has_more"`
}
type luckyGiftUserMultiplierStatDTO struct {
HitType string `json:"hit_type"`
HitTypeLabel string `json:"hit_type_label"`
MultiplierPPM int64 `json:"multiplier_ppm"`
DrawCount int64 `json:"draw_count"`
WagerCoins int64 `json:"wager_coins"`
PayoutCoins int64 `json:"payout_coins"`
}
type luckyGiftUserJackpotConditionDTO struct {
Name string `json:"name"`
Label string `json:"label"`
Actual string `json:"actual"`
Threshold string `json:"threshold"`
Operator string `json:"operator"`
Numerator int64 `json:"numerator"`
Denominator int64 `json:"denominator"`
RatioPPM int64 `json:"ratio_ppm"`
LimitPPM int64 `json:"limit_ppm"`
FactorPPM int64 `json:"factor_ppm"`
Factor string `json:"factor"`
Passed bool `json:"passed"`
Reason string `json:"reason"`
}
type luckyGiftUserJackpotHitDTO struct {
EventKey string `json:"event_key"`
DrawID string `json:"draw_id"`
RequestID string `json:"request_id"`
RuleVersion int64 `json:"rule_version"`
Stage string `json:"stage"`
Mechanism string `json:"mechanism"`
MechanismLabel string `json:"mechanism_label"`
ReasonCode string `json:"reason_code"`
ReasonSummary string `json:"reason_summary"`
TierID string `json:"tier_id"`
MultiplierPPM int64 `json:"multiplier_ppm"`
WagerCoins int64 `json:"wager_coins"`
PayoutCoins int64 `json:"payout_coins"`
OccurredAtMS int64 `json:"occurred_at_ms"`
Conditions []luckyGiftUserJackpotConditionDTO `json:"conditions"`
}
type luckyGiftUserProfileDetailDTO struct {
Profile luckyGiftUserProfileDTO `json:"profile"`
MultiplierDistribution []luckyGiftUserMultiplierStatDTO `json:"multiplier_distribution"`
JackpotHits []luckyGiftUserJackpotHitDTO `json:"jackpot_hits"`
JackpotHitTotal int64 `json:"jackpot_hit_total"`
JackpotPage int32 `json:"jackpot_page"`
JackpotPageSize int32 `json:"jackpot_page_size"`
NextJackpotCursor string `json:"next_jackpot_cursor,omitempty"`
JackpotHasMore bool `json:"jackpot_has_more"`
SnapshotAtMS int64 `json:"snapshot_at_ms"`
}
// ListLuckyGiftUserProfiles 查询已经由 lucky-gift-service 增量物化的用户画像。列表只按 app+pool
// 读取聚合表;内部账号的短号/靓号解析和资料补全统一走 user-service不在 HTTP 请求里扫描开奖事实表。
func (h *Handler) ListLuckyGiftUserProfiles(c *gin.Context) {
appCode, poolID, ok := requiredLuckyGiftProfileScope(c)
if !ok {
return
}
identityType, ok := normalizeLuckyGiftProfileIdentity(firstNonEmpty(c.Query("identity_type"), c.Query("source")))
if !ok {
response.BadRequest(c, "identity_type must be all, internal or external")
return
}
options := shared.ListOptions(c)
if options.PageSize > 100 {
options.PageSize = 100
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
ctx = appctx.WithContext(ctx, appCode)
internalUserID := parseOptionalInt64(firstNonEmpty(c.Query("internal_user_id"), c.Query("user_id")))
externalUserID := strings.TrimSpace(c.Query("external_user_id"))
keyword := strings.TrimSpace(c.Query("user_keyword"))
if keyword != "" {
resolvedInternalID, resolvedExternalID, internalMatched, err := h.resolveLuckyGiftProfileKeyword(ctx, appCode, identityType, keyword, middleware.CurrentRequestID(c))
if err != nil {
response.ServerError(c, "搜索用户失败")
return
}
internalUserID, externalUserID = resolvedInternalID, resolvedExternalID
if identityType == "internal" && !internalMatched {
// 未解析的内部短号/长号/靓号必须返回空页,不能把“查无此人”退化为无条件全量列表。
response.OK(c, luckyGiftUserProfilePageDTO{Items: []luckyGiftUserProfileDTO{}, Page: options.Page, PageSize: options.PageSize})
return
}
}
resp, err := h.luckyGift.ListLuckyGiftUserProfiles(ctx, &luckygiftv1.ListLuckyGiftUserProfilesRequest{
Meta: h.metaForApp(c, appCode),
PoolId: poolID,
IdentityType: identityType,
InternalUserId: internalUserID,
ExternalUserId: externalUserID,
Stage: strings.TrimSpace(c.Query("stage")),
Status: strings.TrimSpace(c.Query("status")),
SortBy: strings.TrimSpace(c.Query("sort_by")),
SortDirection: strings.TrimSpace(c.Query("sort_direction")),
Page: int32(options.Page),
PageSize: int32(options.PageSize),
Cursor: strings.TrimSpace(c.Query("cursor")),
})
if err != nil {
h.writeLuckyGiftUserProfileError(c, err, "获取幸运礼物用户画像失败")
return
}
users, err := h.loadLuckyGiftInternalUsers(ctx, resp.GetProfiles(), middleware.CurrentRequestID(c))
if err != nil {
response.ServerError(c, "获取用户资料失败")
return
}
items := make([]luckyGiftUserProfileDTO, 0, len(resp.GetProfiles()))
for _, profile := range resp.GetProfiles() {
items = append(items, luckyGiftUserProfileFromProto(profile, users[profile.GetInternalUserId()]))
}
response.OK(c, luckyGiftUserProfilePageDTO{
Items: items, Page: options.Page, PageSize: options.PageSize,
Total: resp.GetTotal(), SnapshotAtMS: resp.GetSnapshotAtMs(),
NextCursor: resp.GetNextCursor(), HasMore: resp.GetHasMore(),
})
}
// GetLuckyGiftUserProfile 返回单个用户的倍率分布和大奖解释。大奖明细只在展开行时请求,
// 避免列表页同时解码所有用户的条件 trace保证高密度页面仍保持稳定响应时间。
func (h *Handler) GetLuckyGiftUserProfile(c *gin.Context) {
appCode, poolID, ok := requiredLuckyGiftProfileScope(c)
if !ok {
return
}
identityType, ok := normalizeLuckyGiftProfileIdentity(firstNonEmpty(c.Query("identity_type"), c.Query("source")))
if !ok || identityType == "" {
response.BadRequest(c, "identity_type must be internal or external")
return
}
ctx, cancel := h.luckyGiftContext(c)
defer cancel()
ctx = appctx.WithContext(ctx, appCode)
internalUserID := parseOptionalInt64(firstNonEmpty(c.Query("internal_user_id"), c.Query("user_id")))
externalUserID := strings.TrimSpace(c.Query("external_user_id"))
if keyword := strings.TrimSpace(c.Query("user_keyword")); keyword != "" {
resolvedInternalID, resolvedExternalID, internalMatched, err := h.resolveLuckyGiftProfileKeyword(ctx, appCode, identityType, keyword, middleware.CurrentRequestID(c))
if err != nil {
response.ServerError(c, "搜索用户失败")
return
}
internalUserID, externalUserID = resolvedInternalID, resolvedExternalID
if identityType == "internal" && !internalMatched {
response.NotFound(c, "未找到该用户的幸运礼物画像")
return
}
}
if (identityType == "internal" && internalUserID <= 0) || (identityType == "external" && externalUserID == "") {
response.BadRequest(c, "internal_user_id or external_user_id is required")
return
}
jackpotPage := parseLuckyGiftProfilePositiveInt(c.Query("jackpot_page"), 1, 1_000_000)
jackpotPageSize := parseLuckyGiftProfilePositiveInt(c.Query("jackpot_page_size"), 20, 50)
resp, err := h.luckyGift.GetLuckyGiftUserProfile(ctx, &luckygiftv1.GetLuckyGiftUserProfileRequest{
Meta: h.metaForApp(c, appCode), PoolId: poolID, IdentityType: identityType,
InternalUserId: internalUserID, ExternalUserId: externalUserID,
JackpotPage: int32(jackpotPage), JackpotPageSize: int32(jackpotPageSize),
JackpotCursor: strings.TrimSpace(c.Query("jackpot_cursor")),
})
if err != nil {
h.writeLuckyGiftUserProfileError(c, err, "获取幸运礼物用户画像详情失败")
return
}
users, err := h.loadLuckyGiftInternalUsers(ctx, []*luckygiftv1.LuckyGiftUserProfile{resp.GetProfile()}, middleware.CurrentRequestID(c))
if err != nil {
response.ServerError(c, "获取用户资料失败")
return
}
detail := luckyGiftUserProfileDetailDTO{
Profile: luckyGiftUserProfileFromProto(resp.GetProfile(), users[resp.GetProfile().GetInternalUserId()]),
MultiplierDistribution: make([]luckyGiftUserMultiplierStatDTO, 0, len(resp.GetMultiplierDistribution())),
JackpotHits: make([]luckyGiftUserJackpotHitDTO, 0, len(resp.GetJackpotHits())),
JackpotHitTotal: resp.GetJackpotHitTotal(), JackpotPage: resp.GetJackpotPage(),
JackpotPageSize: resp.GetJackpotPageSize(), NextJackpotCursor: resp.GetNextJackpotCursor(),
JackpotHasMore: resp.GetJackpotHasMore(), SnapshotAtMS: resp.GetSnapshotAtMs(),
}
for _, stat := range resp.GetMultiplierDistribution() {
detail.MultiplierDistribution = append(detail.MultiplierDistribution, luckyGiftUserMultiplierStatFromProto(stat))
}
for _, hit := range resp.GetJackpotHits() {
detail.JackpotHits = append(detail.JackpotHits, luckyGiftUserJackpotHitFromProto(hit))
}
response.OK(c, detail)
}
func parseLuckyGiftProfilePositiveInt(value string, fallback, maximum int) int {
parsed, err := strconv.Atoi(strings.TrimSpace(value))
if err != nil || parsed <= 0 {
return fallback
}
if parsed > maximum {
return maximum
}
return parsed
}
func requiredLuckyGiftProfileScope(c *gin.Context) (string, string, bool) {
appCode := strings.ToLower(strings.TrimSpace(c.Query("app_code")))
poolID := strings.TrimSpace(c.Query("pool_id"))
if appCode == "" || poolID == "" {
response.BadRequest(c, "app_code and pool_id are required")
return "", "", false
}
return appCode, poolID, true
}
func normalizeLuckyGiftProfileIdentity(value string) (string, bool) {
switch strings.ToLower(strings.TrimSpace(value)) {
case "", "all":
// owner 服务用空值表达“内部与外部”,避免引入第三种持久身份类型。
return "", true
case "internal", "external":
return strings.ToLower(strings.TrimSpace(value)), true
default:
return "", false
}
}
func (h *Handler) resolveLuckyGiftProfileKeyword(ctx context.Context, appCode, identityType, keyword, requestID string) (int64, string, bool, error) {
var externalUserID string
if identityType == "" || identityType == "external" {
// 外部接入没有 HyApp 用户主数据,原样按 external_user_id 精确匹配;稳定策略哈希永不出 owner 服务。
externalUserID = keyword
}
if identityType == "external" {
return 0, externalUserID, false, nil
}
if h.users == nil {
return 0, externalUserID, false, fmt.Errorf("lucky gift user profile resolver is not configured")
}
identity, err := h.users.ResolveDisplayUserID(appctx.WithContext(ctx, appCode), userclient.ResolveDisplayUserIDRequest{
RequestID: requestID, Caller: "admin-server", DisplayUserID: keyword,
})
if err == nil && identity != nil && identity.UserID > 0 {
return identity.UserID, externalUserID, true, nil
}
if err != nil {
switch grpcstatus.Code(err) {
case codes.NotFound:
// 继续尝试把纯数字解释为系统长 user_id。
case codes.InvalidArgument:
// all 模式下 keyword 也可能是任意 external_user_id内部展示号格式不合法只代表内部未命中不能阻断外部精确搜索。
return 0, externalUserID, false, nil
default:
return 0, externalUserID, false, err
}
}
// 纯数字先按展示号解析;只有展示号确实不存在时才回退为 64 位长 user_id避免短号与长号碰撞时查错人。
longUserID, parseErr := strconv.ParseInt(keyword, 10, 64)
if parseErr != nil || longUserID <= 0 {
return 0, externalUserID, false, nil
}
user, getErr := h.users.GetUser(appctx.WithContext(ctx, appCode), userclient.GetUserRequest{
RequestID: requestID, Caller: "admin-server", UserID: longUserID,
})
if getErr == nil && user != nil {
return user.UserID, externalUserID, true, nil
}
if getErr != nil && grpcstatus.Code(getErr) != codes.NotFound {
return 0, externalUserID, false, getErr
}
return 0, externalUserID, false, nil
}
func (h *Handler) loadLuckyGiftInternalUsers(ctx context.Context, profiles []*luckygiftv1.LuckyGiftUserProfile, requestID string) (map[int64]*userclient.User, error) {
ids := make([]int64, 0, len(profiles))
seen := make(map[int64]struct{}, len(profiles))
for _, profile := range profiles {
if profile == nil || profile.GetIdentityType() != "internal" || profile.GetInternalUserId() <= 0 {
continue
}
if _, exists := seen[profile.GetInternalUserId()]; exists {
continue
}
seen[profile.GetInternalUserId()] = struct{}{}
ids = append(ids, profile.GetInternalUserId())
}
if len(ids) == 0 {
return map[int64]*userclient.User{}, nil
}
if h.users == nil {
return nil, fmt.Errorf("lucky gift user profile user client is not configured")
}
return h.users.BatchGetUsers(ctx, userclient.BatchGetUsersRequest{RequestID: requestID, Caller: "admin-server", UserIDs: ids})
}
func luckyGiftUserProfileFromProto(profile *luckygiftv1.LuckyGiftUserProfile, user *userclient.User) luckyGiftUserProfileDTO {
if profile == nil {
return luckyGiftUserProfileDTO{}
}
jackpotCount := profile.GetRtpCompensationJackpotCount() + profile.GetCumulativeSpendJackpotCount()
dto := luckyGiftUserProfileDTO{
AppCode: profile.GetAppCode(), PoolID: profile.GetPoolId(), IdentityType: profile.GetIdentityType(),
RuleVersion: profile.GetRuleVersion(),
StrategyVersion: profile.GetStrategyVersion(), Stage: profile.GetStage(), PaidDraws: profile.GetPaidDraws(),
EquivalentDraws: profile.GetEquivalentDraws(), LossStreak: profile.GetLossStreak(),
PendingSpendJackpotTokens: profile.GetPendingSpendJackpotTokens(), GuaranteeDrawsRemaining: profile.GetGuaranteeDrawsRemaining(),
DownweightActive: profile.GetDownweightActive(), DownweightFactorPPM: profile.GetDownweightFactorPpm(),
User24HourRTPThresholdPPM: profile.GetUser_24HRtpThresholdPpm(),
OneHour: luckyGiftUserProfileWindowFromProto(profile.GetOneHour()),
TwentyFourHours: luckyGiftUserProfileWindowFromProto(profile.GetTwentyFourHours()),
FortyEightHours: luckyGiftUserProfileWindowFromProto(profile.GetFortyEightHours()),
Lifetime: luckyGiftUserProfileWindowFromProto(profile.GetLifetime()),
BaseRewardCoins: profile.GetBaseRewardCoins(), OrdinaryWinCount: profile.GetOrdinaryWinCount(),
HighMultiplierOrdinaryWinCount: profile.GetHighMultiplierOrdinaryWinCount(),
RTPCompensationJackpotCount: profile.GetRtpCompensationJackpotCount(),
CumulativeSpendJackpotCount: profile.GetCumulativeSpendJackpotCount(), JackpotWinCount: jackpotCount,
GuaranteeHitCount: profile.GetGuaranteeHitCount(), ZeroDrawCount: profile.GetZeroDrawCount(),
GrantedDrawCount: profile.GetGrantedDrawCount(), PendingDrawCount: profile.GetPendingDrawCount(), FailedDrawCount: profile.GetFailedDrawCount(),
MaxMultiplierPPM: profile.GetMaxMultiplierPpm(), MaxRewardCoins: profile.GetMaxRewardCoins(),
FirstDrawAtMS: profile.GetFirstDrawAtMs(), LastDrawAtMS: profile.GetLastDrawAtMs(),
AggregatedAtMS: profile.GetAggregatedAtMs(), DataLagMS: profile.GetDataLagMs(),
CurrentRTPPPM: profile.GetTwentyFourHours().GetRtpPpm(), HasCurrentRTP: profile.GetTwentyFourHours().GetHasRtp(),
}
if profile.GetIdentityType() == "internal" && profile.GetInternalUserId() > 0 {
// 外部画像即使 owner 发生协议回归误填了 internal_user_id也不得在 admin HTTP 边界透出策略哈希。
dto.InternalUserID = strconv.FormatInt(profile.GetInternalUserId(), 10)
dto.InternalUser = luckyGiftInternalUserFromClient(profile.GetInternalUserId(), user)
} else if profile.GetIdentityType() == "external" {
dto.ExternalUserID = profile.GetExternalUserId()
}
return dto
}
func luckyGiftInternalUserFromClient(userID int64, user *userclient.User) *luckyGiftInternalUserDTO {
dto := &luckyGiftInternalUserDTO{UserID: strconv.FormatInt(userID, 10)}
if user == nil {
return dto
}
dto.DisplayUserID = user.DisplayUserID
dto.DefaultDisplayID = user.DefaultDisplayUserID
dto.PrettyID = user.PrettyID
dto.PrettyDisplayUserID = user.PrettyDisplayUserID
dto.Name = user.Username
dto.Avatar = user.Avatar
dto.Status = user.Status
return dto
}
func luckyGiftUserProfileWindowFromProto(window *luckygiftv1.LuckyGiftUserProfileWindow) luckyGiftUserProfileWindowDTO {
if window == nil {
return luckyGiftUserProfileWindowDTO{}
}
return luckyGiftUserProfileWindowDTO{
Draws: window.GetDraws(), WagerCoins: window.GetWagerCoins(), PayoutCoins: window.GetPayoutCoins(),
NetCoins: window.GetNetCoins(), RTPPPM: window.GetRtpPpm(), HasRTP: window.GetHasRtp(),
}
}
func luckyGiftUserMultiplierStatFromProto(stat *luckygiftv1.LuckyGiftUserMultiplierStat) luckyGiftUserMultiplierStatDTO {
if stat == nil {
return luckyGiftUserMultiplierStatDTO{}
}
return luckyGiftUserMultiplierStatDTO{
HitType: stat.GetHitType(), HitTypeLabel: luckyGiftHitTypeLabel(stat.GetHitType()),
MultiplierPPM: stat.GetMultiplierPpm(), DrawCount: stat.GetDrawCount(),
WagerCoins: stat.GetWagerCoins(), PayoutCoins: stat.GetPayoutCoins(),
}
}
func luckyGiftUserJackpotHitFromProto(hit *luckygiftv1.LuckyGiftUserJackpotHit) luckyGiftUserJackpotHitDTO {
if hit == nil {
return luckyGiftUserJackpotHitDTO{}
}
conditions := make([]luckyGiftUserJackpotConditionDTO, 0, len(hit.GetConditions()))
for _, condition := range hit.GetConditions() {
conditions = append(conditions, luckyGiftUserJackpotConditionFromProto(condition))
}
return luckyGiftUserJackpotHitDTO{
EventKey: hit.GetEventKey(), DrawID: hit.GetDrawId(), RequestID: hit.GetRequestId(), RuleVersion: hit.GetRuleVersion(), Stage: hit.GetStage(),
Mechanism: hit.GetMechanism(), MechanismLabel: luckyGiftJackpotMechanismLabel(hit.GetMechanism()),
ReasonCode: hit.GetReasonCode(), ReasonSummary: luckyGiftJackpotReasonSummary(hit), TierID: hit.GetTierId(),
MultiplierPPM: hit.GetMultiplierPpm(), WagerCoins: hit.GetWagerCoins(), PayoutCoins: hit.GetPayoutCoins(),
OccurredAtMS: hit.GetOccurredAtMs(), Conditions: conditions,
}
}
func luckyGiftUserJackpotConditionFromProto(condition *luckygiftv1.LuckyGiftUserJackpotCondition) luckyGiftUserJackpotConditionDTO {
if condition == nil {
return luckyGiftUserJackpotConditionDTO{}
}
actual, threshold, operator := luckyGiftJackpotConditionValues(condition)
return luckyGiftUserJackpotConditionDTO{
Name: condition.GetName(), Label: luckyGiftJackpotConditionLabel(condition.GetName()),
Actual: actual, Threshold: threshold, Operator: operator,
Numerator: condition.GetNumerator(), Denominator: condition.GetDenominator(),
RatioPPM: condition.GetRatioPpm(), LimitPPM: condition.GetLimitPpm(), FactorPPM: condition.GetFactorPpm(),
Factor: luckyGiftPercent(condition.GetFactorPpm()), Passed: condition.GetPassed(), Reason: condition.GetReason(),
}
}
func luckyGiftJackpotConditionValues(condition *luckygiftv1.LuckyGiftUserJackpotCondition) (string, string, string) {
name := condition.GetName()
switch name {
case "global_rtp", "spend_global_rtp", "user_round_rtp", "user_48h_rtp", "user_day_rtp", "user_72h_rtp":
return fmt.Sprintf("%s返奖 %d / 消耗 %d", luckyGiftPercent(condition.GetRatioPpm()), condition.GetNumerator(), condition.GetDenominator()), luckyGiftPercent(condition.GetLimitPpm()), "≤"
case "user_24h_rtp_downweight":
return fmt.Sprintf("%s返奖 %d / 消耗 %d", luckyGiftPercent(condition.GetRatioPpm()), condition.GetNumerator(), condition.GetDenominator()), luckyGiftPercent(condition.GetLimitPpm()), ">"
case "user_registered_48h":
return luckyGiftDurationHours(condition.GetNumerator()), luckyGiftDurationHours(condition.GetDenominator()), "≥"
case "daily_jackpot_limit", "spend_daily_jackpot_limit":
return fmt.Sprintf("%d 次", condition.GetNumerator()), fmt.Sprintf("%d 次", condition.GetLimitPpm()), "<"
case "settled_round_qualification", "payable_jackpot_candidate":
if condition.GetPassed() {
return "已满足", "必须满足", "="
}
return "未满足", "必须满足", "="
default:
if condition.GetLimitPpm() != 0 || condition.GetRatioPpm() != 0 {
return luckyGiftPercent(condition.GetRatioPpm()), luckyGiftPercent(condition.GetLimitPpm()), "≤"
}
if condition.GetPassed() {
return "已满足", "必须满足", "="
}
return "未满足", "必须满足", "="
}
}
func luckyGiftJackpotConditionLabel(name string) string {
labels := map[string]string{
"global_rtp": "全奖池当前返奖率",
"spend_global_rtp": "累计消费大奖检查时的全奖池返奖率",
"settled_round_qualification": "上一结算轮次是否达到补偿条件",
"user_round_rtp": "用户当前结算轮次返奖率",
"user_48h_rtp": "用户近 48 小时返奖率",
"user_registered_48h": "用户注册是否已满 48 小时",
"user_24h_rtp_downweight": "用户近 24 小时普通中奖降权",
"user_day_rtp": "用户当天返奖率",
"user_72h_rtp": "用户近 72 小时返奖率",
"daily_jackpot_limit": "用户当天已中大奖次数",
"spend_daily_jackpot_limit": "累计消费大奖的当天次数限制",
"payable_jackpot_candidate": "奖池余额是否足够支付所选大奖",
}
if label := labels[name]; label != "" {
return label
}
return strings.ReplaceAll(strings.TrimSpace(name), "_", " ")
}
func luckyGiftJackpotMechanismLabel(mechanism string) string {
switch mechanism {
case "rtp_compensation", "jackpot_mechanism_1":
return "RTP 补偿大奖"
case "spend_milestone", "jackpot_mechanism_2":
return "累计消费大奖"
default:
return "大奖"
}
}
func luckyGiftJackpotReasonSummary(hit *luckygiftv1.LuckyGiftUserJackpotHit) string {
switch hit.GetReasonCode() {
case "rtp_compensation_jackpot":
return "用户与奖池均满足补偿条件,且奖池余额足够支付,因此触发 RTP 补偿大奖。"
case "milestone_token_jackpot":
return "用户累计消费已获得一次大奖机会,且奖池余额足够支付,因此触发累计消费大奖。"
}
if summary := strings.TrimSpace(hit.GetReasonSummary()); summary != "" {
return summary
}
return "本次开奖满足下列全部条件,因此命中该大奖。"
}
func luckyGiftHitTypeLabel(hitType string) string {
switch hitType {
case "zero":
return "未中奖"
case "ordinary":
return "普通奖"
case "rtp_compensation":
return "RTP 补偿大奖"
case "spend_milestone":
return "累计消费大奖"
default:
return hitType
}
}
func luckyGiftPercent(ppm int64) string {
value := strconv.FormatFloat(float64(ppm)/10_000, 'f', 2, 64)
value = strings.TrimRight(strings.TrimRight(value, "0"), ".")
if value == "" {
value = "0"
}
return value + "%"
}
func luckyGiftDurationHours(milliseconds int64) string {
value := strconv.FormatFloat(float64(milliseconds)/3_600_000, 'f', 1, 64)
value = strings.TrimRight(strings.TrimRight(value, "0"), ".")
return value + " 小时"
}
func firstNonEmpty(values ...string) string {
for _, value := range values {
if strings.TrimSpace(value) != "" {
return value
}
}
return ""
}
func (h *Handler) writeLuckyGiftUserProfileError(c *gin.Context, err error, fallback string) {
status := grpcstatus.Convert(err)
switch status.Code() {
case codes.InvalidArgument:
response.BadRequest(c, status.Message())
case codes.NotFound:
response.NotFound(c, "未找到该用户的幸运礼物画像")
default:
response.ServerError(c, fallback)
}
}