package luckygift import ( "context" "fmt" "math" "strings" "time" "hyapp-admin-server/internal/integration/luckygiftadmin" "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" ) type Handler struct { luckyGift luckygiftadmin.Client requestTimeout time.Duration audit shared.OperationLogger } func New(luckyGift luckygiftadmin.Client, requestTimeout time.Duration, audit shared.OperationLogger) *Handler { if requestTimeout <= 0 { requestTimeout = 3 * time.Second } return &Handler{luckyGift: luckyGift, requestTimeout: requestTimeout, audit: audit} } type configRequest struct { AppCode string `json:"app_code"` PoolID string `json:"pool_id"` StrategyVersion string `json:"strategy_version"` Enabled bool `json:"enabled"` TargetRTPPercent float64 `json:"target_rtp_percent"` PoolRatePercent float64 `json:"pool_rate_percent"` ProfitRatePercent float64 `json:"profit_rate_percent"` AnchorRatePercent float64 `json:"anchor_rate_percent"` SettlementWindowWager int64 `json:"settlement_window_wager"` ControlBandPercent float64 `json:"control_band_percent"` GiftPriceReference int64 `json:"gift_price_reference"` NoviceMaxEquivalentDraws int64 `json:"novice_max_equivalent_draws"` NormalMaxEquivalentDraws int64 `json:"normal_max_equivalent_draws"` EffectiveFromMS int64 `json:"effective_from_ms"` InitialPoolCoins int64 `json:"initial_pool_coins"` LossStreakGuarantee int64 `json:"loss_streak_guarantee"` LowWatermarkCoins int64 `json:"low_watermark_coins"` LowWaterNonzeroFactorPercent float64 `json:"low_water_nonzero_factor_percent"` HighWatermarkCoins int64 `json:"high_watermark_coins"` HighWaterNonzeroFactorPercent float64 `json:"high_water_nonzero_factor_percent"` RechargeBoostWindowMS int64 `json:"recharge_boost_window_ms"` RechargeBoostFactorPercent float64 `json:"recharge_boost_factor_percent"` JackpotMultipliers []float64 `json:"jackpot_multipliers"` JackpotGlobalRTPMaxPercent float64 `json:"jackpot_global_rtp_max_percent"` JackpotUserDayRTPMaxPercent float64 `json:"jackpot_user_day_rtp_max_percent"` JackpotUser72hRTPMaxPercent float64 `json:"jackpot_user_72h_rtp_max_percent"` JackpotSpendThresholdCoins int64 `json:"jackpot_spend_threshold_coins"` MaxJackpotHitsPerUserDay int64 `json:"max_jackpot_hits_per_user_day"` MaxSinglePayout int64 `json:"max_single_payout"` UserHourlyPayoutCap int64 `json:"user_hourly_payout_cap"` UserDailyPayoutCap int64 `json:"user_daily_payout_cap"` DeviceDailyPayoutCap int64 `json:"device_daily_payout_cap"` RoomHourlyPayoutCap int64 `json:"room_hourly_payout_cap"` AnchorDailyPayoutCap int64 `json:"anchor_daily_payout_cap"` Stages []stageDTO `json:"stages"` } type configDTO struct { configRequest RuleVersion int64 `json:"rule_version"` CreatedByAdminID int64 `json:"created_by_admin_id"` CreatedAtMS int64 `json:"created_at_ms"` } type stageDTO struct { Stage string `json:"stage"` MinRecharge7DCoins int64 `json:"min_recharge_7d_coins"` MinRecharge30DCoins int64 `json:"min_recharge_30d_coins"` Tiers []tierDTO `json:"tiers"` } type tierDTO struct { TierID string `json:"tier_id"` Multiplier float64 `json:"multiplier"` ProbabilityPercent float64 `json:"probability_percent"` HighWaterOnly bool `json:"high_water_only"` Enabled bool `json:"enabled"` } type drawDTO struct { DrawID string `json:"draw_id"` CommandID string `json:"command_id"` PoolID string `json:"pool_id"` GiftID string `json:"gift_id"` RuleVersion int64 `json:"rule_version"` ExperiencePool string `json:"experience_pool"` SelectedTierID string `json:"selected_tier_id"` MultiplierPPM int64 `json:"multiplier_ppm"` BaseRewardCoins int64 `json:"base_reward_coins"` EffectiveRewardCoins int64 `json:"effective_reward_coins"` RewardStatus string `json:"reward_status"` RTPWindowIndex int64 `json:"rtp_window_index"` GiftRTPWindowIndex int64 `json:"gift_rtp_window_index"` GlobalBaseRTPPPM int64 `json:"global_base_rtp_ppm"` GiftBaseRTPPPM int64 `json:"gift_base_rtp_ppm"` StageFeedback bool `json:"stage_feedback"` HighMultiplier bool `json:"high_multiplier"` CreatedAtMS int64 `json:"created_at_ms"` AppCode string `json:"app_code"` UserID int64 `json:"user_id"` ExternalUserID string `json:"external_user_id"` } type drawSummaryDTO struct { PoolID string `json:"pool_id"` TotalDraws int64 `json:"total_draws"` UniqueUsers int64 `json:"unique_users"` UniqueRooms int64 `json:"unique_rooms"` TotalSpentCoins int64 `json:"total_spent_coins"` TotalRewardCoins int64 `json:"total_reward_coins"` BaseRewardCoins int64 `json:"base_reward_coins"` ActualRTPPPM int64 `json:"actual_rtp_ppm"` PendingDraws int64 `json:"pending_draws"` GrantedDraws int64 `json:"granted_draws"` FailedDraws int64 `json:"failed_draws"` } type poolBalanceDTO struct { AppCode string `json:"app_code"` PoolID string `json:"pool_id"` Balance int64 `json:"balance"` ReserveFloor int64 `json:"reserve_floor"` AvailableBalance int64 `json:"available_balance"` TotalIn int64 `json:"total_in"` TotalOut int64 `json:"total_out"` Materialized bool `json:"materialized"` UpdatedAtMS int64 `json:"updated_at_ms"` } func (h *Handler) GetLuckyGiftConfig(c *gin.Context) { if strings.TrimSpace(c.Query("app_code")) == "" { response.BadRequest(c, "app_code is required") return } ctx, cancel := h.luckyGiftContext(c) defer cancel() resp, err := h.luckyGift.GetLuckyGiftConfig(ctx, &luckygiftv1.GetLuckyGiftConfigRequest{ Meta: h.meta(c), PoolId: strings.TrimSpace(c.Query("pool_id")), }) if err != nil { response.ServerError(c, "获取幸运礼物配置失败") return } response.OK(c, configFromProto(resp.GetConfig())) } func (h *Handler) UpsertLuckyGiftConfig(c *gin.Context) { var req configRequest if err := c.ShouldBindJSON(&req); err != nil { response.BadRequest(c, "幸运礼物配置参数不正确") return } if req.PoolID == "" { req.PoolID = strings.TrimSpace(c.Query("pool_id")) } if req.AppCode == "" { req.AppCode = strings.TrimSpace(c.Query("app_code")) } if strings.TrimSpace(req.AppCode) == "" { response.BadRequest(c, "app_code is required") return } ctx, cancel := h.luckyGiftContext(c) defer cancel() resp, err := h.luckyGift.UpsertLuckyGiftConfig(ctx, &luckygiftv1.UpsertLuckyGiftConfigRequest{ Meta: h.metaForApp(c, req.AppCode), Config: configToProto(req), OperatorAdminId: int64(middleware.CurrentUserID(c)), }) if err != nil { response.BadRequest(c, err.Error()) return } item := configFromProto(resp.GetConfig()) shared.OperationLogWithResourceID(c, h.audit, "upsert-lucky-gift-config", "lucky_gift_rule_versions", item.PoolID, "success", "") response.OK(c, item) } func (h *Handler) ListLuckyGiftConfigs(c *gin.Context) { if strings.TrimSpace(c.Query("app_code")) == "" { response.BadRequest(c, "app_code is required") return } ctx, cancel := h.luckyGiftContext(c) defer cancel() resp, err := h.luckyGift.ListLuckyGiftConfigs(ctx, &luckygiftv1.ListLuckyGiftConfigsRequest{ Meta: h.meta(c), }) if err != nil { response.ServerError(c, "获取幸运礼物奖池列表失败") return } items := make([]configDTO, 0, len(resp.GetConfigs())) for _, config := range resp.GetConfigs() { items = append(items, configFromProto(config)) } response.OK(c, items) } func (h *Handler) ListLuckyGiftDraws(c *gin.Context) { if strings.TrimSpace(c.Query("app_code")) == "" { response.BadRequest(c, "app_code is required") return } options := shared.ListOptions(c) ctx, cancel := h.luckyGiftContext(c) defer cancel() resp, err := h.luckyGift.ListLuckyGiftDraws(ctx, &luckygiftv1.ListLuckyGiftDrawsRequest{ Meta: h.meta(c), PoolId: strings.TrimSpace(c.Query("pool_id")), GiftId: strings.TrimSpace(c.Query("gift_id")), UserId: parseOptionalInt64(c.Query("user_id")), RoomId: strings.TrimSpace(c.Query("room_id")), Status: strings.TrimSpace(options.Status), Page: int32(options.Page), PageSize: int32(options.PageSize), ExternalUserId: strings.TrimSpace(c.Query("external_user_id")), ExternalOnly: parseOptionalBool(c.Query("external_only")), }) if err != nil { response.ServerError(c, "获取幸运礼物抽奖记录失败") return } items := make([]drawDTO, 0, len(resp.GetDraws())) for _, draw := range resp.GetDraws() { items = append(items, drawFromProto(draw)) } response.OK(c, response.Page{Items: items, Page: options.Page, PageSize: options.PageSize, Total: resp.GetTotal()}) } func (h *Handler) GetLuckyGiftDrawSummary(c *gin.Context) { if strings.TrimSpace(c.Query("app_code")) == "" { response.BadRequest(c, "app_code is required") return } ctx, cancel := h.luckyGiftContext(c) defer cancel() resp, err := h.luckyGift.GetLuckyGiftDrawSummary(ctx, &luckygiftv1.GetLuckyGiftDrawSummaryRequest{ Meta: h.meta(c), PoolId: strings.TrimSpace(c.Query("pool_id")), GiftId: strings.TrimSpace(c.Query("gift_id")), UserId: parseOptionalInt64(c.Query("user_id")), RoomId: strings.TrimSpace(c.Query("room_id")), Status: strings.TrimSpace(c.Query("status")), ExternalUserId: strings.TrimSpace(c.Query("external_user_id")), ExternalOnly: parseOptionalBool(c.Query("external_only")), }) if err != nil { response.ServerError(c, "获取幸运礼物抽奖汇总失败") return } response.OK(c, drawSummaryFromProto(resp.GetSummary())) } func (h *Handler) ListLuckyGiftPoolBalances(c *gin.Context) { if strings.TrimSpace(c.Query("app_code")) == "" { response.BadRequest(c, "app_code is required") return } ctx, cancel := h.luckyGiftContext(c) defer cancel() resp, err := h.luckyGift.ListLuckyGiftPoolBalances(ctx, &luckygiftv1.ListLuckyGiftPoolBalancesRequest{ Meta: h.meta(c), PoolId: strings.TrimSpace(c.Query("pool_id")), }) if err != nil { response.ServerError(c, "获取幸运礼物奖池余额失败") return } items := make([]poolBalanceDTO, 0, len(resp.GetPools())) for _, pool := range resp.GetPools() { items = append(items, poolBalanceFromProto(pool)) } response.OK(c, gin.H{"items": items, "total": len(items)}) } func (h *Handler) meta(c *gin.Context) *luckygiftv1.RequestMeta { return h.metaForApp(c, "") } func (h *Handler) metaForApp(c *gin.Context, explicitAppCode string) *luckygiftv1.RequestMeta { appCode := strings.ToLower(strings.TrimSpace(explicitAppCode)) if appCode == "" { appCode = strings.ToLower(strings.TrimSpace(c.Query("app_code"))) } return &luckygiftv1.RequestMeta{ RequestId: middleware.CurrentRequestID(c), Caller: "admin-server", // ops-center 是 admin 的衍生平台,筛选 app_code 通过 query/body 传入; // 这里把它放进 Admin RPC meta,让 lucky-gift-service 继续作为 app 维度过滤和配置发布的唯一 owner。 AppCode: appCode, SentAtMs: time.Now().UTC().UnixMilli(), } } func (h *Handler) luckyGiftContext(c *gin.Context) (context.Context, context.CancelFunc) { return context.WithTimeout(c.Request.Context(), h.requestTimeout) } func configToProto(req configRequest) *luckygiftv1.LuckyGiftRuleConfig { stages := make([]*luckygiftv1.LuckyGiftRuleStage, 0, len(req.Stages)) for _, stage := range req.Stages { stageName := strings.TrimSpace(stage.Stage) tierIDCounts := make(map[string]int, len(stage.Tiers)) tiers := make([]*luckygiftv1.LuckyGiftRuleTier, 0, len(stage.Tiers)) for _, tier := range stage.Tiers { multiplierPPM := multiplierToPPM(tier.Multiplier) tiers = append(tiers, &luckygiftv1.LuckyGiftRuleTier{ Stage: stageName, // 奖档 ID 是 lucky-gift-service 持久化主键的一部分,不能让运营手工维护;admin-server 按阶段和倍率生成稳定 ID, // 同一阶段同倍率出现多行时追加序号,保证进入 lucky-gift-service 前已经满足唯一性。 TierId: generatedLuckyGiftTierID(stageName, multiplierPPM, tierIDCounts), MultiplierPpm: multiplierPPM, BaseWeightPpm: percentToPPM(tier.ProbabilityPercent), HighWaterOnly: tier.HighWaterOnly, Enabled: tier.Enabled, }) } stages = append(stages, &luckygiftv1.LuckyGiftRuleStage{ Stage: stageName, MinRecharge_7DCoins: stage.MinRecharge7DCoins, MinRecharge_30DCoins: stage.MinRecharge30DCoins, Tiers: tiers, }) } return &luckygiftv1.LuckyGiftRuleConfig{ AppCode: strings.ToLower(strings.TrimSpace(req.AppCode)), PoolId: strings.TrimSpace(req.PoolID), StrategyVersion: strings.ToLower(strings.TrimSpace(req.StrategyVersion)), Enabled: req.Enabled, TargetRtpPpm: percentToPPM(req.TargetRTPPercent), PoolRatePpm: percentToPPM(req.PoolRatePercent), ProfitRatePpm: percentToPPM(req.ProfitRatePercent), AnchorRatePpm: percentToPPM(req.AnchorRatePercent), SettlementWindowWager: req.SettlementWindowWager, ControlBandPpm: percentToPPM(req.ControlBandPercent), GiftPriceReference: req.GiftPriceReference, NoviceMaxEquivalentDraws: req.NoviceMaxEquivalentDraws, NormalMaxEquivalentDraws: req.NormalMaxEquivalentDraws, EffectiveFromMs: req.EffectiveFromMS, InitialPoolCoins: req.InitialPoolCoins, LossStreakGuarantee: req.LossStreakGuarantee, LowWatermarkCoins: req.LowWatermarkCoins, LowWaterNonzeroFactorPpm: percentToPPM(req.LowWaterNonzeroFactorPercent), HighWatermarkCoins: req.HighWatermarkCoins, HighWaterNonzeroFactorPpm: percentToPPM(req.HighWaterNonzeroFactorPercent), RechargeBoostWindowMs: req.RechargeBoostWindowMS, RechargeBoostFactorPpm: percentToPPM(req.RechargeBoostFactorPercent), JackpotMultiplierPpms: multipliersToPPM(req.JackpotMultipliers), JackpotGlobalRtpMaxPpm: percentToPPM(req.JackpotGlobalRTPMaxPercent), JackpotUserDayRtpMaxPpm: percentToPPM(req.JackpotUserDayRTPMaxPercent), JackpotUser_72HRtpMaxPpm: percentToPPM(req.JackpotUser72hRTPMaxPercent), JackpotSpendThresholdCoins: req.JackpotSpendThresholdCoins, MaxJackpotHitsPerUserDay: req.MaxJackpotHitsPerUserDay, MaxSinglePayout: req.MaxSinglePayout, UserHourlyPayoutCap: req.UserHourlyPayoutCap, UserDailyPayoutCap: req.UserDailyPayoutCap, DeviceDailyPayoutCap: req.DeviceDailyPayoutCap, RoomHourlyPayoutCap: req.RoomHourlyPayoutCap, AnchorDailyPayoutCap: req.AnchorDailyPayoutCap, Stages: stages, } } func configFromProto(config *luckygiftv1.LuckyGiftRuleConfig) configDTO { if config == nil { return configDTO{} } stages := make([]stageDTO, 0, len(config.GetStages())) for _, stage := range config.GetStages() { tiers := make([]tierDTO, 0, len(stage.GetTiers())) for _, tier := range stage.GetTiers() { tiers = append(tiers, tierDTO{ TierID: tier.GetTierId(), Multiplier: ppmToMultiplier(tier.GetMultiplierPpm()), ProbabilityPercent: ppmToPercent(tier.GetBaseWeightPpm()), HighWaterOnly: tier.GetHighWaterOnly(), Enabled: tier.GetEnabled(), }) } stages = append(stages, stageDTO{ Stage: stage.GetStage(), MinRecharge7DCoins: stage.GetMinRecharge_7DCoins(), MinRecharge30DCoins: stage.GetMinRecharge_30DCoins(), Tiers: tiers, }) } poolID := strings.TrimSpace(config.GetPoolId()) return configDTO{ RuleVersion: config.GetRuleVersion(), CreatedByAdminID: config.GetCreatedByAdminId(), CreatedAtMS: config.GetCreatedAtMs(), configRequest: configRequest{ AppCode: config.GetAppCode(), PoolID: poolID, StrategyVersion: config.GetStrategyVersion(), Enabled: config.GetEnabled(), TargetRTPPercent: ppmToPercent(config.GetTargetRtpPpm()), PoolRatePercent: ppmToPercent(config.GetPoolRatePpm()), ProfitRatePercent: ppmToPercent(config.GetProfitRatePpm()), AnchorRatePercent: ppmToPercent(config.GetAnchorRatePpm()), SettlementWindowWager: config.GetSettlementWindowWager(), ControlBandPercent: ppmToPercent(config.GetControlBandPpm()), GiftPriceReference: config.GetGiftPriceReference(), NoviceMaxEquivalentDraws: config.GetNoviceMaxEquivalentDraws(), NormalMaxEquivalentDraws: config.GetNormalMaxEquivalentDraws(), EffectiveFromMS: config.GetEffectiveFromMs(), InitialPoolCoins: config.GetInitialPoolCoins(), LossStreakGuarantee: config.GetLossStreakGuarantee(), LowWatermarkCoins: config.GetLowWatermarkCoins(), LowWaterNonzeroFactorPercent: ppmToPercent(config.GetLowWaterNonzeroFactorPpm()), HighWatermarkCoins: config.GetHighWatermarkCoins(), HighWaterNonzeroFactorPercent: ppmToPercent(config.GetHighWaterNonzeroFactorPpm()), RechargeBoostWindowMS: config.GetRechargeBoostWindowMs(), RechargeBoostFactorPercent: ppmToPercent(config.GetRechargeBoostFactorPpm()), JackpotMultipliers: ppmsToMultipliers(config.GetJackpotMultiplierPpms()), JackpotGlobalRTPMaxPercent: ppmToPercent(config.GetJackpotGlobalRtpMaxPpm()), JackpotUserDayRTPMaxPercent: ppmToPercent(config.GetJackpotUserDayRtpMaxPpm()), JackpotUser72hRTPMaxPercent: ppmToPercent(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 generatedLuckyGiftTierID(stage string, multiplierPPM int64, counts map[string]int) string { baseID := luckyGiftTierIDBase(stage, multiplierPPM) counts[baseID]++ if counts[baseID] == 1 { return baseID } return fmt.Sprintf("%s_%d", baseID, counts[baseID]) } func luckyGiftTierIDBase(stage string, multiplierPPM int64) string { stage = strings.TrimSpace(stage) if multiplierPPM <= 0 { return stage + "_none" } whole := multiplierPPM / 1_000_000 fraction := multiplierPPM % 1_000_000 if fraction == 0 { return fmt.Sprintf("%s_%dx", stage, whole) } fractionText := strings.TrimRight(fmt.Sprintf("%06d", fraction), "0") return fmt.Sprintf("%s_%d_%sx", stage, whole, fractionText) } func percentToPPM(value float64) int64 { return int64(math.Round(value * 10_000)) } func ppmToPercent(value int64) float64 { return float64(value) / 10_000 } func multiplierToPPM(value float64) int64 { return int64(math.Round(value * 1_000_000)) } func multipliersToPPM(values []float64) []int64 { out := make([]int64, 0, len(values)) for _, value := range values { out = append(out, multiplierToPPM(value)) } return out } func ppmToMultiplier(value int64) float64 { return float64(value) / 1_000_000 } func ppmsToMultipliers(values []int64) []float64 { out := make([]float64, 0, len(values)) for _, value := range values { out = append(out, ppmToMultiplier(value)) } return out } func drawFromProto(draw *luckygiftv1.LuckyGiftDrawResult) drawDTO { if draw == nil { return drawDTO{} } return drawDTO{ DrawID: draw.GetDrawId(), CommandID: draw.GetCommandId(), PoolID: draw.GetPoolId(), GiftID: draw.GetGiftId(), RuleVersion: draw.GetRuleVersion(), ExperiencePool: draw.GetExperiencePool(), SelectedTierID: draw.GetSelectedTierId(), MultiplierPPM: draw.GetMultiplierPpm(), BaseRewardCoins: draw.GetBaseRewardCoins(), EffectiveRewardCoins: draw.GetEffectiveRewardCoins(), RewardStatus: draw.GetRewardStatus(), RTPWindowIndex: draw.GetRtpWindowIndex(), GiftRTPWindowIndex: draw.GetGiftRtpWindowIndex(), GlobalBaseRTPPPM: draw.GetGlobalBaseRtpPpm(), GiftBaseRTPPPM: draw.GetGiftBaseRtpPpm(), StageFeedback: draw.GetStageFeedback(), HighMultiplier: draw.GetHighMultiplier(), CreatedAtMS: draw.GetCreatedAtMs(), AppCode: draw.GetAppCode(), UserID: draw.GetUserId(), ExternalUserID: draw.GetExternalUserId(), } } func drawSummaryFromProto(summary *luckygiftv1.LuckyGiftDrawSummary) drawSummaryDTO { if summary == nil { return drawSummaryDTO{} } return drawSummaryDTO{ PoolID: summary.GetPoolId(), TotalDraws: summary.GetTotalDraws(), UniqueUsers: summary.GetUniqueUsers(), UniqueRooms: summary.GetUniqueRooms(), TotalSpentCoins: summary.GetTotalSpentCoins(), TotalRewardCoins: summary.GetTotalRewardCoins(), BaseRewardCoins: summary.GetBaseRewardCoins(), ActualRTPPPM: summary.GetActualRtpPpm(), PendingDraws: summary.GetPendingDraws(), GrantedDraws: summary.GetGrantedDraws(), FailedDraws: summary.GetFailedDraws(), } } func poolBalanceFromProto(pool *luckygiftv1.LuckyGiftPoolBalance) poolBalanceDTO { if pool == nil { return poolBalanceDTO{} } return poolBalanceDTO{ AppCode: pool.GetAppCode(), PoolID: pool.GetPoolId(), Balance: pool.GetBalance(), ReserveFloor: pool.GetReserveFloor(), AvailableBalance: pool.GetAvailableBalance(), TotalIn: pool.GetTotalIn(), TotalOut: pool.GetTotalOut(), Materialized: pool.GetMaterialized(), UpdatedAtMS: pool.GetUpdatedAtMs(), } } func parseOptionalInt64(value string) int64 { value = strings.TrimSpace(value) if value == "" { return 0 } var result int64 for _, char := range value { if char < '0' || char > '9' { return 0 } result = result*10 + int64(char-'0') } return result } func parseOptionalBool(value string) bool { switch strings.ToLower(strings.TrimSpace(value)) { case "1", "true", "yes", "y": return true default: return false } }