syntax = "proto3"; package hyapp.luckygift.v1; option go_package = "hyapp.local/api/proto/luckygift/v1;luckygiftv1"; // RequestMeta 是 lucky-gift-service 的内部追踪元信息。 // request_id 只用于链路排查,不能被任何业务流程当成幂等键使用。 message RequestMeta { string request_id = 1; string caller = 2; string gateway_node_id = 3; int64 sent_at_ms = 4; string app_code = 5; } // LuckyGiftMeta 是 HyApp 内部房间送礼扣费成功后的抽奖事实输入。 // 金额、目标用户和 command_id 必须来自 wallet-service 已落账的 receipt,不能从客户端 payload 推导。 message LuckyGiftMeta { RequestMeta meta = 1; string command_id = 2; int64 user_id = 3; string device_id = 4; string room_id = 5; string anchor_id = 6; string gift_id = 7; int64 coin_spent = 8; int64 paid_at_ms = 9; string pool_id = 10; int64 target_user_id = 11; int32 gift_count = 12; int64 visible_region_id = 13; int64 country_id = 14; // sender_* 是 room-service 在送礼入口已经确认的展示快照,只进入中奖事实和 IM,不能参与抽奖或账务判断。 string sender_name = 15; string sender_avatar = 16; string sender_display_user_id = 17; string sender_pretty_display_user_id = 18; // 充值分层与充值后短时加权必须由调用方传入已确认的用户事实;幸运礼物服务不跨库回查用户或支付明细。 int64 recharge_7d_coins = 19; int64 recharge_30d_coins = 20; int64 last_recharged_at_ms = 21; // gift_income_coins 是 wallet 已实际入账给收礼人的收益金币;dynamic_v3 用它校验规则主播拆账,缺失或比例不一致均拒绝开奖。 int64 gift_income_coins = 22; // user_registered_at_ms 是 user-service 在送礼入口返回的账号创建事实快照。 // dynamic_v3 只用它判断闭窗时账号是否已满 48 小时;缺失时普通开奖继续,但补偿大奖资格 fail-close。 int64 user_registered_at_ms = 23; } message LuckyGiftRuleTier { string stage = 1; string tier_id = 2; int64 multiplier_ppm = 3; int64 base_weight_ppm = 4; bool high_water_only = 5; bool enabled = 6; } message LuckyGiftRuleStage { string stage = 1; repeated LuckyGiftRuleTier tiers = 2; // 两个充值门槛共同决定 dynamic_v3 分层;novice 固定为 0/0,后续阶段逐维单调递增。 int64 min_recharge_7d_coins = 3; int64 min_recharge_30d_coins = 4; } message LuckyGiftRuleConfig { string app_code = 1; string pool_id = 2; int64 rule_version = 3; bool enabled = 4; int64 target_rtp_ppm = 5; int64 pool_rate_ppm = 6; int64 settlement_window_wager = 7; int64 control_band_ppm = 8; int64 gift_price_reference = 9; int64 novice_max_equivalent_draws = 10; int64 normal_max_equivalent_draws = 11; int64 effective_from_ms = 12; int64 created_by_admin_id = 13; int64 created_at_ms = 14; repeated LuckyGiftRuleStage stages = 15; // strategy_version 隔离历史 fixed_v2 与动态算法 dynamic_v3,避免新增约束破坏已经发布的旧规则。 string strategy_version = 16; int64 profit_rate_ppm = 17; int64 anchor_rate_ppm = 18; int64 initial_pool_coins = 19; int64 loss_streak_guarantee = 20; int64 low_watermark_coins = 21; int64 low_water_nonzero_factor_ppm = 22; int64 high_watermark_coins = 23; int64 high_water_nonzero_factor_ppm = 24; int64 recharge_boost_window_ms = 25; int64 recharge_boost_factor_ppm = 26; repeated int64 jackpot_multiplier_ppms = 27; int64 jackpot_global_rtp_max_ppm = 28; int64 jackpot_user_round_rtp_max_ppm = 29; int64 jackpot_user_48h_rtp_max_ppm = 30; // 用户在同一应用、奖池和 UTC 自然日每累计消费满该金币数,生成一次可跨日保留的消费大奖资格。 int64 jackpot_spend_threshold_coins = 31; int64 max_jackpot_hits_per_user_day = 32; int64 max_single_payout = 33; int64 user_hourly_payout_cap = 34; int64 user_daily_payout_cap = 35; int64 device_daily_payout_cap = 36; int64 room_hourly_payout_cap = 37; int64 anchor_daily_payout_cap = 38; // 用户严格滚动 24 小时 RTP 超过此值时,普通非零奖档按下方因子降权;0 表示关闭该策略。 int64 user_24h_rtp_threshold_ppm = 39; // 触发用户 24 小时 RTP 降权后保留的普通中奖权重;1000000 表示保持原概率。 int64 user_24h_ordinary_win_factor_ppm = 40; // fixed_v2 高水位返奖加权:奖池水位越过阈值后中奖返奖按系数放大(RTP 等比放大), // 滞回保持到水位跌破恢复线才退出。0 表示关闭;dynamic_v3 必须保持三个字段为 0。 int64 v2_high_water_boost_threshold_coins = 41; // 加权系数 ppm;1500000 表示 RTP 变为原值的 150%。启用时必须大于 1000000。 int64 v2_high_water_boost_factor_ppm = 42; // 恢复水位金币;水位低于该值后退出加权。启用时必须为正且不高于阈值。 int64 v2_high_water_boost_recover_coins = 43; } message CheckLuckyGiftRequest { RequestMeta meta = 1; int64 user_id = 2; string room_id = 3; string gift_id = 4; string pool_id = 5; } message CheckLuckyGiftResponse { bool enabled = 1; string reason = 2; string gift_id = 3; int64 gift_price = 4; int64 rule_version = 5; int64 target_rtp_ppm = 6; string experience_pool = 7; string pool_id = 8; // strategy_version 由 lucky-gift owner 返回并由 room saga 固化,用于滚动发布时区分 fixed_v2 兼容与 dynamic_v3 强校验。 string strategy_version = 9; } message LuckyGiftDrawResult { string draw_id = 1; string command_id = 2; string gift_id = 3; int64 rule_version = 4; string experience_pool = 5; string selected_tier_id = 6; int64 base_reward_coins = 7; int64 effective_reward_coins = 8; string reward_status = 9; int64 rtp_window_index = 10; int64 gift_rtp_window_index = 11; int64 global_base_rtp_ppm = 12; int64 gift_base_rtp_ppm = 13; bool stage_feedback = 14; bool high_multiplier = 15; int64 created_at_ms = 16; string pool_id = 17; int64 multiplier_ppm = 18; string wallet_transaction_id = 19; int64 coin_balance_after = 20; int64 user_id = 21; string external_user_id = 22; string app_code = 23; repeated LuckyGiftHit hits = 24; } message ExecuteLuckyGiftDrawRequest { LuckyGiftMeta lucky_gift = 1; } message ExecuteLuckyGiftDrawResponse { LuckyGiftDrawResult result = 1; } message BatchExecuteLuckyGiftDrawRequest { repeated LuckyGiftMeta lucky_gifts = 1; } message BatchExecuteLuckyGiftDrawResponse { repeated LuckyGiftDrawResult results = 1; } // ExternalGiftDrawRequest 是外部 App 接入的抽奖输入。 // App 调用前必须已经完成自己的扣费,lucky-gift-service 只校验金额事实并返回中奖结果。 message ExternalGiftDrawRequest { RequestMeta meta = 1; string app_code = 3; string external_user_id = 4; string request_id = 5; int64 gift_count = 6; int64 unit_amount = 7; int64 total_amount = 8; string currency = 9; int64 paid_at_ms = 10; string metadata_json = 11; string pool_id = 12; // dynamic_v3 必填的稳定设备作用域。luck-gateway 不独立证明其真实性,调用方必须在自己的认证/签名边界内绑定该值;fixed_v2 允许缺失。 string device_id = 13; // user_registered_at_ms 由外部 App 按自己的用户主数据提供;dynamic_v3 缺失或晚于支付时间时补偿大奖资格 fail-close。 int64 user_registered_at_ms = 14; } message ExternalGiftDrawResponse { string draw_id = 1; string request_id = 2; string app_code = 4; string external_user_id = 5; int64 gift_count = 6; int64 unit_amount = 7; int64 total_amount = 8; int64 reward_amount = 9; int64 multiplier_ppm = 10; string reward_status = 11; int64 rule_version = 12; int64 created_at_ms = 13; } message GetLuckyGiftConfigRequest { RequestMeta meta = 1; string pool_id = 2; } message GetLuckyGiftConfigResponse { LuckyGiftRuleConfig config = 1; } message UpsertLuckyGiftConfigRequest { RequestMeta meta = 1; LuckyGiftRuleConfig config = 2; int64 operator_admin_id = 3; } message UpsertLuckyGiftConfigResponse { LuckyGiftRuleConfig config = 1; } message ListLuckyGiftConfigsRequest { RequestMeta meta = 1; } message ListLuckyGiftConfigsResponse { repeated LuckyGiftRuleConfig configs = 1; } message ListLuckyGiftDrawsRequest { RequestMeta meta = 1; string gift_id = 2; int64 user_id = 3; string room_id = 4; string status = 5; int32 page = 6; int32 page_size = 7; string pool_id = 8; string external_user_id = 10; bool external_only = 11; } message ListLuckyGiftDrawsResponse { repeated LuckyGiftDrawResult draws = 1; int64 total = 2; } message LuckyGiftDrawSummary { string pool_id = 1; int64 total_draws = 2; int64 unique_users = 3; int64 unique_rooms = 4; int64 total_spent_coins = 5; int64 total_reward_coins = 6; int64 base_reward_coins = 7; int64 actual_rtp_ppm = 8; int64 pending_draws = 9; int64 granted_draws = 10; int64 failed_draws = 11; } message LuckyGiftPoolBalance { string app_code = 1; string pool_id = 2; int64 balance = 3; int64 reserve_floor = 4; int64 available_balance = 5; int64 total_in = 6; int64 total_out = 7; bool materialized = 8; int64 updated_at_ms = 9; string strategy_version = 10; int64 manual_credit_total = 11; int64 manual_debit_total = 12; } message GetLuckyGiftDrawSummaryRequest { RequestMeta meta = 1; string gift_id = 2; int64 user_id = 3; string room_id = 4; string status = 5; string pool_id = 6; string external_user_id = 8; bool external_only = 9; } message GetLuckyGiftDrawSummaryResponse { LuckyGiftDrawSummary summary = 1; } message ListLuckyGiftPoolBalancesRequest { RequestMeta meta = 1; string pool_id = 2; string strategy_version = 3; } message ListLuckyGiftPoolBalancesResponse { repeated LuckyGiftPoolBalance pools = 1; } // LuckyGiftExperimentOverride 把单个用户强制固定到某个实验组,用于放量前的白名单验证。 // user_key:内部用户为十进制 user_id 字符串;外部用户为 "ext:" + external_user_id。 message LuckyGiftExperimentOverride { string user_key = 1; string arm = 2; } // LuckyGiftExperimentArmStats 是实验开始以来单组的抽奖聚合,由 lucky_draw_records 按 rule_version 现算,非物化口径。 message LuckyGiftExperimentArmStats { string arm = 1; int64 rule_version = 2; string strategy_version = 3; int64 total_draws = 4; int64 unique_users = 5; int64 total_spent_coins = 6; int64 total_reward_coins = 7; int64 actual_rtp_ppm = 8; } // LuckyGiftExperiment 是单奖池 AB 实验事实:两组各钉住一个不可变 rule_version, // 运行时按 bucket_salt+user 哈希分流;同一奖池同时最多一个未完成实验。 message LuckyGiftExperiment { string app_code = 1; string pool_id = 2; string experiment_id = 3; string name = 4; int64 control_rule_version = 5; int64 treatment_rule_version = 6; // treatment 组流量比例(ppm,0..1000000);0 表示只有白名单 override 能进入 treatment。 int64 treatment_traffic_ppm = 7; string bucket_salt = 8; // running / paused / completed;paused 时全部流量(含白名单)回落 control。 string status = 9; string winner_arm = 10; int64 winner_rule_version = 11; // 结束实验时把获胜配置重发布成的新最新版本,全量流量由它承接。 int64 final_rule_version = 12; int64 created_by_admin_id = 13; int64 started_at_ms = 14; int64 ended_at_ms = 15; int64 updated_at_ms = 16; repeated LuckyGiftExperimentOverride overrides = 17; repeated LuckyGiftExperimentArmStats arm_stats = 18; } // CreateLuckyGiftExperimentRequest 在一个事务里发布 treatment 新版本并登记实验, // 避免"先发布后建实验"窗口期内新版本被最新版本语义全量放开。 // control 组自动钉住当前最新已启用版本。 message CreateLuckyGiftExperimentRequest { RequestMeta meta = 1; string name = 2; LuckyGiftRuleConfig treatment_config = 3; int64 treatment_traffic_ppm = 4; int64 operator_admin_id = 5; repeated LuckyGiftExperimentOverride overrides = 6; } message CreateLuckyGiftExperimentResponse { LuckyGiftExperiment experiment = 1; LuckyGiftRuleConfig treatment_config = 2; } message ListLuckyGiftExperimentsRequest { RequestMeta meta = 1; string pool_id = 2; string status = 3; bool with_stats = 4; } message ListLuckyGiftExperimentsResponse { repeated LuckyGiftExperiment experiments = 1; } // UpdateLuckyGiftExperimentRequest 调整放量或暂停/恢复。 // treatment_traffic_ppm 使用显式存在性语义:未设置表示不修改流量,避免 proto3 缺省 0 被误解释成"放量清零"; // status 为空表示不修改状态。 message UpdateLuckyGiftExperimentRequest { RequestMeta meta = 1; string experiment_id = 2; optional int64 treatment_traffic_ppm = 3; string status = 4; int64 operator_admin_id = 5; } message UpdateLuckyGiftExperimentResponse { LuckyGiftExperiment experiment = 1; } message SetLuckyGiftExperimentOverridesRequest { RequestMeta meta = 1; string experiment_id = 2; repeated LuckyGiftExperimentOverride upserts = 3; repeated string removes = 4; int64 operator_admin_id = 5; } message SetLuckyGiftExperimentOverridesResponse { LuckyGiftExperiment experiment = 1; } // EndLuckyGiftExperimentRequest 结束实验:把 winner_arm 钉住的配置快照重发布为新的最新版本, // 同事务内完成实验收口,保证结束瞬间的版本收敛没有中间态。 message EndLuckyGiftExperimentRequest { RequestMeta meta = 1; string experiment_id = 2; string winner_arm = 3; int64 operator_admin_id = 4; } message EndLuckyGiftExperimentResponse { LuckyGiftExperiment experiment = 1; LuckyGiftRuleConfig final_config = 2; } service LuckyGiftService { rpc CheckLuckyGift(CheckLuckyGiftRequest) returns (CheckLuckyGiftResponse); rpc ExecuteLuckyGiftDraw(ExecuteLuckyGiftDrawRequest) returns (ExecuteLuckyGiftDrawResponse); rpc BatchExecuteLuckyGiftDraw(BatchExecuteLuckyGiftDrawRequest) returns (BatchExecuteLuckyGiftDrawResponse); rpc ExecuteExternalGiftDraw(ExternalGiftDrawRequest) returns (ExternalGiftDrawResponse); } service AdminLuckyGiftService { rpc GetLuckyGiftConfig(GetLuckyGiftConfigRequest) returns (GetLuckyGiftConfigResponse); rpc UpsertLuckyGiftConfig(UpsertLuckyGiftConfigRequest) returns (UpsertLuckyGiftConfigResponse); rpc ListLuckyGiftConfigs(ListLuckyGiftConfigsRequest) returns (ListLuckyGiftConfigsResponse); rpc ListLuckyGiftDraws(ListLuckyGiftDrawsRequest) returns (ListLuckyGiftDrawsResponse); rpc GetLuckyGiftDrawSummary(GetLuckyGiftDrawSummaryRequest) returns (GetLuckyGiftDrawSummaryResponse); rpc ListLuckyGiftPoolBalances(ListLuckyGiftPoolBalancesRequest) returns (ListLuckyGiftPoolBalancesResponse); rpc AdjustLuckyGiftPoolBalance(AdjustLuckyGiftPoolBalanceRequest) returns (AdjustLuckyGiftPoolBalanceResponse); rpc CreateLuckyGiftExperiment(CreateLuckyGiftExperimentRequest) returns (CreateLuckyGiftExperimentResponse); rpc ListLuckyGiftExperiments(ListLuckyGiftExperimentsRequest) returns (ListLuckyGiftExperimentsResponse); rpc UpdateLuckyGiftExperiment(UpdateLuckyGiftExperimentRequest) returns (UpdateLuckyGiftExperimentResponse); rpc SetLuckyGiftExperimentOverrides(SetLuckyGiftExperimentOverridesRequest) returns (SetLuckyGiftExperimentOverridesResponse); rpc EndLuckyGiftExperiment(EndLuckyGiftExperimentRequest) returns (EndLuckyGiftExperimentResponse); rpc ListLuckyGiftUserProfiles(ListLuckyGiftUserProfilesRequest) returns (ListLuckyGiftUserProfilesResponse); rpc GetLuckyGiftUserProfile(GetLuckyGiftUserProfileRequest) returns (GetLuckyGiftUserProfileResponse); rpc ListLuckyGiftDailyStats(ListLuckyGiftDailyStatsRequest) returns (ListLuckyGiftDailyStatsResponse); } // LuckyGiftHit 只返回批量逐份开奖中实际中奖的精简位置,不复制全部审计明细。 // 声明追加在文件尾以保持既有 protobuf message index 稳定。 message LuckyGiftHit { int32 gift_index = 1; string draw_id = 2; string selected_tier_id = 3; int64 multiplier_ppm = 4; int64 reward_coins = 5; } // 调账消息也只追加在文件尾,保持所有既有 protobuf message index 稳定。 // adjustment_id 是业务幂等键;RequestMeta.request_id 只做链路追踪,不能替代资金幂等语义。 message AdjustLuckyGiftPoolBalanceRequest { RequestMeta meta = 1; string pool_id = 2; string strategy_version = 3; string adjustment_id = 4; string direction = 5; int64 amount_coins = 6; string reason = 7; int64 operator_admin_id = 8; } message LuckyGiftPoolAdjustment { string adjustment_id = 1; string app_code = 2; string pool_id = 3; string strategy_version = 4; string direction = 5; int64 amount_coins = 6; string reason = 7; int64 operator_admin_id = 8; int64 balance_before = 9; int64 balance_after = 10; int64 created_at_ms = 11; } message AdjustLuckyGiftPoolBalanceResponse { LuckyGiftPoolAdjustment adjustment = 1; LuckyGiftPoolBalance pool = 2; bool idempotent_replay = 3; } // 用户画像协议追加在文件尾,避免改变既有 message index。外部策略哈希只存在于 owner 数据库, // 本协议永远只下发 external_user_id,防止运营误把不可逆内部键当成外部用户身份。 message LuckyGiftUserProfileWindow { int64 draws = 1; int64 wager_coins = 2; int64 payout_coins = 3; int64 net_coins = 4; int64 rtp_ppm = 5; bool has_rtp = 6; } message LuckyGiftUserProfile { string app_code = 1; string pool_id = 2; string identity_type = 3; int64 internal_user_id = 4; string external_user_id = 5; int64 rule_version = 6; string strategy_version = 7; string stage = 8; int64 paid_draws = 9; int64 equivalent_draws = 10; int64 loss_streak = 11; int64 pending_spend_jackpot_tokens = 12; int64 guarantee_draws_remaining = 13; bool downweight_active = 14; int64 downweight_factor_ppm = 15; LuckyGiftUserProfileWindow one_hour = 16; LuckyGiftUserProfileWindow twenty_four_hours = 17; LuckyGiftUserProfileWindow forty_eight_hours = 18; LuckyGiftUserProfileWindow lifetime = 19; int64 base_reward_coins = 20; int64 ordinary_win_count = 21; int64 high_multiplier_ordinary_win_count = 22; int64 rtp_compensation_jackpot_count = 23; int64 cumulative_spend_jackpot_count = 24; int64 guarantee_hit_count = 25; int64 zero_draw_count = 26; int64 granted_draw_count = 27; int64 pending_draw_count = 28; int64 failed_draw_count = 29; int64 max_multiplier_ppm = 30; int64 max_reward_coins = 31; int64 first_draw_at_ms = 32; int64 last_draw_at_ms = 33; int64 aggregated_at_ms = 34; int64 data_lag_ms = 35; int64 user_24h_rtp_threshold_ppm = 36; } message ListLuckyGiftUserProfilesRequest { RequestMeta meta = 1; string pool_id = 2; string identity_type = 3; int64 internal_user_id = 4; string external_user_id = 5; string stage = 6; string status = 7; string sort_by = 8; string sort_direction = 9; int32 page = 10; int32 page_size = 11; // cursor 是 owner service 生成的不透明 keyset 游标;page 仅保留旧客户端协议兼容,服务端不再执行 OFFSET。 string cursor = 12; } message ListLuckyGiftUserProfilesResponse { repeated LuckyGiftUserProfile profiles = 1; int64 total = 2; int64 snapshot_at_ms = 3; string next_cursor = 4; bool has_more = 5; } message GetLuckyGiftUserProfileRequest { RequestMeta meta = 1; string pool_id = 2; string identity_type = 3; int64 internal_user_id = 4; string external_user_id = 5; int32 jackpot_page = 6; int32 jackpot_page_size = 7; // jackpot_cursor 由上一页 next_jackpot_cursor 原样返回;jackpot_page 仅保留协议兼容。 string jackpot_cursor = 8; } message LuckyGiftUserMultiplierStat { string hit_type = 1; int64 multiplier_ppm = 2; int64 draw_count = 3; int64 wager_coins = 4; int64 payout_coins = 5; } message LuckyGiftUserJackpotCondition { string name = 1; int64 numerator = 2; int64 denominator = 3; int64 ratio_ppm = 4; int64 limit_ppm = 5; int64 factor_ppm = 6; bool passed = 7; string reason = 8; } message LuckyGiftUserJackpotHit { string draw_id = 1; string request_id = 2; int64 rule_version = 3; string stage = 4; string mechanism = 5; string reason_code = 6; string reason_summary = 7; string tier_id = 8; int64 multiplier_ppm = 9; int64 wager_coins = 10; int64 payout_coins = 11; int64 occurred_at_ms = 12; repeated LuckyGiftUserJackpotCondition conditions = 13; // event_key 只标识这条画像大奖事实,供前端稳定追加去重;不包含用户策略键或用户哈希。 string event_key = 14; } message GetLuckyGiftUserProfileResponse { LuckyGiftUserProfile profile = 1; repeated LuckyGiftUserMultiplierStat multiplier_distribution = 2; repeated LuckyGiftUserJackpotHit jackpot_hits = 3; int64 jackpot_hit_total = 4; int64 snapshot_at_ms = 5; int32 jackpot_page = 6; int32 jackpot_page_size = 7; string next_jackpot_cursor = 8; bool jackpot_has_more = 9; } // ListLuckyGiftDailyStats 面向 Databi 的按日聚合:只读 lucky_draw_pool_day_stats 读模型 // (北京日 +08:00 分桶,由后台游标增量刷新),不扫抽奖事实表,避免 BI 访问打热事实库。 // strategy_version 过滤用于按策略族拆分展示(如幸运礼物V3 tab 只看 dynamic_v3)。 message ListLuckyGiftDailyStatsRequest { RequestMeta meta = 1; // pool_id 为空表示全部奖池汇总。 string pool_id = 2; // strategy_version 为空表示不按策略过滤;取值 fixed_v2 / dynamic_v3。 string strategy_version = 3; // [start_ms, end_ms) UTC epoch ms;服务端按北京日换算成 stat_day 闭区间。 int64 start_ms = 4; int64 end_ms = 5; // region_ids 为空表示全部区域;按房间可见区域 ID 过滤(0=GLOBAL 房间只在不过滤时计入)。 repeated int64 region_ids = 6; } message LuckyGiftDailyStat { // stat_day 是北京日(+08:00)YYYY-MM-DD。 string stat_day = 1; int64 draw_count = 2; int64 turnover_coin = 3; // payout_coin 为用户可见返奖(effective),与既有 DrawSummary 口径一致,含未发放/失败行。 int64 payout_coin = 4; int64 base_reward_coin = 5; } message ListLuckyGiftDailyStatsResponse { repeated LuckyGiftDailyStat stats = 1; }