# 幸运礼物外接接口对接文档

> 文档版本：2026-07-15  
> 接入方式：服务端对服务端（Server-to-Server）  
> 金额单位：对接 App 的金币最小整数单位

## 1. 接口用途

外部 App 在自己的账务系统完成幸运礼物扣费后，调用本接口获取一次最终抽奖结果。

平台负责：

- 按 `app_code` 隔离规则、奖池、RTP 和抽奖流水。
- 按 `app_code + request_id` 保证抽奖幂等。
- 返回最终 `reward_amount` 和倍率。

对接 App 负责：

- 调用前完成用户扣费。
- 从自己的账务和用户主数据提供真实的扣费时间、稳定设备标识和账号注册时间。
- 在自己的鉴权或请求签名边界内绑定上述业务事实，不能让 App 或 Web 客户端直接伪造。
- 成功收到结果后，在自己的账务系统中向用户发放 `reward_amount`。
- 对超时或网络失败使用原 `request_id` 重试，不得重新生成业务请求 ID。

HyApp 不会操作外部 App 的用户钱包。`reward_status=granted` 表示抽奖结果已经生成并交给调用方，不表示奖励已在外部 App 入账。

## 2. 接口地址

| 类型 | 地址 |
|---|---|
| Base URL | `https://lucky-api.global-interaction.com` |
| 送礼接口 | `POST https://lucky-api.global-interaction.com/api/v1/lucky-gifts/send` |
| 就绪检查 | `GET https://lucky-api.global-interaction.com/healthz/ready` |

就绪检查：

```text
GET https://lucky-api.global-interaction.com/healthz/ready
```

2026-07-15 实测就绪检查返回 HTTP 200，送礼请求可到达幸运礼物网关。

## 3. 调用前置条件

1. 向平台申请唯一 `app_code`，并加入幸运礼物网关白名单。
2. 确认该 App 的幸运礼物规则已启用。
3. 对接 App 必须先扣费，再调用抽奖接口。
4. 如果使用 `dynamic_v3`，对接 App 必须提供真实 `paid_at_ms`；应尽量提供稳定 `device_id` 和真实 `user_registered_at_ms`，以启用完整设备风控与 RTP 补偿资格判断。
5. 本接口只允许对接 App 服务端调用，禁止将请求能力下发到 App 或 Web 客户端。

### 3.1 真实性与信任边界

当前 HTTP 入口只校验 `app_code` 是否在白名单，尚未启用 Authorization Token 或平台侧请求签名。白名单只能决定“这个 App 是否已开通”，不能独立证明某次请求中的用户、设备、扣费时间和注册时间是真实的。

对接 App 必须在自己的服务端边界内完成以下保障：

- `external_user_id` 来自已登录用户，不直接信任客户端自报值。
- 提供 `device_id` 时，必须与该次已鉴权的用户/设备关系绑定，并在同一设备的后续请求中保持稳定；不能用随机数、请求号或会话 token 临时代替。无法提供时应保持为空，平台会跳过设备日上限而不是伪造设备。
- `paid_at_ms` 由扣费成功的账务事实产生，`user_registered_at_ms` 从用户主数据读取，不使用幸运礼物接口的收包时间或用户首次抽奖时间代替。
- 如果对接 App 自身使用请求签名，签名内容必须覆盖这些字段；重试时必须保持原始业务事实不变。

## 4. 发送幸运礼物

### 4.1 请求

```http
POST /api/v1/lucky-gifts/send HTTP/1.1
Host: lucky-api.global-interaction.com
Content-Type: application/json
X-Request-Id: trace-20260715-000001
```

`X-Request-Id` 为可选的链路追踪 ID。如果不传，平台会自动生成。它只用于日志排查，不是抽奖幂等键。

### 4.2 请求体

```json
{
  "app_code": "partner_app",
  "request_id": "gift-order-20260715-000001",
  "external_user_id": "user-10001",
  "device_id": "device-installation-7f80d90b",
  "gift_count": 3,
  "unit_amount": 100,
  "paid_at_ms": 1784073590000,
  "user_registered_at_ms": 1783814400000,
  "currency": "COIN",
  "metadata": {
    "external_order_id": "order-1000001",
    "gift_id": "lucky-rose",
    "room_id": "room-20001"
  }
}
```

### 4.3 字段说明

