# 免费注册奖金接口

**free_register_bonus**

免费注册奖金是面向**新用户**的体验金活动：首访弹窗点「领取」即以游客身份注册并向**免费账户**（【[账户余额](200_finance.md#account_balance)】的 `free_money`）发放一笔体验金，玩家在限时窗口内用免费账户游玩（【[免费账户启动游戏](300_game.md#launch_free)】），快输光时由【[幸运救援奖金](#rescue)】自动补发兜底；**任一笔充值到账**即锁定免费账户（余额全额转入活动记录并清零），**累计充值达门槛**后把该金额拆成多笔、按**有效投注打码**逐笔解锁到现金余额 `user_money`（或直接提款）。是否绑定正式账号**不影响状态流转**，只作为提款前置。

`list` / `detail` / `claim` 走通用活动接口（`kind=free_register_bonus`，详见 [活动接口](400_promotion.md)）。本章描述业务规则、`detail` / `claim` 的数据结构与错误文案。

- `GET /free_register_bonus/eligibility`：**免登录**，首访弹窗前置——当前 IP 能否领取
- `GET /promotion/free_register_bonus/detail`：**免登录**，匿名调用也返回 `config`（供首访弹窗展示 `bonus_amount`），登录后返回完整进度
- `POST /promotion/free_register_bonus/claim`：需登录，领取**当前待解锁的一笔**到现金余额（无 body）
- `POST /free_register_bonus/withdraw`：需登录，解锁当前笔并同事务直接建提款单（**按普通提款规则校验**，见下文）
- `GET /promotion/list`：仅商户开启「列表展示」时出现；试玩中 / 已锁定 / 解锁中另出现在 `triggered[]`；红点 `has_unclaimed` = 解锁中、门禁 `ok` 且当前笔打码进度达标

---

## 业务规则 {#rules}

### 1. 领取（游客注册即领取）

- **唯一领取入口**是【[游客注册](100_auth.md#guest_register)】：注册事务内即发放 `config.bonus_amount`（默认 `30`）到免费账户 `free_money`，记录 `granted_at`；已注册 / 已登录用户不再有领取入口，也**不再**在 `launch_free` 时补发
- 领取时建立活动记录 `record`（`status=4`）并把**达标金额、余额上限、充值门槛、到期时刻**快照在记录上（`detail` 顶层 `target_amount` / `min_total_deposit` / `expire_at` 即快照值），之后商户改配置或关活动对已领取用户**不生效**；拆分序列与打码倍数则在**拆笔时**快照。账户侧只持有 `free_money` 余额
- **防刷（后端）**：每 IP 领取次数上限 `ip_claim_limit`（`0` 不限）+ IP 白名单（单 IP / CIDR，不受次数限制）。超限时游客注册整体失败，返回 `当前网络领取免费奖金的次数已达上限`。「设备上任一账号登录过就不再弹首访弹窗」由**客户端本地**控制，服务端不参与
- 弹窗前先调【[领取资格](#eligibility)】：`claimable=false` 时不弹
- 活动未开启时游客注册不发放；`detail` 仍可访问，前端据 `is_active` 隐藏入口

### 2. 试玩阶段（`stage=playing`）

- **进度 = `free_money` 余额**，分母为 `target_amount`（默认 `50`），`reached_target` = `free_money ≥ target_amount`。达标只是试玩中点「提款」时的拦截条件（引导去玩游戏），**不影响到期作废，也不影响充值后的锁定 / 拆笔**：未达标就充值的玩家同样锁定，按锁定时的实际余额拆笔。试玩中派彩使余额**首次**跨过 `target_amount` 时服务端推一条【[实时提示推送（SSE）](510_message.md#realtime)】`type=msg` toast（`icon=bonus`，文案 `<p>免费奖金任务已达标。</p>`），此后跌落再回到达标不重复推；同一局同时触顶时只推下面的触顶 toast
- 免费余额上限 `free_money_cap`（默认 `100`）：**投注不受限**，派彩使余额超过上限时**截断到上限**；**每次**余额由上限以下跨到上限时服务端都推一条【[实时提示推送（SSE）](510_message.md#realtime)】`type=msg` toast（`icon=bonus`，文案 `<p>免费奖金已达最大值 <strong>%s</strong>。</p>`，`%s` 为上限金额），跌落后再次回触重复推（不去重）。`cap_reached` 反映当前是否处于上限
- 倒计时 `window_hours`（默认 `48` 小时）自 `granted_at` 起算，到期时刻为 `expire_at`；倒计时请用 `remaining_seconds`（服务端算好），**不要用客户端本地时钟**
- 信息免费模式进入条件：已领取、未锁定、未到期、余额 ≥ 最低可下注额 `0.1`；**游客与已绑定会员均可**进入（`launch_free` 不再要求 `is_guest`）
- 免费投注不触发任何活动 / 奖励 / 返水 / VIP 经验，投注记录不进入投注记录相关接口

### 2.1 幸运救援奖金 {#rescue}

试玩阶段的兜底：免费余额快输光时**不作废**，而是直接补发一笔救援金额让玩家继续玩。

- **触发**：`stage=playing` 且未到期，免费模式**一局派彩结算后**（含派彩 0 的局结束信号）`free_money < rescue_threshold`（默认 `0.5`）。仅在派彩结算时判定，下注扣款后、派彩到达前不会触发；`locked` 起余额已冻结不再触发
- **发放**：触发时即在同一事务内将 `rescue_amount`（默认 `10`）计入 `free_money`（受 `free_money_cap` 约束），游戏回调返回的余额已是补发后的值；**无需玩家领取**，没有对应的 claim 接口
- **次数不限**：每次低于门槛都触发；`record.rescue_count` / `record.rescue_total_amount` 累计。倒计时不重置、不延长
- **弹窗与刷新信号**：入账后服务端同时推两条【[实时提示推送（SSE）](510_message.md#realtime)】：① `type=promotion_trigger`，`data.kind=free_register_bonus_rescue`——玩家端据此在当前游戏页上层弹「幸运救援奖金」弹窗（金额回源 `detail.rescue_amount`，或直接用领取时已拉到的快照值），「Claim」与「✕」都只是关闭弹窗，**不调任何接口**；② `type=balance_changed`，`data.reason=rescue`——回源刷一次【[账户余额](200_finance.md#account_balance)】让站点顶栏 / 进度卡的 `free_money` 跟上（游戏内钱包已由游戏回调更新）。信号均可丢失，余额以游戏回调 / `detail.free_money` 为准
- **关闭救援**：商户将 `rescue_threshold` / `rescue_amount` 配为 `0` 时不补发，回退为旧口径：结算后 `free_money < 0.1` 即作废（见下节）
- 救援门槛与金额在**领取时快照**到记录（`detail.rescue_threshold` / `detail.rescue_amount`），之后改配置对已领取用户不生效

### 3. 作废（`stage=void`）

以下任一情况本轮奖金作废：余额清零、所有进度卡消失（不下发「已过期」态，`triggered[]` 不再出现）：

- 试玩中或已锁定，**倒计时归零**（不论余额是否达标——达标也作废）
- **仅当商户关闭了幸运救援**（`rescue_threshold=0`）：试玩中某局结算后 `free_money < 0.1`（输光）。开启救援时余额低于门槛会先被补发，试玩中**只有倒计时归零一条作废路径**

到期作废由三处保证：**惰性结算**（`detail` / `launch_free` / 充值到账时）+ **免费模式下注回调**（倒计时归零后仍在游戏内下注，服务端先作废再拒绝该笔）+ **服务端定时兑底**（每分钟扫描到期记录作废并清零 `free_money`），因此到期后即使玩家不再进详情，【[账户余额](200_finance.md#account_balance)】的 `free_money` 也会在一分钟内归零；`GET /promotion/list` 只读，到期后直接不再返回该入口。

**到期提示**：不论由哪一处触发，到期作废提交后服务端推一条【[实时提示推送（SSE）](510_message.md#realtime)】`type=msg` toast（`icon=bonus`，文案 `<p>免费奖金活动已到期，免费余额已失效，请充值后继续游戏。</p>`），每人只推一次（作废只发生一次）；作废时 `free_money` 确有清零则同时推 `type=balance_changed`，`data.reason=void`，玩家端据此回源刷一次【[账户余额](200_finance.md#account_balance)】。信号均可丢失，到期与否仍以 `detail` / `remaining_seconds` 为准。

### 4. 锁定（`stage=locked`）

- **任一笔充值到账**（按实收金额）即锁定：`free_money` **全额转入 `record.total_amount` 并清零**（`detail.free_money` 与【[账户余额](200_finance.md#account_balance)】的 `free_money` 均变为 `"0.00"`）、不再可投注、不再触发幸运救援、锁定后回来的在途派彩**没收**；`record.status` 由 `4` 转 `2`。锁定时是否达标**不影响**流转：如 `30/50` 时充值，`total_amount=30`，后续按 `30` 拆笔
- 锁定起 `reached_target` **恒为 `true`**，进度条直接渲染满格，**不再展示分子 / 分母数字**（不让玩家看到未满的 `30/50`）
- 锁定不停表：倒计时归零仍未累计充值达标 → 作废（`record.status=3`）
- 点「提款」按门禁：游客 → `guest`；已绑定 → `deposit_short`（提示并直接跳转存款页）

### 5. 拆笔与解锁（`stage=unlocking`）

- 使**累计充值 ≥ `min_total_deposit`**（默认 `49`，实收）的那笔到账事件触发拆笔：把锁定时转入的 `record.total_amount` 拆成多笔（账户 `free_money` 已在锁定时清零，此处不再变动），**与是否绑定账号无关**；首笔充值就达标则由试玩中直接进入解锁中
- **拆笔**：按拆笔时配置 `chunk_amounts` 序列逐笔拆分（默认 `[20, 10]`）；序列用完后按最后一个值继续拆，余数为最后一笔；总额小于首笔时只有一笔。如 `[20, 20, 10]` 与总额 `72.36` → `20 / 20 / 10 / 10 / 10 / 2.36`
- 每笔打码要求 `bet_required` = 笔金额 × `chunk_bet_multiple`（默认 `15`），**首笔也不例外**
- 打码**从拆笔那一刻起算**：锁定前 / 锁定期间用真实余额的投注一律不计
- 按 `record.current_index` **顺序**解锁，一次只能领当前笔；当前笔 `bet_progress` = 账户**累计有效投注**（与【[打码任务](201_wager_tasks.md)】同一口径：首次派彩时按游戏有效投注比例折算，同一笔投注**双计**、互不扣减）− 拆笔或上一笔领取时的快照。当前笔达标后到领取前的投注不计入任何一笔，**超额不结转**
- `bet_progress ≥ bet_required` **且** `gate=ok` 才能 `claim` / `withdraw`
- `claim` 金额直接入**现金余额 `user_money`**（打码倍数 `0`，**不新增**打码任务），钱包流水 reason 为 `free_register_bonus_claim`；领取后重新拉取【[余额](200_finance.md#account_balance)】
- 全部领完 → `stage=finished`，`record.status=1`

### 6. 门禁 `gate` {#gate}

按序判定，**第一个不满足的即为结果**（对应 PRD「点『提款』的条件表」）：

gate | 触发条件 | 前端建议
---|---|---
`progress` | `stage=playing` 且 `free_money < target_amount` | 轻提示「任务未达标，请先游戏」
`guest` | 仍是游客（`is_guest=true`），任何阶段 | 弹游客绑定弹窗引导【[绑定账号](100_auth.md#bind_account)】；绑定成功后重新判断。**不影响状态流转**，游客充满门槛同样进入 `unlocking`
`no_deposit` | 已绑定但**无任何充值**（`has_deposit=false`） | 零充值拦截弹窗；档位表复用【[首充奖励 detail](413_promotion_first_recharge_bonus.md#detail)】，金额 = `min(档位门槛 × 比例, 上限)` 向下取整
`deposit_short` | 已充值但 `total_deposit < min_total_deposit` | 提示「累计充值满 X 才可以提款」并**直接跳转**存款页
`ok` | 以上均满足 | 打开详情浮层，按 `record` 渲染拆笔与进度

> 匿名调用恒为 `progress`。`stage=locked` 起余额已转入记录，不再以 `progress` 拦截。

### 7. 阶段 `stage` {#stage}

stage | 说明 | 倒计时
---|---|---
`none` | 未领取 / 未登录 | —
`playing` | 试玩中：免费账户可投注 | 有
`locked` | 已锁定：已充值但累计未达门槛，余额已转入记录（`free_money="0.00"`） | 有（延续）
`unlocking` | 已拆笔，逐笔打码解锁中 | 无
`finished` | 全部领完 | —
`void` | 已作废（到期；关闭救援时另含输光） | —

### 8. 前端展示约定

- `stage=playing`：进度条分子 = `free_money`，分母 = `target_amount`；`reached_target=true` 后满格（余额仍可继续涨到 `free_money_cap`，进度条不再反映真实比例）
- `stage=locked`：`reached_target` 恒为 `true`，进度条**直接画满格（100%），不展示分子 / 分母文字**（此时 `free_money` 已为 `"0.00"`，不可再用作分子）；倒计时继续
- 首页 / 赌场常驻卡在 `stage ∈ {playing, locked}` 期间**一直显示**（目的是达标 / 锁定后继续提醒充值），只随 `reached_target` 切换两态：`false` 为「免费奖金总额」样式，`true` 为「提款金额」样式（`locked` 必为此态）；进入 `unlocking` 后常驻卡消失，只在「大厅 / 奖金」列表展示；`void` / `finished` 所有卡消失
- 幸运救援弹窗由 `promotion_trigger` `kind=free_register_bonus_rescue` 触发，在游戏页上层展示 `rescue_amount`，关闭即可（见【[幸运救援奖金](#rescue)】）
- 详情浮层的跑马灯为前端虚拟数据，服务端不下发
- `stage=unlocking` 时：当前笔 `bet_progress < bet_required` → 按钮「现在玩」；`bet_progress ≥ bet_required` 且 `gate=ok` → 按钮「提款」（进提款页调 `withdraw`，或「转入您的余额」调 `claim`）

---

## 领取资格 {#eligibility}

**URL:** `GET` /free_register_bonus/eligibility

免登录。首访弹窗弹出前调用：当前请求 IP 能否领取本活动。

**输出:**

字段 | 类型 | 说明
---|---|---
claimable | bool | 是否可领取（活动开启 + 当前未登录 + IP 未超限或在白名单）
reason | string | 不可领取原因：`inactive` 活动未开启 / `logged_in` 已登录（已注册用户无领取入口）/ `ip_limit` 该 IP 领取次数已达上限；可领取时为空串
bonus_amount | string | 当前配置的赠送金额（供弹窗展示）

> `claimable=true` 后客户端仍需自行判断「本设备是否有账号登录过」再决定是否弹窗；游客注册时服务端会再次校验 IP。

---

## 活动详情字段 {#detail}

通用接口 `GET /promotion/free_register_bonus/detail` 的 `data` 字段（免登录；金额字段均为十进制字符串，见【[金额与币种](200_finance.md#amount_currency)】）：

字段 | 类型 | 说明
---|---|---
kind | string | 固定 `free_register_bonus`
is_active | bool | 活动是否开启（商户启用且玩法开关打开）。**本接口不受活动开关拦截**，关闭时仍正常返回，需自行据此隐藏入口；已领取用户按快照继续走完全流程
description | string | 活动说明文案（商户后台配置，可能为空字符串）
config | Config | **当前**玩法配置，见下表；匿名调用也下发（首访弹窗用 `bonus_amount`）
stage | string | 当前阶段，见【[阶段 stage](#stage)】
free_money | string | 免费账户当前余额（试玩中的进度分子）；`locked` 起为 `"0.00"`（已转入 `record.total_amount`）
target_amount | string | 达标金额（领取时快照；未领取时为当前配置）
min_total_deposit | string | 拆笔所需累计充值（领取时快照；未领取时为当前配置）
rescue_threshold | string | 幸运救援门槛（领取时快照；未领取时为当前配置）；`"0.00"` 表示关闭救援
rescue_amount | string | 每次幸运救援补发金额（领取时快照；未领取时为当前配置），救援弹窗展示用
reached_target | bool | `playing`：`free_money ≥ target_amount`；`locked` 起恒为 `true`（进度条满格）
cap_reached | bool | 免费余额是否已处于上限（上限为 `0` 时恒 `false`）
granted_at | string | 领取时刻（RFC3339 带时区）；未领取为 `null`
expire_at | string | 倒计时截止时刻（领取时快照，RFC3339 带时区）；**仅 `stage ∈ {playing, locked}` 下发**，未领取 / 不限时 / 拆笔后（`unlocking` 起）为 `null`
remaining_seconds | number | 距 `expire_at` 的剩余秒数，已到期、无窗口或拆笔后为 `0`；倒计时请以此为准，`0` 时不渲染倒计时
gate | string | 门禁结果，见【[门禁 gate](#gate)】
is_guest | bool | 当前会员是否游客（匿名为 `false`）
has_deposit | bool | 是否有过任意一笔成功充值（= 已锁定）
total_deposit | string | 历史累计充值金额（实收）；匿名为 `"0.00"`
record | Record | 活动记录，领取起即存在；匿名 / 未领取为 `null`

**`Config` 字段:**

字段 | 类型 | 说明
---|---|---
bonus_amount | string | 领取即发放到免费账户的额度（默认 `30.00`）
target_amount | string | 试玩达标金额（默认 `50.00`）
free_money_cap | string | 免费余额上限，派彩超出部分截断（默认 `100.00`）
window_hours | number | 倒计时小时数，自 `granted_at` 起（默认 `48`）
min_total_deposit | string | 拆笔门槛：累计充值（实收）≥ 该值（默认 `49.00`）
rescue_threshold | string | 幸运救援门槛：试玩中派彩结算后 `free_money` 低于该值即补发（默认 `0.50`；`0.00` 关闭）
rescue_amount | string | 每次幸运救援补发金额（默认 `10.00`）
chunk_amounts | string[] | 拆分金额序列，用完按末值继续（默认 `["20.00", "10.00"]`）
chunk_bet_multiple | number | 每笔打码倍数，`bet_required = 笔金额 × 倍数`（默认 `15`）

**`Record` 字段:**

字段 | 类型 | 说明
---|---|---
total_amount | string | 锁定时从 `free_money` 全额转入的金额（拆笔总额）；试玩中为 `"0.00"`
claimed_amount | string | 已领取金额合计
claimed_count | number | 已领取笔数
current_index | number | 当前待领取笔的下标（0-based）；全部领完时等于 `chunks` 长度
rescue_count | number | 试玩中幸运救援发放次数
rescue_total_amount | string | 试玩中幸运救援累计补发金额
status | number | `4` 试玩中 / `2` 已锁定待充值达标 / `0` 打码解锁中 / `1` 已全部领完 / `3` 已作废
locked_at | string | 锁定时刻（RFC3339）；未锁定为 `null`
converted_at | string | 拆笔时刻（RFC3339）；未拆笔为 `null`
chunks | list<Chunk\> | 拆笔明细，按 `index` 升序；拆笔前为空数组

**`Chunk` 字段:**

字段 | 类型 | 说明
---|---|---
index | number | 笔序号（0-based）
amount | string | 本笔金额
bet_required | string | 本笔所需有效投注
bet_progress | string | 本笔打码进度：已领取笔 = `bet_required`；当前笔 = `min(累计有效投注 − 快照, bet_required)`；后续未开始的笔为 `"0.00"`
status | string | `locked` 未领取 / `claimed` 已领取

---

## 领取当前笔（转入余额） {#claim}

通用接口 `POST /promotion/free_register_bonus/claim`，需登录。对应 PRD「转入您的余额」（**不限金额是否达到提款渠道最低额**），与下文【[直接提款](#withdraw)】二选一，两者都会把当前笔标为已领并推进到下一笔。

**输入:** 无（body 被忽略，传 `{}`）

**输出:**

字段 | 类型 | 说明
---|---|---
amount | string | 本次领取金额（即当前笔 `amount`），已入现金余额 `user_money`
chunk_index | number | 本次领取的笔序号（0-based）
claimed_amount | string | 领取后的累计已领金额
total_amount | string | 记录总额
finished | bool | 是否已全部领完（`true` 时 `stage` 变为 `finished`）

**业务错误:**

message | 触发条件
---|---
`请先登录` | 未登录
`免费奖金进度未达标` | 试玩中且 `free_money < target_amount`（`gate=progress`）
`请先绑定账号完成注册` | 仍是游客（`gate=guest`）
`请先完成充值后再解锁奖金` | 无任何充值（`gate=no_deposit`）
`累计充值需达到 %s 才能解锁奖金` | 累计充值未达 `min_total_deposit`（`gate=deposit_short`），`%s` 为门槛金额
`暂无可解锁的奖金` | 未领取 / 已作废 / 未拆笔，或当前笔状态异常
`奖金已全部领取` | `record.status=1`
`当前奖金打码量未达标` | 当前笔 `bet_progress < bet_required`
`领取冲突，请重试` | 并发领取同一笔，重试即可

> 领取成功后请重新拉取 `detail` 与【[余额](200_finance.md#account_balance)】；本活动领取**不生成**打码任务。到账会同时落一条站内信并推【[实时提示](510_message.md#realtime-i18n-keys)】`type=msg` toast（活动名为「免费注册奖金」），与其他活动领奖一致。

---

## 直接提款当前笔 {#withdraw}

`POST /free_register_bonus/withdraw`，需登录。对应 PRD 详情浮层底部「提款」：解锁当前笔并在**同一事务**内以该笔金额建一笔提款单（先入现金再冻结，对余额净影响为 0）；任一步失败整体回滚，当前笔仍可重试或改走 `claim` 转入余额。

提款校验**与【[统一提款申请](200_finance.md#withdraw_create)】完全相同**（禁提/游客拦截、方式与币种、收款资料、单笔最低/最高额、一人一在途单、账户打码任务、每日限额、手续费、审核分流）；差异仅在：

- 金额不可传，固定为当前笔 `amount`
- 订单带后台审核标注「免费注册奖金提款」，仅供审核人员辨识，不改变分流
- 提款单后续**失败 / 取消**时款项按普通提款流程退回真实余额，**该笔份额仍算已提取**（等同于转入了余额），不回滚

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
method | string | 是 | 提款方式，同统一提款 `method`
channel | string | 是 | 代付通道，同统一提款 `channel`

**输出:** 同 `claim`（`amount` / `chunk_index` / `claimed_amount` / `total_amount` / `finished`），另加：

字段 | 类型 | 说明
---|---|---
withdraw | object | `{id, order_sn}` 新建提款单，后续状态走【[我的提现订单](200_finance.md#withdraw_my)】

**业务错误:** `claim` 的全部错误，另加统一提款的全部错误（含 `单笔最小提现 %s` / `您还有未完成的提现订单` / 打码类）。金额低于渠道最低额时客户端应提示「切换提款方式或转入您的余额」并提供 `claim` 入口。

---

## 相关接口变更 {#related}

- 【[免费账户启动游戏](300_game.md#launch_free)】不再要求 `is_guest`，已绑定会员在锁定前同样可进；不再惰性补发额度。业务错误：`尚未领取免费奖金` / `已完成充值，免费账户不再可用` / `免费奖金已到期`（同时作废）/ `免费账户余额不足`（余额 < `0.1`）
- 【[实时提示推送](510_message.md#realtime-promotion-trigger)】新增 `promotion_trigger` 标识 `free_register_bonus_rescue`：幸运救援入账时推送，玩家端据此在游戏页弹救援弹窗
- 【[游客注册](100_auth.md#guest_register)】新增业务错误 `当前网络领取免费奖金的次数已达上限`（活动开启且 IP 超限时注册整体失败）
- 【[账户余额](200_finance.md#account_balance)】`free_money` 在首笔充值到账（锁定）时即清零；到期时刻等活动规则不在余额接口下发，以本活动 `detail` 为准
- 【[绑定账号](100_auth.md#bind_account)】不再触碰免费账户：转正不清零、不拆笔，仅使 `gate` 从 `guest` 放行
