# 打码任务接口

---

## 版本与接入范围 {#release}

本章对应当前代码中的双钱包与打码任务版本，**尚未正式部署，版本发布前需完成配套数据库迁移**。本文更新日期不是上线日期；请确认目标环境已发布对应版本后再接入。

本章接口均需登录，使用 `Authorization: Bearer <jwt>`。路径省略 `/api/game` 前缀。余额接口的实际路径为 [获取账户余额](200_finance.md#account_balance) 的 `GET /finance/account/balance`，没有 `/finance/balance` 别名。

本文不承诺所有游戏场景均已接入双钱包；**房间混合钱包及通用异常冲正不在本章已支持能力范围内**，不要根据任务规则自行推导这些场景的结算结果。

---

## 双钱包与提现门槛 {#wallet_rules}

- 余额响应中，`user_money` 是现金余额，`bonus_money` 是奖金余额，两者均为标准字段；总余额 `total = user_money + bonus_money`。响应不返回 `cash`、`bonus` 字段。
- 充值本金计入 `user_money`，按实际到账金额建立本金打码任务，生效倍数至少为 `1`；本金任务不可主动放弃。
- 套餐赠送和活动奖励有打码要求时计入 `bonus_money` 并建立奖励任务；无打码要求时计入 `user_money`，不建立任务。任务目标为发放金额乘以生效倍数，按金额精度保留两位小数，以返回的 `target_amount` 为准。
- **任何待完成任务或旧剩余打码都会阻止提现整个现金账户**，不是只限制某一笔奖励。无码奖励进入现金也不代表立刻能提现。
- `bet_amount` 表示旧剩余打码量，不包含 `task_wager_remaining`；新任务不累加到它。总剩余要求为 `bet_amount + task_wager_remaining`，不另设总剩余字段；余额响应不返回 `legacy_wager_remaining`、`required_bet_amount`。
- 旧打码为 `0` 且所有任务均不再是待完成（`not_started` / `pending`）时，打码门槛才满足；结算处理会将剩余奖金余额 `bonus_money` 全额转入现金余额 `user_money`。单个任务完成不会按笔释放奖金。
- 余额接口的 `can_withdraw` 只判断打码门槛。实际提现仍会校验现金余额、账号资格、未完成提现单及通道限额等条件，**余额不等于可提现金额**。

**余额响应字段示例（仅展示打码相关字段）:**

```json
{
	"data": {
		"bet_amount": "0",
		"task_wager_remaining": "200",
		"pending_task_count": 1,
		"can_withdraw": false,
		"can_abandon_all": false,
		"block_reasons": ["pending_tasks"]
	}
}
```

本例仍有一个不可放弃的待完成任务，总剩余打码量为 `0 + 200 = 200`；`bet_amount = 0` 不代表可提现。若旧码未清零，`block_reasons` 仍使用 `legacy_wager`，可与 `pending_tasks` 同时出现；该枚举不因响应字段精简而改变。

---

## 进度、解锁与余额清理 {#settlement}

新任务按服务端确认的**首次派彩**固定该局有效投注并推进，不能在下注时由客户端自行减码。首次派彩可以是零金额结算；同一局后续派彩不会再次增加任务进度。有效投注受游戏结算规则影响，不保证等于下注面额。

分配顺序为：先消耗旧打码，再按任务创建顺序 FIFO（`sequence ASC, id ASC`）推进待完成任务。一次有效投注可以依次完成多个任务，超出全部剩余要求的部分不留给未来任务。

结算时**先尝试解锁奖金，再处理低余额清理**：

1. 旧打码为 `0` 且没有待完成任务，奖金先全额转为现金，本次不再执行下面的尾数清理；因此不足 `0.1` 的奖金也可能正常解锁。
2. 未能解锁时，若适用既有旧打码低余额清理规则，先处理旧打码，再尝试解锁。该规则与下面的新任务 `0.1` 阈值不是同一规则。
3. 仍不能解锁时，`bonus_money < 0.1` 则清零奖金并将待完成奖励任务置为 `expired`；`user_money < 0.1` 则清零现金尾数。本金任务与后台新增任务（`source_kind` 为 `recharge` / `adjust`）仅在**现金与奖金都小于 `0.1`** 时置为 `expired`。
4. 清理后再尝试解锁。阈值为严格小于，余额恰好为 `0.1` 不触发对应清理。

这是结算侧规则，不是余额查询时执行的清理，也不是客户端定时清零逻辑。低余额自动失效与主动放弃不同，不受任务 `can_abandon` 或商户是否允许主动放弃的限制。

---

## WagerTask 对象 {#task}

金额和倍数为十进制字符串，ID 按字符串处理。

字段 | 类型 | 说明
---|---|---
id | string | 任务 ID
created_at | string | 创建时间
updated_at | string | 更新时间
merchant_id | string | 所属商户 ID；查询已按当前登录身份限定，无需传入
member_id | string | 所属会员 ID
source_kind | string | `recharge`（充值本金）/ `reward`（奖励）/ `adjust`（后台新增打码要求，不伴随任何入账，也不会出现在【[奖金列表](#rewards)】中）
source | string | 发放来源标识，如 `recharge`、`recharge_gift`、`treasure_box`、`vip`、`redeem_code`、`admin_wager_adjust`；开放集，未知来源仍正常展示任务。**客户端须为 `admin_wager_adjust` 增加来源名称映射**（建议文案：中文「平台打码任务」/ 英文「Rollover Requirement」/ 葡语「Requisito de Rollover」/ 西语「Requisito de Rollover」），并按 `source_kind=adjust` 只展示 `target_amount`（不展示 `amount × multiple`）
source_id | string | 发放来源记录标识，不等同于任务 ID；客户端按不透明字符串处理
source_detail_id | string | 来源明细标识；客户端按不透明字符串处理
credit_event_id | string | 入账关联标识；无需解析或展示
amount | string | 建立任务的本金或奖励金额，不是当前余额；`source_kind=adjust` 的任务无入账金额，此字段与 `multiple` 均为 `0`，展示时只用 `target_amount`
multiple | string | 建立任务时固化的打码倍数；`source_kind=adjust` 为 `0`
target_amount | string | 目标打码量
progress_amount | string | 已完成打码量；待完成任务剩余量为 `target_amount - progress_amount`
sequence | number | 任务创建顺序，用于 FIFO 排序，不是分页页码
status | string | `not_started`（未开始，创建默认，尚未写入任何进度）/ `pending`（进行中，首次写入进度后）/ `completed`（打码完成）/ `abandoned`（主动整体放弃）/ `expired`（低余额规则导致失效）。`not_started` 与 `pending` 合称“待完成”，均占用打码额度、阵止提现并参与 FIFO / 放弃 / 失效；队首任务被旧打码挡住时也保持 `not_started`
can_abandon | bool | 创建任务时固化的放弃权限；本金任务恒为 `false`，商户开关切换不修改此快照；当前整体放弃资格以余额接口的 `can_abandon_all` 为准
expire_at | string | 可为 `null`；当前任务创建流程不设置定时到期时间，不能据此推导奖励领取期限
terminal_at | string | 进入终态的时间，待完成时为 `null`
terminal_reason | string | 待完成时为空；当前终态原因有 `fifo_completed`（完成）、`admin_force_complete`（后台一键完成，进度补满至目标额，奖金整体转入现金）、`user_abandon`（主动放弃）、`bonus_depleted`（奖金耗尽）、`both_depleted`（双余额耗尽）
config_snapshot | object | 发放时固化的配置信息，结构因来源而异；不要依赖未约定的键。活动任务含 `bet_multiple`（string）与 `can_abandon`（bool），界面判断优先使用任务顶层字段

`expired` 不等于已完成目标，`abandoned` 也不会补满进度。任务状态与活动奖励的待领取、已领取状态是两套不同状态。

---

## 我的打码任务 {#list}

**URL:** `GET` /finance/wager_tasks

**Query:**

字段 | 类型 | 必填 | 说明
---|---|---|---
status | string | 否 | 按 `WagerTask.status` 精确过滤（`not_started` / `pending` / `completed` / `abandoned` / `expired`）；另支持虚拟值 **`active`** 一次返回 `not_started` + `pending` 全部待完成任务（待完成页应传此值，不要客户端分页后再本地过滤）；不传返回全部状态
page | number | 否 | 页码
size | number | 否 | 每页条数

**输出:**

字段 | 类型 | 说明
---|---|---
list | list<WagerTask\> | 当前会员任务列表，按 `created_at DESC, id DESC` 排序（最新任务在前；消耗仍按 `sequence` FIFO 推进，与展示顺序无关）
total | number | 当前筛选条件下的总条数，不是账户待完成任务数

分页列表不代表全部任务。提现与整体放弃按钮请使用 [余额接口](200_finance.md#account_balance) 的 `pending_task_count`、`can_withdraw`、`can_abandon_all`、`block_reasons`，不能仅用当前页计算。

---

## 我的奖金列表 {#rewards}

**URL:** `GET` /finance/rewards

面向「奖金详情」页：每条记录对应一次奖励入账（活动奖励 / 套餐赠送 / 后台赠送），**不包含充值本金**。有打码要求的奖励内联其打码任务当前进度，无打码要求的奖励 `task.status` 固定为 `none`；列表已含详情弹窗所需全部字段，点击展开无需再请求。

**Query:**

字段 | 类型 | 必填 | 说明
---|---|---|---
kind | string | 否 | 按奖励来源精确过滤，取值同 `WagerTask.source`（如 `vip` / `rakeback` / `recharge_gift` / `admin_gift`，开放集）；不传返回全部来源
start_time | string | 否 | 入账时间下限（含），`YYYY-MM-DD` 或 `YYYY-MM-DD HH:mm:ss`，按商户时区解析
end_time | string | 否 | 入账时间上限（含），格式同上；传纯日期时包含当天整日
page | number | 否 | 页码
size | number | 否 | 每页条数

**输出:**

字段 | 类型 | 说明
---|---|---
list | list<RewardRecord\> | 按 `created_at DESC, id DESC` 排序（最新在前）
total | number | 当前筛选条件下的总条数
summary.total_amount | string | 当前筛选条件下（不受分页影响）全部奖励入账金额之和，即页头「领取的奖金总额」

页头「锁定奖金」请直接使用 [余额接口](200_finance.md#account_balance) 的 `bonus_money`，本接口不重复下发。

**RewardRecord 对象:**

字段 | 类型 | 说明
---|---|---
id | string | 奖励入账记录 ID；不等同于任务 ID
created_at | string | 入账时间（获取时间）
kind | string | 奖励来源，含义同 `WagerTask.source`；标题与图标由客户端按 kind 做多语言映射，服务端不下发名称。当前取值：活动类 `agent_rebate` / `battle_pass` / `cumulative_recharge` / `daily_bankrupt_pack` / `daily_checkin` / `daily_deposit_bonus` / `deposit_first_n_bonus` / `first_recharge_bonus` / `free_register_bonus` / `loss_rescue` / `magnate` / `mystery_bonus` / `rakeback` / `recharge_bonanza` / `redeem_code` / `red_packet_rain` / `rescue_bonus` / `treasure_box` / `vip` / `weekly_cashback` / `weekly_rescue_bonus`，非活动类 `recharge_gift`（充值套餐赠送）/ `admin_gift`（后台赠送）；开放集，未知值请回退为通用文案与样式
amount | string | 奖励金额
reason | string | 发放动作标识（如 `rakeback_claim`）或来源备注；仅供展示/排障，不要据此做逻辑判断
config_snapshot | object | 发放时固化的活动上下文，**结构因 `kind` 而异**，用于拼装标题；不要依赖未约定的键，缺键时只显示类型文案。已约定的键见下表
task | object | 打码任务摘要，字段见下表；无打码要求时 `status="none"`、其余金额字段均为 `"0"`

**config_snapshot 已约定键（按 `kind`）:**

kind | 键 | 说明
---|---|---
`rakeback` | `rate_percent`(number) / `bet_basis`(string) / `period_start`(string) / `tier_index` / `claim_id` | 返水按「上次领取以来的增量打码」结算，**没有自然日归属**：`period_start` 为上次领取时刻（RFC3339，首次领取为空串），结算区间为 `[period_start, created_at]`，标题请用 `created_at` 的日期或该区间表达，不要把它理解为某一天的返水
`loss_rescue` | `rate`(string, %) / `loss_amount`(string) / `user_type` / `window_start_at` / `window_end_at`(RFC3339) / `tier_index` / `window_id` | 标题日期取 `window_start_at`（开窗日，商户时区展示），比例取 `rate`
`vip` | `level`(number) / `kind`(`upgrade` / `weekly` / `monthly`) / `period_key` | 如「VIP 3 周俸」
`deposit_first_n_bonus` | `step_index` / `breakpoint_rate`(number, %) / `recharge_amount` | 如「首存五次奖 850%」
其他存款类（`first_recharge_bonus` / `daily_deposit_bonus` / `recharge_bonanza` / `daily_bankrupt_pack`） | `tier_index` / `recharge_amount`，部分含 `claim_date` | —
`recharge_gift` | `package_id` | 套餐赠送
`admin_gift` | 无 | 后台赠送不下发内部信息

**task 字段:**

字段 | 类型 | 说明
---|---|---
id | string | 任务 ID，`status="none"` 时为空串
status | string | `none`（无需打码，已直接进入现金）/ `not_started` / `pending` / `completed` / `abandoned` / `expired`，后五者含义同 [WagerTask](#task)
multiple | string | 打码倍数
target_amount | string | 所需打码
progress_amount | string | 已完成打码
remaining_amount | string | 剩余打码；仅 `not_started` / `pending` 时为 `target_amount - progress_amount`，终态与 `none` 一律为 `"0"`（`expired` / `abandoned` 不会再推进，不要自行按目标减进度显示）
can_abandon | bool | 任务创建时固化的放弃权限，含义同 `WagerTask.can_abandon`
terminal_at | string | 进入终态的时间，待完成与 `none` 时为 `null`
terminal_reason | string | 终态原因，取值同 `WagerTask.terminal_reason`

`task.progress_amount` 为实时值，结算推进后重新拉取列表即可刷新。

---

## 整体放弃待完成任务 {#abandon_all}

**URL:** `POST` /finance/wager_tasks/abandon_all

**输入:**

字段 | 类型 | 必填 | 说明
---|---|---|---
request_id | string | 是 | 本次放弃操作的幂等标识，非空、最长 128 字符；客户端生成并保留，同一次操作重试必须复用

**输出:** 无

该操作没有任务 ID 列表、单任务放弃或部分放弃参数，也不接受客户端指定放弃金额。

- 商户须允许主动放弃（默认关闭），至少存在一个待完成（`not_started` / `pending`）任务，且**全部**待完成任务（包括排队任务）的 `can_abandon` 都为 `true`，余额接口的综合资格 `can_abandon_all` 才为 `true`。商户关闭时，即使每个任务都可放弃，新放弃请求仍会拒绝；开启也不能覆盖充值等不可放弃任务的限制。
- 服务端会再次校验商户策略和全部待完成任务，余额接口的预检结果不能替代提交结果。玩家端只读取综合 `can_abandon_all`，不增加商户开关参数或重复响应字段。
- 校验通过后，全部待完成任务置为 `abandoned`，**当前奖金余额整体清零**；现金余额及旧剩余打码不变。存在任一不可放弃任务时整体失败，不会只放弃其中一部分，也不会扣除奖金。
- 旧剩余打码不影响放弃接口本身的资格，但仍会阻止放弃后的提现。充值本金任务不可放弃，存在这样的待完成任务时无法整体放弃。
- 幂等按当前商户、当前会员和 `request_id` 区分。同一标识已成功后，即使商户随后关闭主动放弃，重试仍返回成功，不重复扣款、不处理后来新增的任务；新的放弃操作必须使用新的标识，并受当时商户策略约束。
- 校验失败不记为成功操作；使用相同标识重试会重新校验当时状态。网络超时不能视作失败，请保留原标识重试，避免换标识后误放弃新任务。
- 商户切换策略会影响已有待完成任务的整体放弃资格，但不修改任务快照、旧 `bet_amount`、提现门槛或系统耗尽清理规则。策略直接读取配置，不使用本地缓存；关闭影响后续配置读取，不撤销已通过校验的在途事务，不保证配置与资金事务跨库瞬时原子生效。
- 提交前须明确告知玩家将损失当前全部奖金。成功后重新获取余额和任务列表；成功不代表必然可提现。

**业务错误:**

message | 触发条件
---|---
当前不允许放弃打码任务 | 新放弃请求校验时商户未允许主动放弃；已成功 `request_id` 的重试不受随后关闭影响
没有可放弃的打码任务 | 新操作执行时无待完成（`not_started` / `pending`）任务
存在不可放弃的打码任务 | 当前任一待完成任务的 `can_abandon` 为 `false`

`request_id` 缺失或超过长度限制时返回参数校验错误。业务错误按请求语言翻译，客户端不应按展示文案硬编码判断。