# 幸运补偿接口

**lucky_compensation**

幸运补偿是**限时触发型**活动：玩家一笔**支付通道存款**到账后，服务端开一个观察窗口（默认 30 分钟）；窗口内已结算注单的净亏损 ≥（到账金额 + 随该笔存款发放的赠金）× 判定比例（默认 90%）时命中，按**这笔存款金额**命中一个补偿档位，在该档的比例区间内抽定补偿金额、在该档的倍数区间内抽定幸运倍数，生成一张**待开箱**的补偿（offer）。

玩家分两步拿：

1. **开宝箱**（`claim`）：开箱时限内点开宝箱，揭示补偿本金并即时入账（打码按命中档位），随后进入**待加赠**态并开始加赠倒计时
2. **幸运加赠**：加赠有效期内，在存款页选中本活动（创建充值订单传 `source=lucky_compensation`），新存款 ≥ 加赠最低存款并到账时，与本金**同一入账事务**全额发放 `补偿 × 幸运倍数`（固定金额，不随新存款金额缩放、不封顶）

与「[每日幸运补偿](417_promotion_daily_bankrupt_pack.md)」（`daily_bankrupt_pack`）是**两个独立活动**，`kind`、充值 `source`、接口字段互不相干。

本金计入现金余额 `user_money` 并建立不可放弃的本金任务；补偿与加赠有码时计入奖金余额 `bonus_money` 并建立奖励任务，无码计入 `user_money`。任务倍数与放弃权限在发奖时固化，按 [任务列表](201_wager_tasks.md#list) 返回值展示。整个账户须满足 [全部任务与旧打码门槛](400_promotion.md#wager_rules) 才能通过提现打码校验。

命中那一刻会通过【[实时提示推送（SSE）](510_message.md#realtime-promotion-trigger)】推一条 `type=promotion_trigger` / `data.kind=lucky_compensation` 的 toast，游戏内收到后**直接覆盖在游戏画面上弹宝箱**；不在线时下次拉【[站点配置](500_site_content.md#config)】的 `popups` 补弹。`list` / `detail` / `claim` 走通用活动接口（`kind=lucky_compensation`，详见 [活动接口](400_promotion.md)）。

---

## 生命周期 {#lifecycle}

```
无资格 ──存款到账(≥最低触发存款，当日次数未满)──▶ 观察中
观察中 ──窗口期满未达线──▶ 无资格
观察中 ──净亏损 ≥ 判定基数×判定比例──▶ 待开箱(pending)      ← 推 promotion_trigger
待开箱 ──claim 开箱，补偿入账──▶ 待加赠(boostable)
待开箱 ──开箱倒计时归零──▶ 未开箱过期(expired)：补偿与加赠一起作废
待加赠 ──source=lucky_compensation 的新存款到账──▶ 已加赠(boosted)
待加赠 ──加赠倒计时归零──▶ 加赠过期(boost_expired)：已领的补偿本金不收回
```

规则要点：

- **触发存款**仅限玩家经支付通道完成并到账的订单（任意 `source`，含本活动加赠那一笔）；后台人工上分、后台赠送不开窗口
- **首充有效期**：商户配置 `first_deposit_valid_hours > 0` 时，只有玩家首次充值成功后 N 小时内到账的存款才开窗口（首充那一笔本身算在内），超过后老玩家不再触发；已生成的在途补偿不受影响
- **判定基数** = 到账金额 + 随该笔存款一起发放的全部赠金（套餐赠送 / 其它存款奖金 / 本活动加赠）；**补偿基数**只取存款到账金额
- **净亏损**统计全部游戏（投注 − 派彩 − 退款），以结算落账时刻归属窗口，窗口关闭后结算的不计。服务端在玩家余额下降后约 **3 秒无新变动**时判定，连续投注时计时不断重置，**停手后**才判定
- **一次只持一张**：待开箱 / 待加赠期间再次命中不生成新补偿、不计次；多笔存款的窗口同时命中只生成一张，按金额较大的那笔存款计算
- **每日上限**：每人每自然日（商户时区）最多生成 N 次（默认 2），未开箱过期 / 加赠过期 / 已加赠都算用掉一次；次数用完当天不再开窗口
- **开箱前不露金额**：待开箱态的 `compensation_amount` / `multiplier` / `boost_amount` 一律下发 `"0"` / `0`，开箱成功后才揭示
- 两段倒计时都以服务端 `*_expire_at − server_now` 计算，**不要用客户端本地时钟**；服务端对截止时刻有秒级宽限（供存款到账时钟对齐），前端不应依赖，倒计时归零即收起入口。刚过截止、仍在宽限内时 `detail` 按 `offer_status=none` 下发，避免弹「已过期」

---

## Offer 对象 {#offer}

`detail` / `claim` 响应、`popups` 元素与 `/promotion/list` 的 `triggered[].payload` 共用以下字段：

字段 | 类型 | 说明
---|---|---
offer_id | string | 补偿 ID（Snowflake，字符串处理）；无在途补偿时为 `""`
offer_status | string | `none` 无 / `pending` 待开箱 / `boostable` 待加赠 / `boosted` 已加赠 / `expired` 未开箱过期 / `boost_expired` 加赠过期。`detail` 只会下发 `none` / `pending` / `boostable`
seq | number | 当日第几次（1-based）；无补偿为 `0`
deposit_amount | string | 触发存款到账金额（十进制字符串），弹窗第一步「您的 US$100.00 存款运气欠佳」用
open_expire_at | string | 开箱截止，**UTC** RFC3339；无补偿为 `null`
boost_expire_at | string | 加赠截止，**UTC** RFC3339；未开箱为 `null`
server_now | string | 服务端当前时刻，**UTC** RFC3339，倒计时校准用
compensation_amount | string | 补偿本金；**待开箱态恒为 `"0"`**，开箱后才揭示
multiplier | number | 幸运倍数；**待开箱态恒为 `0`**
boost_amount | string | 可得加赠 = 补偿 × 倍数；**待开箱态恒为 `"0"`**
boost_min_deposit | string | 加赠最低存款（快照）；`"0"` 表示不另设门槛，按全站最低存款（即支付通道单笔最低充值）展示
boost_bet_multiple | number | 加赠打码倍数（快照），弹窗第二步与存款页候选卡「加赠打码 ×N」用；补偿本金的打码倍数不对玩家展示

---

## 活动详情 {#detail}

**URL:** `GET` /promotion/lucky_compensation/detail

免登录；匿名时只有配置字段，`offer_status=none`。

**输出:** 在【[Offer 对象](#offer)】之上另有：

字段 | 类型 | 说明
---|---|---
is_active | bool | 活动是否可开新窗口；`false` 时已生成的在途补偿仍照常走完
popup_enabled | bool | 商户是否开启自动弹窗；`false` 时不下发 `popups`，只走奖金页卡片入口
description | string | 卡片未触发态副标题（商户后台「活动文案」）
max_multiplier | number | 配置内最高幸运倍数，供文案「存款最高 ×20 加赠」
min_trigger_deposit | number | 最低触发存款
first_deposit_valid_hours | number | 首充有效期（小时）：仅玩家首次充值成功后 N 小时内的存款会开观察窗口；`0` 不限
window_minutes | number | 观察窗口（分钟）
loss_percent | number | 判定比例（%）
open_minutes | number | 开箱时限（分钟）
boost_minutes | number | 加赠有效期（分钟）
daily_max_offers | number | 每人每日最多生成次数
amount_tiers | list<Tier\> | 存款页候选统一契约（同 [存款档位类活动](413_promotion_first_recharge_bonus.md)）：**仅 `offer_status=boostable` 时**有一档 `{min_amount: boost_min_deposit, bonus: boost_amount, bonus_percent: 0, bet_multiple: boost_bet_multiple}`，其余状态为 `[]`（存款页不出现本活动候选）
bonus_type | string | 恒 `fixed`（加赠是固定金额，不按百分比折算）
max_bonus | string | = `boost_amount`（待加赠态），否则 `"0"`
min_tier_threshold | string | = `boost_min_deposit`（待加赠态），否则 `"0"`

> 奖金页「幸运补偿」卡片三态按 `offer_status` 渲染：`none` → 副标题 `description`、按钮「玩」；`pending` → 「有一个幸运宝箱待开启」（不显金额）、按钮「开启」+ 开箱倒计时；`boostable` → 标题「幸运补偿 ×{multiplier}」、副标题「存款额外 +{boost_amount}」、按钮「存款」+ 加赠倒计时。本卡片**不带 ⓘ**，无详情浮层。

---

## 开宝箱 {#claim}

**URL:** `POST` /promotion/lucky_compensation/claim

需登录。开当前待开箱的宝箱：补偿本金即时入账并进入待加赠态。

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
offer_id | string | 否 | 要开的补偿 ID；传了则必须与当前待开箱的一致，缺省开当前那一张

**输出:** 【[Offer 对象](#offer)】（`offer_status=boostable`，此时 `compensation_amount` / `multiplier` / `boost_amount` / `boost_expire_at` 已揭示）

错误文案：`没有待开启的幸运宝箱`（无在途待开箱补偿或 `offer_id` 不匹配）/ `幸运宝箱已过期`（已超出开箱时限）/ `幸运宝箱已开启`（并发重复开箱）。

> 前端流程：点宝箱 → 调本接口 → 播开箱动画并展示 `compensation_amount`「已存入余额」→ 约 1.5 秒后翻到第二步展示 `multiplier` / `boost_amount` / `boost_min_deposit` / `boost_bet_multiple` 与加赠倒计时。开箱成功后刷新 [余额](200_finance.md#account_balance) 与 [任务列表](201_wager_tasks.md#list)。

---

## 幸运加赠（随新存款发放） {#boost}

无独立接口。待加赠态下：

1. 弹窗第二步「去存款」/ 卡片「存款」跳存款页，**默认选中本活动候选**（存款页候选来自 `detail` 的 `amount_tiers`，候选卡多一行加赠倒计时；金额档位角标统一显示 `+{boost_amount}` 固定金额，低于 `boost_min_deposit` 的档位不挂角标）
2. 调用【[创建充值订单](200_finance.md#recharge_create)】，**必须传 `source=lucky_compensation`**，`amount` ≥ `boost_min_deposit`（为 `"0"` 时只需满足通道最低充值）
3. 支付成功时服务端在同一入账事务中发放本金 + 加赠；补偿变为 `boosted`，候选消失

> - 选了别的存款奖金候选或「不要存款奖金」：这笔存款按所选候选正常处理，**加赠资格保留直到过期**
> - 存款创建时未过期、到账时已过宽限：存款正常入账不加赠，服务端推一条 `type=msg` / `icon=bonus` toast「幸运加赠已过期，本次存款未加赠」；存款失败或未支付不消耗资格
> - 加赠这笔存款本身可再开一个新的观察窗口（当日次数未满时）
> - `source=lucky_compensation` 与充值套餐赠送互斥（同 [充值 `source` 说明](200_finance.md#recharge_create)）

---

## 弹窗与列表入口 {#popup}

- **`GET /site_content/config` 的 `popups`**：商户开启自动弹窗且玩家持有**待开箱**补偿时出现一条 `kind=lucky_compensation` 的元素，字段同【[Offer 对象](#offer)】。只在待开箱态下发（开箱后第二步不重复自动弹）；前端**按 `offer_id` 去重**，同一宝箱只自动弹一次，之后切换游戏、回大厅不重复弹
- **SSE `promotion_trigger`**：`data.kind=lucky_compensation`，命中那一刻推一次；收到后回源拉 `GET /site_content/config`（或本活动 `detail`）取宝箱字段并在当前页面（含游戏内）上层弹第一步。toast 只是提醒不是数据源
- **`GET /promotion/list`**：`data[]` 常驻行仅在商户开启「在活动列表展示」时出现，`payload` = `{description, max_multiplier, min_trigger_deposit, window_minutes}`；玩家持有待开箱 / 待加赠补偿时 `triggered[]` 另出一行，`entry_id=offer_id`，`expire_at` 为当前阶段截止（开箱截止或加赠截止），`payload` 同【[Offer 对象](#offer)】。红点 `has_unclaimed` / `unclaimed_count` = 有一个仍在开箱时限内的宝箱待开（`true` / `1`）
