# 邀请绑定（bind + landing-track）

> 所属:Invite 组（前缀 `/api/v1/invite`） | 鉴权:bind 需 Bearer / landing-track 无需鉴权 | [← 返回 API 索引](../README.md)

---

## POST /bind — 绑定邀请人

把当前登录用户（被邀请人）绑定到某邀请码。支持三种归因路径：clipboard（首启读剪贴板）、manual（手动输入邀请码）、fingerprint（指纹兜底反查）。**#113 起绑定只建关系、不发奖**——发奖后置到被邀请人「比价并实际下单」（`POST /order/report` 触发，给邀请人发**邀请奖励金**，`compare_reward_granted` 幂等闸一人一次）。

### 入参

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `invite_code` | string \| null | ❌ | 邀请码；走指纹兜底时为空 |
| `channel` | string | ✅ | 归因来源：`clipboard` / `manual` / `fingerprint` |
| `fingerprint` | object \| null | ❌ | 指纹兜底时必传 |
| `fingerprint.screen` | string | - | 屏幕分辨率（如 `1080x2400`） |
| `fingerprint.device_model` | string | - | 设备型号（如 `24115RA8EC`） |

Mock 入参（clipboard 归因）:
```json
{
  "invite_code": "A3F8K2",
  "channel": "clipboard",
  "fingerprint": null
}
```

Mock 入参（指纹兜底）:
```json
{
  "invite_code": null,
  "channel": "fingerprint",
  "fingerprint": {
    "screen": "1080x2400",
    "device_model": "24115RA8EC"
  }
}
```

### 出参

响应 `200`:`BindInviteOut`

| 字段 | 类型 | 说明 |
|---|---|---|
| `status` | string | `success` / `already_bound` / `invalid_code` / `self_invite` / `not_eligible` / `fp_not_found` |
| `coins_awarded` | int | 兼容保留字段（#113 前"绑定即发金币"口径）。**#113 起新绑定恒 0**，前端不应再据此展示发奖 |
| `message` | string | 给前端直接展示的文案 |

Mock 出参:
```json
{
  "status": "success",
  "coins_awarded": 0,
  "message": "邀请绑定成功"
}
```

---

## POST /landing-track — 落地页指纹采集

B 浏览器打开 `dl.html?ref=xxx` 时上报指纹（剪贴板归因失败时兜底）。**无需鉴权**（浏览器没 token）。

### 入参

| 字段 | 类型 | 必填 | 说明 |
|---|---|---|---|
| `ref` | string | ✅（4-16 位） | 邀请码（落地页 `?ref=`） |
| `screen` | string | ❌ | 屏幕分辨率（如 `1080x2400`） |

Mock 入参:
```json
{
  "ref": "A3F8K2",
  "screen": "1080x2400"
}
```

### 出参

响应 `200`:`LandingTrackOut`

| 字段 | 类型 | 说明 |
|---|---|---|
| `status` | string | `ok` / `invalid_code` / `no_ip` |

Mock 出参:
```json
{"status": "ok"}
```

---

## 说明
- `bind`: 绑定只在注册后首次有效（`not_eligible` = 已过新人期），自邀屏蔽
- `landing-track`: IP/UA 服务端从 HTTP 头自动拿，JS 无需上报
- 指纹反查窗口期由 `INVITE_FP_WINDOW_DAYS` 控制
