# 损失救援接口

**loss_rescue**

损失救援的 `list` / `detail` / `claim` 走通用活动接口（`kind=loss_rescue`，详见 [活动接口](400_promotion.md)），无独立路由。

---

## 玩法简介 {#intro}

- 玩家**单笔存款**达到开窗门槛（`deposit_threshold`）时，为该笔存款开启一个**净损累计窗口**，从存款时刻起滚动计时（新用户默认 24 小时 / 老用户默认 120 小时，实际时长以 `detail` 下发的 `window_hours` 为准）；窗口进行中**不可领取**，仅用于累计该笔存款起 `window_hours` 内的老虎机净损；
- 窗口到期后**冻结净损结算**：命中亏损返奖阶梯（`tiers`）则按 `净损 × rate%` 生成一笔**待领取**救援金（金额自此固定，不再随后续投注变化），并开启独立的领取有效期倒计时；未命中则记为「未中奖」，不产生待领取记录；
- 待领取救援金须在领取有效期（`claim_expire_hours`，从窗口结算时刻起算）内领取，入账带打码倍数（`bet_multiple`）；**超期作废**，不可再领；
- 「新用户 / 老用户」是**本活动内身份**：新用户窗口一旦结算命中（进入待领取，无论是否已实际点击领取、含超期未领作废）即视为已用掉新用户资格，之后新开的窗口一律按老用户配置；未命中（作废）不占用资格，仍可按新用户身份重新触发。窗口时长、领取有效期、阶梯新老独立配置，前端一律按 `detail` 当前下发值动态渲染，**不要写死默认值**；
- 同一时间**最多一个「进行中」窗口**：存在未结算的进行中窗口时，新的达标存款不会开启新窗口；窗口到期结算（命中生成待领取记录 / 未命中作废）后，下一笔达标存款即可开新窗口——**待领取救援金未领取不会阻挡新窗口开启**，可与新窗口同时存在，各自独立领取；领取接口按 `id` 领取（一次 `claim` 领一笔）。