| 字段 | 类型 | 必填 | 约束 | 说明 |
|---|---|---:|---|---|
| `app_code` | string | 是 | 去除首尾空格后 1–32 字符 | 平台分配的 App 编码；按小写存储和校验。 |
| `request_id` | string | 是 | 去除首尾空格后 1–128 字符 | 对接 App 生成的业务幂等 ID；建议直接使用送礼订单号或 UUID。 |
| `external_user_id` | string | 是 | 去除首尾空格后 1–128 字符 | 用户在对接 App 中的稳定唯一 ID。 |
| `device_id` | string | 否 | 空或去除首尾空格后 1–128 字符 | 提供可信稳定值时启用设备日返奖上限；缺失时只跳过设备维度，其他返奖上限继续生效。不得每次请求重新生成。 |
| `gift_count` | int64 | 是 | `> 0` | 本次已扣费的礼物数量。 |
| `unit_amount` | int64 | 是 | `> 0` | 单个礼物的金币整数价格。 |
| `paid_at_ms` | int64 | `dynamic_v3` 是 | `> 0`，Unix epoch milliseconds | 对接 App 账务系统完成本次扣费的真实 UTC 时间；系统不会用接口收到请求的时间代替。 |
| `user_registered_at_ms` | int64 | `dynamic_v3` 业务必填 | `> 0`，Unix epoch milliseconds | 用户账号的真实创建时间，必须从对接 App 的用户主数据获取。技术上缺失不会阻断普通抽奖，但系统会按不满足注册 48 小时处理，关闭该用户的 RTP 补偿大奖资格。 |
| `currency` | string | 否 | 只支持 `COIN` | 缺省或空字符串时按 `COIN` 处理。 |
| `metadata` | object | 否 | JSON object | 对接方的展示或排查元数据；不参与金额、规则或幂等判定。 |

服务端按下式计算本次金额：

```text
total_amount = gift_count * unit_amount
```

请求体不得传入 `total_amount` 或 `pool_id`。`total_amount` 由系统根据数量和单价计算，奖池由平台已发布的规则选择。接口会拒绝所有未定义的顶层字段。

`dynamic_v3` 下，缺失 `paid_at_ms` 会使整次抽奖失败；缺失 `device_id` 不阻断开奖，只跳过设备日返奖上限，用户、房间、主播、单次等其他上限仍生效。缺失、不合理或尚未满 48 小时的 `user_registered_at_ms` 不阻断普通抽奖，但 RTP 补偿大奖资格会严格按“不符合”处理。

一个请求只返回一个聚合抽奖结果；`gift_count` 不会产生多个独立响应。

### 4.4 成功响应

HTTP Status：`200 OK`

```json
{
  "code": 0,
  "message": "ok",
  "request_id": "trace-20260715-000001",
  "data": {
    "draw_id": "external_lucky_draw_xxxxxxxxxx",
    "request_id": "gift-order-20260715-000001",
    "app_code": "partner_app",
    "external_user_id": "user-10001",
    "gift_count": 3,
    "unit_amount": 100,
    "total_amount": 300,
    "reward_amount": 600,
    "multiplier_ppm": 2000000,
    "reward_status": "granted",
    "rule_version": 1,
    "created_at_ms": 1784073600000
  }
}
```

### 4.5 响应字段

| 字段 | 类型 | 说明 |
|---|---|---|
| `code` | int | 业务结果码；`0` 表示请求成功。 |
| `message` | string | 结果说明。 |
| 外层 `request_id` | string | HTTP 链路追踪 ID，来自 `X-Request-Id` 或由平台生成。 |
| `data.draw_id` | string | 平台生成的唯一抽奖 ID，对账时使用。 |
| `data.request_id` | string | 请求体中的业务幂等 ID。 |
| `data.app_code` | string | 归一化后的 App 编码。 |
| `data.external_user_id` | string | 对接 App 的用户 ID。 |
| `data.gift_count` | int64 | 礼物数量。 |
| `data.unit_amount` | int64 | 单件礼物金币价格。 |
| `data.total_amount` | int64 | 服务端计算的已扣费总额。 |
| `data.reward_amount` | int64 | 对接 App 应向用户发放的金币整数金额；以该字段为最终结算依据。 |
| `data.multiplier_ppm` | int64 | 倍率 ppm；`1000000` 表示 1x，`2000000` 表示 2x。 |
| `data.reward_status` | string | 当前为 `granted`，表示抽奖已完成且结果已交给对接 App 处理。 |
| `data.rule_version` | int64 | 本次抽奖命中的规则版本。 |
| `data.created_at_ms` | int64 | 抽奖结果创建时间，Unix epoch milliseconds，UTC。 |

未中奖时 `reward_amount` 和 `multiplier_ppm` 为 `0`。当前 JSON 序列化会省略数值为 `0` 的结果字段，对接方必须将缺失的 `reward_amount` 或 `multiplier_ppm` 按 `0` 处理，不得将字段缺失视为请求失败。

