# 活动接口

---

平台采用插件式活动框架，每种活动有一个 `kind` 标识，已注册的 `kind` 包含：

`account_tasks` / `agent_rebate` / `battle_pass` / `cumulative_recharge` / `daily_bankrupt_pack` / `daily_checkin` / `first_recharge_bonus` / `free_register_bonus` / `loss_rescue` / `lucky_compensation` / `magnate` / `mystery_bonus` / `rakeback` / `recharge_bonanza` / `red_packet_rain` / `rescue_bonus` / `treasure_box` / `vip` / `weekly_cashback` / `weekly_rescue_bonus` / `win_back`

其中 [`daily_bankrupt_pack`（每日幸运补偿）](417_promotion_daily_bankrupt_pack.md) 为限时触发型活动，**不支持 `claim`**，奖励由充值订单自动发放。[`free_register_bonus`（免费注册奖金）](418_promotion_free_register_bonus.md) 的 `detail` 匿名可访问且下发配置，`claim` 按拆笔逐笔领取。[`lucky_compensation`（幸运补偿）](419_promotion_lucky_compensation.md) 为限时触发型，`claim` 是开宝箱领补偿本金，加赠随新存款自动发放。[`win_back`（捞回活动）](420_promotion_win_back.md) 的 `claim` 领取宝箱赠送奖金；玩家须在存款页主动选择该来源，合格充值订单绑定具体宝箱并在到账时结算一次加倍奖金。

下面 5 个接口对所有 kind 通用；具体活动的领取参数 / detail 数据结构由各 kind 决定（见对应专章）。

---

## 奖励钱包与打码规则 {#wager_rules}

**双钱包版本待发布，需完成配套数据库迁移后生效**；本文不是上线公告。完整规则见 [打码任务](201_wager_tasks.md)。