活动入口红点（[活动列表](400_promotion.md#list) 的 `has_unclaimed`）= 存在未过期的待领取救援金。

---

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

通用接口 `GET /promotion/loss_rescue/detail` 的 `data` 字段（免登录；**未登录时仅返回 `is_active`/`description`/`deposit_threshold`/`user_type`/`max_rate`/`window_hours`/`claim_expire_hours`/`tiers` 8 个字段**，`user_type` 恒为 `new`、`max_rate`/`window_hours`/`claim_expire_hours`/`tiers` 均按新用户档位下发，供招徕文案展示完整费率表；`windows`/`in_progress`/`pending` 仅登录后返回）：

字段 | 类型 | 说明
---|---|---
is_active | bool | 活动是否启用
description | string | 活动说明文案
deposit_threshold | number | 开窗门槛（单笔存款金额，新老用户共用）
user_type | string | 当前玩家活动内身份：`new` 新用户 / `old` 老用户
max_rate | number | 当前身份组阶梯中的最高返奖比例（%）；**仅用于「未充值」常驻卡（无任何窗口时）**，有 `in_progress`/`pending` 时该卡应改用对应条目自己的 `max_rate`
window_hours | number | 当前身份组窗口时长（小时）
claim_expire_hours | number | 当前身份组领取有效期（小时），`0` 表示不过期
tiers | list<Tier\> | 当前身份组亏损返奖阶梯（按 `loss` 升序）；同上，仅用于无窗口时的常驻卡
windows | list<object\> | 进行中的净损累计窗口（未到结算时刻，不可领取），按结束时间升序
in_progress | list<object\> | 进行中窗口的**实时预估**（仅供前端展示「待结算金额」，**不可据此发起 claim**）；只有当前净损已命中最低档位才下发，未命中时不下发（前端无法区分“未充值”与“已充值但未达最低档”，两者都不下发此字段，按未充值文案展示即可）
pending | list<object\> | 已结算的待领取救援金，**只含未过期记录**（服务端实时过滤），按领取截止时间升序（不过期的排最后）

`Tier` 字段：

字段 | 类型 | 说明
---|---|---
loss | number | 命中该阶梯所需的最低窗口净损
rate | number | 按净损返奖的百分比（%）
bet_multiple | number | 该阶梯救援金的打码倍数

`windows` 单项字段：

字段 | 类型 | 说明
---|---|---
id | string | 窗口 ID
window_end_at | string | 窗口结束（结算）时间

`in_progress` 单项字段：

字段 | 类型 | 说明
---|---|---
id | string | 窗口 ID
loss_amount | number | 实时净损（随后续投注变化，仅供展示）
tier_index | number | 当前命中档位序号（从 1 开始）
amount | number | 按当前净损测算的预估可领金额（非最终值，窗口结算后可能变化）
bet_multiple | number | 打码倍数
window_end_at | string | 窗口结束（结算）时间，前端按此倒计时
user_type | string | 该窗口开窗时的身份（`new`/`old`），可能与顶层当前 `user_type` 不同（存在多笔窗口并发时）
max_rate | number | 该窗口自己身份组的最高返奖比例（%），供这张卡的标题展示，**不要用顶层 `max_rate`**
tiers | list<Tier\> | 该窗口自己身份组的阶梯表，供这张卡弹层展示，**不要用顶层 `tiers`**

`pending` 单项字段：

字段 | 类型 | 说明
---|---|---
id | string | 救援金 ID（领取时回传）
window_start_at | string | 窗口开始（触发存款）时间，供卡片日期前缀「{日期} 损失救援」展示
loss_amount | number | 该窗口结算净损
tier_index | number | 命中档位序号（从 1 开始）
amount | number | 可领金额
bet_multiple | number | 打码倍数
claim_expires_at | string | 领取截止时间；`null` 表示不过期
user_type | string | 该窗口开窗时的身份（`new`/`old`），是它自己的快照，不随之后玩家身份变化而变
max_rate | number | 该窗口自己身份组的最高返奖比例（%），供这张卡的标题展示，**不要用顶层 `max_rate`**
tiers | list<Tier\> | 该窗口自己身份组的阶梯表，供这张卡弹层展示，**不要用顶层 `tiers`**

> `in_progress`/`pending` 现在可以同时存在（见上方开窗规则），且彼此的开窗身份可能不同（比如待领取那笔是新用户命中的，同时进行中这笔已经是老用户）。前端渲染每张卡（常驻卡 / 领取卡）时，标题比例和阶梯表**必须用这张卡自己条目里的 `max_rate`/`tiers`**，不能取 `detail` 顶层的 `max_rate`/`tiers`（顶层字段只代表玩家"当前"身份，仅用于还没有任何窗口时的常驻卡展示）。

登录态响应示例：

```json
{
  "data": {
    "is_active": true,
    "description": "活动说明文案",
    "deposit_threshold": 5.00,
    "user_type": "old",
    "max_rate": 15.00,
    "window_hours": 120,
    "claim_expire_hours": 120,
    "tiers": [
      { "loss": 100.00, "rate": 3.00, "bet_multiple": 3 },
      { "loss": 1000.00, "rate": 8.00, "bet_multiple": 3 },
      { "loss": 10000.00, "rate": 15.00, "bet_multiple": 3 }
    ],
    "windows": [
      { "id": "8850", "window_end_at": "2026-09-15T10:12:44Z" }
    ],
    "in_progress": [
      { "id": "8850", "loss_amount": 47.00, "tier_index": 1,
        "amount": 1.41, "bet_multiple": 3,
        "window_end_at": "2026-09-15T10:12:44Z" }
    ],
    "pending": [
      { "id": "8841", "loss_amount": 640.00, "tier_index": 2,
        "amount": 96.00, "bet_multiple": 3,
        "claim_expires_at": "2026-09-16T10:12:44Z" }
    ]
  }
}
```

---

## 领取 {#claim}

通用接口 `POST /promotion/loss_rescue/claim`，需登录，逐笔领取。

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
id | string | 是 | 待领取救援金 ID（取 `detail` 的 `pending[].id`）

**输出:**

字段 | 类型 | 说明
---|---|---
amount | number | 入账金额

示例：

```json
// 请求
{ "id": "8841" }

// 200
{ "data": { "amount": 96.00 } }
```

业务错误（`message`）：

message | HTTP | 触发条件
---|---|---
请先登录 | 400 | 未登录调用（框架统一）
活动不存在 | 400 | kind 未注册（框架统一）
活动未启用 | 400 | `is_active=false`
救援金不存在或已领取 | 400 | `id` 为空 / 非本人 / 状态非「待领取」（含窗口尚未结算、含并发重复领取）
救援金已领取超时 | 400 | 领取瞬间已过 `claim_expires_at`（该笔随即作废，不可再领）

领取成功后奖励经资金账本入账并按 `bet_multiple` 生成打码任务（规则见 [活动通用规则](400_promotion.md#wager_rules)），随后应重新获取余额与 `detail`。

---

## 损失金额明细 {#loss_detail}

「损失金额」明细归属**单笔充值触发的滚动窗口**，日历按商户时区筛选该窗口内指定日期的数据。翻日期时必须保留同一个 `window_id`，不能切换窗口或混入窗口外的数据。需登录。

**URL:** `GET` /loss_rescue/loss_detail

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
window_id | string | 否 | 限定当前会员的一笔窗口；从活动卡进入及翻日期时均携带。省略时仅作旧入口兼容，按 `date` 查当天开出的最新窗口（开窗时间、ID 倒序）
date | string | 否 | `YYYY-MM-DD`，查询窗口内商户时区这一天的数据；与 `window_id` 同时生效。指定窗口但省略日期时默认其开窗日；两者均省略时查今天开出的窗口。非空但非法的日期兼容回退今天

统计范围为 `[max(所选日期零点, 窗口开始), min(所选日期次日零点, 窗口结束, 当前时刻))`，没有交集时返回 `{"data": null}`。旧入口未找到窗口时同样返回空态，不回退旧的待领取记录。

例如 12 日 15:00 开始、15 日 15:00 结束的 72 小时窗口：12 日只查 15:00 之后，13、14 日各查全天，15 日只查 15:00 之前。所有查询仍归属同一个窗口 ID。

本接口金额均为**所选日期在窗口内的明细金额**，不是整笔窗口累计值。活动详情的整笔预估/冻结金额、实际结算与奖励计算不变；每日非负损失金额之和不保证等于整笔窗口净损（窗口结算会跨日抵扣盈利和奖励）。

**输出:**

字段 | 类型 | 说明
---|---|---
date | string | 本次查询的商户时区日期
window_id | string | 实际查询的窗口 ID
window_start_at | string | 整笔窗口开始时刻，不随日历切换
window_end_at | string | 整笔窗口结束时刻，不随日历切换
items | list<object\> | 统计范围内有终态投注的老虎机游戏：`game_code` / `game_name` / `bet` / `loss`；盈利游戏的 `loss` 为负，参与净额抵扣
total_bet | number | 统计范围内总投注
total_loss | number | 统计范围内各游戏净输合计（包含盈利游戏的负净输）
reward_amount | number | 统计范围内已领奖励/赠送入账合计
loss_amount | number | 当天窗口内 `max(total_loss − reward_amount, 0)`，不以整笔冻结值覆盖；历史投影晚到或更正可使明细变化，但不改已结算金额

> 「货币」框取值属充值/钱包域，由前端从自身钱包上下文获取，本活动接口不下发币种。

示例：

```json
// GET /api/game/loss_rescue/loss_detail?window_id=8841&date=2026-09-01
{
  "data": {
    "date": "2026-09-01",
    "window_id": "8841",
    "window_start_at": "2026-09-01T10:00:00-03:00",
    "window_end_at": "2026-09-02T10:00:00-03:00",
    "items": [
      { "game_code": "pg_98", "game_name": "Fortune Tiger", "bet": 100.00, "loss": 77.60 },
      { "game_code": "pp_1602", "game_name": "Gates of Olympus 1000", "bet": 50.00, "loss": 23.35 }
    ],
    "total_bet": 150.00,
    "total_loss": 100.95,
    "reward_amount": 24.08,
    "loss_amount": 76.87
  }
}
```

业务错误：`请先登录`（400，未登录）；`救援金不存在或已领取`（400，指定的窗口不存在或不属于当前会员）。日期与窗口有交集但没有终态投注时 `items` 为空数组；无窗口或日期与窗口没有交集时 `data` 为 `null`。

---

## 前端展示建议 {#ui}

活动卡片状态优先级 **`pending > open > idle`**：

- 有待领取（`pending` 非空）→ 展示**最早过期**的一笔：金额 + 领取按钮 + 领取倒计时；
- 仅有进行中窗口（`windows` 非空）→ 展示**最早结束**的一笔：结算倒计时；
- 皆无 → 展示未触发态（按 `deposit_threshold` 提示开窗门槛文案）。

> 存款/充值页联动（卡片带活动跳转充值页、存款奖金默认选中等）属充值侧功能，不在本活动范围内。

领取成功后按上述优先级依次回落刷新。