## 5. 幂等与重试

幂等键固定为：

```text
app_code + request_id
```

- 首次请求成功后，平台保存抽奖结果。
- 使用相同 `app_code + request_id` 重试时，直接返回首次结果，不会重新抽奖。
- 同一 `request_id` 不得对应不同用户、数量或金额。平台在幂等命中时会返回首次结果，不会用新参数覆盖。
- 超时、连接中断或 HTTP 5xx 时，使用原请求体和原 `request_id` 重试。
- 客户端超时建议设置为大于网关内部 3 秒处理超时，并对重试使用有上限的退避。

如果多次重试后仍无法确认结果，请保留 `app_code`、`request_id`、外层 `request_id` 和扣费流水，由平台根据抽奖记录对账。

## 6. 错误响应

### 6.1 统一格式

```json
{
  "code": 40000,
  "message": "request_id is required",
  "request_id": "trace-20260715-000001"
}
```

### 6.2 错误码

| HTTP Status | `code` | 场景 | 对接方动作 |
|---:|---:|---|---|
| 400 | 40000 | JSON 无效、出现未定义字段、必填字段缺失、金额非正数或币种不支持 | 修正请求，不要原样重试。 |
| 403 | 40300 | `app_code` 未加入当前环境白名单 | 联系平台开通或检查环境。 |
| 404 | 无统一业务码 | 请求路径或 HTTP 方法错误 | 检查 Base URL、路径和请求方法。 |
| 500 | 50000 | 抽奖服务超时、内部处理失败，或 `dynamic_v3` 在规则层发现 `paid_at_ms` 缺失 | 先本地校验 V3 扣费事实已齐全；事实齐全时保持原 `request_id` 重试，不得重新扣费。 |

业务成功必须同时满足 HTTP Status 为 `200` 且响应 `code` 为 `0`。

## 7. cURL 示例

```bash
curl --request POST \
  'https://lucky-api.global-interaction.com/api/v1/lucky-gifts/send' \
  --header 'Content-Type: application/json' \
  --header 'X-Request-Id: trace-20260715-000001' \
  --data '{
    "app_code": "partner_app",
    "request_id": "gift-order-20260715-000001",
    "external_user_id": "user-10001",
    "device_id": "device-installation-7f80d90b",
    "gift_count": 3,
    "unit_amount": 100,
    "paid_at_ms": 1784073590000,
    "user_registered_at_ms": 1783814400000,
    "currency": "COIN",
    "metadata": {
      "external_order_id": "order-1000001",
      "gift_id": "lucky-rose",
      "room_id": "room-20001"
    }
  }'
```

## 8. 标准处理流程

1. 对接 App 在已鉴权的服务端上确定 `external_user_id`、稳定 `device_id` 和用户主数据中的 `user_registered_at_ms`。
2. 对接 App 生成全局唯一的 `request_id`。
3. 对接 App 在自有账务系统扣除 `gift_count * unit_amount`，并以扣费成功事实的 UTC 毫秒时间作为 `paid_at_ms`。
4. 对接 App 调用幸运礼物接口，一次传入上述业务事实。
5. 平台按 `app_code + request_id` 创建或返回已有抽奖结果。
6. 对接 App 在响应成功后按 `reward_amount` 入账，并保存 `draw_id`。
7. 任何未知结果都使用原 `request_id` 和原始业务事实重试；禁止重复扣费或重新生成抽奖业务号。

## 9. 联调检查清单

- [ ] `app_code` 已加入幸运礼物网关白名单。
- [ ] 规则已启用，并确认金币单位一致。
- [ ] `dynamic_v3` 请求中的 `paid_at_ms` 来自真实扣费事实，不是当前时间的临时填充值。
- [ ] `device_id` 已绑定已鉴权设备且跨请求稳定，不用请求号或会话 token 代替。
- [ ] `user_registered_at_ms` 来自用户主数据；已验证缺失时普通抽奖可继续、RTP 补偿大奖资格会关闭。
- [ ] 调用能力仅保留在对接 App 服务端，上述业务事实已纳入自身鉴权或请求签名边界。
- [ ] 正常送礼可获得 HTTP 200 / `code=0`。
- [ ] 重复发送相同 `request_id` 可获得相同 `draw_id` 和结果。
- [ ] 模拟超时后只重试抽奖请求，不重复扣费。
- [ ] 未中奖响应中缺失的数值字段可正确按 `0` 处理。
- [ ] 奖励按 `reward_amount` 在外部 App 入账，并持久化 `draw_id`。
- [ ] 上线前完成小额真实扣费、抽奖、返奖和幂等重试验收。