- 活动奖励有打码要求时计入奖金余额 `bonus_money` 并生成奖励任务，无打码要求时计入现金余额 `user_money`，不生成任务。余额接口的总余额 `total = user_money + bonus_money`。`bet_multiple` 描述本次奖励的要求，不再直接增加旧 `bet_amount`。
- 领取成功表示奖励已入账，不表示已解锁或可提现。**所有待完成任务及旧打码均清零**后才满足提现打码门槛；单个活动任务完成不会单独释放对应奖金。
- 各活动独立配置 `can_abandon`，未开启时按不可放弃处理；发奖创建任务时将生效倍数与放弃权限固化，后续活动配置变更不改写已有任务。兑换码按该兑换码配置固化，充值本金任务始终不可放弃。
- 上述规则覆盖宝箱、战令、累计充值、签到、拼多多、神秘奖金、返水、红包雨、救援金、每周返现、每周救援金、代理返佣、首充奖励、充值大酬宾、VIP、兑换码和每日幸运补偿。活动详情与领取响应并没有统一新增 `can_abandon` 字段，**已发放奖励以任务列表中的 `can_abandon` 为准**。
- 只有全部待完成任务都可放弃时，才可调用 [整体放弃](201_wager_tasks.md#abandon_all)；它会清空当前全部奖金，不支持只放弃某个活动。现金和旧打码不变。
- 活动待领奖励的领取期限、礼包倒计时与已入账打码任务的状态分开处理；不要把活动的 `expire_at` / `expire_days` 当成打码任务到期时间。

领取或自动发奖后，重新获取 [余额](200_finance.md#account_balance) 与 [任务列表](201_wager_tasks.md#list)；保留各活动原有请求参数，不要给通用 `claim` 额外添加任务接口的 `request_id`。

---

## 活动列表 {#list}

返回该商户当前启用（`is_active=true`）的所有活动。游客可访问，但 `claim` / `purchase` 仍需登录。

**活动投放设置**：商户可让活动只对部分推广商 / 渠道的会员开放，并划出一部分会员作为对照组。登录会员不在开放范围内或落在对照组时，该活动对他的表现与「活动未开启」完全一致：不出现在 `data` / `triggered` / `enabled_kinds`，不计入 `unclaimed_count`，[活动详情](#detail) / [批量活动详情](#detail_batch) 不返回该活动，[领取](#claim) / [购买](#purchase) 报「活动未启用」。游客不受影响。分组只按会员注册渠道与固定规则判定，客户端无需额外处理。

**URL:** `GET` /promotion/list

**Query:**

字段 | 类型 | 必填 | 说明
---|---|---|---
with_unclaimed | bool | 否 | 传 `1` / `true` 时额外计算并下发顶层 `unclaimed_count` 与 `data[].unclaimed_count`（口径见 [可领奖励项数](#unclaimed_count)）；不传则这两个字段**不出现**，服务端也不做任何额外计算（老客户端零开销）。游客传了也不下发

**输出:**

字段 | 类型 | 说明
---|---|---
data | list<object\> | **常驻**活动入口卡片列表（商户开启「在活动列表展示」的 kind，每 kind 一行）
triggered | list<object\> | **触发态**入口列表（会员名下已激活的限时 / 阶段性记录，每条记录一行，按 `expire_at` 升序、不限时项在后）；匿名恒为 `[]`。**同一 kind 可同时出现在 `data` 与 `triggered`**（如「幸运充值礼包」常驻卡 + 手上一张 15 分钟礼包）；kind 未开启列表展示时只出现在 `triggered`
enabled_kinds | list<string\> | 商户当前已开启且平台已上架的活动 kind；不受「在活动列表展示」开关影响，未开启时为空数组；不含该会员投放范围外 / 对照组的 kind
unclaimed_count | int | 仅 `with_unclaimed=1` 且已登录时下发。**全站可领奖励项数**，口径同 [可领奖励项数](#unclaimed_count)；覆盖商户当前已开启的**全部** kind（含未开启列表展示、仅弹窗触达的），不是 `data[]` 逐行相加

`data` 单项字段：

字段 | 类型 | 说明
---|---|---
kind | string | 活动 kind
banner_url | string | banner 图（商户未配置时回退到平台默认）
icon_url | string | icon 图（同上）
sort_order | int | 展示排序（商户自定义排序优先，未配置回退平台默认）
has_unclaimed | bool | 是否有未领取奖励（游客按 kind 默认红点判定）。**老字段，判定逻辑与缓存未变**；部分 kind 的红点语义是「可参与」而非「有奖励待领」（如存款加赠类「今日未参加」、拼多多「今日可转动」），因此可能出现 `has_unclaimed=true` 而 `unclaimed_count=0`
unclaimed_count | int | 仅 `with_unclaimed=1` 且已登录时下发。该 kind 当前**可领奖励项数**（口径见 [可领奖励项数](#unclaimed_count)）；无手动领取项的 kind 恒为 `0`
payload | object | 可选，部分 kind 下发的卡片摘要（无该能力的 kind 不下发此字段）：`daily_deposit_bonus` / `deposit_first_n_bonus` → 与各自 `detail.data` 同结构（含 `can_join` 等字段），前端应以 `can_join` 判断卡片是否可参与展示，**不要用 `is_active`**（`is_active` 只代表后台开关，未考虑今日已参与 / 次数用完等资格）；`loss_rescue` → 与 [活动详情](418_promotion_loss_rescue.md#detail) 的 `data` 完全同结构（含 `pending`/`in_progress`/`windows` 等），常驻卡/领取卡不必再单独请求 `GET /promotion/loss_rescue/detail`

`triggered` 单项字段：

字段 | 类型 | 说明
---|---|---
kind | string | 活动 kind，点击仍走 `GET /promotion/{kind}/detail`
entry_id | string | 触发记录 id（offer_id / record_id 等），前端以 `kind + entry_id` 作唯一键与「已关闭」记忆
banner_url / icon_url | string | 同 `data`
expire_at | string \| null | 限时截止（UTC）；`null` = 不限时
server_now | string | 服务端当前时间（UTC），倒计时请用 `expire_at - server_now` 校时
sort_order | int | 展示排序，与 `data` 同一 kind 取值一致，供前端统一排序 `data` + `triggered`
payload | object | 卡片摘要，按 kind 自定义：`daily_bankrupt_pack` → `{offer_id, seq, bonus_type, group_index, tiers, recommended_tier_index, description}`；`free_register_bonus` → `{stage, gate, free_money, target_amount, min_total_deposit, reached_target, cap_reached, granted_at, expire_at, remaining_seconds, has_deposit, total_deposit}`（`stage ∈ playing / locked / unlocking`），已锁定 / 解锁中另有 `{total_amount, claimed_amount, claimed_count, chunk_count}`，解锁中再加 `{current_amount, current_bet_required, current_bet_progress}`；`win_back` → `{status, expire_at, boost_enabled}`，开启充值宝箱且已领取时另有 `{gift_amount, boost_min_amount, multiplier_min, multiplier_max, boost_bet_multiple}`，仅免费宝箱领取后不再返回入口；`entry_id` 为活动记录 ID

> **渲染建议**：`triggered` 排在 `data` 之前。老客户端只读 `data` 即行为不变（`data` 的生成逻辑与排序未改）。`triggered` 只读现有记录、不会新建触发（如幸运充值礼包 offer 仍由 `site_content/config` 弹窗链路生成）。

---

## 可领奖励项数 {#unclaimed_count}

返回当前玩家在商户**全部已开启活动**中的可领奖励项数，供首页角标 / 个人中心入口展示，不需要拉整张活动列表。与 `GET /promotion/list?with_unclaimed=1` 顶层 `unclaimed_count` 同源。游客可访问，恒返回 `0`。

**URL:** `GET` /promotion/unclaimed_count

**输出:**

字段 | 类型 | 说明
---|---|---
data.count | int | 可领奖励项数（各 kind 之和）

**口径**：一“项”= 一次独立领取动作能领到的单元，只统计**当前就能调 `claim` 成功**的奖励（已达领取门槛）；存款加赠类（需充值才触发）、拼多多与兑换码不计入。不下发金额。

kind | 一“项”的定义
---|---
`treasure_box` | 每个未过期待领宝箱
`loss_rescue` | 每笔已结算未过期的待领救援金（`pending[]`）；到期未结算的窗口不计
`cumulative_recharge` | 每个已达标未领的阶梯
`battle_pass` | 每个已解锁未领的节点奖励（free / 已购 premium 各算一项）
`vip` | 升级礼金（所有未领等级一键领，算 1 项）+ 本期周俸 + 本期月俸，最多 3 项
`agent_rebate` / `rakeback` / `daily_checkin` / `weekly_cashback` / `rescue_bonus` / `weekly_rescue_bonus` / `free_register_bonus` / `mystery_bonus` | 单项类：与该 kind 的 `has_unclaimed` 完全一致，`true` 即 `1`
`red_packet_rain` | 当前就能领（在窗口内、今日次数未满、本窗口未领；不含充值门槛）算 1 项，与 `can_claim` 一致，实时判定不走缓存
`daily_bankrupt_pack` / `daily_deposit_bonus` / `deposit_first_n_bonus` / `first_recharge_bonus` / `recharge_bonanza` / `magnate` / `redeem_code` | 无手动领取项（或红点语义为「可参与」），恒 `0`

> 结果按会员×kind 缓存 1 分钟，`claim` / `purchase` 成功后即时失效（与 `has_unclaimed` 红点同一套失效时机；单项类 kind 直接复用红点缓存，不产生额外查询）。`free_register_bonus` 与 `red_packet_rain` 例外：前者逐笔打码解锁的达标由下注推进、拆笔由充值触发，后者随时间进出窗口，红点与可领项数都实时判定、不走缓存。领取后建议直接重拉本接口或 `/list?with_unclaimed=1` 刷新角标，不要在客户端自行减 1。

---

## 活动详情 {#detail}

返回某个 kind 的详细配置 + 当前玩家进度。游客可访问，登录后字段更完整。

**URL:** `GET` /promotion/{kind}/detail

**Path:**

字段 | 类型 | 必填 | 说明
---|---|---|---
kind | string | 是 | 活动 kind

**输出:**

字段 | 类型 | 说明
---|---|---
data | object | 活动详情，结构由 kind 决定

---

## 批量活动详情 {#detail_batch}

一次请求返回多个 kind 各自的 `detail`（与逐个调用 `GET /promotion/{kind}/detail` 完全等价，只是合并成一次请求，减少并行请求数）；**不经过 [活动列表](#list) 的 `show_in_list` 过滤**，即使某 kind 未开启列表展示（仅靠自己的入口，如充值页展示存款类活动费率）也能拿到详情。游客可访问，登录后字段更完整。未知 / 无效 kind 静默跳过，不中断其它 kind。

**URL:** `GET` /promotion/detail_batch

**Query:**

字段 | 类型 | 必填 | 说明
---|---|---|---
kinds | string | 是 | 逗号分隔的 kind 列表，如 `daily_deposit_bonus,deposit_first_n_bonus,first_recharge_bonus`

**输出:**

字段 | 类型 | 说明
---|---|---
data | object | key 为 kind，value 为该 kind 的 `detail.data`（结构与单独调用 `/detail` 完全一致）；请求的 kind 不存在或该 kind detail 报错时，该 key 直接缺失（不是 `null`）

---

## 领取活动奖励 {#claim}

需要登录。`body` 字段由各 kind 自行约定（如 `treasure_box` 接受可选的 `box_id`），未约定时传 `{}`。

**URL:** `POST` /promotion/{kind}/claim

**Path:**

字段 | 类型 | 必填 | 说明
---|---|---|---
kind | string | 是 | 活动 kind

**输入:** kind 自定义的 JSON 对象

**输出:** kind 自定义的对象（典型字段：`amount`、`claimed_box_ids` 等）

可选字段 `data.new_reward_granted` 表示本次是否实际新增发奖，由后端生成，不接受客户端输入控制。明确返回布尔 `false` 时不触发本次领奖风控钩子；`true` 或字段缺失保留原行为，类型异常记录日志并保留原行为。旧活动无需补字段。[账号任务奖励](420_promotion_account_tasks.md#claim)首次入账在事务提交成功后返回 `true`，重复领取返回 `false`。

---

## 购买活动 {#purchase}

需要登录。仅支持声明了 purchase 入口的 kind（如 `battle_pass`），其它 kind 返回「活动不存在或不支持购买」。

**URL:** `POST` /promotion/{kind}/purchase

**Path:**

字段 | 类型 | 必填 | 说明
---|---|---|---
kind | string | 是 | 活动 kind

**输入:** 无

**输出:** kind 自定义的对象
